1 /*
2 * DesktopOptions.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 "DesktopOptions.hpp"
17
18 #include <QtGui>
19 #include <QApplication>
20 #include <QDesktopWidget>
21
22 #include <core/Random.hpp>
23 #include <core/system/System.hpp>
24 #include <core/system/Environment.hpp>
25
26 #include "DesktopInfo.hpp"
27 #include "DesktopUtils.hpp"
28
29 using namespace rstudio::core;
30
31 namespace rstudio {
32 namespace desktop {
33
34 #ifdef _WIN32
35 // Defined in DesktopRVersion.cpp
36 QString binDirToHomeDir(QString binDir);
37 #endif
38
39 #define kMainWindowGeometry (QStringLiteral("mainwindow/geometry"))
40 #define kFixedWidthFont (QStringLiteral("font.fixedWidth"))
41 #define kProportionalFont (QStringLiteral("font.proportional"))
42
43 QString scratchPath;
44
options()45 Options& options()
46 {
47 static Options singleton;
48 return singleton;
49 }
50
Options()51 Options::Options() :
52 settings_(FORMAT,
53 QSettings::UserScope,
54 QString::fromUtf8("RStudio"),
55 QString::fromUtf8("desktop")),
56 runDiagnostics_(false)
57 {
58 #ifndef _WIN32
59 // ensure that the options file is only readable by this user
60 FilePath optionsFile(settings_.fileName().toStdString());
61 if (!optionsFile.exists())
62 {
63 // file doesn't yet exist - QT can lazily write to the settings file
64 // create an empty file so we can set its permissions before it's created by QT
65 std::shared_ptr<std::ostream> pOfs;
66 Error error = optionsFile.openForWrite(pOfs, false);
67 if (error)
68 LOG_ERROR(error);
69 }
70
71 Error error = optionsFile.changeFileMode(FileMode::USER_READ_WRITE);
72 if (error)
73 LOG_ERROR(error);
74 #endif
75 }
76
initFromCommandLine(const QStringList & arguments)77 void Options::initFromCommandLine(const QStringList& arguments)
78 {
79 for (int i=1; i<arguments.size(); i++)
80 {
81 const QString &arg = arguments.at(i);
82 if (arg == QString::fromUtf8(kRunDiagnosticsOption))
83 runDiagnostics_ = true;
84 }
85
86 // synchronize zoom level with desktop frame
87 desktopInfo().setZoomLevel(zoomLevel());
88 }
89
restoreMainWindowBounds(QMainWindow * win)90 void Options::restoreMainWindowBounds(QMainWindow* win)
91 {
92 // NOTE: on macOS, if the display configuration has changed, the attempt to
93 // restore window geometry can fail and the use can be left with a tiny
94 // RStudio window.
95 //
96 // to avoid this, we always first attempt to resize to a 'good' size, and
97 // then restore a saved window geometry (which may just silently fail if the
98 // display configuration has indeed changed)
99 //
100 // https://github.com/rstudio/rstudio/issues/3498
101 // https://github.com/rstudio/rstudio/issues/3159
102 //
103
104 QSize size = QSize(1200, 900).boundedTo(
105 QApplication::primaryScreen()->availableGeometry().size());
106 if (size.width() > 800 && size.height() > 500)
107 {
108 // Only use default size if it seems sane; otherwise let Qt set it
109 win->resize(size);
110 }
111
112 if (settings_.contains(kMainWindowGeometry))
113 {
114 // try to restore the geometry
115 win->restoreGeometry(settings_.value(kMainWindowGeometry).toByteArray());
116
117 // double-check that we haven't accidentally restored a geometry that
118 // places the Window off-screen (can happen if the screen configuration
119 // changes between the time geometry was saved and loaded)
120 QRect desktopRect = QApplication::primaryScreen()->availableGeometry();
121 QRect winRect = win->geometry();
122
123 // shrink the window rectangle a bit just to capture cases like RStudio
124 // too close to edge of window and hardly showing at all
125 QRect checkRect(
126 winRect.topLeft() + QPoint(5, 5),
127 winRect.bottomRight() - QPoint(5, 5));
128
129 // check for intersection
130 if (!desktopRect.intersects(checkRect))
131 {
132 // restore size and center the window
133 win->resize(size);
134 win->move(
135 desktopRect.width() / 2 - size.width() / 2,
136 desktopRect.height() / 2 - size.height() / 2);
137 }
138 }
139
140 // ensure a minimum width, height for the window on restore
141 win->resize(
142 std::max(300, win->width()),
143 std::max(200, win->height()));
144
145 }
146
saveMainWindowBounds(QMainWindow * win)147 void Options::saveMainWindowBounds(QMainWindow* win)
148 {
149 settings_.setValue(kMainWindowGeometry, win->saveGeometry());
150 }
151
portNumber() const152 QString Options::portNumber() const
153 {
154 // lookup / generate on demand
155 if (portNumber_.length() == 0)
156 {
157 // Use a random-ish port number to avoid collisions between different
158 // instances of rdesktop-launched rsessions
159 int base = std::abs(core::random::uniformRandomInteger<int>());
160 portNumber_ = QString::number((base % 40000) + 8080);
161
162 // recalculate the local peer and set RS_LOCAL_PEER so that
163 // rsession and it's children can use it
164 #ifdef _WIN32
165 QString localPeer = QString::fromUtf8("\\\\.\\pipe\\") +
166 portNumber_ + QString::fromUtf8("-rsession");
167 localPeer_ = localPeer.toUtf8().constData();
168 core::system::setenv("RS_LOCAL_PEER", localPeer_);
169 #endif
170 }
171
172 return portNumber_;
173 }
174
newPortNumber()175 QString Options::newPortNumber()
176 {
177 portNumber_.clear();
178 return portNumber();
179 }
180
localPeer() const181 std::string Options::localPeer() const
182 {
183 return localPeer_;
184 }
185
desktopRenderingEngine() const186 QString Options::desktopRenderingEngine() const
187 {
188 return settings_.value(QStringLiteral("desktop.renderingEngine")).toString();
189 }
190
setDesktopRenderingEngine(QString engine)191 void Options::setDesktopRenderingEngine(QString engine)
192 {
193 settings_.setValue(QStringLiteral("desktop.renderingEngine"), engine);
194 }
195
196 namespace {
197
findFirstMatchingFont(const QStringList & fonts,QString defaultFont,bool fixedWidthOnly)198 QString findFirstMatchingFont(const QStringList& fonts,
199 QString defaultFont,
200 bool fixedWidthOnly)
201 {
202 for (int i = 0; i < fonts.size(); i++)
203 {
204 QFont font(fonts.at(i));
205 if (font.exactMatch())
206 if (!fixedWidthOnly || isFixedWidthFont(QFont(fonts.at(i))))
207 return fonts.at(i);
208 }
209 return defaultFont;
210 }
211
212 } // anonymous namespace
213
setFont(QString key,QString font)214 void Options::setFont(QString key, QString font)
215 {
216 if (font.isEmpty())
217 settings_.remove(key);
218 else
219 settings_.setValue(key, font);
220 }
221
setProportionalFont(QString font)222 void Options::setProportionalFont(QString font)
223 {
224 setFont(kProportionalFont, font);
225 }
226
proportionalFont() const227 QString Options::proportionalFont() const
228 {
229 static QString detectedFont;
230
231 QString font =
232 settings_.value(kProportionalFont).toString();
233 if (!font.isEmpty())
234 {
235 return font;
236 }
237
238 if (!detectedFont.isEmpty())
239 return detectedFont;
240
241 QStringList fontList;
242 #if defined(_WIN32)
243 fontList <<
244 QString::fromUtf8("Segoe UI") << QString::fromUtf8("Verdana") << // Windows
245 QString::fromUtf8("Lucida Sans") << QString::fromUtf8("DejaVu Sans") << // Linux
246 QString::fromUtf8("Lucida Grande") << // Mac
247 QString::fromUtf8("Helvetica");
248 #elif defined(__APPLE__)
249 fontList <<
250 QString::fromUtf8("Lucida Grande") << // Mac
251 QString::fromUtf8("Lucida Sans") << QString::fromUtf8("DejaVu Sans") << // Linux
252 QString::fromUtf8("Segoe UI") << QString::fromUtf8("Verdana") << // Windows
253 QString::fromUtf8("Helvetica");
254 #else
255 fontList <<
256 QString::fromUtf8("Lucida Sans") << QString::fromUtf8("DejaVu Sans") << // Linux
257 QString::fromUtf8("Lucida Grande") << // Mac
258 QString::fromUtf8("Segoe UI") << QString::fromUtf8("Verdana") << // Windows
259 QString::fromUtf8("Helvetica");
260 #endif
261
262 QString sansSerif = QStringLiteral("sans-serif");
263 QString selectedFont = findFirstMatchingFont(fontList, sansSerif, false);
264
265 // NOTE: browsers will refuse to render a default font if the name is in
266 // quotes; e.g. "sans-serif" is a signal to look for a font called sans-serif
267 // rather than use the default sans-serif font!
268 if (selectedFont == sansSerif)
269 return sansSerif;
270 else
271 return QStringLiteral("\"%1\"").arg(selectedFont);
272 }
273
setFixedWidthFont(QString font)274 void Options::setFixedWidthFont(QString font)
275 {
276 setFont(kFixedWidthFont, font);
277 }
278
fixedWidthFont() const279 QString Options::fixedWidthFont() const
280 {
281 static QString detectedFont;
282
283 QString font =
284 settings_.value(kFixedWidthFont).toString();
285 if (!font.isEmpty())
286 {
287 return QString::fromUtf8("\"") + font + QString::fromUtf8("\"");
288 }
289
290 if (!detectedFont.isEmpty())
291 return detectedFont;
292
293 QStringList fontList;
294 fontList <<
295 #if defined(Q_OS_MAC)
296 QString::fromUtf8("Monaco")
297 #elif defined (Q_OS_LINUX)
298 QString::fromUtf8("Ubuntu Mono") << QString::fromUtf8("Droid Sans Mono") << QString::fromUtf8("DejaVu Sans Mono") << QString::fromUtf8("Monospace")
299 #else
300 QString::fromUtf8("Lucida Console") << QString::fromUtf8("Consolas") // Windows;
301 #endif
302 ;
303
304 // NOTE: browsers will refuse to render a default font if the name is in
305 // quotes; e.g. "monospace" is a signal to look for a font called monospace
306 // rather than use the default monospace font!
307 QString monospace = QStringLiteral("monospace");
308 QString matchingFont = findFirstMatchingFont(fontList, monospace, true);
309 if (matchingFont == monospace)
310 return monospace;
311 else
312 return QStringLiteral("\"%1\"").arg(matchingFont);
313 }
314
315
zoomLevel() const316 double Options::zoomLevel() const
317 {
318 QVariant zoom = settings_.value(QString::fromUtf8("view.zoomLevel"), 1.0);
319 return zoom.toDouble();
320 }
321
setZoomLevel(double zoomLevel)322 void Options::setZoomLevel(double zoomLevel)
323 {
324 desktopInfo().setZoomLevel(zoomLevel);
325 settings_.setValue(QString::fromUtf8("view.zoomLevel"), zoomLevel);
326 }
327
enableAccessibility() const328 bool Options::enableAccessibility() const
329 {
330 QVariant accessibility = settings_.value(QString::fromUtf8("view.accessibility"), false);
331 return accessibility.toBool();
332 }
333
setEnableAccessibility(bool enable)334 void Options::setEnableAccessibility(bool enable)
335 {
336 settings_.setValue(QString::fromUtf8("view.accessibility"), enable);
337 }
338
clipboardMonitoring() const339 bool Options::clipboardMonitoring() const
340 {
341 QVariant monitoring = settings_.value(QString::fromUtf8("clipboard.monitoring"), true);
342 return monitoring.toBool();
343 }
344
setClipboardMonitoring(bool monitoring)345 void Options::setClipboardMonitoring(bool monitoring)
346 {
347 settings_.setValue(QString::fromUtf8("clipboard.monitoring"), monitoring);
348 }
349
ignoreGpuExclusionList() const350 bool Options::ignoreGpuExclusionList() const
351 {
352 QVariant ignore = settings_.value(QStringLiteral("general.ignoreGpuExclusionList"), false);
353 return ignore.toBool();
354 }
355
setIgnoreGpuExclusionList(bool ignore)356 void Options::setIgnoreGpuExclusionList(bool ignore)
357 {
358 settings_.setValue(QStringLiteral("general.ignoreGpuExclusionList"), ignore);
359 }
360
disableGpuDriverBugWorkarounds() const361 bool Options::disableGpuDriverBugWorkarounds() const
362 {
363 QVariant disable = settings_.value(QStringLiteral("general.disableGpuDriverBugWorkarounds"), false);
364 return disable.toBool();
365 }
366
setDisableGpuDriverBugWorkarounds(bool disable)367 void Options::setDisableGpuDriverBugWorkarounds(bool disable)
368 {
369 settings_.setValue(QStringLiteral("general.disableGpuDriverBugWorkarounds"), disable);
370 }
371
useFontConfigDatabase() const372 bool Options::useFontConfigDatabase() const
373 {
374 QVariant use = settings_.value(QStringLiteral("general.useFontConfigDatabase"), true);
375 return use.toBool();
376 }
377
setUseFontConfigDatabase(bool use)378 void Options::setUseFontConfigDatabase(bool use)
379 {
380 settings_.setValue(QStringLiteral("general.useFontConfigDatabase"), use);
381 }
382
383 #ifdef _WIN32
rBinDir() const384 QString Options::rBinDir() const
385 {
386 // HACK: If RBinDir doesn't appear at all, that means the user has never
387 // specified a preference for R64 vs. 32-bit R. In this situation we should
388 // accept either. We'll distinguish between this case (where preferR64
389 // should be ignored) and the other case by using null for this case and
390 // empty string for the other.
391 if (!settings_.contains(QString::fromUtf8("RBinDir")))
392 return QString();
393
394 QString value = settings_.value(QString::fromUtf8("RBinDir")).toString();
395 return value.isNull() ? QString() : value;
396 }
397
setRBinDir(QString path)398 void Options::setRBinDir(QString path)
399 {
400 settings_.setValue(QString::fromUtf8("RBinDir"), path);
401 }
402
preferR64() const403 bool Options::preferR64() const
404 {
405 if (!core::system::isWin64())
406 return false;
407
408 if (!settings_.contains(QString::fromUtf8("PreferR64")))
409 return true;
410 return settings_.value(QString::fromUtf8("PreferR64")).toBool();
411 }
412
setPreferR64(bool preferR64)413 void Options::setPreferR64(bool preferR64)
414 {
415 settings_.setValue(QString::fromUtf8("PreferR64"), preferR64);
416 }
417 #endif
418
scriptsPath() const419 FilePath Options::scriptsPath() const
420 {
421 return scriptsPath_;
422 }
423
setScriptsPath(const FilePath & scriptsPath)424 void Options::setScriptsPath(const FilePath& scriptsPath)
425 {
426 scriptsPath_ = scriptsPath;
427 }
428
executablePath() const429 FilePath Options::executablePath() const
430 {
431 if (executablePath_.isEmpty())
432 {
433 Error error = core::system::executablePath(QApplication::arguments().at(0).toUtf8(),
434 &executablePath_);
435 if (error)
436 LOG_ERROR(error);
437 }
438 return executablePath_;
439 }
440
supportingFilePath() const441 FilePath Options::supportingFilePath() const
442 {
443 if (supportingFilePath_.isEmpty())
444 {
445 // default to install path
446 core::system::installPath("..",
447 QApplication::arguments().at(0).toUtf8(),
448 &supportingFilePath_);
449
450 // adapt for OSX resource bundles
451 #ifdef __APPLE__
452 if (supportingFilePath_.completePath("Info.plist").exists())
453 supportingFilePath_ = supportingFilePath_.completePath("Resources");
454 #endif
455 }
456 return supportingFilePath_;
457 }
458
resourcesPath() const459 FilePath Options::resourcesPath() const
460 {
461 if (resourcesPath_.isEmpty())
462 {
463 #ifdef RSTUDIO_PACKAGE_BUILD
464 // release configuration: the 'resources' folder is
465 // part of the supporting files folder
466 resourcesPath_ = supportingFilePath().completePath("resources");
467 #else
468 // developer configuration: the 'resources' folder is
469 // a sibling of the RStudio executable
470 resourcesPath_ = scriptsPath().completePath("resources");
471 #endif
472 }
473
474 return resourcesPath_;
475 }
476
wwwDocsPath() const477 FilePath Options::wwwDocsPath() const
478 {
479 FilePath supportingFilePath = desktop::options().supportingFilePath();
480 FilePath wwwDocsPath = supportingFilePath.completePath("www/docs");
481 if (!wwwDocsPath.exists())
482 wwwDocsPath = supportingFilePath.completePath("../gwt/www/docs");
483 #ifdef __APPLE__
484 if (!wwwDocsPath.exists())
485 wwwDocsPath = supportingFilePath.completePath("../../../../../gwt/www/docs");
486 #endif
487 return wwwDocsPath;
488 }
489
490 #ifdef _WIN32
491
urlopenerPath() const492 FilePath Options::urlopenerPath() const
493 {
494 FilePath parentDir = scriptsPath();
495
496 // detect dev configuration
497 if (parentDir.getFilename() == "desktop")
498 parentDir = parentDir.completePath("urlopener");
499
500 return parentDir.completePath("urlopener.exe");
501 }
502
rsinversePath() const503 FilePath Options::rsinversePath() const
504 {
505 FilePath parentDir = scriptsPath();
506
507 // detect dev configuration
508 if (parentDir.getFilename() == "desktop")
509 parentDir = parentDir.completePath("synctex/rsinverse");
510
511 return parentDir.completePath("rsinverse.exe");
512 }
513
514 #endif
515
ignoredUpdateVersions() const516 QStringList Options::ignoredUpdateVersions() const
517 {
518 return settings_.value(QString::fromUtf8("ignoredUpdateVersions"), QStringList()).toStringList();
519 }
520
setIgnoredUpdateVersions(const QStringList & ignoredVersions)521 void Options::setIgnoredUpdateVersions(const QStringList& ignoredVersions)
522 {
523 settings_.setValue(QString::fromUtf8("ignoredUpdateVersions"), ignoredVersions);
524 }
525
scratchTempDir(core::FilePath defaultPath)526 core::FilePath Options::scratchTempDir(core::FilePath defaultPath)
527 {
528 core::FilePath dir(scratchPath.toUtf8().constData());
529
530 if (!dir.isEmpty() && dir.exists())
531 {
532 dir = dir.completeChildPath("tmp");
533 core::Error error = dir.ensureDirectory();
534 if (!error)
535 return dir;
536 }
537 return defaultPath;
538 }
539
cleanUpScratchTempDir()540 void Options::cleanUpScratchTempDir()
541 {
542 core::FilePath temp = scratchTempDir(core::FilePath());
543 if (!temp.isEmpty())
544 temp.removeIfExists();
545 }
546
lastRemoteSessionUrl(const QString & serverUrl)547 QString Options::lastRemoteSessionUrl(const QString& serverUrl)
548 {
549 settings_.beginGroup(QString::fromUtf8("remote-sessions-list"));
550 QString sessionUrl = settings_.value(serverUrl).toString();
551 settings_.endGroup();
552 return sessionUrl;
553 }
554
setLastRemoteSessionUrl(const QString & serverUrl,const QString & sessionUrl)555 void Options::setLastRemoteSessionUrl(const QString& serverUrl, const QString& sessionUrl)
556 {
557 settings_.beginGroup(QString::fromUtf8("remote-sessions-list"));
558 settings_.setValue(serverUrl, sessionUrl);
559 settings_.endGroup();
560 }
561
cookiesFromList(const QStringList & cookieStrs) const562 QList<QNetworkCookie> Options::cookiesFromList(const QStringList& cookieStrs) const
563 {
564 QList<QNetworkCookie> cookies;
565
566 for (const QString& cookieStr : cookieStrs)
567 {
568 QByteArray cookieArr = QByteArray::fromStdString(cookieStr.toStdString());
569 QList<QNetworkCookie> innerCookies = QNetworkCookie::parseCookies(cookieArr);
570 for (const QNetworkCookie& cookie : innerCookies)
571 {
572 cookies.push_back(cookie);
573 }
574 }
575
576 return cookies;
577 }
578
authCookies() const579 QList<QNetworkCookie> Options::authCookies() const
580 {
581 QStringList cookieStrs = settings_.value(QString::fromUtf8("cookies"), QStringList()).toStringList();
582 return cookiesFromList(cookieStrs);
583 }
584
tempAuthCookies() const585 QList<QNetworkCookie> Options::tempAuthCookies() const
586 {
587 QStringList cookieStrs = settings_.value(QString::fromUtf8("temp-auth-cookies"), QStringList()).toStringList();
588 return cookiesFromList(cookieStrs);
589 }
590
cookiesToList(const QList<QNetworkCookie> & cookies) const591 QStringList Options::cookiesToList(const QList<QNetworkCookie>& cookies) const
592 {
593 QStringList cookieStrs;
594 for (const QNetworkCookie& cookie : cookies)
595 {
596 cookieStrs.push_back(QString::fromStdString(cookie.toRawForm().toStdString()));
597 }
598
599 return cookieStrs;
600 }
601
setAuthCookies(const QList<QNetworkCookie> & cookies)602 void Options::setAuthCookies(const QList<QNetworkCookie>& cookies)
603 {
604 settings_.setValue(QString::fromUtf8("cookies"), cookiesToList(cookies));
605 }
606
setTempAuthCookies(const QList<QNetworkCookie> & cookies)607 void Options::setTempAuthCookies(const QList<QNetworkCookie>& cookies)
608 {
609 settings_.setValue(QString::fromUtf8("temp-auth-cookies"), cookiesToList(cookies));
610 }
611
612 } // namespace desktop
613 } // namespace rstudio
614
615