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