1 /*
2  * DesktopUtils.cpp
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 
16 #include "DesktopUtils.hpp"
17 
18 #include <set>
19 
20 #include <QPushButton>
21 #include <QTimer>
22 #include <QDesktopServices>
23 
24 #include <core/FileSerializer.hpp>
25 #include <core/system/Environment.hpp>
26 #include <core/system/Xdg.hpp>
27 
28 #include "DesktopOptions.hpp"
29 #include "DesktopMainWindow.hpp"
30 
31 #ifdef Q_OS_WIN
32 #include <windows.h>
33 #endif
34 
35 using namespace rstudio::core;
36 
37 namespace rstudio {
38 namespace desktop {
39 
40 #ifdef Q_OS_WIN
41 
reattachConsoleIfNecessary()42 void reattachConsoleIfNecessary()
43 {
44    if (::AttachConsole(ATTACH_PARENT_PROCESS))
45    {
46       freopen("CONOUT$","wb",stdout);
47       freopen("CONOUT$","wb",stderr);
48       freopen("CONIN$","rb",stdin);
49       std::ios::sync_with_stdio();
50    }
51 }
52 
53 #else
54 
55 void reattachConsoleIfNecessary()
56 {
57 
58 }
59 
60 #endif
61 
62 // NOTE: this code is duplicated in diagnostics as well (and also in
63 // SessionOptions.hpp although the code path isn't exactly the same)
userLogPath()64 FilePath userLogPath()
65 {
66    return core::system::xdg::userDataDir().completeChildPath("log");
67 }
68 
userWebCachePath()69 FilePath userWebCachePath()
70 {
71    return core::system::xdg::userDataDir().completeChildPath("web-cache");
72 }
73 
isWindows()74 bool isWindows()
75 {
76 #ifdef Q_OS_WIN
77    return true;
78 #else
79    return false;
80 #endif
81 }
82 
83 #ifndef Q_OS_MAC
devicePixelRatio(QMainWindow * pMainWindow)84 double devicePixelRatio(QMainWindow* pMainWindow)
85 {
86    return 1.0;
87 }
88 
isMacOS()89 bool isMacOS()
90 {
91    return false;
92 }
93 
94 // NOTE: also RHEL
isCentOS()95 bool isCentOS()
96 {
97    FilePath redhatRelease("/etc/redhat-release");
98    if (!redhatRelease.exists())
99       return false;
100 
101    std::string contents;
102    Error error = readStringFromFile(redhatRelease, &contents);
103    if (error)
104       return false;
105 
106    return contents.find("CentOS") != std::string::npos ||
107           contents.find("Red Hat Enterprise Linux") != std::string::npos;
108 }
109 
browseDirectory(const QString & caption,const QString & label,const QString & dir,QWidget * pOwner)110 QString browseDirectory(const QString& caption,
111                         const QString& label,
112                         const QString& dir,
113                         QWidget* pOwner)
114 {
115    QFileDialog dialog(
116             pOwner,
117             caption,
118             resolveAliasedPath(dir));
119 
120    dialog.setLabelText(QFileDialog::Accept, label);
121    dialog.setFileMode(QFileDialog::Directory);
122    dialog.setOption(QFileDialog::ShowDirsOnly, true);
123    dialog.setWindowModality(Qt::WindowModal);
124 
125    QString result;
126    if (dialog.exec() == QDialog::Accepted)
127       result = dialog.selectedFiles().value(0);
128 
129    if (pOwner)
130       raiseAndActivateWindow(pOwner);
131 
132    return createAliasedPath(result);
133 }
134 
135 #endif
136 
isGnomeDesktop()137 bool isGnomeDesktop()
138 {
139    if (core::system::getenv("DESKTOP_SESSION") == "gnome")
140       return true;
141 
142    std::string desktop = core::system::getenv("XDG_CURRENT_DESKTOP");
143    if (desktop.find("GNOME") != std::string::npos)
144       return true;
145 
146    return false;
147 }
148 
149 #ifndef Q_OS_MAC
150 
getFixedWidthFontList()151 QString getFixedWidthFontList()
152 {
153    return desktopInfo().getFixedWidthFontList();
154 }
155 
156 #endif
157 
applyDesktopTheme(QWidget * window,bool isDark)158 void applyDesktopTheme(QWidget* window, bool isDark)
159 {
160 #ifndef Q_OS_MAC
161    std::string lightSheetName = isWindows()
162          ? "rstudio-windows-light.qss"
163          : "rstudio-gnome-light.qss";
164 
165    std::string darkSheetName = isWindows()
166          ? "rstudio-windows-dark.qss"
167          : "rstudio-gnome-dark.qss";
168 
169    FilePath stylePath = isDark
170          ? options().resourcesPath().completePath("stylesheets").completePath(darkSheetName)
171          : options().resourcesPath().completePath("stylesheets").completePath(lightSheetName);
172 
173    std::string stylesheet;
174    Error error = core::readStringFromFile(stylePath, &stylesheet);
175    if (error)
176       LOG_ERROR(error);
177 
178    window->setStyleSheet(QString::fromStdString(stylesheet));
179 #endif
180 }
181 
182 #ifndef Q_OS_MAC
183 
enableFullscreenMode(QMainWindow * pMainWindow,bool primary)184 void enableFullscreenMode(QMainWindow* pMainWindow, bool primary)
185 {
186 
187 }
188 
toggleFullscreenMode(QMainWindow * pMainWindow)189 void toggleFullscreenMode(QMainWindow* pMainWindow)
190 {
191 
192 }
193 
supportsFullscreenMode(QMainWindow * pMainWindow)194 bool supportsFullscreenMode(QMainWindow* pMainWindow)
195 {
196    return false;
197 }
198 
initializeLang()199 void initializeLang()
200 {
201 }
202 
finalPlatformInitialize(MainWindow * pMainWindow)203 void finalPlatformInitialize(MainWindow* pMainWindow)
204 {
205 
206 }
207 
208 #endif
209 
raiseAndActivateWindow(QWidget * pWindow)210 void raiseAndActivateWindow(QWidget* pWindow)
211 {
212    // WId wid = pWindow->effectiveWinId(); -- gets X11 window id
213    // gtk_window_present_with_time(GTK_WINDOW, timestamp)
214 
215    if (pWindow->isMinimized())
216    {
217       pWindow->setWindowState(
218                      pWindow->windowState() & ~Qt::WindowMinimized);
219    }
220 
221    pWindow->raise();
222    pWindow->activateWindow();
223 }
224 
moveWindowBeneath(QWidget * pTop,QWidget * pBottom)225 void moveWindowBeneath(QWidget* pTop, QWidget* pBottom)
226 {
227 #ifdef WIN32
228    HWND hwndTop = reinterpret_cast<HWND>(pTop->winId());
229    HWND hwndBottom = reinterpret_cast<HWND>(pBottom->winId());
230    ::SetWindowPos(hwndBottom, hwndTop, 0, 0, 0, 0,
231                   SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
232 #endif
233    // not currently supported on Linux--Qt doesn't provide a way to view or
234    // change the window stacking order
235 }
236 
closeWindow(QWidget * pWindow)237 void closeWindow(QWidget* pWindow)
238 {
239    pWindow->close();
240 }
241 
safeMessageBoxIcon(QMessageBox::Icon icon)242 QMessageBox::Icon safeMessageBoxIcon(QMessageBox::Icon icon)
243 {
244    // if a gtk theme has a missing or corrupt icon for one of the stock
245    // dialog images, qt crashes when attempting to show the dialog
246 #ifdef Q_OS_LINUX
247    return QMessageBox::NoIcon;
248 #else
249    return icon;
250 #endif
251 }
252 
showYesNoDialog(QMessageBox::Icon icon,QWidget * parent,const QString & title,const QString & text,const QString & informativeText,bool yesDefault)253 bool showYesNoDialog(QMessageBox::Icon icon,
254                      QWidget *parent,
255                      const QString &title,
256                      const QString& text,
257                      const QString& informativeText,
258                      bool yesDefault)
259 {
260    // basic message box attributes
261    QMessageBox messageBox(parent);
262    messageBox.setIcon(safeMessageBoxIcon(icon));
263    messageBox.setWindowTitle(title);
264    messageBox.setText(text);
265    if (informativeText.length() > 0)
266       messageBox.setInformativeText(informativeText);
267    messageBox.setWindowModality(Qt::WindowModal);
268    messageBox.setWindowFlag(Qt::WindowContextHelpButtonHint, false);
269 
270    // initialize buttons
271    QPushButton* pYes = messageBox.addButton(QMessageBox::Yes);
272    QPushButton* pNo = messageBox.addButton(QMessageBox::No);
273    if (yesDefault)
274       messageBox.setDefaultButton(pYes);
275    else
276       messageBox.setDefaultButton(pNo);
277 
278    // show the dialog modally
279    messageBox.exec();
280 
281    // return true if the user clicked yes
282    return messageBox.clickedButton() == pYes;
283 }
284 
showMessageBox(QMessageBox::Icon icon,QWidget * parent,const QString & title,const QString & text,const QString & informativeText)285 void showMessageBox(QMessageBox::Icon icon,
286                     QWidget *parent,
287                     const QString &title,
288                     const QString& text,
289                     const QString& informativeText)
290 {
291    QMessageBox messageBox(parent);
292    messageBox.setIcon(safeMessageBoxIcon(icon));
293    messageBox.setWindowTitle(title);
294    messageBox.setText(text);
295    if (informativeText.length() > 0)
296       messageBox.setInformativeText(informativeText);
297    messageBox.setWindowModality(Qt::WindowModal);
298    messageBox.setWindowFlag(Qt::WindowContextHelpButtonHint, false);
299    messageBox.addButton(new QPushButton(QString::fromUtf8("OK")), QMessageBox::AcceptRole);
300    messageBox.exec();
301 }
302 
showError(QWidget * parent,const QString & title,const QString & text,const QString & informativeText)303 void showError(QWidget *parent,
304                const QString &title,
305                const QString& text,
306                const QString& informativeText)
307 {
308    showMessageBox(QMessageBox::Critical, parent, title, text, informativeText);
309 }
310 
showWarning(QWidget * parent,const QString & title,const QString & text,const QString & informativeText)311 void showWarning(QWidget *parent,
312                  const QString &title,
313                  const QString& text,
314                  const QString& informativeText)
315 {
316    showMessageBox(QMessageBox::Warning, parent, title, text, informativeText);
317 }
318 
showInfo(QWidget * parent,const QString & title,const QString & text,const QString & informativeText)319 void showInfo(QWidget* parent,
320               const QString& title,
321               const QString& text,
322               const QString& informativeText)
323 {
324    showMessageBox(QMessageBox::Information, parent, title, text, informativeText);
325 }
326 
showFileError(const QString & action,const QString & file,const QString & error)327 void showFileError(const QString& action,
328                    const QString& file,
329                    const QString& error)
330 {
331    QString msg = QString::fromUtf8("Error ") + action +
332                  QString::fromUtf8(" ") + file +
333                  QString::fromUtf8(" - ") + error;
334    showMessageBox(QMessageBox::Critical,
335                   nullptr,
336                   QString::fromUtf8("File Error"),
337                   msg,
338                   QString());
339 }
340 
isFixedWidthFont(const QFont & font)341 bool isFixedWidthFont(const QFont& font)
342 {
343    QFontMetrics metrics(font);
344    int width = metrics.horizontalAdvance(QChar::fromLatin1(' '));
345    char chars[] = {'m', 'i', 'A', '/', '-', '1', 'l', '!', 'x', 'X', 'y', 'Y'};
346    for (char i : chars)
347    {
348       if (metrics.horizontalAdvance(QChar::fromLatin1(i)) != width)
349          return false;
350    }
351    return true;
352 }
353 
getDpi()354 int getDpi()
355 {
356    // TODO: we may need to tweak this to ensure that the DPI
357    // discovered respects the screen a particular instance
358    // that RStudio lives on (e.g. for users with multiple
359    // displays with different DPIs)
360    return (int) qApp->primaryScreen()->logicalDotsPerInch();
361 }
362 
getDpiZoomScaling()363 double getDpiZoomScaling()
364 {
365    // TODO: because Qt is already high-DPI aware and automatically
366    // scales in most scenarios, we no longer need to detect and
367    // apply a custom scale -- but more testing is warranted
368    return 1.0;
369 }
370 
371 #ifdef _WIN32
372 
openFile(const QString & file)373 void openFile(const QString& file)
374 {
375    return openUrl(QUrl::fromLocalFile(file));
376 }
377 
378 // on Win32 open urls using our special urlopener.exe -- this is
379 // so that the shell exec is made out from under our windows "job"
openUrl(const QUrl & url)380 void openUrl(const QUrl& url)
381 {
382    // we allow default handling for  mailto and file schemes because qt
383    // does custom handling for them and they aren't affected by the chrome
384    //job object issue noted above
385    if (url.scheme() == QString::fromUtf8("mailto") ||
386        url.scheme() == QString::fromUtf8("file"))
387    {
388       QDesktopServices::openUrl(url);
389    }
390    else
391    {
392       core::system::ProcessOptions options;
393       options.breakawayFromJob = true;
394       options.detachProcess = true;
395 
396       std::vector<std::string> args;
397       args.push_back(url.toString().toStdString());
398 
399       core::system::ProcessResult result;
400       Error error = core::system::runProgram(
401             desktop::options().urlopenerPath().getAbsolutePath(),
402             args,
403             "",
404             options,
405             &result);
406 
407       if (error)
408          LOG_ERROR(error);
409       else if (result.exitStatus != EXIT_SUCCESS)
410          LOG_ERROR_MESSAGE(result.stdErr);
411    }
412 }
413 
414 // Qt 4.8.3 on Win7 (32-bit) has problems with opening the ~ directory
415 // (it attempts to navigate to the "Documents library" and then hangs)
416 // So we use the Qt file dialog implementations when we are running
417 // on Win32
standardFileDialogOptions()418 QFileDialog::Options standardFileDialogOptions()
419 {
420    return 0;
421 }
422 
423 #else
424 
openFile(const QString & file)425 void openFile(const QString& file)
426 {
427    QDesktopServices::openUrl(QUrl::fromLocalFile(file));
428 }
429 
openUrl(const QUrl & url)430 void openUrl(const QUrl& url)
431 {
432    QDesktopServices::openUrl(url);
433 }
434 
standardFileDialogOptions()435 QFileDialog::Options standardFileDialogOptions()
436 {
437    return nullptr;
438 }
439 
440 #endif
441 
userHomePath()442 FilePath userHomePath()
443 {
444    return core::system::userHomePath("R_USER|HOME");
445 }
446 
createAliasedPath(const QString & path)447 QString createAliasedPath(const QString& path)
448 {
449    std::string aliased = FilePath::createAliasedPath(
450          FilePath(path.toUtf8().constData()), desktop::userHomePath());
451    return QString::fromUtf8(aliased.c_str());
452 }
453 
resolveAliasedPath(const QString & path)454 QString resolveAliasedPath(const QString& path)
455 {
456    FilePath resolved(FilePath::resolveAliasedPath(path.toUtf8().constData(),
457                                                   userHomePath()));
458    return QString::fromUtf8(resolved.getAbsolutePath().c_str());
459 }
460 
461 } // namespace desktop
462 } // namespace rstudio
463