1 /*
2     SuperCollider Qt IDE
3     Copyright (c) 2012 Jakob Leben & Tim Blechmann
4     http://www.audiosynth.com
5 
6     This program is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10 
11     This program is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with this program; if not, write to the Free Software
18     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
19 */
20 
21 #define QT_NO_DEBUG_OUTPUT
22 
23 #include "cmd_line.hpp"
24 #include "doc_list.hpp"
25 #include "documents_dialog.hpp"
26 #include "find_replace_tool.hpp"
27 #include "goto_line_tool.hpp"
28 #include "lookup_dialog.hpp"
29 #include "main_window.hpp"
30 #include "multi_editor.hpp"
31 #include "popup_text_input.hpp"
32 #include "post_window.hpp"
33 #include "session_switch_dialog.hpp"
34 #include "sessions_dialog.hpp"
35 #include "tool_box.hpp"
36 #include "audio_status_box.hpp"
37 #include "lang_status_box.hpp"
38 #include "../core/main.hpp"
39 #include "../core/doc_manager.hpp"
40 #include "../core/session_manager.hpp"
41 #include "../core/sc_server.hpp"
42 #include "../core/util/standard_dirs.hpp"
43 #include "code_editor/sc_editor.hpp"
44 #include "settings/dialog.hpp"
45 
46 #ifdef SC_USE_QTWEBENGINE
47 #    include "help_browser.hpp"
48 #endif // SC_USE_QTWEBENGINE
49 
50 #include "QtCollider/hacks/hacks_qt.hpp"
51 
52 #include "SC_Version.hpp"
53 
54 #include <QAction>
55 #include <QApplication>
56 #include <QDesktopServices>
57 #include <QStandardPaths>
58 #include <QDesktopWidget>
59 #include <QFileDialog>
60 #include <QFileInfo>
61 #include <QGridLayout>
62 #include <QInputDialog>
63 #include <QMenu>
64 #include <QMenuBar>
65 #include <QMessageBox>
66 #include <QPointer>
67 #include <QShortcut>
68 #include <QStatusBar>
69 #include <QVBoxLayout>
70 #include <QUrl>
71 #include <QMimeData>
72 #include <QMetaMethod>
73 
74 namespace ScIDE {
75 
findFirstResponder(QWidget * widget,const char * methodSignature,int & methodIndex)76 static QWidget* findFirstResponder(QWidget* widget, const char* methodSignature, int& methodIndex) {
77     methodIndex = -1;
78     while (widget) {
79         methodIndex = widget->metaObject()->indexOfMethod(methodSignature);
80         if (methodIndex != -1)
81             break;
82         if (widget->isWindow())
83             break;
84         widget = widget->parentWidget();
85     }
86     return widget;
87 }
88 
invokeMethodOnFirstResponder(QByteArray const & signature)89 static void invokeMethodOnFirstResponder(QByteArray const& signature) {
90     int methodIdx = -1;
91     QWidget* widget = findFirstResponder(QApplication::focusWidget(), signature.constData(), methodIdx);
92     if (widget && methodIdx != -1)
93         widget->metaObject()->method(methodIdx).invoke(widget, Qt::DirectConnection);
94 }
95 
96 MainWindow* MainWindow::mInstance = 0;
97 
MainWindow(Main * main)98 MainWindow::MainWindow(Main* main): mMain(main), mClockLabel(0), mDocDialog(0) {
99     Q_ASSERT(!mInstance);
100     mInstance = this;
101 
102     setAcceptDrops(true);
103 
104     // Construct status bar:
105 
106     mLangStatus = new LangStatusBox(main->scProcess());
107     mServerStatus = new AudioStatusBox(main->scServer());
108 
109     mStatusBar = statusBar();
110     mStatusBar->addPermanentWidget(new QLabel(tr("Interpreter:")));
111     mStatusBar->addPermanentWidget(mLangStatus);
112     mStatusBar->addPermanentWidget(new QLabel(tr("Server:")));
113     mStatusBar->addPermanentWidget(mServerStatus);
114 
115     // Code editor
116     mEditors = new MultiEditor(main);
117 
118     // Tools
119 
120     mCmdLine = new CmdLine(tr("Command Line:"));
121     connect(mCmdLine, SIGNAL(invoked(QString, bool)), main->scProcess(), SLOT(evaluateCode(QString, bool)));
122 
123     mFindReplaceTool = new TextFindReplacePanel;
124 
125     mGoToLineTool = new GoToLineTool();
126     connect(mGoToLineTool, SIGNAL(activated(int)), this, SLOT(hideToolBox()));
127 
128     mToolBox = new ToolBox;
129     mToolBox->addWidget(mCmdLine);
130     mToolBox->addWidget(mFindReplaceTool);
131     mToolBox->addWidget(mGoToLineTool);
132     mToolBox->hide();
133 
134     // Docks
135     mDocumentsDocklet = new DocumentsDocklet(main->documentManager(), this);
136     mDocumentsDocklet->setObjectName("documents-dock");
137     addDockWidget(Qt::LeftDockWidgetArea, mDocumentsDocklet->dockWidget());
138     mDocumentsDocklet->hide();
139 
140 #ifdef SC_USE_QTWEBENGINE
141     mHelpBrowserDocklet = new HelpBrowserDocklet(this);
142     mHelpBrowserDocklet->setObjectName("help-dock");
143     addDockWidget(Qt::RightDockWidgetArea, mHelpBrowserDocklet->dockWidget());
144     // mHelpBrowserDockable->hide();
145 #endif // SC_USE_QTWEBENGINE
146 
147     mPostDocklet = new PostDocklet(this);
148     mPostDocklet->setObjectName("post-dock");
149     addDockWidget(Qt::RightDockWidgetArea, mPostDocklet->dockWidget());
150 
151     // Layout
152     QVBoxLayout* center_box = new QVBoxLayout;
153     center_box->setContentsMargins(0, 0, 0, 0);
154     center_box->setSpacing(0);
155     center_box->addWidget(mEditors);
156     center_box->addWidget(mToolBox);
157 
158     QWidget* central = new QWidget;
159     central->setLayout(center_box);
160     setCentralWidget(central);
161 
162     // Session management
163     connect(main->sessionManager(), SIGNAL(saveSessionRequest(Session*)), this, SLOT(saveSession(Session*)));
164     connect(main->sessionManager(), SIGNAL(switchSessionRequest(Session*)), this, SLOT(switchSession(Session*)));
165     connect(main->sessionManager(), SIGNAL(currentSessionNameChanged()), this, SLOT(updateWindowTitle()));
166     // A system for easy evaluation of pre-defined code:
167     connect(this, SIGNAL(evaluateCode(QString, bool)), main->scProcess(), SLOT(evaluateCode(QString, bool)));
168     // Interpreter: post output
169     connect(main->scProcess(), SIGNAL(scPost(QString)), mPostDocklet->mPostWindow, SLOT(post(QString)));
170     // Interpreter: monitor running state
171     connect(main->scProcess(), SIGNAL(stateChanged(QProcess::ProcessState)), this,
172             SLOT(onInterpreterStateChanged(QProcess::ProcessState)));
173     // Interpreter: forward status messages
174     connect(main->scProcess(), SIGNAL(statusMessage(const QString&)), this, SLOT(showStatusMessage(const QString&)));
175 
176     // Document list interaction
177     connect(mDocumentsDocklet->list(), SIGNAL(clicked(Document*)), mEditors, SLOT(setCurrent(Document*)));
178     connect(mEditors, SIGNAL(currentDocumentChanged(Document*)), mDocumentsDocklet->list(), SLOT(setCurrent(Document*)),
179             Qt::QueuedConnection);
180     connect(mDocumentsDocklet->list(), SIGNAL(updateTabsOrder(QList<Document*>)), mEditors,
181             SLOT(updateTabsOrder(QList<Document*>)));
182     connect(mEditors, SIGNAL(updateDockletOrder(int, int)), mDocumentsDocklet->list(),
183             SLOT(updateDockletOrder(int, int)), Qt::QueuedConnection);
184 
185     // Update actions on document change
186     connect(mEditors, SIGNAL(currentDocumentChanged(Document*)), this, SLOT(onCurrentDocumentChanged(Document*)));
187     // Document management
188     DocumentManager* docMng = main->documentManager();
189     connect(docMng, SIGNAL(changedExternally(Document*)), this, SLOT(onDocumentChangedExternally(Document*)));
190     connect(docMng, SIGNAL(recentsChanged()), this, SLOT(updateRecentDocsMenu()));
191     connect(docMng, SIGNAL(saved(Document*)), this, SLOT(updateWindowTitle()));
192     connect(docMng, SIGNAL(titleChanged(Document*)), this, SLOT(updateWindowTitle()));
193 
194     connect(main, SIGNAL(applySettingsRequest(Settings::Manager*)), this, SLOT(applySettings(Settings::Manager*)));
195     connect(main, SIGNAL(storeSettingsRequest(Settings::Manager*)), this, SLOT(storeSettings(Settings::Manager*)));
196 
197     // ToolBox
198     connect(mToolBox->closeButton(), SIGNAL(clicked()), this, SLOT(hideToolBox()));
199 
200     createActions();
201     createMenus();
202 
203     // Must be called after createAtions(), because it accesses an action:
204     toggleInterpreterActions(false);
205 
206     // Initialize recent documents menu
207     updateRecentDocsMenu();
208 
209     QIcon icon;
210     // Unfortunately, the SVG icon shows up as a tiny dot on some Linux window
211     // managers (see #3905, #2646). Best we can do here is PNGs.
212     // icon.addFile(":icons/sc-ide-svg");
213     icon.addFile(":icons/sc-ide-16");
214     icon.addFile(":icons/sc-ide-24");
215     icon.addFile(":icons/sc-ide-32");
216     icon.addFile(":icons/sc-ide-48");
217     icon.addFile(":icons/sc-ide-64");
218     icon.addFile(":icons/sc-ide-128");
219     icon.addFile(":icons/sc-ide-256");
220     icon.addFile(":icons/sc-ide-512");
221     icon.addFile(":icons/sc-ide-1024");
222     QApplication::setWindowIcon(icon);
223 
224     updateWindowTitle();
225 
226     applyCursorBlinkingSettings(main->settings());
227 
228     // Custom event handling:
229     qApp->installEventFilter(this);
230 }
231 
createActions()232 void MainWindow::createActions() {
233     Settings::Manager* settings = mMain->settings();
234 
235     QAction* action;
236     const QString ideCategory("IDE");
237     const QString editorCategory(tr("Text Editor"));
238     const QString helpCategory(tr("Help"));
239 
240     // File
241     mActions[Quit] = action = new QAction(QIcon::fromTheme("application-exit"), tr("&Quit..."), this);
242     action->setShortcut(tr("Ctrl+Q", "Quit application"));
243     action->setStatusTip(tr("Quit SuperCollider IDE"));
244     // explicitly states that this action can be triggered by macOS QUIT events
245     // (such as cmd+q or window closing)
246     action->setMenuRole(QAction::QuitRole);
247 
248     QObject::connect(action, SIGNAL(triggered()), this, SLOT(onQuit()));
249     settings->addAction(action, "ide-quit", ideCategory);
250 
251     mActions[DocNew] = action = new QAction(QIcon::fromTheme("document-new"), tr("&New"), this);
252     action->setShortcut(tr("Ctrl+N", "New document"));
253     action->setStatusTip(tr("Create a new document"));
254     connect(action, SIGNAL(triggered()), this, SLOT(newDocument()));
255     settings->addAction(action, "ide-document-new", ideCategory);
256 
257     mActions[DocOpen] = action = new QAction(QIcon::fromTheme("document-open"), tr("&Open..."), this);
258     action->setShortcut(tr("Ctrl+O", "Open document"));
259     action->setStatusTip(tr("Open an existing file"));
260     connect(action, SIGNAL(triggered()), this, SLOT(openDocument()));
261     settings->addAction(action, "ide-document-open", ideCategory);
262 
263     mActions[DocOpenStartup] = action = new QAction(QIcon::fromTheme("document-open"), tr("Open startup file"), this);
264     action->setStatusTip(tr("Open startup file"));
265     connect(action, SIGNAL(triggered()), this, SLOT(openStartupFile()));
266     settings->addAction(action, "ide-document-open-startup", ideCategory);
267 
268     mActions[DocOpenSupportDir] = action =
269         new QAction(QIcon::fromTheme("document-open"), tr("Open user support directory"), this);
270     action->setStatusTip(tr("Open user support directory"));
271     connect(action, SIGNAL(triggered()), this, SLOT(openUserSupportDirectory()));
272     settings->addAction(action, "ide-document-open-support-directory", ideCategory);
273 
274     mActions[DocSave] = action = new QAction(QIcon::fromTheme("document-save"), tr("&Save"), this);
275     action->setShortcut(tr("Ctrl+S", "Save document"));
276     action->setStatusTip(tr("Save the current document"));
277     connect(action, SIGNAL(triggered()), this, SLOT(saveDocument()));
278     settings->addAction(action, "ide-document-save", ideCategory);
279 
280     mActions[DocSaveAs] = action = new QAction(QIcon::fromTheme("document-save-as"), tr("Save &As..."), this);
281     action->setShortcut(tr("Ctrl+Shift+S", "Save &As..."));
282     action->setStatusTip(tr("Save the current document into a different file"));
283     connect(action, SIGNAL(triggered()), this, SLOT(saveDocumentAs()));
284     settings->addAction(action, "ide-document-save-as", ideCategory);
285 
286     mActions[DocSaveAsExtension] = action =
287         new QAction(QIcon::fromTheme("document-save-as"), tr("Save As Extension..."), this);
288     action->setStatusTip(tr("Save the current document into a different file in the extensions folder"));
289     connect(action, SIGNAL(triggered()), this, SLOT(saveDocumentAsExtension()));
290     settings->addAction(action, "ide-document-save-as-extension", ideCategory);
291 
292     mActions[DocSaveAll] = action = new QAction(QIcon::fromTheme("document-save"), tr("Save All..."), this);
293     action->setShortcut(tr("Ctrl+Alt+S", "Save all documents"));
294     action->setStatusTip(tr("Save all open documents"));
295     connect(action, SIGNAL(triggered()), this, SLOT(saveAllDocuments()));
296     settings->addAction(action, "ide-document-save-all", ideCategory);
297 
298     mActions[DocCloseAll] = action = new QAction(QIcon::fromTheme("window-close"), tr("Close All..."), this);
299     action->setShortcut(tr("Ctrl+Shift+W", "Close all documents"));
300     action->setStatusTip(tr("Close all documents"));
301     connect(action, SIGNAL(triggered()), this, SLOT(closeAllDocuments()));
302     settings->addAction(action, "ide-document-close-all", ideCategory);
303 
304     mActions[DocReload] = action = new QAction(QIcon::fromTheme("view-refresh"), tr("&Reload"), this);
305     action->setShortcut(tr("F5", "Reload document"));
306     action->setStatusTip(tr("Reload the current document"));
307     connect(action, SIGNAL(triggered()), this, SLOT(reloadDocument()));
308     settings->addAction(action, "ide-document-reload", ideCategory);
309 
310     mActions[ClearRecentDocs] = action = new QAction(tr("Clear", "Clear recent documents"), this);
311     action->setStatusTip(tr("Clear list of recent documents"));
312     connect(action, SIGNAL(triggered()), Main::instance()->documentManager(), SLOT(clearRecents()));
313     settings->addAction(action, "ide-clear-recent-documents", ideCategory);
314 
315     // Sessions
316     mActions[NewSession] = action = new QAction(QIcon::fromTheme("document-new"), tr("&New Session"), this);
317     action->setStatusTip(tr("Open a new session"));
318     connect(action, SIGNAL(triggered()), this, SLOT(newSession()));
319     settings->addAction(action, "ide-session-new", ideCategory);
320 
321     mActions[SaveSessionAs] = action =
322         new QAction(QIcon::fromTheme("document-save-as"), tr("Save Session &As..."), this);
323     action->setStatusTip(tr("Save the current session with a different name"));
324     connect(action, SIGNAL(triggered()), this, SLOT(saveCurrentSessionAs()));
325     settings->addAction(action, "ide-session-save-as", ideCategory);
326 
327     mActions[ManageSessions] = action = new QAction(tr("&Manage Sessions..."), this);
328     connect(action, SIGNAL(triggered()), this, SLOT(openSessionsDialog()));
329     settings->addAction(action, "ide-session-manage", ideCategory);
330 
331     mActions[OpenSessionSwitchDialog] = action = new QAction(tr("&Switch Session..."), this);
332     connect(action, SIGNAL(triggered()), this, SLOT(showSwitchSessionDialog()));
333     action->setShortcut(tr("Ctrl+Shift+Q", "Switch Session"));
334     settings->addAction(action, "ide-session-switch", ideCategory);
335 
336     // Edit
337     mActions[Find] = action = new QAction(QIcon::fromTheme("edit-find"), tr("&Find..."), this);
338     action->setShortcut(tr("Ctrl+F", "Find"));
339     action->setStatusTip(tr("Find text in document"));
340     connect(action, SIGNAL(triggered()), this, SLOT(showFindTool()));
341     settings->addAction(action, "editor-find", editorCategory);
342 
343     mActions[Replace] = action = new QAction(QIcon::fromTheme("edit-replace"), tr("&Replace..."), this);
344     action->setShortcut(tr("Ctrl+R", "Replace"));
345     action->setStatusTip(tr("Find and replace text in document"));
346     connect(action, SIGNAL(triggered()), this, SLOT(showReplaceTool()));
347     settings->addAction(action, "editor-replace", editorCategory);
348 
349     // View
350     mActions[ShowCmdLine] = action = new QAction(tr("&Command Line"), this);
351     action->setStatusTip(tr("Command line for quick code evaluation"));
352     action->setShortcut(tr("Ctrl+E", "Show command line"));
353     connect(action, SIGNAL(triggered()), this, SLOT(showCmdLine()));
354     settings->addAction(action, "ide-command-line-show", ideCategory);
355 
356     mActions[CmdLineForCursor] = action = new QAction(tr("&Command Line from selection"), this);
357     action->setShortcut(tr("Ctrl+Shift+E", "Fill command line with current selection"));
358     connect(action, SIGNAL(triggered()), this, SLOT(cmdLineForCursor()));
359     settings->addAction(action, "ide-command-line-fill", ideCategory);
360 
361 
362     mActions[ShowGoToLineTool] = action = new QAction(tr("&Go To Line"), this);
363     action->setStatusTip(tr("Tool to jump to a line by number"));
364     action->setShortcut(tr("Ctrl+L", "Show go-to-line tool"));
365     connect(action, SIGNAL(triggered()), this, SLOT(showGoToLineTool()));
366     settings->addAction(action, "editor-go-to-line", editorCategory);
367 
368     mActions[CloseToolBox] = action = new QAction(QIcon::fromTheme("window-close"), tr("&Close Tool Panel"), this);
369     action->setStatusTip(tr("Close any open tool panel"));
370     action->setShortcut(tr("Esc", "Close tool box"));
371     connect(action, SIGNAL(triggered()), this, SLOT(hideToolBox()));
372     settings->addAction(action, "ide-tool-panel-hide", ideCategory);
373 
374     mActions[ShowFullScreen] = action = new QAction(tr("&Full Screen"), this);
375     action->setCheckable(false);
376     action->setShortcut(tr("Ctrl+Shift+F", "Show ScIDE in Full Screen"));
377     connect(action, SIGNAL(triggered()), this, SLOT(toggleFullScreen()));
378     settings->addAction(action, "ide-show-fullscreen", ideCategory);
379 
380     mActions[FocusPostWindow] = action = new QAction(tr("Focus Post Window"), this);
381     action->setStatusTip(tr("Focus post window"));
382     action->setShortcut(tr("Ctrl+P", "Focus post window"));
383     connect(action, SIGNAL(triggered()), mPostDocklet, SLOT(focus()));
384     settings->addAction(action, "post-focus", ideCategory);
385 
386     // Language
387     mActions[LookupImplementation] = action =
388         new QAction(QIcon::fromTheme("window-lookupdefinition"), tr("Look Up Implementations..."), this);
389     action->setShortcut(tr("Ctrl+Shift+I", "Look Up Implementations"));
390     action->setStatusTip(tr("Open dialog to look up implementations of a class or a method"));
391     connect(action, SIGNAL(triggered()), this, SLOT(lookupImplementation()));
392     settings->addAction(action, "ide-lookup-implementation", ideCategory);
393 
394     mActions[LookupImplementationForCursor] = action = new QAction(tr("Look Up Implementations for Cursor"), this);
395     action->setShortcut(tr("Ctrl+I", "Look Up Implementations for Cursor"));
396     action->setStatusTip(tr("Look up implementations of class or method under cursor"));
397     connect(action, SIGNAL(triggered(bool)), this, SLOT(lookupImplementationForCursor()));
398     settings->addAction(action, "ide-lookup-implementation-for-cursor", ideCategory);
399 
400     mActions[LookupReferences] = action =
401         new QAction(QIcon::fromTheme("window-lookupreferences"), tr("Look Up References..."), this);
402     action->setShortcut(tr("Ctrl+Shift+U", "Look Up References"));
403     action->setStatusTip(tr("Open dialog to look up references to a class or a method"));
404     connect(action, SIGNAL(triggered()), this, SLOT(lookupReferences()));
405     settings->addAction(action, "ide-lookup-references", ideCategory);
406 
407     mActions[LookupReferencesForCursor] = action = new QAction(tr("Look Up References for Cursor"), this);
408     action->setShortcut(tr("Ctrl+U", "Look Up References For Selection"));
409     action->setStatusTip(tr("Look up references to class or method under cursor"));
410     connect(action, SIGNAL(triggered(bool)), this, SLOT(lookupReferencesForCursor()));
411     settings->addAction(action, "ide-lookup-references-for-cursor", ideCategory);
412 
413     // Settings
414     mActions[ShowSettings] = action = new QAction(tr("Preferences"), this);
415 #ifdef Q_OS_MAC
416     action->setShortcut(tr("Ctrl+,", "Show configuration dialog"));
417 #endif
418     action->setStatusTip(tr("Show configuration dialog"));
419     connect(action, SIGNAL(triggered()), this, SLOT(showSettings()));
420     settings->addAction(action, "ide-settings-dialog", ideCategory);
421 
422     // Help
423     mActions[ReportABug] = action = new QAction(QIcon::fromTheme("system-help"), tr("Report a bug..."), this);
424     action->setStatusTip(tr("Report a bug"));
425     connect(action, SIGNAL(triggered()), this, SLOT(doBugReport()));
426 
427 #ifdef SC_USE_QTWEBENGINE
428     mActions[Help] = action = new QAction(tr("Show &Help Browser"), this);
429     action->setStatusTip(tr("Show and focus the Help Browser"));
430     connect(action, SIGNAL(triggered()), this, SLOT(openHelp()));
431     settings->addAction(action, "help-browser", helpCategory);
432 
433     mActions[HelpAboutIDE] = action =
434         new QAction(QIcon::fromTheme("system-help"), tr("How to Use SuperCollider IDE"), this);
435     action->setStatusTip(tr("Open the SuperCollider IDE guide"));
436     connect(action, SIGNAL(triggered()), this, SLOT(openHelpAboutIDE()));
437 
438     mActions[LookupDocumentationForCursor] = action = new QAction(tr("Look Up Documentation for Cursor"), this);
439     action->setShortcut(tr("Ctrl+D", "Look Up Documentation for Cursor"));
440     action->setStatusTip(tr("Look up documentation for text under cursor"));
441     connect(action, SIGNAL(triggered()), this, SLOT(lookupDocumentationForCursor()));
442     settings->addAction(action, "help-lookup-for-cursor", helpCategory);
443 
444     mActions[LookupDocumentation] = action = new QAction(tr("Look Up Documentation..."), this);
445     action->setShortcut(tr("Ctrl+Shift+D", "Look Up Documentation"));
446     action->setStatusTip(tr("Enter text to look up in documentation"));
447     connect(action, SIGNAL(triggered()), this, SLOT(lookupDocumentation()));
448     settings->addAction(action, "help-lookup", helpCategory);
449 #endif // SC_USE_QTWEBENGINE
450 
451     mActions[ShowAbout] = action = new QAction(QIcon::fromTheme("help-about"), tr("&About SuperCollider"), this);
452     connect(action, SIGNAL(triggered()), this, SLOT(showAbout()));
453     settings->addAction(action, "ide-about", ideCategory);
454 
455     mActions[ShowAboutQT] = action = new QAction(QIcon::fromTheme("show-about-qt"), tr("About &Qt"), this);
456     connect(action, SIGNAL(triggered()), this, SLOT(showAboutQT()));
457     settings->addAction(action, "ide-about-qt", ideCategory);
458 
459     // Add external actions to settings:
460     action = mPostDocklet->toggleViewAction();
461     action->setIcon(QIcon::fromTheme("utilities-terminal"));
462     action->setStatusTip(tr("Show/hide Post docklet"));
463     settings->addAction(mPostDocklet->toggleViewAction(), "ide-docklet-post", ideCategory);
464 
465     action = mDocumentsDocklet->toggleViewAction();
466     action->setIcon(QIcon::fromTheme("text-x-generic"));
467     action->setStatusTip(tr("Show/hide Documents docklet"));
468     settings->addAction(mDocumentsDocklet->toggleViewAction(), "ide-docklet-documents", ideCategory);
469 
470 #ifdef SC_USE_QTWEBENGINE
471     action = mHelpBrowserDocklet->toggleViewAction();
472     action->setIcon(QIcon::fromTheme("system-help"));
473     action->setStatusTip(tr("Show/hide Help browser docklet"));
474     settings->addAction(mHelpBrowserDocklet->toggleViewAction(), "ide-docklet-help", ideCategory);
475 #endif // SC_USE_QTWEBENGINE
476 
477     // In Mac OS, all menu item shortcuts need a modifier, so add the action with
478     // the "Escape" default shortcut to the main window widget.
479     // FIXME: This is not perfect, as any other action customized to "Escape" will
480     // still not work.
481     addAction(mActions[CloseToolBox]);
482 
483     // Add actions to docklets, so shortcuts work when docklets detached:
484 
485 #ifdef SC_USE_QTWEBENGINE
486     mPostDocklet->widget()->addAction(mActions[LookupDocumentation]);
487     mPostDocklet->widget()->addAction(mActions[LookupDocumentationForCursor]);
488 #endif // SC_USE_QTWEBENGINE
489     mPostDocklet->widget()->addAction(mActions[LookupImplementation]);
490     mPostDocklet->widget()->addAction(mActions[LookupImplementationForCursor]);
491     mPostDocklet->widget()->addAction(mActions[LookupReferences]);
492     mPostDocklet->widget()->addAction(mActions[LookupReferencesForCursor]);
493 
494 #ifdef SC_USE_QTWEBENGINE
495     mHelpBrowserDocklet->widget()->addAction(mActions[LookupDocumentation]);
496     mHelpBrowserDocklet->widget()->addAction(mActions[LookupDocumentationForCursor]);
497     mHelpBrowserDocklet->widget()->addAction(mActions[LookupImplementation]);
498     mHelpBrowserDocklet->widget()->addAction(mActions[LookupImplementationForCursor]);
499     mHelpBrowserDocklet->widget()->addAction(mActions[LookupReferences]);
500     mHelpBrowserDocklet->widget()->addAction(mActions[LookupReferencesForCursor]);
501 #endif // SC_USE_QTWEBENGINE
502 }
503 
createMenus()504 void MainWindow::createMenus() {
505     QMenuBar* menuBar;
506     QMenu* menu;
507     QMenu* submenu;
508 
509     // On Mac, create a parent-less menu bar to be shared by all windows:
510 #ifdef Q_OS_MAC
511     menuBar = new QMenuBar(0);
512 #else
513     menuBar = this->menuBar();
514 #endif
515 
516     menu = new QMenu(tr("&File"), this);
517     menu->addAction(mActions[DocNew]);
518     menu->addAction(mActions[DocOpen]);
519     mRecentDocsMenu = menu->addMenu(tr("Open Recent", "Open a recent document"));
520     connect(mRecentDocsMenu, SIGNAL(triggered(QAction*)), this, SLOT(onOpenRecentDocument(QAction*)));
521     menu->addAction(mActions[DocOpenStartup]);
522     menu->addAction(mActions[DocOpenSupportDir]);
523     menu->addAction(mActions[DocSave]);
524     menu->addAction(mActions[DocSaveAs]);
525     menu->addAction(mActions[DocSaveAsExtension]);
526     menu->addAction(mActions[DocSaveAll]);
527     menu->addSeparator();
528     menu->addAction(mActions[DocReload]);
529     menu->addSeparator();
530     menu->addAction(mEditors->action(MultiEditor::DocClose));
531     menu->addAction(mActions[DocCloseAll]);
532     menu->addSeparator();
533     menu->addAction(mActions[Quit]);
534 
535     menuBar->addMenu(menu);
536 
537     menu = new QMenu(tr("&Session"), this);
538     menu->addAction(mActions[NewSession]);
539     menu->addAction(mActions[SaveSessionAs]);
540     submenu = menu->addMenu(tr("&Open Session"));
541     connect(submenu, SIGNAL(triggered(QAction*)), this, SLOT(onOpenSessionAction(QAction*)));
542     mSessionsMenu = submenu;
543     updateSessionsMenu();
544     menu->addSeparator();
545     menu->addAction(mActions[ManageSessions]);
546     menu->addAction(mActions[OpenSessionSwitchDialog]);
547 
548     menuBar->addMenu(menu);
549 
550     menu = new QMenu(tr("&Edit"), this);
551     menu->addAction(mEditors->action(MultiEditor::Undo));
552     menu->addAction(mEditors->action(MultiEditor::Redo));
553     menu->addSeparator();
554     menu->addAction(mEditors->action(MultiEditor::Cut));
555     menu->addAction(mEditors->action(MultiEditor::Copy));
556     menu->addAction(mEditors->action(MultiEditor::Paste));
557     menu->addSeparator();
558     menu->addAction(mActions[Find]);
559     menu->addAction(mFindReplaceTool->action(TextFindReplacePanel::FindNext));
560     menu->addAction(mFindReplaceTool->action(TextFindReplacePanel::FindPrevious));
561     menu->addAction(mActions[Replace]);
562     menu->addSeparator();
563     menu->addAction(mEditors->action(MultiEditor::IndentWithSpaces));
564     menu->addAction(mEditors->action(MultiEditor::IndentLineOrRegion));
565     menu->addAction(mEditors->action(MultiEditor::ToggleComment));
566     menu->addAction(mEditors->action(MultiEditor::ToggleOverwriteMode));
567     menu->addAction(mEditors->action(MultiEditor::SelectRegion));
568     menu->addAction(mEditors->action(MultiEditor::SelectEnclosingBlock));
569 
570     menu->addSeparator();
571     menu->addAction(mActions[ShowSettings]);
572 
573     menuBar->addMenu(menu);
574 
575     menu = new QMenu(tr("&View"), this);
576     submenu = new QMenu(tr("&Docklets"), this);
577     submenu->addAction(mPostDocklet->toggleViewAction());
578     submenu->addAction(mDocumentsDocklet->toggleViewAction());
579 #ifdef SC_USE_QTWEBENGINE
580     submenu->addAction(mHelpBrowserDocklet->toggleViewAction());
581 #endif // SC_USE_QTWEBENGINE
582     menu->addMenu(submenu);
583     menu->addSeparator();
584     submenu = menu->addMenu(tr("&Tool Panels"));
585     submenu->addAction(mActions[Find]);
586     submenu->addAction(mActions[Replace]);
587     submenu->addAction(mActions[ShowCmdLine]);
588     submenu->addAction(mActions[CmdLineForCursor]);
589     submenu->addAction(mActions[ShowGoToLineTool]);
590     submenu->addSeparator();
591     submenu->addAction(mActions[CloseToolBox]);
592     menu->addSeparator();
593     menu->addAction(mEditors->action(MultiEditor::EnlargeFont));
594     menu->addAction(mEditors->action(MultiEditor::ShrinkFont));
595     menu->addAction(mEditors->action(MultiEditor::ResetFontSize));
596     menu->addSeparator();
597     menu->addAction(mEditors->action(MultiEditor::ShowWhitespace));
598     menu->addAction(mEditors->action(MultiEditor::ShowLinenumber));
599     menu->addSeparator();
600     menu->addAction(mEditors->action(MultiEditor::ShowAutocompleteHelp));
601     menu->addSeparator();
602     menu->addAction(mEditors->action(MultiEditor::NextDocument));
603     menu->addAction(mEditors->action(MultiEditor::PreviousDocument));
604     menu->addAction(mEditors->action(MultiEditor::SwitchDocument));
605     menu->addSeparator();
606     menu->addAction(mEditors->action(MultiEditor::SplitHorizontally));
607     menu->addAction(mEditors->action(MultiEditor::SplitVertically));
608     menu->addAction(mEditors->action(MultiEditor::RemoveCurrentSplit));
609     menu->addAction(mEditors->action(MultiEditor::RemoveAllSplits));
610     menu->addSeparator();
611     menu->addAction(mActions[FocusPostWindow]);
612 
613     menuBar->addMenu(menu);
614 
615     menu = new QMenu(tr("&Language"), this);
616     menu->addAction(mMain->scProcess()->action(ScProcess::ToggleRunning));
617     menu->addAction(mMain->scProcess()->action(ScProcess::Restart));
618     menu->addAction(mMain->scProcess()->action(ScProcess::RecompileClassLibrary));
619     menu->addSeparator();
620     menu->addAction(mMain->scProcess()->action(ScProcess::ShowQuarks));
621     menu->addSeparator();
622     menu->addAction(mEditors->action(MultiEditor::EvaluateCurrentDocument));
623     menu->addAction(mEditors->action(MultiEditor::EvaluateRegion));
624     menu->addAction(mEditors->action(MultiEditor::EvaluateLine));
625     menu->addAction(mMain->scProcess()->action(ScIDE::ScProcess::StopMain));
626     menu->addSeparator();
627     menu->addAction(mActions[LookupImplementationForCursor]);
628     menu->addAction(mActions[LookupImplementation]);
629     menu->addAction(mActions[LookupReferencesForCursor]);
630     menu->addAction(mActions[LookupReferences]);
631 
632     menuBar->addMenu(menu);
633 
634     menu = new QMenu(tr("Se&rver"), this);
635     menu->addAction(mMain->scServer()->action(ScServer::ToggleRunning));
636     menu->addAction(mMain->scServer()->action(ScServer::Reboot));
637     menu->addAction(mMain->scServer()->action(ScServer::KillAll));
638     menu->addSeparator();
639     menu->addAction(mMain->scServer()->action(ScServer::ShowMeters));
640     menu->addAction(mMain->scServer()->action(ScServer::ShowScope));
641     menu->addAction(mMain->scServer()->action(ScServer::ShowFreqScope));
642     menu->addAction(mMain->scServer()->action(ScServer::DumpNodeTree));
643     menu->addAction(mMain->scServer()->action(ScServer::DumpNodeTreeWithControls));
644     menu->addAction(mMain->scServer()->action(ScServer::PlotTree));
645     menu->addAction(mMain->scServer()->action(ScServer::DumpOSC));
646     menu->addAction(mMain->scServer()->action(ScServer::Record));
647     menu->addAction(mMain->scServer()->action(ScServer::PauseRecord));
648     menu->addAction(mMain->scServer()->action(ScServer::VolumeUp));
649     menu->addAction(mMain->scServer()->action(ScServer::VolumeDown));
650     menu->addAction(mMain->scServer()->action(ScServer::VolumeRestore));
651     menu->addAction(mMain->scServer()->action(ScServer::Mute));
652 
653     menuBar->addMenu(menu);
654 
655     menu = new QMenu(tr("&Help"), this);
656 #ifdef SC_USE_QTWEBENGINE
657     menu->addAction(mActions[HelpAboutIDE]);
658 #endif
659     menu->addAction(mActions[ReportABug]);
660 #ifdef SC_USE_QTWEBENGINE
661     menu->addSeparator();
662     menu->addAction(mActions[Help]);
663     menu->addAction(mActions[LookupDocumentationForCursor]);
664     menu->addAction(mActions[LookupDocumentation]);
665 #endif // SC_USE_QTWEBENGINE
666     menu->addSeparator();
667     menu->addAction(mActions[ShowAbout]);
668     menu->addAction(mActions[ShowAboutQT]);
669 
670     menuBar->addMenu(menu);
671 }
672 
saveDetachedState(Docklet * docklet,QVariantMap & data)673 static void saveDetachedState(Docklet* docklet, QVariantMap& data) {
674     data.insert(docklet->objectName(), docklet->saveDetachedState().toBase64());
675 }
676 
saveWindowState(T * settings)677 template <class T> void MainWindow::saveWindowState(T* settings) {
678     QVariantMap detachedData;
679     saveDetachedState(mPostDocklet, detachedData);
680     saveDetachedState(mDocumentsDocklet, detachedData);
681 #ifdef SC_USE_QTWEBENGINE
682     saveDetachedState(mHelpBrowserDocklet, detachedData);
683 #endif // SC_USE_QTWEBENGINE
684 
685     settings->beginGroup("mainWindow");
686     settings->setValue("geometry", this->saveGeometry().toBase64());
687     settings->setValue("state", this->saveState().toBase64());
688     settings->setValue("detached", QVariant::fromValue(detachedData));
689     settings->endGroup();
690 }
691 
saveWindowState()692 void MainWindow::saveWindowState() {
693     Settings::Manager* settings = Main::settings();
694     settings->beginGroup("IDE");
695     saveWindowState(settings);
696     settings->endGroup();
697 }
698 
restoreDetachedState(Docklet * docklet,const QVariantMap & data)699 static void restoreDetachedState(Docklet* docklet, const QVariantMap& data) {
700     QByteArray base64data = data.value(docklet->objectName()).value<QByteArray>();
701     docklet->restoreDetachedState(QByteArray::fromBase64(base64data));
702 }
703 
restoreWindowState(T * settings)704 template <class T> void MainWindow::restoreWindowState(T* settings) {
705     qDebug("------------ restore window state ------------");
706 
707     settings->beginGroup("mainWindow");
708     QVariant varGeom = settings->value("geometry");
709     QVariant varState = settings->value("state");
710     QVariant varDetached = settings->value("detached");
711     settings->endGroup();
712 
713     QByteArray geom = QByteArray::fromBase64(varGeom.value<QByteArray>());
714     QByteArray state = QByteArray::fromBase64(varState.value<QByteArray>());
715     QVariantMap detachedData = varDetached.value<QVariantMap>();
716 
717     if (!geom.isEmpty()) {
718         // Workaround for Qt bug 4397:
719         setWindowState(Qt::WindowNoState);
720         restoreGeometry(geom);
721     } else
722         setWindowState(windowState() & ~Qt::WindowFullScreen | Qt::WindowMaximized);
723 
724     restoreDetachedState(mPostDocklet, detachedData);
725     restoreDetachedState(mDocumentsDocklet, detachedData);
726 #ifdef SC_USE_QTWEBENGINE
727     restoreDetachedState(mHelpBrowserDocklet, detachedData);
728 #endif // SC_USE_QTWEBENGINE
729 
730     qDebug("restoring state");
731 
732     if (!state.isEmpty())
733         restoreState(state);
734 
735     qDebug("setting dock area corners");
736 
737     setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea);
738     setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea);
739     setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea);
740     setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea);
741 
742     updateClockWidget(isFullScreen());
743 
744     qDebug("------------ END restore window state ------------");
745 }
746 
restoreWindowState()747 void MainWindow::restoreWindowState() {
748     Settings::Manager* settings = Main::settings();
749     settings->beginGroup("IDE");
750     restoreWindowState(settings);
751     settings->endGroup();
752 }
753 
focusCodeEditor()754 void MainWindow::focusCodeEditor() {
755     if (mEditors->currentEditor())
756         mEditors->currentEditor()->setFocus();
757     else
758         mEditors->setFocus();
759 }
760 
newSession()761 void MainWindow::newSession() { mMain->sessionManager()->newSession(); }
762 
saveCurrentSessionAs()763 void MainWindow::saveCurrentSessionAs() {
764     QString name = QInputDialog::getText(this, tr("Save Current Session"), tr("Enter a name for the session:"));
765 
766     if (name.isEmpty())
767         return;
768 
769     mMain->sessionManager()->saveSessionAs(name);
770 
771     updateSessionsMenu();
772 }
773 
onOpenSessionAction(QAction * action)774 void MainWindow::onOpenSessionAction(QAction* action) { openSession(action->text()); }
775 
switchSession(Session * session)776 void MainWindow::switchSession(Session* session) {
777     if (session)
778         restoreWindowState(session);
779 
780     updateWindowTitle();
781 
782     mEditors->switchSession(session);
783 }
784 
saveSession(Session * session)785 void MainWindow::saveSession(Session* session) {
786     saveWindowState(session);
787 
788     mEditors->saveSession(session);
789 }
790 
openSessionsDialog()791 void MainWindow::openSessionsDialog() {
792     QPointer<MainWindow> mainwin(this);
793     SessionsDialog dialog(mMain->sessionManager(), this);
794     dialog.exec();
795     if (mainwin)
796         mainwin->updateSessionsMenu();
797 }
798 
action(ActionRole role)799 QAction* MainWindow::action(ActionRole role) {
800     Q_ASSERT(role < ActionCount);
801     return mActions[role];
802 }
803 
quit()804 bool MainWindow::quit() {
805     if (!promptSaveDocs())
806         return false;
807 
808     Main::instance()->documentManager()->deleteRestore();
809 
810     saveWindowState();
811 
812     mMain->quit();
813 
814     return true;
815 }
816 
onQuit()817 void MainWindow::onQuit() { quit(); }
818 
onCurrentDocumentChanged(Document * doc)819 void MainWindow::onCurrentDocumentChanged(Document* doc) {
820     updateWindowTitle();
821 
822     mActions[DocCloseAll]->setEnabled(doc);
823     mActions[DocReload]->setEnabled(doc);
824     mActions[DocSave]->setEnabled(doc);
825     mActions[DocSaveAs]->setEnabled(doc);
826     mActions[DocSaveAsExtension]->setEnabled(doc);
827 
828     GenericCodeEditor* editor = mEditors->currentEditor();
829     mFindReplaceTool->setEditor(editor);
830     mGoToLineTool->setEditor(editor);
831 }
832 
onDocumentChangedExternally(Document * doc)833 void MainWindow::onDocumentChangedExternally(Document* doc) {
834     if (mDocDialog)
835         return;
836 
837     mDocDialog = new DocumentsDialog(DocumentsDialog::ExternalChange, this);
838     mDocDialog->addDocument(doc);
839     connect(mDocDialog, SIGNAL(finished(int)), this, SLOT(onDocDialogFinished()));
840     mDocDialog->open();
841 }
842 
onDocDialogFinished()843 void MainWindow::onDocDialogFinished() {
844     mDocDialog->deleteLater();
845     mDocDialog = 0;
846 }
847 
updateRecentDocsMenu()848 void MainWindow::updateRecentDocsMenu() {
849     mRecentDocsMenu->clear();
850 
851     const QStringList& recent = mMain->documentManager()->recents();
852 
853     foreach (const QString& path, recent)
854         mRecentDocsMenu->addAction(path);
855 
856     if (!recent.isEmpty()) {
857         mRecentDocsMenu->addSeparator();
858         mRecentDocsMenu->addAction(mActions[ClearRecentDocs]);
859     }
860 }
861 
onOpenRecentDocument(QAction * action)862 void MainWindow::onOpenRecentDocument(QAction* action) { mMain->documentManager()->open(action->text()); }
863 
onInterpreterStateChanged(QProcess::ProcessState state)864 void MainWindow::onInterpreterStateChanged(QProcess::ProcessState state) {
865     switch (state) {
866     case QProcess::NotRunning:
867         toggleInterpreterActions(false);
868 
869     case QProcess::Starting:
870         break;
871 
872     case QProcess::Running:
873         toggleInterpreterActions(true);
874         break;
875     }
876 }
877 
closeEvent(QCloseEvent * event)878 void MainWindow::closeEvent(QCloseEvent* event) {
879     if (!quit())
880         event->ignore();
881 }
882 
close(Document * doc)883 bool MainWindow::close(Document* doc) {
884     if (doc->textDocument()->isModified() && doc->promptsToSave()) {
885         QMessageBox::StandardButton ret;
886         ret = QMessageBox::warning(mInstance, tr("SuperCollider IDE"),
887                                    tr("There are unsaved changes in document '%1'.\n\n"
888                                       "Do you want to save it?")
889                                        .arg(doc->title()),
890                                    QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
891                                    QMessageBox::Save // the default
892         );
893 
894         switch (ret) {
895         case QMessageBox::Cancel:
896             return false;
897         case QMessageBox::Save:
898             if (!MainWindow::save(doc))
899                 return false;
900             break;
901         default:;
902         }
903     }
904 
905     Main::instance()->documentManager()->close(doc);
906     return true;
907 }
908 
reload(Document * doc)909 bool MainWindow::reload(Document* doc) {
910     if (doc->filePath().isEmpty())
911         return false;
912 
913     if (doc->textDocument()->isModified()) {
914         QMessageBox::StandardButton ret;
915         ret = QMessageBox::warning(mInstance, tr("SuperCollider IDE"),
916                                    tr("There are unsaved changes in document '%1'.\n\n"
917                                       "Do you want to reload it?")
918                                        .arg(doc->title()),
919                                    QMessageBox::Yes | QMessageBox::No,
920                                    QMessageBox::No // the default
921         );
922         if (ret == QMessageBox::No)
923             return false;
924     }
925 
926     return Main::instance()->documentManager()->reload(doc);
927 }
928 
documentSavePath(Document * document) const929 QString MainWindow::documentSavePath(Document* document) const {
930     if (!document->filePath().isEmpty())
931         return document->filePath();
932 
933     if (!mLastDocumentSavePath.isEmpty())
934         return QFileInfo(mLastDocumentSavePath).path();
935 
936     QString interpreterWorkingDir = Main::settings()->value("IDE/interpreter/runtimeDir").toString();
937     if (!interpreterWorkingDir.isEmpty())
938         return interpreterWorkingDir;
939 
940     return QStandardPaths::standardLocations(QStandardPaths::HomeLocation)[0];
941 }
942 
save(Document * doc,bool forceChoose,bool saveInExtensionFolder)943 bool MainWindow::save(Document* doc, bool forceChoose, bool saveInExtensionFolder) {
944     const bool documentHasPath = !doc->filePath().isEmpty();
945 
946     if (!forceChoose && !(doc->isModified()) && documentHasPath)
947         return true;
948 
949     DocumentManager* documentManager = Main::instance()->documentManager();
950 
951     bool fileIsWritable = true;
952     if ((!forceChoose) && documentHasPath) {
953         QFileInfo fileInfo(doc->filePath());
954         fileIsWritable = fileInfo.isWritable();
955 
956         if (!fileIsWritable) {
957             QMessageBox::warning(instance(), tr("Saving read-only file"),
958                                  tr("File is read-only. Please select a new location to save to."), QMessageBox::Ok,
959                                  QMessageBox::NoButton);
960         }
961     }
962 
963     if (forceChoose || !documentHasPath || !fileIsWritable) {
964         QFileDialog dialog(mInstance);
965         dialog.setAcceptMode(QFileDialog::AcceptSave);
966         dialog.setFileMode(QFileDialog::AnyFile);
967 
968         QStringList filters = QStringList()
969             << tr("All Files (*)") << tr("SuperCollider Document (*.scd)") << tr("SuperCollider Class File (*.sc)")
970             << tr("SuperCollider Help Source (*.schelp)");
971 
972         dialog.setNameFilters(filters);
973 
974         if (saveInExtensionFolder) {
975             dialog.setDirectory(standardDirectory(ScExtensionUserDir));
976         } else {
977             QString path = mInstance->documentSavePath(doc);
978             QFileInfo path_info(path);
979 
980             if (path_info.isDir())
981                 // FIXME:
982                 // KDE native file dialog shows parent directory instead (KDE bug 229375)
983                 dialog.setDirectory(path);
984             else
985                 dialog.selectFile(path);
986 
987             // NOTE: do not use QFileDialog::setDefaultSuffix(), because it only adds
988             // the suffix after the dialog is closed, without showing a warning if the
989             // filepath with added suffix already exists!
990         }
991 
992 #ifdef Q_OS_MAC
993         QWidget* last_active_window = QApplication::activeWindow();
994 #endif
995 
996         int result = dialog.exec();
997 
998         // FIXME: workaround for Qt bug 25295
999         // See SC issue #678
1000 #ifdef Q_OS_MAC
1001         if (last_active_window)
1002             last_active_window->activateWindow();
1003 #endif
1004 
1005         QString save_path;
1006 
1007         if (result == QDialog::Accepted) {
1008             save_path = dialog.selectedFiles()[0];
1009 
1010             if (save_path.indexOf('.') == -1 && !QFile::exists(save_path)) {
1011                 save_path.append(".scd");
1012                 QFileInfo save_path_info(save_path);
1013                 if (save_path_info.exists()) {
1014                     QString msg = tr("Extenstion \".scd\" was automatically added to the "
1015                                      "selected file name, but the file \"%1\" already exists.\n\n"
1016                                      "Do you wish to overwrite it?")
1017                                       .arg(save_path_info.fileName());
1018                     QMessageBox::StandardButton result =
1019                         QMessageBox::warning(mInstance, tr("Overwrite File?"), msg, QMessageBox::Yes | QMessageBox::No);
1020                     if (result != QMessageBox::Yes)
1021                         save_path.clear();
1022                 }
1023             }
1024         }
1025 
1026         if (!save_path.isEmpty()) {
1027             if (!saveInExtensionFolder)
1028                 mInstance->mLastDocumentSavePath = save_path;
1029             return documentManager->saveAs(doc, save_path);
1030         } else {
1031             return false;
1032         }
1033     } else
1034         return documentManager->save(doc);
1035 }
1036 
newDocument()1037 void MainWindow::newDocument() { mMain->documentManager()->create(); }
1038 
documentOpenPath() const1039 QString MainWindow::documentOpenPath() const {
1040     GenericCodeEditor* currentEditor = mEditors->currentEditor();
1041     if (currentEditor) {
1042         QString currentEditorPath = currentEditor->document()->filePath();
1043         if (!currentEditorPath.isEmpty())
1044             return currentEditorPath;
1045     }
1046 
1047     const QStringList& recentDocuments = Main::documentManager()->recents();
1048     if (!recentDocuments.isEmpty())
1049         return recentDocuments[0];
1050 
1051     QString interpreterWorkingDir = Main::settings()->value("IDE/interpreter/runtimeDir").toString();
1052     if (!interpreterWorkingDir.isEmpty())
1053         return interpreterWorkingDir;
1054 
1055     return QStandardPaths::standardLocations(QStandardPaths::HomeLocation)[0];
1056 }
1057 
openDocument()1058 void MainWindow::openDocument() {
1059     QFileDialog dialog(this, Qt::Dialog);
1060     dialog.setModal(true);
1061     dialog.setWindowModality(Qt::ApplicationModal);
1062 
1063     dialog.setFileMode(QFileDialog::ExistingFiles);
1064 
1065     QString path = documentOpenPath();
1066     QFileInfo path_info(path);
1067     if (path_info.isDir())
1068         dialog.setDirectory(path);
1069     else
1070         dialog.setDirectory(path_info.dir());
1071 
1072     QStringList filters;
1073     filters << tr("All Files (*)") << tr("SuperCollider (*.scd *.sc)") << tr("SuperCollider Help Source (*.schelp)");
1074     dialog.setNameFilters(filters);
1075 
1076 #ifdef Q_OS_MAC
1077     QWidget* last_active_window = QApplication::activeWindow();
1078 #endif
1079 
1080     if (dialog.exec()) {
1081         QStringList filenames = dialog.selectedFiles();
1082         foreach (QString filename, filenames)
1083             mMain->documentManager()->open(filename);
1084     }
1085 
1086     // FIXME: workaround for Qt bug 25295
1087     // See SC issue #678
1088 #ifdef Q_OS_MAC
1089     if (last_active_window)
1090         last_active_window->activateWindow();
1091 #endif
1092 }
1093 
restoreDocuments()1094 void MainWindow::restoreDocuments() {
1095     DocumentManager* docMng = Main::instance()->documentManager();
1096 
1097     if (docMng->needRestore()) {
1098         QString msg = tr("Supercollider didn't quit properly last time\n"
1099                          "Do you want to restore files saved as temporary backups?");
1100         QMessageBox::StandardButton restore =
1101             QMessageBox::warning(mInstance, tr("Restore files?"), msg, QMessageBox::Yes | QMessageBox::No);
1102         if (restore == QMessageBox::Yes)
1103             docMng->restore();
1104         else
1105             docMng->deleteRestore();
1106     }
1107 }
1108 
openStartupFile()1109 void MainWindow::openStartupFile() {
1110     QString configDir = standardDirectory(ScConfigUserDir);
1111 
1112     QDir dir;
1113     // Create the config dir if non existent:
1114     dir.mkpath(configDir);
1115     if (!dir.cd(configDir)) {
1116         qWarning() << "Could not access config dir:" << configDir;
1117         return;
1118     }
1119 
1120     QString filePath = dir.filePath("startup.scd");
1121     // Try creating the file if non-existent:
1122     if (!QFile::exists(filePath)) {
1123         QFile file(filePath);
1124         if (!file.open(QIODevice::WriteOnly)) {
1125             file.close();
1126             qWarning() << "Could not create startup file:" << filePath;
1127             return;
1128         }
1129         file.close();
1130     }
1131 
1132     mMain->documentManager()->open(filePath, -1, 0, false);
1133 }
1134 
openUserSupportDirectory()1135 void MainWindow::openUserSupportDirectory() {
1136     QUrl dirUrl = QUrl::fromLocalFile(standardDirectory(ScAppDataUserDir));
1137     QDesktopServices::openUrl(dirUrl);
1138 }
1139 
saveDocument()1140 void MainWindow::saveDocument() {
1141     GenericCodeEditor* editor = mEditors->currentEditor();
1142     if (!editor)
1143         return;
1144 
1145     Document* doc = editor->document();
1146     Q_ASSERT(doc);
1147 
1148     MainWindow::save(doc);
1149 }
1150 
saveDocumentAs()1151 void MainWindow::saveDocumentAs() {
1152     GenericCodeEditor* editor = mEditors->currentEditor();
1153     if (!editor)
1154         return;
1155 
1156     Document* doc = editor->document();
1157     Q_ASSERT(doc);
1158 
1159     MainWindow::save(doc, true);
1160 }
1161 
saveDocumentAsExtension()1162 void MainWindow::saveDocumentAsExtension() {
1163     GenericCodeEditor* editor = mEditors->currentEditor();
1164     if (!editor)
1165         return;
1166 
1167     Document* doc = editor->document();
1168     Q_ASSERT(doc);
1169 
1170     MainWindow::save(doc, true, true);
1171 }
1172 
saveAllDocuments()1173 void MainWindow::saveAllDocuments() {
1174     QList<Document*> docs = mMain->documentManager()->documents();
1175     foreach (Document* doc, docs)
1176         if (!MainWindow::save(doc))
1177             return;
1178 }
1179 
reloadDocument()1180 void MainWindow::reloadDocument() {
1181     GenericCodeEditor* editor = mEditors->currentEditor();
1182     if (!editor)
1183         return;
1184 
1185     Q_ASSERT(editor->document());
1186     MainWindow::reload(editor->document());
1187 }
1188 
closeDocument()1189 void MainWindow::closeDocument() {
1190     GenericCodeEditor* editor = mEditors->currentEditor();
1191     if (!editor)
1192         return;
1193 
1194     Q_ASSERT(editor->document());
1195     MainWindow::close(editor->document());
1196 }
1197 
closeAllDocuments()1198 void MainWindow::closeAllDocuments() {
1199     if (promptSaveDocs()) {
1200         QList<Document*> docs = mMain->documentManager()->documents();
1201         foreach (Document* doc, docs)
1202             mMain->documentManager()->close(doc);
1203     }
1204 }
1205 
promptSaveDocs()1206 bool MainWindow::promptSaveDocs() {
1207     // LATER: maybe this should go to the DocumentManager class?
1208 
1209     QList<Document*> docs = mMain->documentManager()->documents();
1210     QList<Document*> unsavedDocs;
1211     foreach (Document* doc, docs)
1212         if (doc->textDocument()->isModified() && doc->promptsToSave())
1213             unsavedDocs.append(doc);
1214 
1215     if (!unsavedDocs.isEmpty()) {
1216         DocumentsDialog dialog(unsavedDocs, DocumentsDialog::Quit, this);
1217 
1218         if (!dialog.exec())
1219             return false;
1220     }
1221 
1222     return true;
1223 }
1224 
updateWindowTitle()1225 void MainWindow::updateWindowTitle() {
1226     Session* session = mMain->sessionManager()->currentSession();
1227     GenericCodeEditor* editor = mEditors->currentEditor();
1228     Document* doc = editor ? editor->document() : 0;
1229 
1230     QString title;
1231 
1232     if (session) {
1233         title.append(session->name());
1234         if (doc)
1235             title.append(": ");
1236     }
1237 
1238     if (doc) {
1239         if (!doc->filePath().isEmpty()) {
1240             QFileInfo info = QFileInfo(doc->filePath());
1241             QString pathString = info.dir().path();
1242 
1243             QString homePath = QDir::homePath();
1244             if (pathString.startsWith(homePath))
1245                 pathString.replace(0, homePath.size(), QStringLiteral("~"));
1246 
1247             QString titleString = QStringLiteral("%1 (%2)").arg(info.fileName(), pathString);
1248 
1249             title.append(titleString);
1250 
1251             setWindowFilePath(doc->filePath());
1252         } else {
1253             title.append(tr("Untitled"));
1254             setWindowFilePath("");
1255         }
1256     } else {
1257         setWindowFilePath("");
1258     }
1259 
1260     if (!title.isEmpty())
1261         title.append(" - ");
1262 
1263     title.append("SuperCollider IDE");
1264 
1265     setWindowTitle(title);
1266 }
1267 
toggleFullScreen()1268 void MainWindow::toggleFullScreen() {
1269     if (isFullScreen()) {
1270         setWindowState(windowState() & ~Qt::WindowFullScreen);
1271 
1272         updateClockWidget(false);
1273     } else {
1274         setWindowState(windowState() | Qt::WindowFullScreen);
1275 
1276         updateClockWidget(true);
1277     }
1278 }
1279 
updateClockWidget(bool isFullScreen)1280 void MainWindow::updateClockWidget(bool isFullScreen) {
1281     if (!isFullScreen) {
1282         if (mClockLabel) {
1283             delete mClockLabel;
1284             mClockLabel = NULL;
1285         }
1286     } else {
1287         if (mClockLabel == NULL) {
1288             mClockLabel = new ClockStatusBox(this);
1289             statusBar()->insertWidget(0, mClockLabel);
1290         }
1291     }
1292 }
1293 
openSession(const QString & sessionName)1294 void MainWindow::openSession(const QString& sessionName) { mMain->sessionManager()->openSession(sessionName); }
1295 
lookupImplementationForCursor()1296 void MainWindow::lookupImplementationForCursor() {
1297     static const QByteArray signature = QMetaObject::normalizedSignature("openDefinition()");
1298 
1299     invokeMethodOnFirstResponder(signature);
1300 }
1301 
lookupImplementation()1302 void MainWindow::lookupImplementation() { Main::openDefinition(QString(), QApplication::activeWindow()); }
1303 
lookupReferencesForCursor()1304 void MainWindow::lookupReferencesForCursor() {
1305     static const QByteArray signature = QMetaObject::normalizedSignature("findReferences()");
1306 
1307     invokeMethodOnFirstResponder(signature);
1308 }
1309 
lookupReferences()1310 void MainWindow::lookupReferences() { Main::findReferences(QString(), QApplication::activeWindow()); }
1311 
showStatusMessage(QString const & string)1312 void MainWindow::showStatusMessage(QString const& string) { mStatusBar->showMessage(string, 3000); }
1313 
applySettings(Settings::Manager * settings)1314 void MainWindow::applySettings(Settings::Manager* settings) {
1315     applyCursorBlinkingSettings(settings);
1316 
1317     mPostDocklet->mPostWindow->applySettings(settings);
1318 #ifdef SC_USE_QTWEBENGINE
1319     mHelpBrowserDocklet->browser()->applySettings(settings);
1320 #endif // SC_USE_QTWEBENGINE
1321     mCmdLine->applySettings(settings);
1322 }
1323 
applyCursorBlinkingSettings(Settings::Manager * settings)1324 void MainWindow::applyCursorBlinkingSettings(Settings::Manager* settings) {
1325     const bool disableBlinkingCursor = settings->value("IDE/editor/disableBlinkingCursor").toBool();
1326     const int defaultCursorFlashTime = settings->defaultCursorFlashTime();
1327     QApplication::setCursorFlashTime(disableBlinkingCursor ? 0 : defaultCursorFlashTime);
1328 }
1329 
storeSettings(Settings::Manager * settings)1330 void MainWindow::storeSettings(Settings::Manager* settings) { mPostDocklet->mPostWindow->storeSettings(settings); }
1331 
updateSessionsMenu()1332 void MainWindow::updateSessionsMenu() {
1333     mSessionsMenu->clear();
1334     QStringList sessions = mMain->sessionManager()->availableSessions();
1335     foreach (const QString& session, sessions)
1336         mSessionsMenu->addAction(session);
1337 }
1338 
showSwitchSessionDialog()1339 void MainWindow::showSwitchSessionDialog() {
1340     SessionSwitchDialog* dialog = new SessionSwitchDialog(this);
1341     int result = dialog->exec();
1342 
1343     if (result == QDialog::Accepted)
1344         openSession(dialog->activeElement());
1345 
1346     delete dialog;
1347 }
1348 
showAbout()1349 void MainWindow::showAbout() {
1350     QString aboutString = "<h3>SuperCollider %1</h3>"
1351                           "<p>%2</p>"
1352                           "&copy; James McCartney and others.<br>"
1353                           "<h3>SuperCollider IDE</h3>"
1354                           "&copy; Jakob Leben, Tim Blechmann and others.<br>";
1355     aboutString = aboutString.arg(SC_VersionString().c_str()).arg(SC_BuildString().c_str());
1356 
1357     QMessageBox::about(this, tr("About SuperCollider IDE"), aboutString);
1358 }
1359 
showAboutQT()1360 void MainWindow::showAboutQT() { QMessageBox::aboutQt(this); }
1361 
toggleInterpreterActions(bool enabled)1362 void MainWindow::toggleInterpreterActions(bool enabled) {
1363     mEditors->action(MultiEditor::EvaluateCurrentDocument)->setEnabled(enabled);
1364     mEditors->action(MultiEditor::EvaluateLine)->setEnabled(enabled);
1365     mEditors->action(MultiEditor::EvaluateRegion)->setEnabled(enabled);
1366 }
1367 
1368 
showCmdLine()1369 void MainWindow::showCmdLine() {
1370     mToolBox->setCurrentWidget(mCmdLine);
1371     mToolBox->show();
1372 
1373     mCmdLine->setFocus(Qt::OtherFocusReason);
1374 }
1375 
showCmdLine(const QString & cmd)1376 void MainWindow::showCmdLine(const QString& cmd) {
1377     mCmdLine->setText(cmd);
1378     showCmdLine();
1379 }
1380 
cmdLineForCursor()1381 void MainWindow::cmdLineForCursor() {
1382     static const QByteArray signature = QMetaObject::normalizedSignature("openCommandLine()");
1383 
1384     invokeMethodOnFirstResponder(signature);
1385 }
1386 
showGoToLineTool()1387 void MainWindow::showGoToLineTool() {
1388     GenericCodeEditor* editor = mEditors->currentEditor();
1389     mGoToLineTool->setValue(editor ? editor->textCursor().blockNumber() + 1 : 0);
1390 
1391     mToolBox->setCurrentWidget(mGoToLineTool);
1392     mToolBox->show();
1393 
1394     mGoToLineTool->setFocus();
1395 }
1396 
showFindTool()1397 void MainWindow::showFindTool() {
1398     mFindReplaceTool->setMode(TextFindReplacePanel::Find);
1399     mFindReplaceTool->initiate();
1400 
1401     mToolBox->setCurrentWidget(mFindReplaceTool);
1402     mToolBox->show();
1403 
1404     mFindReplaceTool->setFocus(Qt::OtherFocusReason);
1405 }
1406 
showReplaceTool()1407 void MainWindow::showReplaceTool() {
1408     mFindReplaceTool->setMode(TextFindReplacePanel::Replace);
1409     mFindReplaceTool->initiate();
1410 
1411     mToolBox->setCurrentWidget(mFindReplaceTool);
1412     mToolBox->show();
1413 
1414     mFindReplaceTool->setFocus(Qt::OtherFocusReason);
1415 }
1416 
hideToolBox()1417 void MainWindow::hideToolBox() {
1418     GenericCodeEditor* editor = mEditors->currentEditor();
1419     if (editor) {
1420         // This slot is mapped to Escape, so also clear highlighting
1421         // whenever invoked:
1422         editor->clearSearchHighlighting();
1423         if (!editor->hasFocus())
1424             editor->setFocus(Qt::OtherFocusReason);
1425     }
1426 
1427     mToolBox->hide();
1428 }
1429 
showSettings()1430 void MainWindow::showSettings() {
1431     static std::atomic<bool> showingSettings { false };
1432 
1433     if (showingSettings.load())
1434         return;
1435 
1436     showingSettings = true;
1437     try {
1438         Settings::Dialog dialog(mMain->settings());
1439         dialog.resize(700, 400);
1440         int result = dialog.exec();
1441         if (result == QDialog::Accepted)
1442             mMain->applySettings();
1443     } catch (std::exception const& e) {
1444         qWarning() << "Error while executing settings dialog:" << e.what();
1445     }
1446     showingSettings = false;
1447 }
1448 
1449 
lookupDocumentation()1450 void MainWindow::lookupDocumentation() {
1451     PopupTextInput* dialog = new PopupTextInput(tr("Look up Documentation For"), QApplication::activeWindow());
1452 
1453     bool success = dialog->exec();
1454     if (success)
1455         Main::openDocumentation(dialog->textValue());
1456 
1457     delete dialog;
1458 }
1459 
lookupDocumentationForCursor()1460 void MainWindow::lookupDocumentationForCursor() {
1461     static const QByteArray signature = QMetaObject::normalizedSignature("openDocumentation()");
1462 
1463     bool documentationOpened = false;
1464     QWidget* widget = QApplication::focusWidget();
1465     int methodIdx = -1;
1466 
1467     widget = findFirstResponder(widget, signature.constData(), methodIdx);
1468 
1469     if (widget && methodIdx != -1) {
1470         widget->metaObject()->method(methodIdx).invoke(widget, Qt::DirectConnection,
1471                                                        Q_RETURN_ARG(bool, documentationOpened));
1472     };
1473 
1474     if (!documentationOpened)
1475         openHelp();
1476 }
1477 
openHelp()1478 void MainWindow::openHelp() {
1479 #ifdef SC_USE_QTWEBENGINE
1480     if (mHelpBrowserDocklet->browser()->url().isEmpty())
1481         mHelpBrowserDocklet->browser()->goHome();
1482     mHelpBrowserDocklet->focus();
1483 #endif // SC_USE_QTWEBENGINE
1484 }
1485 
openHelpAboutIDE()1486 void MainWindow::openHelpAboutIDE() {
1487 #ifdef SC_USE_QTWEBENGINE
1488     mHelpBrowserDocklet->browser()->gotoHelpFor("Guides/SCIde");
1489     mHelpBrowserDocklet->focus();
1490 #endif // SC_USE_QTWEBENGINE
1491 }
1492 
doBugReport()1493 void MainWindow::doBugReport() {
1494     Settings::Manager* settings = mMain->settings();
1495     bool useGitHubBugReport = false;
1496 
1497     if (settings->contains("IDE/useGitHubBugReport")) {
1498         useGitHubBugReport = settings->value("IDE/useGitHubBugReport").toBool();
1499 
1500     } else {
1501         QMessageBox* dialog = new QMessageBox();
1502         dialog->setText("Do you want to submit bugs using <a href=\"https://www.github.com\">GitHub</a>?");
1503         dialog->setInformativeText("This requires a GitHub account.");
1504         dialog->addButton("Submit using GitHub", QMessageBox::YesRole);
1505         dialog->addButton("Submit anonymously", QMessageBox::NoRole);
1506         dialog->addButton("Cancel", QMessageBox::RejectRole);
1507         dialog->exec();
1508         QMessageBox::ButtonRole clicked = dialog->buttonRole(dialog->clickedButton());
1509 
1510         if (clicked == QMessageBox::YesRole || clicked == QMessageBox::NoRole) {
1511             useGitHubBugReport = (clicked == QMessageBox::YesRole);
1512             settings->setValue("IDE/useGitHubBugReport", useGitHubBugReport);
1513         } else {
1514             // Dialog was cancelled, so bail
1515             return;
1516         }
1517     }
1518 
1519     if (useGitHubBugReport) {
1520         QString url("https://github.com/supercollider/supercollider/issues/new");
1521         QString formData("?labels=bug&body=Bug%20description%3A%0A%0ASteps%20to%20reproduce%3A%0A1.%0A2.%0A3.%0A%"
1522                          "0AActual%20result%3A%0A%0AExpected%20result%3A%0A");
1523         QDesktopServices::openUrl(url + formData);
1524     } else {
1525         QDesktopServices::openUrl(QStringLiteral("https://gitreports.com/issue/supercollider/supercollider"));
1526     }
1527 }
1528 
dragEnterEvent(QDragEnterEvent * event)1529 void MainWindow::dragEnterEvent(QDragEnterEvent* event) {
1530     if (event->mimeData()->hasUrls()) {
1531         foreach (QUrl url, event->mimeData()->urls()) {
1532             if (QURL_IS_LOCAL_FILE(url)) {
1533                 // LATER: check mime type ?
1534                 event->acceptProposedAction();
1535                 return;
1536             }
1537         }
1538     }
1539 }
1540 
checkFileExtension(const QString & fpath)1541 bool MainWindow::checkFileExtension(const QString& fpath) {
1542     if (fpath.endsWith(".sc") || fpath.endsWith(".scd") || fpath.endsWith(".txt") || fpath.endsWith(".schelp")) {
1543         return true;
1544     }
1545     int ret = QMessageBox::question(this, tr("Open binary file?"),
1546                                     fpath
1547                                         + tr("\n\nThe file has an unrecognized extension. It may be a binary file. "
1548                                              "Would you still like to open it?"),
1549                                     QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel);
1550     if (ret != QMessageBox::Ok)
1551         return false;
1552 
1553     return true;
1554 }
1555 
dropEvent(QDropEvent * event)1556 void MainWindow::dropEvent(QDropEvent* event) {
1557     const QMimeData* data = event->mimeData();
1558     if (data->hasUrls()) {
1559         foreach (QUrl url, data->urls()) {
1560             if (QURL_IS_LOCAL_FILE(url)) {
1561                 QString fpath = url.toLocalFile();
1562                 if (MainWindow::checkFileExtension(fpath))
1563                     Main::documentManager()->open(fpath);
1564             }
1565         }
1566     }
1567 }
1568 
eventFilter(QObject * object,QEvent * event)1569 bool MainWindow::eventFilter(QObject* object, QEvent* event) {
1570     switch (event->type()) {
1571     case QEvent::ShortcutOverride: {
1572         QKeyEvent* key_event = static_cast<QKeyEvent*>(event);
1573         if (key_event->key() == 0) {
1574             // FIXME:
1575             // On Mac OS, for some global menu items, there is a  ShortcutOverride event with
1576             // key == 0, which seems like a Qt bug.
1577             // Text widgets override all events with key < Qt::Key_Escape, which includes 0.
1578             // Instead, prevent overriding such events:
1579             event->ignore();
1580             return true;
1581         }
1582         break;
1583     }
1584     default:
1585         break;
1586     }
1587 
1588     return QMainWindow::eventFilter(object, event);
1589 }
1590 
1591 //////////////////////////// ClockStatusBox ////////////////////////////
1592 
ClockStatusBox(QWidget * parent)1593 ClockStatusBox::ClockStatusBox(QWidget* parent): StatusLabel(parent) {
1594     setTextColor(Qt::green);
1595     mTimerId = startTimer(1000);
1596     updateTime();
1597 }
1598 
~ClockStatusBox()1599 ClockStatusBox::~ClockStatusBox() { killTimer(mTimerId); }
1600 
timerEvent(QTimerEvent * e)1601 void ClockStatusBox::timerEvent(QTimerEvent* e) {
1602     if (e->timerId() == mTimerId)
1603         updateTime();
1604 }
1605 
updateTime()1606 void ClockStatusBox::updateTime() { setText(QTime::currentTime().toString()); }
1607 
1608 } // namespace ScIDE
1609