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