1 /*
2  * DesktopMainWindow.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 "DesktopMainWindow.hpp"
17 
18 #include <QToolBar>
19 #include <QWebChannel>
20 #include <QWebEngineSettings>
21 
22 #include <boost/format.hpp>
23 #include <boost/bind/bind.hpp>
24 
25 #include <core/FileSerializer.hpp>
26 #include <core/Macros.hpp>
27 #include <core/text/TemplateFilter.hpp>
28 
29 #include "DesktopOptions.hpp"
30 #include "DesktopSlotBinders.hpp"
31 #include "DesktopSessionLauncher.hpp"
32 #include "DesktopJobLauncherOverlay.hpp"
33 #include "RemoteDesktopSessionLauncherOverlay.hpp"
34 #include "DockTileView.hpp"
35 #include "DesktopActivationOverlay.hpp"
36 #include "DesktopSessionServersOverlay.hpp"
37 #include "DesktopRCommandEvaluator.hpp"
38 
39 using namespace rstudio::core;
40 using namespace boost::placeholders;
41 
42 namespace rstudio {
43 namespace desktop {
44 
45 namespace {
46 
47 #ifdef _WIN32
48 
onDialogStart(HWINEVENTHOOK hook,DWORD event,HWND hwnd,LONG idObject,LONG idChild,DWORD dwEventThread,DWORD dwmsEventTime)49 void CALLBACK onDialogStart(HWINEVENTHOOK hook, DWORD event, HWND hwnd,
50                             LONG idObject, LONG idChild,
51                             DWORD dwEventThread, DWORD dwmsEventTime)
52 {
53    ::BringWindowToTop(hwnd);
54 }
55 
56 #endif
57 
58 // number of times we've tried to reload in startup
59 int s_reloadCount = 0;
60 
61 // maximum number of times to try reloading
62 const int s_maxReloadCount = 10;
63 
64 // amount of time to wait before each reload, in milliseconds
65 const int s_reloadWaitDuration = 200;
66 
67 } // end anonymous namespace
68 
MainWindow(QUrl url,bool isRemoteDesktop)69 MainWindow::MainWindow(QUrl url,
70                        bool isRemoteDesktop) :
71       GwtWindow(false, false, QString(), url, nullptr, nullptr, isRemoteDesktop),
72       isRemoteDesktop_(isRemoteDesktop),
73       menuCallback_(this),
74       gwtCallback_(this, this, isRemoteDesktop),
75       pSessionLauncher_(nullptr),
76       pRemoteSessionLauncher_(nullptr),
77       pLauncher_(new JobLauncher(this)),
78       pCurrentSessionProcess_(nullptr),
79       isErrorDisplayed_(false)
80 {
81    RCommandEvaluator::setMainWindow(this);
82    pToolbar_->setVisible(false);
83 
84 #ifdef _WIN32
85    eventHook_ = nullptr;
86 #endif
87 
88    // bind GWT callbacks
89    auto* channel = webPage()->webChannel();
90    channel->registerObject(QStringLiteral("desktop"), &gwtCallback_);
91    if (isRemoteDesktop_)
92    {
93       // since the object registration is asynchronous, during the GWT setup code
94       // there is a race condition where the initialization can happen before the
95       // remoteDesktop object is registered, making the GWT application think that
96       // it should use regular desktop objects - to circumvent this, we use a custom
97       // user agent string that the GWT code can detect with 100% success rate to
98       // get around this race condition
99       QString userAgent = webPage()->profile()->httpUserAgent().append(
100                QStringLiteral("; RStudio Remote Desktop"));
101       webPage()->profile()->setHttpUserAgent(userAgent);
102       channel->registerObject(QStringLiteral("remoteDesktop"), &gwtCallback_);
103    }
104    channel->registerObject(QStringLiteral("desktopMenuCallback"), &menuCallback_);
105 
106    // disable error page on main window -- we don't want to show the default 404 page
107    // on failure as we show our own error / loading page instead
108    webPage()->settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false);
109 
110    // Dummy menu bar to deal with the fact that
111    // the real menu bar isn't ready until well
112    // after startup.
113 #ifndef Q_OS_MAC
114    auto* pMainMenuStub = new QMenuBar(this);
115    pMainMenuStub->addMenu(QString::fromUtf8("File"));
116    pMainMenuStub->addMenu(QString::fromUtf8("Edit"));
117    pMainMenuStub->addMenu(QString::fromUtf8("Code"));
118    pMainMenuStub->addMenu(QString::fromUtf8("View"));
119    pMainMenuStub->addMenu(QString::fromUtf8("Plots"));
120    pMainMenuStub->addMenu(QString::fromUtf8("Session"));
121    pMainMenuStub->addMenu(QString::fromUtf8("Build"));
122    pMainMenuStub->addMenu(QString::fromUtf8("Debug"));
123    pMainMenuStub->addMenu(QString::fromUtf8("Profile"));
124    pMainMenuStub->addMenu(QString::fromUtf8("Tools"));
125    pMainMenuStub->addMenu(QString::fromUtf8("Help"));
126    setMenuBar(pMainMenuStub);
127 #endif
128 
129    connect(&menuCallback_, SIGNAL(menuBarCompleted(QMenuBar*)),
130            this, SLOT(setMenuBar(QMenuBar*)));
131    connect(&menuCallback_, SIGNAL(commandInvoked(QString)),
132            this, SLOT(invokeCommand(QString)));
133 
134    connect(&menuCallback_, SIGNAL(zoomActualSize()), this, SLOT(zoomActualSize()));
135    connect(&menuCallback_, SIGNAL(zoomIn()), this, SLOT(zoomIn()));
136    connect(&menuCallback_, SIGNAL(zoomOut()), this, SLOT(zoomOut()));
137 
138    connect(&gwtCallback_, SIGNAL(workbenchInitialized()),
139            this, SIGNAL(firstWorkbenchInitialized()));
140    connect(&gwtCallback_, SIGNAL(workbenchInitialized()),
141            this, SLOT(onWorkbenchInitialized()));
142    connect(&gwtCallback_, SIGNAL(sessionQuit()),
143            this, SLOT(onSessionQuit()));
144 
145    connect(webView(), SIGNAL(onCloseWindowShortcut()),
146            this, SLOT(onCloseWindowShortcut()));
147 
148    connect(webView(), &WebView::urlChanged,
149            this, &MainWindow::onUrlChanged);
150 
151    connect(webView(), &WebView::loadFinished,
152            this, &MainWindow::onLoadFinished);
153 
154    connect(webPage(), &QWebEnginePage::loadFinished,
155            &menuCallback_, &MenuCallback::cleanUpActions);
156 
157    connect(&desktopInfo(), &DesktopInfo::fixedWidthFontListChanged, [this]() {
158       QString js = QStringLiteral(
159          "if (typeof window.onFontListReady === 'function') window.onFontListReady()");
160       this->webPage()->runJavaScript(js);
161    });
162 
163    connect(qApp, SIGNAL(commitDataRequest(QSessionManager&)),
164            this, SLOT(commitDataRequest(QSessionManager&)),
165            Qt::DirectConnection);
166 
167    setWindowIcon(QIcon(QString::fromUtf8(":/icons/RStudio.ico")));
168 
169    setWindowTitle(desktop::activation().editionName());
170 
171 #ifdef Q_OS_MAC
172    auto* pDefaultMenu = new QMenuBar(this);
173    pDefaultMenu->addMenu(new WindowMenu());
174 #endif
175 
176    desktop::enableFullscreenMode(this, true);
177 
178    Error error = pLauncher_->initialize();
179    if (error)
180    {
181       LOG_ERROR(error);
182       showError(nullptr,
183                 QStringLiteral("Initialization error"),
184                 QStringLiteral("Could not initialize Job Launcher"),
185                 QString());
186       ::_exit(EXIT_FAILURE);
187    }
188 }
189 
isRemoteDesktop() const190 bool MainWindow::isRemoteDesktop() const
191 {
192    return isRemoteDesktop_;
193 }
194 
getSumatraPdfExePath()195 QString MainWindow::getSumatraPdfExePath()
196 {
197    return desktopInfo().getSumatraPdfExePath();
198 }
199 
launchSession(bool reload)200 void MainWindow::launchSession(bool reload)
201 {
202    // we're about to start another session, so clear the workbench init flag
203    // (will get set again once the new session has initialized the workbench)
204    workbenchInitialized_ = false;
205 
206    Error error = pSessionLauncher_->launchNextSession(reload);
207    if (error)
208    {
209       LOG_ERROR(error);
210 
211       showMessageBox(QMessageBox::Critical,
212                      this,
213                      desktop::activation().editionName(),
214                      QString::fromUtf8("The R session failed to start."),
215                      QString());
216 
217       quit();
218    }
219 }
220 
launchRStudio(const std::vector<std::string> & args,const std::string & initialDir)221 void MainWindow::launchRStudio(const std::vector<std::string> &args,
222                                const std::string& initialDir)
223 {
224     pAppLauncher_->launchRStudio(args, initialDir);
225 }
226 
saveRemoteAuthCookies(const boost::function<QList<QNetworkCookie> ()> & loadCookies,const boost::function<void (QList<QNetworkCookie>)> & saveCookies,bool saveSessionCookies)227 void MainWindow::saveRemoteAuthCookies(const boost::function<QList<QNetworkCookie>()>& loadCookies,
228                                        const boost::function<void(QList<QNetworkCookie>)>& saveCookies,
229                                        bool saveSessionCookies)
230 {
231    std::map<std::string, QNetworkCookie> cookieMap;
232    auto addCookie = [&](const QNetworkCookie& cookie)
233    {
234       // ensure we don't save expired cookies
235       // due to how cookie domains are fluid, it's possible
236       // we could continue to save expired cookies if we don't filter them out
237       if (!cookie.isSessionCookie() && cookie.expirationDate().toUTC() <= QDateTime::currentDateTimeUtc())
238          return;
239 
240       // also do not save the cookie if it is a session cookie and we were not told to explicitly save them
241       if (!saveSessionCookies && cookie.isSessionCookie())
242          return;
243 
244       for (const auto& sessionServer : sessionServerSettings().servers())
245       {
246          if (sessionServer.cookieBelongs(cookie))
247          {
248             cookieMap[sessionServer.label() + "." + cookie.name().toStdString()] = cookie;
249          }
250       }
251    };
252 
253    // merge with existing cookies on disk
254    QList<QNetworkCookie> cookies = loadCookies();
255    for (const QNetworkCookie& cookie : cookies)
256    {
257       addCookie(cookie);
258    }
259 
260    if (pRemoteSessionLauncher_)
261    {
262       std::map<std::string, QNetworkCookie> remoteCookies = pRemoteSessionLauncher_->getCookies();
263 
264       if (!remoteCookies.empty())
265       {
266          for (const auto& pair : remoteCookies)
267          {
268             addCookie(pair.second);
269          }
270       }
271       else
272       {
273          // cookies were empty, meaning they needed to be cleared (for example, due to sign out)
274          // ensure that all cookies for the domain are cleared out
275          for (auto it = cookieMap.cbegin(); it != cookieMap.cend();)
276          {
277             if (pRemoteSessionLauncher_->sessionServer().cookieBelongs(it->second))
278             {
279                it = cookieMap.erase(it);
280             }
281             else
282             {
283                ++it;
284             }
285          }
286       }
287    }
288 
289    if (pLauncher_)
290    {
291       std::map<std::string, QNetworkCookie> cookies = pLauncher_->getCookies();
292       for (const auto& pair : cookies)
293       {
294          addCookie(pair.second);
295       }
296    }
297 
298    QList<QNetworkCookie> mergedCookies;
299    for (const auto& pair: cookieMap)
300    {
301       mergedCookies.push_back(pair.second);
302    }
303 
304    saveCookies(mergedCookies);
305 }
306 
launchRemoteRStudio()307 void MainWindow::launchRemoteRStudio()
308 {
309    saveRemoteAuthCookies(boost::bind(&Options::tempAuthCookies, &options()),
310                          boost::bind(&Options::setTempAuthCookies, &options(), _1),
311                          true);
312 
313    std::vector<std::string> args;
314    args.push_back(kSessionServerOption);
315    args.push_back(pRemoteSessionLauncher_->sessionServer().url());
316    args.push_back(kTempCookiesOption);
317 
318    pAppLauncher_->launchRStudio(args);
319 }
320 
launchRemoteRStudioProject(const QString & projectUrl)321 void MainWindow::launchRemoteRStudioProject(const QString& projectUrl)
322 {
323    saveRemoteAuthCookies(boost::bind(&Options::tempAuthCookies, &options()),
324                          boost::bind(&Options::setTempAuthCookies, &options(), _1),
325                          true);
326 
327    std::vector<std::string> args;
328    args.push_back(kSessionServerOption);
329    args.push_back(pRemoteSessionLauncher_->sessionServer().url());
330    args.push_back(kSessionServerUrlOption);
331    args.push_back(projectUrl.toStdString());
332    args.push_back(kTempCookiesOption);
333 
334    pAppLauncher_->launchRStudio(args);
335 }
336 
workbenchInitialized()337 bool MainWindow::workbenchInitialized()
338 {
339     return workbenchInitialized_;
340 }
341 
setErrorDisplayed()342 void MainWindow::setErrorDisplayed()
343 {
344    LOCK_MUTEX(mutex_)
345    {
346       isErrorDisplayed_ = true;
347    }
348    END_LOCK_MUTEX
349 }
350 
onWorkbenchInitialized()351 void MainWindow::onWorkbenchInitialized()
352 {
353    //QTimer::singleShot(300, this, SLOT(resetMargins()));
354 
355    // reset state (in case this occurred in response to a manual reload
356    // or reload for a new project context)
357    quitConfirmed_ = false;
358    geometrySaved_ = false;
359    workbenchInitialized_ = true;
360 
361    webPage()->runJavaScript(
362             QStringLiteral("window.desktopHooks.getActiveProjectDir()"),
363             [&](QVariant qProjectDir)
364    {
365       QString projectDir = qProjectDir.toString();
366 
367       if (projectDir.length() > 0)
368       {
369          setWindowTitle(tr("%1 - %2").arg(projectDir).arg(desktop::activation().editionName()));
370          DockTileView::setLabel(projectDir);
371       }
372       else
373       {
374          setWindowTitle(desktop::activation().editionName());
375          DockTileView::setLabel(QString());
376       }
377 
378       avoidMoveCursorIfNecessary();
379    });
380 
381 #ifdef Q_OS_MAC
382    webView()->setFocus();
383 #endif
384 }
385 
resetMargins()386 void MainWindow::resetMargins()
387 {
388    setContentsMargins(0, 0, 0, 0);
389 }
390 
391 // this notification occurs when windows or X11 is shutting
392 // down -- in this case we want to be a good citizen and just
393 // exit right away so we notify the gwt callback that a legit
394 // quit and exit is on the way and we set the quitConfirmed_
395 // flag so no prompting occurs (note that source documents
396 // have already been auto-saved so will be restored next time
397 // the current project context is opened)
commitDataRequest(QSessionManager & manager)398 void MainWindow::commitDataRequest(QSessionManager &manager)
399 {
400    gwtCallback_.setPendingQuit(PendingQuitAndExit);
401    quitConfirmed_ = true;
402 }
403 
loadUrl(const QUrl & url)404 void MainWindow::loadUrl(const QUrl& url)
405 {
406    webView()->setBaseUrl(url);
407    webView()->load(url);
408 }
409 
loadRequest(const QWebEngineHttpRequest & request)410 void MainWindow::loadRequest(const QWebEngineHttpRequest& request)
411 {
412    webView()->setBaseUrl(request.url());
413    webView()->load(request);
414 }
415 
loadHtml(const QString & html)416 void MainWindow::loadHtml(const QString& html)
417 {
418    webView()->setHtml(html);
419 }
420 
getPageProfile()421 QWebEngineProfile* MainWindow::getPageProfile()
422 {
423    return webView()->page()->profile();
424 }
425 
quit()426 void MainWindow::quit()
427 {
428    RCommandEvaluator::setMainWindow(nullptr);
429    quitConfirmed_ = true;
430    close();
431 }
432 
invokeCommand(QString commandId)433 void MainWindow::invokeCommand(QString commandId)
434 {
435 #ifdef Q_OS_MAC
436    QString fmt = QStringLiteral(R"EOF(
437 var wnd;
438 try {
439    wnd = window.$RStudio.last_focused_window;
440 } catch (e) {
441    wnd = window;
442 }
443 (wnd || window).desktopHooks.invokeCommand('%1');
444 )EOF");
445 #else
446    QString fmt = QStringLiteral("window.desktopHooks.invokeCommand('%1')");
447 #endif
448 
449    QString command = fmt.arg(commandId);
450    webPage()->runJavaScript(command);
451 }
452 
runJavaScript(QString script)453 void MainWindow::runJavaScript(QString script)
454 {
455    webPage()->runJavaScript(script);
456 }
457 
458 namespace {
459 
closeAllSatellites(QWidget * mainWindow)460 void closeAllSatellites(QWidget* mainWindow)
461 {
462    for (auto window: QApplication::topLevelWidgets())
463       if (window != mainWindow)
464          window->close();
465 }
466 
467 } // end anonymous namespace
468 
onSessionQuit()469 void MainWindow::onSessionQuit()
470 {
471    if (isRemoteDesktop_)
472    {
473       int pendingQuit = collectPendingQuitRequest();
474       if (pendingQuit == PendingQuitAndExit || quitConfirmed_)
475       {
476          closeAllSatellites(this);
477          quit();
478       }
479    }
480 }
481 
closeEvent(QCloseEvent * pEvent)482 void MainWindow::closeEvent(QCloseEvent* pEvent)
483 {
484 #ifdef _WIN32
485    if (eventHook_)
486       ::UnhookWinEvent(eventHook_);
487 #endif
488 
489    desktopInfo().onClose();
490    saveRemoteAuthCookies(boost::bind(&Options::authCookies, &options()),
491                          boost::bind(&Options::setAuthCookies, &options(), _1),
492                          false);
493 
494    if (!geometrySaved_)
495    {
496       desktop::options().saveMainWindowBounds(this);
497       geometrySaved_ = true;
498    }
499 
500    CloseServerSessions close = sessionServerSettings().closeServerSessionsOnExit();
501 
502    if (quitConfirmed_ ||
503        (!isRemoteDesktop_ && pCurrentSessionProcess_ == nullptr) ||
504        (!isRemoteDesktop_ && pCurrentSessionProcess_->state() != QProcess::Running))
505    {
506       closeAllSatellites(this);
507       pEvent->accept();
508       return;
509    }
510 
511    auto quit = [this]()
512    {
513       closeAllSatellites(this);
514       this->quit();
515    };
516 
517    pEvent->ignore();
518    webPage()->runJavaScript(
519             QStringLiteral("!!window.desktopHooks"),
520             [=](QVariant hasQuitR) {
521 
522       if (!hasQuitR.toBool())
523       {
524          LOG_ERROR_MESSAGE("Main window closed unexpectedly");
525 
526          // exit to avoid user having to kill/force-close the application
527          quit();
528       }
529       else
530       {
531          if (!isRemoteDesktop_ ||
532              close == CloseServerSessions::Always)
533          {
534             webPage()->runJavaScript(
535                      QStringLiteral("window.desktopHooks.quitR()"),
536                      [=](QVariant ignored)
537             {
538                quitConfirmed_ = true;
539             });
540          }
541          else if (close == CloseServerSessions::Never)
542          {
543             quit();
544          }
545          else
546          {
547             webPage()->runJavaScript(
548                      QStringLiteral("window.desktopHooks.promptToQuitR()"),
549                      [=](QVariant ignored)
550             {
551                quitConfirmed_ = true;
552             });
553          }
554       }
555    });
556 }
557 
setMenuBar(QMenuBar * pMenubar)558 void MainWindow::setMenuBar(QMenuBar *pMenubar)
559 {
560    delete menuBar();
561    this->QMainWindow::setMenuBar(pMenubar);
562 }
563 
openFileInRStudio(QString path)564 void MainWindow::openFileInRStudio(QString path)
565 {
566    QFileInfo fileInfo(path);
567    if (!fileInfo.isAbsolute() || !fileInfo.exists() || !fileInfo.isFile())
568       return;
569 
570    path = path.replace(QString::fromUtf8("\\"), QString::fromUtf8("\\\\"))
571          .replace(QString::fromUtf8("\""), QString::fromUtf8("\\\""))
572          .replace(QString::fromUtf8("\n"), QString::fromUtf8("\\n"));
573 
574    webView()->page()->runJavaScript(
575             QString::fromUtf8("window.desktopHooks.openFile(\"") + path + QString::fromUtf8("\")"));
576 }
577 
onPdfViewerClosed(QString pdfPath)578 void MainWindow::onPdfViewerClosed(QString pdfPath)
579 {
580    webView()->page()->runJavaScript(
581             QString::fromUtf8("window.synctexNotifyPdfViewerClosed(\"") +
582             pdfPath + QString::fromUtf8("\")"));
583 }
584 
onPdfViewerSyncSource(QString srcFile,int line,int column)585 void MainWindow::onPdfViewerSyncSource(QString srcFile, int line, int column)
586 {
587    boost::format fmt("window.desktopSynctexInverseSearch(\"%1%\", %2%, %3%)");
588    std::string js = boost::str(fmt % srcFile.toStdString() % line % column);
589    webView()->page()->runJavaScript(QString::fromStdString(js));
590 }
591 
onLicenseLost(QString licenseMessage)592 void MainWindow::onLicenseLost(QString licenseMessage)
593 {
594    webView()->page()->runJavaScript(
595             QString::fromUtf8("window.desktopHooks.licenseLost('") + licenseMessage +
596             QString::fromUtf8("');"));
597 }
598 
onUpdateLicenseWarningBar(QString message)599 void MainWindow::onUpdateLicenseWarningBar(QString message)
600 {
601    webView()->page()->runJavaScript(
602             QString::fromUtf8("window.desktopHooks.updateLicenseWarningBar('") + message +
603             QString::fromUtf8("');"));
604 }
605 
606 // private interface for SessionLauncher
607 
setSessionLauncher(SessionLauncher * pSessionLauncher)608 void MainWindow::setSessionLauncher(SessionLauncher* pSessionLauncher)
609 {
610    pSessionLauncher_ = pSessionLauncher;
611 }
612 
getRemoteDesktopSessionLauncher()613 RemoteDesktopSessionLauncher* MainWindow::getRemoteDesktopSessionLauncher()
614 {
615    return pRemoteSessionLauncher_;
616 }
617 
getJobLauncher()618 boost::shared_ptr<JobLauncher> MainWindow::getJobLauncher()
619 {
620    return pLauncher_;
621 }
622 
setRemoteDesktopSessionLauncher(RemoteDesktopSessionLauncher * pSessionLauncher)623 void MainWindow::setRemoteDesktopSessionLauncher(RemoteDesktopSessionLauncher* pSessionLauncher)
624 {
625    pRemoteSessionLauncher_ = pSessionLauncher;
626 }
627 
setSessionProcess(QProcess * pSessionProcess)628 void MainWindow::setSessionProcess(QProcess* pSessionProcess)
629 {
630    pCurrentSessionProcess_ = pSessionProcess;
631 
632    // when R creates dialogs (e.g. through utils::askYesNo), their first
633    // invocation might show behind the RStudio window. this allows us
634    // to detect when those Windows are opened and focused, and raise them
635    // to the front.
636 #ifdef _WIN32
637    if (eventHook_)
638       ::UnhookWinEvent(eventHook_);
639 
640    if (pSessionProcess)
641    {
642       eventHook_ = ::SetWinEventHook(
643                EVENT_SYSTEM_DIALOGSTART, EVENT_SYSTEM_DIALOGSTART,
644                nullptr,
645                onDialogStart,
646                pSessionProcess->processId(),
647                0,
648                WINEVENT_OUTOFCONTEXT);
649    }
650 #endif
651 
652 }
653 
setAppLauncher(ApplicationLaunch * pAppLauncher)654 void MainWindow::setAppLauncher(ApplicationLaunch *pAppLauncher)
655 {
656     pAppLauncher_ = pAppLauncher;
657 }
658 
659 // allow SessionLauncher to collect restart requests from GwtCallback
collectPendingQuitRequest()660 int MainWindow::collectPendingQuitRequest()
661 {
662    return gwtCallback_.collectPendingQuitRequest();
663 }
664 
desktopHooksAvailable()665 bool MainWindow::desktopHooksAvailable()
666 {
667    return desktopInfo().desktopHooksAvailable();
668 }
669 
onActivated()670 void MainWindow::onActivated()
671 {
672 }
673 
onUrlChanged(QUrl url)674 void MainWindow::onUrlChanged(QUrl url)
675 {
676    urlChanged(url);
677 }
678 
reload()679 void MainWindow::reload()
680 {
681    s_reloadCount++;
682    loadUrl(webView()->baseUrl());
683 }
684 
onLoadFinished(bool ok)685 void MainWindow::onLoadFinished(bool ok)
686 {
687    LOCK_MUTEX(mutex_)
688    {
689       if (ok)
690       {
691          // we've successfully loaded!
692       }
693       else if (isErrorDisplayed_)
694       {
695          // the session failed to launch and we're already showing
696          // an error page to the user; nothing else to do here.
697       }
698       else
699       {
700          if (s_reloadCount < s_maxReloadCount)
701          {
702             // the load failed, but we haven't yet received word that the
703             // session has failed to load. let the user know that the R
704             // session is still initializing, and then reload the page.
705             std::map<std::string, std::string> vars = {};
706 
707             std::ostringstream oss;
708             Error error = text::renderTemplate(
709                      options().resourcesPath().completePath("html/loading.html"),
710                      vars,
711                      oss);
712 
713             if (error)
714                LOG_ERROR(error);
715 
716             loadHtml(QString::fromStdString(oss.str()));
717 
718             QTimer::singleShot(
719                      s_reloadWaitDuration * s_reloadCount,
720                      [&]() { reload(); });
721          }
722          else
723          {
724             s_reloadCount = 0;
725             onLoadFailed();
726          }
727       }
728    }
729    END_LOCK_MUTEX
730 }
731 
onLoadFailed()732 void MainWindow::onLoadFailed()
733 {
734    if (pRemoteSessionLauncher_ || isErrorDisplayed_)
735       return;
736 
737    std::map<std::string, std::string> vars = {
738       { "url",  webView()->baseUrl().toString().toStdString() }
739    };
740 
741    std::ostringstream oss;
742    Error error = text::renderTemplate(
743             options().resourcesPath().completePath("html/connect.html"),
744             vars,
745             oss);
746 
747    if (error)
748    {
749       LOG_ERROR(error);
750       return;
751    }
752 
753    loadHtml(QString::fromStdString(oss.str()));
754 }
755 
getWebView()756 WebView* MainWindow::getWebView()
757 {
758    return webView();
759 }
760 
761 } // namespace desktop
762 } // namespace rstudio
763