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 "© James McCartney and others.<br>"
1353 "<h3>SuperCollider IDE</h3>"
1354 "© 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