1 /*
2 * DesktopGwtCallback.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 "DesktopGwtCallback.hpp"
17 #include "DesktopGwtWindow.hpp"
18
19 #ifdef _WIN32
20 #include <shlobj.h>
21 #endif
22
23 #include <QFileDialog>
24 #include <QMessageBox>
25 #include <QApplication>
26 #include <QAbstractButton>
27
28 #include <QtPrintSupport/QPrinter>
29 #include <QtPrintSupport/QPrintPreviewDialog>
30
31 #include <shared_core/FilePath.hpp>
32 #include <core/FileUtils.hpp>
33 #include <core/DateTime.hpp>
34 #include <shared_core/SafeConvert.hpp>
35 #include <core/system/System.hpp>
36 #include <core/system/Environment.hpp>
37 #include <core/r_util/RUserData.hpp>
38
39 #include "DesktopActivationOverlay.hpp"
40 #include "DesktopSessionServersOverlay.hpp"
41 #include "DesktopOptions.hpp"
42 #include "DesktopUtils.hpp"
43 #include "DesktopBrowserWindow.hpp"
44 #include "DesktopWindowTracker.hpp"
45 #include "DesktopInputDialog.hpp"
46 #include "DesktopSecondaryWindow.hpp"
47 #include "DesktopRVersion.hpp"
48 #include "DesktopMainWindow.hpp"
49 #include "DesktopSynctex.hpp"
50 #include "DesktopJobLauncherOverlay.hpp"
51 #include "RemoteDesktopSessionLauncherOverlay.hpp"
52
53 #ifdef __APPLE__
54 #include <Carbon/Carbon.h>
55 #endif
56
57 using namespace rstudio::core;
58
59 namespace rstudio {
60 namespace desktop {
61
62 namespace {
63 WindowTracker s_windowTracker;
64
65 #ifdef Q_OS_LINUX
66 QString s_globalMouseSelection;
67 bool s_ignoreNextClipboardSelectionChange;
68 #endif
69
70 } // end anonymous namespace
71
72 extern QString scratchPath;
73
GwtCallback(MainWindow * pMainWindow,GwtWindow * pOwner,bool isRemoteDesktop)74 GwtCallback::GwtCallback(MainWindow* pMainWindow, GwtWindow* pOwner, bool isRemoteDesktop)
75 : pMainWindow_(pMainWindow),
76 pOwner_(pOwner),
77 isRemoteDesktop_(isRemoteDesktop),
78 pSynctex_(nullptr),
79 pendingQuit_(PendingQuitNone)
80 {
81 initialize();
82
83 #ifdef Q_OS_LINUX
84 // assume light theme on startup (theme will be dynamically updated
85 // based on editor theme chosen by user)
86 syncToEditorTheme(false);
87
88 // listen for clipboard selection change events (X11 only); allow override in options or
89 // via environment variable. clipboard monitoring enables us to support middle-click paste on
90 // Linux, but it causes problems on some systems.
91 if (desktop::options().clipboardMonitoring() &&
92 core::system::getenv("RSTUDIO_NO_CLIPBOARD_MONITORING").empty())
93 {
94 QClipboard* clipboard = QApplication::clipboard();
95 if (clipboard->supportsSelection())
96 {
97 QObject::connect(
98 clipboard, &QClipboard::selectionChanged,
99 this, &GwtCallback::onClipboardSelectionChanged,
100 Qt::DirectConnection);
101
102 // initialize the global selection
103 const QMimeData* mimeData = clipboard->mimeData(QClipboard::Selection);
104 if (mimeData->hasText())
105 s_globalMouseSelection = mimeData->text();
106 }
107 }
108 #endif
109 }
110
111 #ifndef Q_OS_MAC
initialize()112 void GwtCallback::initialize()
113 {
114 }
115 #endif
116
synctex()117 Synctex& GwtCallback::synctex()
118 {
119 if (pSynctex_ == nullptr)
120 pSynctex_ = Synctex::create(pMainWindow_);
121
122 return *pSynctex_;
123 }
124
printText(QString text)125 void GwtCallback::printText(QString text)
126 {
127 QPrinter printer;
128 printer.setPageMargins(1.0, 1.0, 1.0, 1.0, QPrinter::Inch);
129
130 QPrintPreviewDialog dialog(&printer);
131 dialog.setWindowModality(Qt::WindowModal);
132
133 // QPrintPreviewDialog will call us back to paint the contents
134 connect(&dialog, SIGNAL(paintRequested(QPrinter*)),
135 this, SLOT(paintPrintText(QPrinter*)));
136 connect(&dialog, SIGNAL(finished(int)),
137 this, SLOT(printFinished(int)));
138
139 // cache the requested print text to replay for the print preview
140 printText_ = text;
141
142 dialog.exec();
143 }
144
paintPrintText(QPrinter * printer)145 void GwtCallback::paintPrintText(QPrinter* printer)
146 {
147 QPainter painter;
148 painter.begin(printer);
149
150 // look up the system fixed font
151 QFont fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
152 fixedFont.setPointSize(10);
153 painter.setFont(fixedFont);
154
155 // flags used for drawing text
156 int textFlags = Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap;
157
158 // y coordinate for new line of text to be drawn
159 int y = 0;
160
161 // compute page boundaries (used as bounds when painting on page)
162 int pageWidth = printer->pageRect().width();
163 int pageHeight = printer->pageRect().height();
164 QRect pageRect(0, 0, pageWidth, pageHeight);
165
166 // start drawing line-by-line
167 QStringList lines = printText_.split(QStringLiteral("\n"));
168 for (auto line : lines)
169 {
170 // figure out what rectangle is needed to fit
171 // (use dummy text for blank lines when computing bounds)
172 QRect paintRect = painter.boundingRect(
173 pageRect,
174 textFlags,
175 line.isEmpty() ? QStringLiteral("Z") : line);
176
177 // move the bounding rectangle down
178 paintRect.moveTo(0, y);
179
180 // check for page overflow and start a new page if so
181 if (paintRect.bottom() > pageHeight)
182 {
183 printer->newPage();
184 paintRect.moveTo(0, 0);
185 y = 0;
186 }
187
188 // draw the text
189 painter.drawText(paintRect, textFlags, line);
190
191 // update y
192 y += paintRect.height();
193 }
194
195 painter.end();
196 }
197
printFinished(int result)198 void GwtCallback::printFinished(int result)
199 {
200 // signal emitted by QPrintPreviewDialog when the dialog is dismissed
201 printText_.clear();
202 }
203
browseUrl(QString url)204 void GwtCallback::browseUrl(QString url)
205 {
206 QUrl qurl = QUrl::fromEncoded(url.toUtf8());
207 desktop::openUrl(qurl);
208 }
209
210 #ifndef Q_OS_MAC
211
getOpenFileName(const QString & caption,const QString & label,const QString & dir,const QString & filter,bool canChooseDirectories,bool focusOwner)212 QString GwtCallback::getOpenFileName(const QString& caption,
213 const QString& label,
214 const QString& dir,
215 const QString& filter,
216 bool canChooseDirectories,
217 bool focusOwner)
218 {
219 QString resolvedDir = desktop::resolveAliasedPath(dir);
220
221 QWidget* owner = focusOwner ? pOwner_->asWidget() : qApp->focusWidget();
222 QFileDialog dialog(
223 owner,
224 caption,
225 resolvedDir,
226 filter);
227
228 QFileDialog::FileMode mode = (canChooseDirectories)
229 ? QFileDialog::AnyFile
230 : QFileDialog::ExistingFile;
231
232 dialog.setFileMode(mode);
233 dialog.setLabelText(QFileDialog::Accept, label);
234 dialog.setWindowModality(Qt::WindowModal);
235
236 // don't resolve links on non-Windows platforms
237 // https://github.com/rstudio/rstudio/issues/2476
238 // https://github.com/rstudio/rstudio/issues/7327
239 #ifndef _WIN32
240 dialog.setOption(QFileDialog::DontResolveSymlinks, true);
241 #endif
242
243 QString result;
244 if (dialog.exec() == QDialog::Accepted)
245 result = dialog.selectedFiles().value(0);
246
247 desktop::raiseAndActivateWindow(owner);
248 return createAliasedPath(result);
249 }
250
251 namespace {
252
getSaveFileNameImpl(QWidget * pParent,const QString & caption,const QString & label,const QString & dir,QFileDialog::Options options)253 QString getSaveFileNameImpl(QWidget* pParent,
254 const QString& caption,
255 const QString& label,
256 const QString& dir,
257 QFileDialog::Options options)
258 {
259 // initialize dialog
260 QFileDialog dialog(pParent, caption, dir);
261 dialog.setOptions(options);
262 dialog.setLabelText(QFileDialog::Accept, label);
263 dialog.setAcceptMode(QFileDialog::AcceptSave);
264 dialog.setWindowModality(Qt::WindowModal);
265
266 // execute dialog and check for success
267 if (dialog.exec() == QDialog::Accepted)
268 return dialog.selectedFiles().value(0);
269
270 return QString();
271 }
272
273 } // end anonymous namespace
274
getSaveFileName(const QString & caption,const QString & label,const QString & dir,const QString & defaultExtension,bool forceDefaultExtension,bool focusOwner)275 QString GwtCallback::getSaveFileName(const QString& caption,
276 const QString& label,
277 const QString& dir,
278 const QString& defaultExtension,
279 bool forceDefaultExtension,
280 bool focusOwner)
281 {
282 QString resolvedDir = desktop::resolveAliasedPath(dir);
283
284 while (true)
285 {
286 QWidget* owner = focusOwner ? pOwner_->asWidget() : qApp->focusWidget();
287 QString result = getSaveFileNameImpl(
288 owner,
289 caption,
290 label,
291 resolvedDir,
292 standardFileDialogOptions());
293
294 desktop::raiseAndActivateWindow(owner);
295 if (result.isEmpty())
296 return result;
297
298 if (!defaultExtension.isEmpty())
299 {
300 FilePath fp(result.toUtf8().constData());
301 if (fp.getExtension().empty() ||
302 (forceDefaultExtension &&
303 (fp.getExtension() != defaultExtension.toStdString())))
304 {
305 result += defaultExtension;
306 FilePath newExtPath(result.toUtf8().constData());
307 if (newExtPath.exists())
308 {
309 std::string message = "\"" + newExtPath.getFilename() + "\" already "
310 "exists. Do you want to overwrite it?";
311 if (QMessageBox::Cancel == QMessageBox::warning(
312 pOwner_->asWidget(),
313 QString::fromUtf8("Save File"),
314 QString::fromUtf8(message.c_str()),
315 QMessageBox::Ok | QMessageBox::Cancel,
316 QMessageBox::Ok))
317 {
318 resolvedDir = result;
319 continue;
320 }
321 }
322 }
323 }
324
325 return createAliasedPath(result);
326 }
327 }
328
getExistingDirectory(const QString & caption,const QString & label,const QString & dir,bool focusOwner)329 QString GwtCallback::getExistingDirectory(const QString& caption,
330 const QString& label,
331 const QString& dir,
332 bool focusOwner)
333 {
334 return desktop::browseDirectory(caption,
335 label,
336 dir,
337 focusOwner ? pOwner_->asWidget() : qApp->focusWidget());
338 }
339
340 #endif
341
onClipboardSelectionChanged()342 void GwtCallback::onClipboardSelectionChanged()
343 {
344 #ifdef Q_OS_LINUX
345 // for some reason, Qt can get stalled querying the clipboard
346 // while a modal is active, so disable any such behavior here
347 if (QApplication::activeModalWidget() != nullptr)
348 return;
349
350 // check to see if this was a clipboard change synthesized by us;
351 // if so, discard it
352 if (s_ignoreNextClipboardSelectionChange)
353 {
354 s_ignoreNextClipboardSelectionChange = false;
355 return;
356 }
357
358 // we only care about text-related changes, so bail if we didn't
359 // get text in the selection clipboard
360 QClipboard* clipboard = QApplication::clipboard();
361 const QMimeData* mimeData = clipboard->mimeData(QClipboard::Selection);
362 if (mimeData->hasText())
363 {
364 // extract clipboard selection text
365 QString text = mimeData->text();
366
367 // when one clicks on an Ace instance, a hidden length-one selection
368 // will sneak in here -- explicitly screen those out
369 if (text == QStringLiteral("\x01"))
370 {
371 // ignore the next clipboard change (just in case modifying it below triggers
372 // this slot recursively)
373 s_ignoreNextClipboardSelectionChange = true;
374
375 // restore the old global selection
376 clipboard->setText(s_globalMouseSelection, QClipboard::Selection);
377 }
378 else
379 {
380 // otherwise, update our tracked global selection
381 s_globalMouseSelection = text;
382 }
383 }
384 #endif
385 }
386
doAction(const QKeySequence & keys)387 void GwtCallback::doAction(const QKeySequence& keys)
388 {
389 int keyCode = keys[0];
390 auto modifiers = static_cast<Qt::KeyboardModifier>(keyCode & Qt::KeyboardModifierMask);
391 keyCode &= ~Qt::KeyboardModifierMask;
392
393 QKeyEvent* keyEvent = new QKeyEvent(QKeyEvent::KeyPress, keyCode, modifiers);
394 pOwner_->postWebViewEvent(keyEvent);
395 }
396
doAction(QKeySequence::StandardKey key)397 void GwtCallback::doAction(QKeySequence::StandardKey key)
398 {
399 QList<QKeySequence> bindings = QKeySequence::keyBindings(key);
400 if (bindings.empty())
401 return;
402
403 doAction(bindings.first());
404 }
405
undo()406 void GwtCallback::undo()
407 {
408 doAction(QKeySequence::Undo);
409 }
410
redo()411 void GwtCallback::redo()
412 {
413 // NOTE: On Windows, the default redo key sequence is 'Ctrl+Y';
414 // however, we bind this to 'yank' and so 'redo' actions executed
415 // from the menu will fail. We instead use 'Ctrl+Shift+Z' which is
416 // supported across all platforms using Qt.
417 static const QKeySequence keys =
418 QKeySequence::fromString(QString::fromUtf8("Ctrl+Shift+Z"));
419
420 doAction(keys);
421 }
422
clipboardCut()423 void GwtCallback::clipboardCut()
424 {
425 doAction(QKeySequence::Cut);
426 }
427
clipboardCopy()428 void GwtCallback::clipboardCopy()
429 {
430 doAction(QKeySequence::Copy);
431 }
432
clipboardPaste()433 void GwtCallback::clipboardPaste()
434 {
435 doAction(QKeySequence::Paste);
436 }
437
setClipboardText(QString text)438 void GwtCallback::setClipboardText(QString text)
439 {
440 QClipboard* pClipboard = QApplication::clipboard();
441 pClipboard->setText(text, QClipboard::Clipboard);
442 }
443
getClipboardText()444 QString GwtCallback::getClipboardText()
445 {
446 QClipboard* pClipboard = QApplication::clipboard();
447 return pClipboard->text(QClipboard::Clipboard);
448 }
449
getClipboardUris()450 QJsonArray GwtCallback::getClipboardUris()
451 {
452 QJsonArray urisJson;
453 QClipboard* pClipboard = QApplication::clipboard();
454 if (pClipboard->mimeData()->hasUrls())
455 {
456 // build buffer of urls
457 auto urls = pClipboard->mimeData()->urls();
458 for (auto url : urls)
459 {
460 // append (converting file-based urls)
461 if (url.scheme() == QString::fromUtf8("file"))
462 urisJson.append(QJsonValue(createAliasedPath(url.toLocalFile())));
463 else
464 urisJson.append(QJsonValue(url.toString()));
465 }
466 }
467 return urisJson;
468 }
469
getClipboardImage()470 QString GwtCallback::getClipboardImage()
471 {
472 QClipboard* pClipboard = QApplication::clipboard();
473 if (pClipboard->mimeData()->hasImage())
474 {
475 QImage image = qvariant_cast<QImage>(pClipboard->mimeData()->imageData());
476 FilePath tempDir = options().scratchTempDir();
477 FilePath imagePath = file_utils::uniqueFilePath(tempDir, "paste-", ".png");
478 QString imageFile = QString::fromStdString(imagePath.getAbsolutePath());
479 if (image.save(imageFile))
480 return imageFile;
481 }
482 return QString();
483 }
484
setGlobalMouseSelection(QString selection)485 void GwtCallback::setGlobalMouseSelection(QString selection)
486 {
487 #ifdef Q_OS_LINUX
488 QClipboard* clipboard = QApplication::clipboard();
489 if (clipboard->supportsSelection())
490 clipboard->setText(selection, QClipboard::Selection);
491 s_globalMouseSelection = selection;
492 #endif
493 }
494
getGlobalMouseSelection()495 QString GwtCallback::getGlobalMouseSelection()
496 {
497 #ifdef Q_OS_LINUX
498 return s_globalMouseSelection;
499 #else
500 return QString();
501 #endif
502 }
503
getCursorPosition()504 QJsonObject GwtCallback::getCursorPosition()
505 {
506 QPoint cursorPosition = QCursor::pos();
507
508 return QJsonObject {
509 { QStringLiteral("x"), cursorPosition.x() },
510 { QStringLiteral("y"), cursorPosition.y() }
511 };
512 }
513
doesWindowExistAtCursorPosition()514 bool GwtCallback::doesWindowExistAtCursorPosition()
515 {
516 return qApp->topLevelAt(QCursor::pos()) != nullptr;
517 }
518
proportionalFont()519 QString GwtCallback::proportionalFont()
520 {
521 return options().proportionalFont();
522 }
523
fixedWidthFont()524 QString GwtCallback::fixedWidthFont()
525 {
526 return options().fixedWidthFont();
527 }
528
onWorkbenchInitialized(QString scratchPath)529 void GwtCallback::onWorkbenchInitialized(QString scratchPath)
530 {
531 workbenchInitialized();
532 desktop::scratchPath = scratchPath;
533 }
534
showFolder(QString path)535 void GwtCallback::showFolder(QString path)
536 {
537 if (path.isNull() || path.isEmpty())
538 return;
539
540 path = desktop::resolveAliasedPath(path);
541
542 QDir dir(path);
543 if (dir.exists())
544 {
545 desktop::openUrl(QUrl::fromLocalFile(dir.absolutePath()));
546 }
547 }
548
showFile(QString path)549 void GwtCallback::showFile(QString path)
550 {
551 if (path.isNull() || path.isEmpty())
552 return;
553
554 path = desktop::resolveAliasedPath(path);
555
556 desktop::openUrl(QUrl::fromLocalFile(path));
557 }
558
559 #ifndef Q_OS_MAC
560
showWordDoc(QString path)561 void GwtCallback::showWordDoc(QString path)
562 {
563 #ifdef Q_OS_WIN32
564
565 path = desktop::resolveAliasedPath(path);
566 Error error = wordViewer_.showItem(path.toStdWString());
567 if (error)
568 {
569 LOG_ERROR(error);
570 showFile(path);
571 }
572
573 #else
574 // Invoke default viewer on other platforms
575 showFile(path);
576 #endif
577 }
578
showPptPresentation(QString path)579 void GwtCallback::showPptPresentation(QString path)
580 {
581 #ifdef Q_OS_WIN32
582
583 path = desktop::resolveAliasedPath(path);
584 Error error = pptViewer_.showItem(path.toStdWString());
585 if (error)
586 {
587 LOG_ERROR(error);
588 showFile(path);
589 }
590 #else
591 // Invoke default viewer on other platforms
592 showFile(path);
593 #endif
594 }
595
596 #endif
597
showPDF(QString path,int pdfPage)598 void GwtCallback::showPDF(QString path, int pdfPage)
599 {
600 path = desktop::resolveAliasedPath(path);
601
602 #ifdef Q_OS_MAC
603 desktop::openFile(path);
604 #else
605 synctex().view(path, pdfPage);
606 #endif
607 }
608
prepareShowWordDoc()609 void GwtCallback::prepareShowWordDoc()
610 {
611 #ifdef Q_OS_WIN32
612 Error error = wordViewer_.closeLastViewedItem();
613 if (error)
614 {
615 LOG_ERROR(error);
616 }
617 #endif
618 }
619
prepareShowPptPresentation()620 void GwtCallback::prepareShowPptPresentation()
621 {
622 #ifdef Q_OS_WIN32
623 Error error = pptViewer_.closeLastViewedItem();
624 if (error)
625 {
626 LOG_ERROR(error);
627 }
628 #endif
629 }
630
getRVersion()631 QString GwtCallback::getRVersion()
632 {
633 #ifdef Q_OS_WIN32
634 bool defaulted = options().rBinDir().isEmpty();
635 QString rDesc = defaulted
636 ? QString::fromUtf8("[Default] ") + autoDetect().description()
637 : RVersion(options().rBinDir()).description();
638 return rDesc;
639 #else
640 return QString();
641 #endif
642 }
643
chooseRVersion()644 QString GwtCallback::chooseRVersion()
645 {
646 #ifdef Q_OS_WIN32
647 RVersion rVersion = desktop::detectRVersion(true, pOwner_->asWidget());
648 if (rVersion.isValid())
649 return getRVersion();
650 else
651 return QString();
652 #else
653 return QString();
654 #endif
655 }
656
devicePixelRatio()657 double GwtCallback::devicePixelRatio()
658 {
659 return desktop::devicePixelRatio(pMainWindow_);
660 }
661
openMinimalWindow(QString name,QString url,int width,int height)662 void GwtCallback::openMinimalWindow(QString name,
663 QString url,
664 int width,
665 int height)
666 {
667 bool named = !name.isEmpty() && name != QString::fromUtf8("_blank");
668
669 BrowserWindow* browser = nullptr;
670 if (named)
671 browser = s_windowTracker.getWindow(name);
672
673 if (!browser)
674 {
675 bool isViewerZoomWindow =
676 (name == QString::fromUtf8("_rstudio_viewer_zoom"));
677
678 // create the new browser window; pass along our own base URL so that the new window's
679 // WebProfile knows how to apply the appropriate headers
680 browser = new BrowserWindow(false, !isViewerZoomWindow, name,
681 pMainWindow_->webView()->baseUrl(), nullptr, pMainWindow_->webPage());
682
683 browser->setAttribute(Qt::WA_DeleteOnClose, true);
684 browser->setAttribute(Qt::WA_QuitOnClose, true);
685
686 // ensure minimal windows can be closed with Ctrl+W (Cmd+W on macOS)
687 QAction* closeWindow = new QAction(browser);
688 closeWindow->setShortcut(Qt::CTRL + Qt::Key_W);
689 connect(closeWindow, &QAction::triggered,
690 browser, &BrowserWindow::close);
691 browser->addAction(closeWindow);
692
693 connect(this, &GwtCallback::destroyed, browser, &BrowserWindow::close);
694
695 if (named)
696 s_windowTracker.addWindow(name, browser);
697
698 // set title for viewer zoom
699 if (isViewerZoomWindow)
700 browser->setWindowTitle(QString::fromUtf8("Viewer Zoom"));
701 }
702
703 browser->webView()->load(QUrl(url));
704 browser->resize(width, height);
705 browser->show();
706 browser->activateWindow();
707 }
708
activateMinimalWindow(QString name)709 void GwtCallback::activateMinimalWindow(QString name)
710 {
711 // we can only activate named windows
712 bool named = !name.isEmpty() && name != QString::fromUtf8("_blank");
713 if (!named)
714 return;
715
716 pOwner_->webPage()->activateWindow(name);
717 }
718
prepareForSatelliteWindow(QString name,int x,int y,int width,int height)719 void GwtCallback::prepareForSatelliteWindow(QString name,
720 int x,
721 int y,
722 int width,
723 int height)
724 {
725 pOwner_->webPage()->prepareForWindow(
726 PendingWindow(name, pMainWindow_, x, y, width, height));
727 }
728
prepareForNamedWindow(QString name,bool allowExternalNavigate,bool showDesktopToolbar)729 void GwtCallback::prepareForNamedWindow(QString name,
730 bool allowExternalNavigate,
731 bool showDesktopToolbar)
732 {
733 pOwner_->webPage()->prepareForWindow(
734 PendingWindow(name, allowExternalNavigate, showDesktopToolbar));
735 }
736
closeNamedWindow(QString name)737 void GwtCallback::closeNamedWindow(QString name)
738 {
739 // close the requested window
740 pOwner_->webPage()->closeWindow(name);
741
742 // bring the main window to the front (so we don't lose RStudio context
743 // entirely)
744 desktop::raiseAndActivateWindow(pMainWindow_);
745 }
746
activateSatelliteWindow(QString name)747 void GwtCallback::activateSatelliteWindow(QString name)
748 {
749 pOwner_->webPage()->activateWindow(name);
750 }
751
copyPageRegionToClipboard(int left,int top,int width,int height)752 void GwtCallback::copyPageRegionToClipboard(int left, int top, int width, int height)
753 {
754 auto* view = pMainWindow_->webView();
755
756 double scale = view->zoomFactor();
757 left = left * scale;
758 top = top * scale;
759 width = width * scale;
760 height = height * scale;
761
762 QPixmap pixmap = view->grab(QRect(left, top, width, height));
763 QApplication::clipboard()->setPixmap(pixmap);
764 }
765
exportPageRegionToFile(QString targetPath,QString format,int left,int top,int width,int height)766 void GwtCallback::exportPageRegionToFile(QString targetPath,
767 QString format,
768 int left,
769 int top,
770 int width,
771 int height)
772 {
773 // resolve target path
774 targetPath = desktop::resolveAliasedPath(targetPath);
775
776 // get the pixmap
777 #if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
778 QPixmap pixmap = QPixmap::grabWidget(pMainWindow_->webView(),
779 left,
780 top,
781 width,
782 height);
783 #else
784 QPixmap pixmap = pMainWindow_->webView()->grab(QRect(left, top, width, height));
785 #endif
786
787 // save the file
788 pixmap.save(targetPath, format.toUtf8().constData(), 100);
789 }
790
791
supportsClipboardMetafile()792 bool GwtCallback::supportsClipboardMetafile()
793 {
794 #ifdef Q_OS_WIN32
795 return true;
796 #else
797 return false;
798 #endif
799 }
800
801 #ifndef Q_OS_MAC
802
803 namespace {
captionToRole(const QString & caption)804 QMessageBox::ButtonRole captionToRole(const QString& caption)
805 {
806 if (caption == QString::fromUtf8("OK"))
807 return QMessageBox::AcceptRole;
808 else if (caption == QString::fromUtf8("Cancel"))
809 return QMessageBox::RejectRole;
810 else if (caption == QString::fromUtf8("Yes"))
811 return QMessageBox::YesRole;
812 else if (caption == QString::fromUtf8("No"))
813 return QMessageBox::NoRole;
814 else if (caption == QString::fromUtf8("Save"))
815 return QMessageBox::AcceptRole;
816 else if (caption == QString::fromUtf8("Don't Save"))
817 return QMessageBox::DestructiveRole;
818 else
819 return QMessageBox::ActionRole;
820 }
821 } // anonymous namespace
822
showMessageBox(int type,QString caption,QString message,QString buttons,int defaultButton,int cancelButton)823 int GwtCallback::showMessageBox(int type,
824 QString caption,
825 QString message,
826 QString buttons,
827 int defaultButton,
828 int cancelButton)
829 {
830 // cancel other message box if it's visible
831 auto* pMsgBox = qobject_cast<QMessageBox*>(
832 QApplication::activeModalWidget());
833 if (pMsgBox != nullptr)
834 pMsgBox->close();
835
836 QMessageBox msgBox(pOwner_->asWidget());
837 msgBox.setWindowTitle(caption);
838 msgBox.setText(message);
839 msgBox.setIcon(safeMessageBoxIcon(static_cast<QMessageBox::Icon>(type)));
840 msgBox.setWindowFlags(Qt::Dialog | Qt::Sheet);
841 msgBox.setWindowModality(Qt::WindowModal);
842 msgBox.setWindowFlag(Qt::WindowContextHelpButtonHint, false);
843 msgBox.setTextFormat(Qt::PlainText);
844
845 QStringList buttonList = buttons.split(QChar::fromLatin1('|'));
846
847 for (int i = 0; i != buttonList.size(); i++)
848 {
849 QPushButton* pBtn = msgBox.addButton(buttonList.at(i),
850 captionToRole(buttonList.at(i)));
851 if (defaultButton == i)
852 msgBox.setDefaultButton(pBtn);
853 }
854
855 msgBox.exec();
856
857 QAbstractButton* button = msgBox.clickedButton();
858 if (!button)
859 return cancelButton;
860
861 for (int i = 0; i < buttonList.size(); i++)
862 if (buttonList.at(i) == button->text())
863 return i;
864
865 return cancelButton;
866 }
867
868 #endif
869
promptForText(QString title,QString caption,QString defaultValue,int inputType,QString extraOptionPrompt,bool extraOptionByDefault,int selectionStart,int selectionLength,QString okButtonCaption)870 QString GwtCallback::promptForText(QString title,
871 QString caption,
872 QString defaultValue,
873 int inputType,
874 QString extraOptionPrompt,
875 bool extraOptionByDefault,
876 int selectionStart,
877 int selectionLength,
878 QString okButtonCaption)
879 {
880 InputDialog dialog(pOwner_->asWidget());
881 dialog.setWindowTitle(title);
882 dialog.setCaption(caption);
883 InputType type = static_cast<InputType>(inputType);
884 bool usePasswordMask = type == InputPassword;
885 dialog.setRequired(type == InputRequiredText);
886
887 if (usePasswordMask)
888 dialog.setEchoMode(QLineEdit::Password);
889
890 if (!extraOptionPrompt.isEmpty())
891 {
892 dialog.setExtraOptionPrompt(extraOptionPrompt);
893 dialog.setExtraOption(extraOptionByDefault);
894 }
895
896 if (usePasswordMask)
897 {
898 // password prompts are shown higher up (because they relate to
899 // console progress dialogs which are at the top of the screen)
900 QRect parentGeom = pOwner_->asWidget()->geometry();
901 int x = parentGeom.left() + (parentGeom.width() / 2) - (dialog.width() / 2);
902 dialog.move(x, parentGeom.top() + 75);
903 }
904
905 if (type == InputNumeric)
906 dialog.setNumbersOnly(true);
907
908 if (!defaultValue.isEmpty())
909 {
910 dialog.setTextValue(defaultValue);
911 if (selectionStart >= 0 && selectionLength >= 0)
912 {
913 dialog.setSelection(selectionStart, selectionLength);
914 }
915 else
916 {
917 dialog.setSelection(0, defaultValue.size());
918 }
919 }
920
921 if (dialog.exec() == QDialog::Accepted)
922 {
923 QString value = dialog.textValue();
924 bool extraOption = dialog.extraOption();
925 QString values;
926 values += value;
927 values += QString::fromUtf8("\n");
928 values += extraOption ? QString::fromUtf8("1") : QString::fromUtf8("0");
929 return values;
930 }
931 else
932 return QString();
933 }
934
supportsFullscreenMode()935 bool GwtCallback::supportsFullscreenMode()
936 {
937 return desktop::supportsFullscreenMode(pMainWindow_);
938 }
939
toggleFullscreenMode()940 void GwtCallback::toggleFullscreenMode()
941 {
942 desktop::toggleFullscreenMode(pMainWindow_);
943 }
944
showKeyboardShortcutHelp()945 void GwtCallback::showKeyboardShortcutHelp()
946 {
947 FilePath keyboardHelpPath = options().wwwDocsPath().completePath("keyboard.htm");
948 QString file = QString::fromUtf8(keyboardHelpPath.getAbsolutePath().c_str());
949 QUrl url = QUrl::fromLocalFile(file);
950 desktop::openUrl(url);
951 }
952
bringMainFrameToFront()953 void GwtCallback::bringMainFrameToFront()
954 {
955 desktop::raiseAndActivateWindow(pMainWindow_);
956 }
957
bringMainFrameBehindActive()958 void GwtCallback::bringMainFrameBehindActive()
959 {
960 desktop::moveWindowBeneath(QApplication::activeWindow(), pMainWindow_);
961 }
962
desktopRenderingEngine()963 QString GwtCallback::desktopRenderingEngine()
964 {
965 return desktop::options().desktopRenderingEngine();
966 }
967
setDesktopRenderingEngine(QString engine)968 void GwtCallback::setDesktopRenderingEngine(QString engine)
969 {
970 desktop::options().setDesktopRenderingEngine(engine);
971 }
972
filterText(QString text)973 QString GwtCallback::filterText(QString text)
974 {
975 // Ace doesn't do well with NFD Unicode text. To repro on
976 // Mac OS X, create a folder on disk with accented characters
977 // in the name, then create a file in that folder. Do a
978 // Get Info on the file and copy the path. Now you'll have
979 // an NFD string on the clipboard.
980 return text.normalized(QString::NormalizationForm_C);
981 }
982
983 #ifdef __APPLE__
984
985 namespace {
986
987 template <typename TValue>
988 class CFReleaseHandle
989 {
990 public:
CFReleaseHandle(TValue value=nullptr)991 CFReleaseHandle(TValue value=nullptr)
992 {
993 value_ = value;
994 }
995
~CFReleaseHandle()996 ~CFReleaseHandle()
997 {
998 if (value_)
999 CFRelease(value_);
1000 }
1001
value()1002 TValue& value()
1003 {
1004 return value_;
1005 }
1006
operator TValue() const1007 operator TValue () const
1008 {
1009 return value_;
1010 }
1011
operator &()1012 TValue* operator& ()
1013 {
1014 return &value_;
1015 }
1016
1017 private:
1018 TValue value_;
1019 };
1020
addToPasteboard(PasteboardRef pasteboard,int slot,CFStringRef flavor,const QByteArray & data)1021 OSStatus addToPasteboard(PasteboardRef pasteboard,
1022 int slot,
1023 CFStringRef flavor,
1024 const QByteArray& data)
1025 {
1026 CFReleaseHandle<CFDataRef> dataRef = CFDataCreate(
1027 nullptr,
1028 reinterpret_cast<const UInt8*>(data.constData()),
1029 data.length());
1030
1031 if (!dataRef)
1032 return memFullErr;
1033
1034 return ::PasteboardPutItemFlavor(pasteboard,
1035 reinterpret_cast<PasteboardItemID>(slot),
1036 flavor, dataRef, 0);
1037 }
1038
1039 } // anonymous namespace
1040
cleanClipboard(bool stripHtml)1041 void GwtCallback::cleanClipboard(bool stripHtml)
1042 {
1043 CFReleaseHandle<PasteboardRef> clipboard;
1044 if (::PasteboardCreate(kPasteboardClipboard, &clipboard))
1045 return;
1046
1047 ::PasteboardSynchronize(clipboard);
1048
1049 ItemCount itemCount;
1050 if (::PasteboardGetItemCount(clipboard, &itemCount) || itemCount < 1)
1051 return;
1052
1053 PasteboardItemID itemId;
1054 if (::PasteboardGetItemIdentifier(clipboard, 1, &itemId))
1055 return;
1056
1057
1058 /*
1059 CFReleaseHandle<CFArrayRef> flavorTypes;
1060 if (::PasteboardCopyItemFlavors(clipboard, itemId, &flavorTypes))
1061 return;
1062 for (int i = 0; i < CFArrayGetCount(flavorTypes); i++)
1063 {
1064 CFStringRef flavorType = (CFStringRef)CFArrayGetValueAtIndex(flavorTypes, i);
1065 char buffer[1000];
1066 if (!CFStringGetCString(flavorType, buffer, 1000, kCFStringEncodingMacRoman))
1067 return;
1068 qDebug() << buffer;
1069 }
1070 */
1071
1072 CFReleaseHandle<CFDataRef> data;
1073 if (::PasteboardCopyItemFlavorData(clipboard,
1074 itemId,
1075 CFSTR("public.utf16-plain-text"),
1076 &data))
1077 {
1078 return;
1079 }
1080
1081 CFReleaseHandle<CFDataRef> htmlData;
1082 OSStatus err;
1083 if (!stripHtml && (err = ::PasteboardCopyItemFlavorData(clipboard, itemId, CFSTR("public.html"), &htmlData)))
1084 {
1085 if (err != badPasteboardFlavorErr)
1086 return;
1087 }
1088
1089 CFIndex len = ::CFDataGetLength(data);
1090 QByteArray buffer;
1091 buffer.resize(len);
1092 ::CFDataGetBytes(data, CFRangeMake(0, len), reinterpret_cast<UInt8*>(buffer.data()));
1093 QString str = QString::fromUtf16(reinterpret_cast<const ushort*>(buffer.constData()), buffer.length()/2);
1094
1095 if (::PasteboardClear(clipboard))
1096 return;
1097 if (addToPasteboard(clipboard, 1, CFSTR("public.utf8-plain-text"), str.toUtf8()))
1098 return;
1099
1100 if (htmlData.value())
1101 {
1102 ::PasteboardPutItemFlavor(clipboard,
1103 (PasteboardItemID)1,
1104 CFSTR("public.html"),
1105 htmlData,
1106 0);
1107 }
1108 }
1109 #else
1110
cleanClipboard(bool stripHtml)1111 void GwtCallback::cleanClipboard(bool stripHtml)
1112 {
1113 }
1114
1115 #endif
1116
setPendingQuit(int pendingQuit)1117 void GwtCallback::setPendingQuit(int pendingQuit)
1118 {
1119 pendingQuit_ = pendingQuit;
1120 }
1121
collectPendingQuitRequest()1122 int GwtCallback::collectPendingQuitRequest()
1123 {
1124 if (pendingQuit_ != PendingQuitNone)
1125 {
1126 int pendingQuit = pendingQuit_;
1127 pendingQuit_ = PendingQuitNone;
1128 return pendingQuit;
1129 }
1130 else
1131 {
1132 return PendingQuitNone;
1133 }
1134 }
1135
openProjectInNewWindow(QString projectFilePath)1136 void GwtCallback::openProjectInNewWindow(QString projectFilePath)
1137 {
1138 if (!isRemoteDesktop_)
1139 {
1140 std::vector<std::string> args;
1141 args.push_back(desktop::resolveAliasedPath(projectFilePath).toStdString());
1142 pMainWindow_->launchRStudio(args);
1143 }
1144 else
1145 {
1146 // start new Remote Desktop RStudio process with the session URL
1147 pMainWindow_->launchRemoteRStudioProject(projectFilePath);
1148 }
1149 }
1150
openSessionInNewWindow(QString workingDirectoryPath)1151 void GwtCallback::openSessionInNewWindow(QString workingDirectoryPath)
1152 {
1153 if (!isRemoteDesktop_)
1154 {
1155 workingDirectoryPath = desktop::resolveAliasedPath(workingDirectoryPath);
1156 pMainWindow_->launchRStudio(std::vector<std::string>(),
1157 workingDirectoryPath.toStdString());
1158 }
1159 else
1160 {
1161 // start the new session on the currently connected server
1162 pMainWindow_->launchRemoteRStudio();
1163 }
1164 }
1165
openTerminal(QString terminalPath,QString workingDirectory,QString extraPathEntries,QString shellType)1166 void GwtCallback::openTerminal(QString terminalPath,
1167 QString workingDirectory,
1168 QString extraPathEntries,
1169 QString shellType)
1170 {
1171 // append extra path entries to our path before launching
1172 std::string path = core::system::getenv("PATH");
1173 std::string previousPath = path;
1174 core::system::addToPath(&path, extraPathEntries.toStdString());
1175 core::system::setenv("PATH", path);
1176
1177 #if defined(Q_OS_MAC)
1178
1179 // call Terminal.app with an applescript that navigates it
1180 // to the specified directory. note we don't reference the
1181 // passed terminalPath because this setting isn't respected
1182 // on the Mac (we always use Terminal.app)
1183 FilePath macTermScriptFilePath =
1184 desktop::options().scriptsPath().completePath("mac-terminal");
1185 QString macTermScriptPath = QString::fromUtf8(
1186 macTermScriptFilePath.getAbsolutePath().c_str());
1187 QStringList args;
1188 args.append(desktop::resolveAliasedPath(workingDirectory));
1189 QProcess::startDetached(macTermScriptPath, args);
1190
1191 #elif defined(Q_OS_WIN)
1192
1193 if (terminalPath.length() == 0)
1194 {
1195 terminalPath = QString::fromUtf8("cmd.exe");
1196 shellType = QString::fromUtf8("win-cmd");
1197 }
1198
1199 QStringList args;
1200 std::string previousHome = core::system::getenv("HOME");
1201
1202 if (shellType == QString::fromUtf8("win-git-bash") ||
1203 shellType == QString::fromUtf8("win-wsl-bash"))
1204 {
1205 args.append(QString::fromUtf8("--login"));
1206 args.append(QString::fromUtf8("-i"));
1207 }
1208
1209 if (shellType != QString::fromUtf8("win-wsl-bash"))
1210 {
1211 // set HOME to USERPROFILE so msys ssh can find our keys
1212 std::string userProfile = core::system::getenv("USERPROFILE");
1213 core::system::setenv("HOME", userProfile);
1214 }
1215
1216 QProcess process;
1217 process.setProgram(terminalPath);
1218 process.setArguments(args);
1219 process.setWorkingDirectory(desktop::resolveAliasedPath(workingDirectory));
1220 process.setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments* cpa)
1221 {
1222 cpa->flags |= CREATE_NEW_CONSOLE;
1223 cpa->startupInfo->dwFlags &= ~STARTF_USESTDHANDLES;
1224 });
1225 process.startDetached();
1226
1227 // revert to previous home
1228 core::system::setenv("HOME", previousHome);
1229
1230 #elif defined(Q_OS_LINUX)
1231
1232 // start the auto-detected terminal (or user-specified override)
1233 if (terminalPath.length() != 0)
1234 {
1235 QStringList args;
1236 QProcess::startDetached(terminalPath,
1237 args,
1238 desktop::resolveAliasedPath(workingDirectory));
1239 }
1240 else
1241 {
1242 desktop::showWarning(
1243 nullptr,
1244 QString::fromUtf8("Terminal Not Found"),
1245 QString::fromUtf8(
1246 "Unable to find a compatible terminal program to launch"),
1247 QString());
1248 }
1249
1250 #endif
1251
1252 // restore previous path
1253 core::system::setenv("PATH", previousPath);
1254 }
1255
isProportionalFont(const QString & fontFamily)1256 bool isProportionalFont(const QString& fontFamily)
1257 {
1258 QFont font(fontFamily, 12);
1259 return !isFixedWidthFont(font);
1260 }
1261
getFixedWidthFontList()1262 QString GwtCallback::getFixedWidthFontList()
1263 {
1264 return desktop::getFixedWidthFontList();
1265 }
1266
getFixedWidthFont()1267 QString GwtCallback::getFixedWidthFont()
1268 {
1269 return options().fixedWidthFont();
1270 }
1271
setFixedWidthFont(QString font)1272 void GwtCallback::setFixedWidthFont(QString font)
1273 {
1274 options().setFixedWidthFont(font);
1275 }
1276
getZoomLevels()1277 QString GwtCallback::getZoomLevels()
1278 {
1279 QStringList zoomLevels;
1280 for (double zoomLevel : pMainWindow_->zoomLevels())
1281 {
1282 zoomLevels.append(QString::fromStdString(
1283 safe_convert::numberToString(zoomLevel)));
1284 }
1285 return zoomLevels.join(QString::fromUtf8("\n"));
1286 }
1287
getZoomLevel()1288 double GwtCallback::getZoomLevel()
1289 {
1290 return desktopInfo().getZoomLevel();
1291 }
1292
setZoomLevel(double zoomLevel)1293 void GwtCallback::setZoomLevel(double zoomLevel)
1294 {
1295 pOwner_->setZoomLevel(zoomLevel);
1296 }
1297
zoomIn()1298 void GwtCallback::zoomIn()
1299 {
1300 pOwner_->zoomIn();
1301 }
1302
zoomOut()1303 void GwtCallback::zoomOut()
1304 {
1305 pOwner_->zoomOut();
1306 }
1307
zoomActualSize()1308 void GwtCallback::zoomActualSize()
1309 {
1310 pOwner_->zoomActualSize();
1311 }
1312
setBackgroundColor(QJsonArray rgbColor)1313 void GwtCallback::setBackgroundColor(QJsonArray rgbColor)
1314 {
1315 #if defined(NDEBUG) || !defined(_WIN32)
1316 // don't set the background color in win32 debug builds because Chromium can crash on a fatal assert in
1317 // debug builds when the background color is changed.
1318 // https://bitbucket.org/chromiumembedded/cef/issues/2144
1319 int red = rgbColor.at(0).toInt();
1320 int green = rgbColor.at(1).toInt();
1321 int blue = rgbColor.at(2).toInt();
1322
1323 QColor color = QColor::fromRgb(red, green, blue);
1324 pOwner_->webPage()->setBackgroundColor(color);
1325 changeTitleBarColor(red, green, blue);
1326 #endif
1327 }
1328
1329 #ifndef Q_OS_MAC
1330
changeTitleBarColor(int red,int green,int blue)1331 void GwtCallback::changeTitleBarColor(int red, int green, int blue)
1332 {
1333 }
1334
1335 #endif
1336
syncToEditorTheme(bool isDark)1337 void GwtCallback::syncToEditorTheme(bool isDark)
1338 {
1339 if (isGnomeDesktop() || isWindows())
1340 {
1341 applyDesktopTheme(pOwner_, isDark);
1342 }
1343 }
1344
getEnableAccessibility()1345 bool GwtCallback::getEnableAccessibility()
1346 {
1347 return options().enableAccessibility();
1348 }
1349
setEnableAccessibility(bool enable)1350 void GwtCallback::setEnableAccessibility(bool enable)
1351 {
1352 options().setEnableAccessibility(enable);
1353 }
1354
getClipboardMonitoring()1355 bool GwtCallback::getClipboardMonitoring()
1356 {
1357 return options().clipboardMonitoring();
1358 }
1359
setClipboardMonitoring(bool monitoring)1360 void GwtCallback::setClipboardMonitoring(bool monitoring)
1361 {
1362 options().setClipboardMonitoring(monitoring);
1363 }
1364
getIgnoreGpuExclusionList()1365 bool GwtCallback::getIgnoreGpuExclusionList()
1366 {
1367 return options().ignoreGpuExclusionList();
1368 }
1369
setIgnoreGpuExclusionList(bool ignore)1370 void GwtCallback::setIgnoreGpuExclusionList(bool ignore)
1371 {
1372 options().setIgnoreGpuExclusionList(ignore);
1373 }
1374
getDisableGpuDriverBugWorkarounds()1375 bool GwtCallback::getDisableGpuDriverBugWorkarounds()
1376 {
1377 return options().disableGpuDriverBugWorkarounds();
1378 }
1379
setDisableGpuDriverBugWorkarounds(bool disable)1380 void GwtCallback::setDisableGpuDriverBugWorkarounds(bool disable)
1381 {
1382 options().setDisableGpuDriverBugWorkarounds(disable);
1383 }
1384
showLicenseDialog()1385 void GwtCallback::showLicenseDialog()
1386 {
1387 activation().showLicenseDialog(false /*showQuitButton*/);
1388 }
1389
showSessionServerOptionsDialog()1390 void GwtCallback::showSessionServerOptionsDialog()
1391 {
1392 sessionServers().showSessionServerOptionsDialog(pMainWindow_);
1393 }
1394
getInitMessages()1395 QString GwtCallback::getInitMessages()
1396 {
1397 std::string message = activation().currentLicenseStateMessage();
1398 return QString::fromStdString(message);
1399 }
1400
getLicenseStatusMessage()1401 QString GwtCallback::getLicenseStatusMessage()
1402 {
1403 std::string message = activation().licenseStatus();
1404 return QString::fromStdString(message);
1405 }
1406
allowProductUsage()1407 bool GwtCallback::allowProductUsage()
1408 {
1409 return activation().allowProductUsage();
1410 }
1411
getDesktopSynctexViewer()1412 QString GwtCallback::getDesktopSynctexViewer()
1413 {
1414 return Synctex::desktopViewerInfo().name;
1415 }
1416
externalSynctexPreview(QString pdfPath,int page)1417 void GwtCallback::externalSynctexPreview(QString pdfPath, int page)
1418 {
1419 synctex().syncView(desktop::resolveAliasedPath(pdfPath), page);
1420 }
1421
externalSynctexView(const QString & pdfFile,const QString & srcFile,int line,int column)1422 void GwtCallback::externalSynctexView(const QString& pdfFile,
1423 const QString& srcFile,
1424 int line,
1425 int column)
1426 {
1427 synctex().syncView(desktop::resolveAliasedPath(pdfFile),
1428 desktop::resolveAliasedPath(srcFile),
1429 QPoint(line, column));
1430 }
1431
launchSession(bool reload)1432 void GwtCallback::launchSession(bool reload)
1433 {
1434 pMainWindow_->launchSession(reload);
1435 }
1436
1437
activateAndFocusOwner()1438 void GwtCallback::activateAndFocusOwner()
1439 {
1440 desktop::raiseAndActivateWindow(pOwner_->asWidget());
1441 }
1442
reloadZoomWindow()1443 void GwtCallback::reloadZoomWindow()
1444 {
1445 BrowserWindow* pBrowser = s_windowTracker.getWindow(
1446 QString::fromUtf8("_rstudio_zoom"));
1447 if (pBrowser)
1448 pBrowser->webView()->reload();
1449 }
1450
setTutorialUrl(QString url)1451 void GwtCallback::setTutorialUrl(QString url)
1452 {
1453 pOwner_->webPage()->setTutorialUrl(url);
1454 }
1455
setViewerUrl(QString url)1456 void GwtCallback::setViewerUrl(QString url)
1457 {
1458 pOwner_->webPage()->setViewerUrl(url);
1459 }
1460
setShinyDialogUrl(QString url)1461 void GwtCallback::setShinyDialogUrl(QString url)
1462 {
1463 pOwner_->webPage()->setShinyDialogUrl(url);
1464 }
1465
reloadViewerZoomWindow(QString url)1466 void GwtCallback::reloadViewerZoomWindow(QString url)
1467 {
1468 BrowserWindow* pBrowser = s_windowTracker.getWindow(
1469 QString::fromUtf8("_rstudio_viewer_zoom"));
1470 if (pBrowser)
1471 pBrowser->webView()->setUrl(url);
1472 }
1473
isMacOS()1474 bool GwtCallback::isMacOS()
1475 {
1476 return desktop::isMacOS();
1477 }
1478
isCentOS()1479 bool GwtCallback::isCentOS()
1480 {
1481 return desktop::isCentOS();
1482 }
1483
getScrollingCompensationType()1484 QString GwtCallback::getScrollingCompensationType()
1485 {
1486 #if defined(Q_OS_MAC)
1487 return QString::fromUtf8("Mac");
1488 #elif defined(Q_OS_WIN)
1489 return QString::fromUtf8("Win");
1490 #else
1491 return QString::fromUtf8("None");
1492 #endif
1493 }
1494
setBusy(bool)1495 void GwtCallback::setBusy(bool)
1496 {
1497 #if defined(Q_OS_MAC)
1498 // call AppNap apis for Mac (we use Cocoa on the Mac though so
1499 // this codepath will never be hit)
1500 #endif
1501 }
1502
setWindowTitle(QString title)1503 void GwtCallback::setWindowTitle(QString title)
1504 {
1505 pMainWindow_->setWindowTitle(title + QString::fromUtf8(" - ") + desktop::activation().editionName());
1506 }
1507
1508 #ifdef Q_OS_WIN
installRtools(QString version,QString installerPath)1509 void GwtCallback::installRtools(QString version, QString installerPath)
1510 {
1511 // silent install
1512 QStringList args;
1513 args.push_back(QString::fromUtf8("/SP-"));
1514 args.push_back(QString::fromUtf8("/SILENT"));
1515
1516 // custom install directory
1517 std::string systemDrive = core::system::getenv("SYSTEMDRIVE");
1518 if (!systemDrive.empty() && FilePath(systemDrive).exists())
1519 {
1520 std::string dir = systemDrive + "\\RBuildTools\\" + version.toStdString();
1521 std::string dirArg = "/DIR=" + dir;
1522 args.push_back(QString::fromStdString(dirArg));
1523 }
1524
1525 // launch installer
1526 QProcess::startDetached(installerPath, args);
1527 }
1528 #else
installRtools(QString version,QString installerPath)1529 void GwtCallback::installRtools(QString version, QString installerPath)
1530 {
1531 }
1532 #endif
1533
getDisplayDpi()1534 QString GwtCallback::getDisplayDpi()
1535 {
1536 return QString::fromStdString(
1537 safe_convert::numberToString(getDpi()));
1538 }
1539
onSessionQuit()1540 void GwtCallback::onSessionQuit()
1541 {
1542 sessionQuit();
1543 }
1544
getSessionServer()1545 QJsonObject GwtCallback::getSessionServer()
1546 {
1547 if (pMainWindow_->getRemoteDesktopSessionLauncher() != nullptr)
1548 return pMainWindow_->getRemoteDesktopSessionLauncher()->sessionServer().toJson();
1549 else
1550 return QJsonObject();
1551 }
1552
getLauncherServer()1553 QJsonObject GwtCallback::getLauncherServer()
1554 {
1555 SessionServer server = pMainWindow_->getJobLauncher()->getLauncherServer();
1556 return server.toJson();
1557 }
1558
getSessionServers()1559 QJsonArray GwtCallback::getSessionServers()
1560 {
1561 QJsonArray serversArray;
1562 for (const SessionServer& server : sessionServerSettings().servers())
1563 {
1564 serversArray.append(server.toJson());
1565 }
1566
1567 return serversArray;
1568 }
1569
reconnectToSessionServer(const QJsonValue & sessionServerJson)1570 void GwtCallback::reconnectToSessionServer(const QJsonValue& sessionServerJson)
1571 {
1572 SessionServer server = !sessionServerJson.isNull() ?
1573 SessionServer::fromJson(sessionServerJson.toObject()) :
1574 SessionServer("", "");
1575 sessionServers().setPendingSessionServerReconnect(server);
1576 pMainWindow_->close();
1577 }
1578
setLauncherServer(const QJsonObject & sessionServerJson)1579 bool GwtCallback::setLauncherServer(const QJsonObject& sessionServerJson)
1580 {
1581 SessionServer server = SessionServer::fromJson(sessionServerJson);
1582 return pMainWindow_->getJobLauncher()->setSessionServer(server);
1583 }
1584
connectToLauncherServer()1585 void GwtCallback::connectToLauncherServer()
1586 {
1587 pMainWindow_->getJobLauncher()->signIn();
1588 }
1589
startLauncherJobStatusStream(QString jobId)1590 void GwtCallback::startLauncherJobStatusStream(QString jobId)
1591 {
1592 pMainWindow_->getJobLauncher()->startLauncherJobStatusStream(jobId.toStdString());
1593 }
1594
stopLauncherJobStatusStream(QString jobId)1595 void GwtCallback::stopLauncherJobStatusStream(QString jobId)
1596 {
1597 pMainWindow_->getJobLauncher()->stopLauncherJobStatusStream(jobId.toStdString());
1598 }
1599
startLauncherJobOutputStream(QString jobId)1600 void GwtCallback::startLauncherJobOutputStream(QString jobId)
1601 {
1602 pMainWindow_->getJobLauncher()->startLauncherJobOutputStream(jobId.toStdString());
1603 }
1604
stopLauncherJobOutputStream(QString jobId)1605 void GwtCallback::stopLauncherJobOutputStream(QString jobId)
1606 {
1607 pMainWindow_->getJobLauncher()->stopLauncherJobOutputStream(jobId.toStdString());
1608 }
1609
controlLauncherJob(QString jobId,QString operation)1610 void GwtCallback::controlLauncherJob(QString jobId, QString operation)
1611 {
1612 pMainWindow_->getJobLauncher()->controlLauncherJob(jobId.toStdString(), operation.toStdString());
1613 }
1614
submitLauncherJob(const QJsonObject & job)1615 void GwtCallback::submitLauncherJob(const QJsonObject& job)
1616 {
1617 // convert qt json object to core json object
1618 QJsonDocument doc(job);
1619 std::string jsonStr = doc.toJson().toStdString();
1620
1621 json::Value val;
1622 Error error = val.parse(jsonStr);
1623 if (error)
1624 {
1625 // a parse error should not occur here - if it does it indicates a programmer
1626 // error while invoking this method - as such, forward on the invalid job
1627 // so the appropriate error event is eventually delivered
1628 log::logError(error, ERROR_LOCATION);
1629 }
1630
1631 json::Object obj;
1632 if (val.isObject())
1633 obj = val.getObject();
1634
1635 if (obj.isEmpty())
1636 {
1637 // same as above - if this is not a valid object, it indicates a programmer error
1638 LOG_ERROR_MESSAGE("Empty job object submitted");
1639 }
1640
1641 pMainWindow_->getJobLauncher()->submitLauncherJob(obj);
1642 }
1643
getJobContainerUser()1644 void GwtCallback::getJobContainerUser()
1645 {
1646 pMainWindow_->getJobLauncher()->getJobContainerUser();
1647 }
1648
validateJobsConfig()1649 void GwtCallback::validateJobsConfig()
1650 {
1651 pMainWindow_->getJobLauncher()->validateJobsConfig();
1652 }
1653
getProxyPortNumber()1654 int GwtCallback::getProxyPortNumber()
1655 {
1656 return pMainWindow_->getJobLauncher()->getProxyPortNumber();
1657 }
1658
signOut()1659 void GwtCallback::signOut()
1660 {
1661 // set the currently connected session server as a pending reload
1662 // this ensure that once we sign out, we can reconnect to the server
1663 // with a completely clean state
1664 if (pMainWindow_->getRemoteDesktopSessionLauncher() != nullptr)
1665 {
1666 sessionServers().setPendingSessionServerReconnect(
1667 pMainWindow_->getRemoteDesktopSessionLauncher()->sessionServer());
1668 pMainWindow_->getRemoteDesktopSessionLauncher()->closeOnSignOut();
1669 }
1670 }
1671
1672 } // namespace desktop
1673 } // namespace rstudio
1674