1 /*
2 This file is part of Rocs.
3 SPDX-FileCopyrightText: 2008-2011 Tomaz Canabrava <tomaz.canabrava@gmail.com>
4 SPDX-FileCopyrightText: 2008 Ugo Sangiori <ugorox@gmail.com>
5 SPDX-FileCopyrightText: 2010-2011 Wagner Reck <wagner.reck@gmail.com>
6 SPDX-FileCopyrightText: 2011-2014 Andreas Cord-Landwehr <cordlandwehr@kde.org>
7
8 SPDX-License-Identifier: GPL-2.0-or-later
9 */
10
11 #include "mainwindow.h"
12 #include "rocsversion.h"
13 #include "settings.h"
14
15 #include "libgraphtheory/kernel/kernel.h"
16 #include "libgraphtheory/view.h"
17
18 #include <QApplication>
19 #include <QCloseEvent>
20 #include <QGraphicsView>
21 #include <QLabel>
22 #include <QLayout>
23 #include <QSplitter>
24 #include <QToolButton>
25 #include <QGridLayout>
26 #include <QDebug>
27 #include <QIcon>
28 #include <QPushButton>
29 #include <QInputDialog>
30 #include <QActionGroup>
31 #include <QFileDialog>
32 #include <QQuickWidget>
33 #include <QPointer>
34
35 #include <KActionCollection>
36 #include <KRecentFilesAction>
37 #include <KActionMenu>
38 #include <KTar>
39 #include <KMessageBox>
40 #include <KLocalizedString>
41 #include <KConfigDialog>
42 #include <KToolBar>
43 #include <ktexteditor/configpage.h>
44 #include <ktexteditor/view.h>
45 #include <ktexteditor/editor.h>
46 #include <ktexteditor/document.h>
47
48 #include "ui/documenttypeswidget.h"
49 #include "ui/codeeditorwidget.h"
50 #include "ui/scriptoutputwidget.h"
51 #include "ui/sidedockwidget.h"
52 #include "ui/fileformatdialog.h"
53 #include "ui/journalwidget.h"
54 #include "grapheditorwidget.h"
55 #include "plugins/scriptapi/scriptapiwidget.h"
56 #include "project/project.h"
57
58 using namespace GraphTheory;
59
MainWindow()60 MainWindow::MainWindow()
61 : KXmlGuiWindow()
62 , m_currentProject(nullptr)
63 , m_kernel(new Kernel)
64 , m_codeEditorWidget(new CodeEditorWidget(this))
65 , m_graphEditorWidget(new GraphEditorWidget(this))
66 , m_outputWidget(new ScriptOutputWidget(this))
67 {
68 setObjectName("RocsMainWindow");
69 m_graphEditor = new GraphTheory::Editor();
70
71 setupWidgets();
72 setupActions();
73 setupGUI(Keys | Save | Create);
74
75 setupToolsPluginsAction();
76
77 // setup kernel
78 connect(m_kernel, &Kernel::message, m_outputWidget, &ScriptOutputWidget::processMessage);
79
80 // TODO: use welcome widget instead of creating default empty project
81 createProject();
82 updateCaption();
83
84 // update rocs config version
85 Settings::setVersion(ROCS_VERSION_STRING);
86
87 // disable save action from kpart, since we take care for the editor by global save action
88 // here "file_save" is the action identifier from katepartui.rc
89 // note that we may not use that name for our own actions
90 const auto allCollections = KActionCollection::allCollections();
91 for (KActionCollection *ac : allCollections) {
92 if (ac->action("file_save")) {
93 ac->action("file_save")->setDisabled(true);
94 }
95 }
96 }
97
~MainWindow()98 MainWindow::~MainWindow()
99 {
100 Settings::setVSplitterSizeTop(m_vSplitter->sizes() [0]);
101 Settings::setVSplitterSizeBottom(m_vSplitter->sizes() [1]);
102 Settings::setHSplitterSizeLeft(m_hSplitter->sizes() [0]);
103 Settings::setHSplitterSizeRight(m_hSplitter->sizes() [1]);
104 Settings::setHScriptSplitterSizeLeft(m_hScriptSplitter->sizes() [0]);
105 Settings::setHScriptSplitterSizeRight(m_hScriptSplitter->sizes() [1]);
106 m_recentProjects->saveEntries(Settings::self()->config()->group("RecentFiles"));
107
108 Settings::self()->save();
109
110 m_graphEditor->deleteLater();
111 m_kernel->deleteLater();
112 }
113
closeEvent(QCloseEvent * event)114 void MainWindow::closeEvent(QCloseEvent *event)
115 {
116 if (queryClose() == true) {
117 event->accept();
118 } else {
119 event->ignore();
120 }
121 return;
122 }
123
setupWidgets()124 void MainWindow::setupWidgets()
125 {
126 // setup main widgets
127 QWidget *sidePanel = setupSidePanel();
128 QWidget *scriptPanel = setupScriptPanel();
129
130 // splits the main window horizontally
131 m_vSplitter = new QSplitter(this);
132 m_vSplitter->setOrientation(Qt::Vertical);
133 m_vSplitter->addWidget(m_graphEditorWidget);
134 m_vSplitter->addWidget(scriptPanel);
135
136 // horizontal arrangement
137 m_hSplitter = new QSplitter(this);
138 m_hSplitter->setOrientation(Qt::Horizontal);
139 m_hSplitter->addWidget(m_vSplitter);
140 m_hSplitter->addWidget(sidePanel);
141
142 // set sizes for script panel
143 m_hScriptSplitter->setSizes({
144 Settings::hScriptSplitterSizeLeft(),
145 Settings::hScriptSplitterSizeRight(),
146 80
147 });
148
149 // set sizes for vertical splitter
150 m_vSplitter->setSizes({
151 Settings::vSplitterSizeTop(),
152 Settings::vSplitterSizeBottom()
153 });
154
155 // set sizes for side panel
156 // the following solves the setting of the panel width if it was closed at previous session
157 int panelWidth = Settings::hSplitterSizeRight();
158 if (panelWidth == 0) {
159 //FIXME this is only a workaround
160 // that fixes the wrong saving of hSplitterSizeRight
161 panelWidth = 400;
162 }
163
164 m_hSplitter->setSizes({
165 Settings::hSplitterSizeLeft(),
166 panelWidth
167 });
168
169 setCentralWidget(m_hSplitter);
170 }
171
setupScriptPanel()172 QWidget* MainWindow::setupScriptPanel()
173 {
174 m_hScriptSplitter = new QSplitter(this);
175 m_hScriptSplitter->setOrientation(Qt::Horizontal);
176
177 KToolBar *executeCommands = new KToolBar(this);
178 executeCommands->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
179 executeCommands->setOrientation(Qt::Vertical);
180 m_runScript = new QAction(QIcon::fromTheme("media-playback-start"), i18nc("@action:intoolbar Script Execution", "Run"), this);
181 m_runScript->setToolTip(i18nc("@info:tooltip", "Execute currently active script on active graph document."));
182 m_stopScript = new QAction(QIcon::fromTheme("process-stop"), i18nc("@action:intoolbar Script Execution", "Stop"), this);
183 m_stopScript->setToolTip(i18nc("@info:tooltip", "Stop script execution."));
184 m_stopScript->setEnabled(false);
185 m_openDebugger = new QAction(QIcon::fromTheme("system-run"), i18nc("@action:intoolbar Open Debugger", "Debugger"), this);
186 m_openDebugger->setToolTip(i18nc("@info:tooltip", "Open the Javascript code debugger."));
187 m_openDebugger->setCheckable(true);
188 executeCommands->addAction(m_runScript);
189 executeCommands->addAction(m_stopScript);
190 executeCommands->addAction(m_openDebugger);
191 // add actions to action collection to be able to set shortcuts on them in the ui
192 actionCollection()->addAction("_runScript", m_runScript);
193 actionCollection()->addAction("_stopScript", m_stopScript);
194 actionCollection()->addAction("_openDebugger", m_openDebugger);
195
196 connect(m_runScript, &QAction::triggered, this, &MainWindow::executeScript);
197 connect(m_stopScript, &QAction::triggered, this, &MainWindow::stopScript);
198 connect(m_openDebugger, &QAction::triggered, this, &MainWindow::checkDebugger);
199
200 m_hScriptSplitter->addWidget(m_codeEditorWidget);
201 m_hScriptSplitter->addWidget(m_outputWidget);
202
203 QWidget *scriptInterface = new QWidget(this);
204 scriptInterface->setLayout(new QHBoxLayout);
205 scriptInterface->layout()->addWidget(m_hScriptSplitter);
206 scriptInterface->layout()->addWidget(executeCommands);
207
208 return scriptInterface;
209 }
210
setupSidePanel()211 QWidget* MainWindow::setupSidePanel()
212 {
213 // add sidebar
214 SidedockWidget* sideDock = new SidedockWidget(this);
215 addToolBar(Qt::RightToolBarArea, sideDock->toolbar());
216
217 // add widgets to dock
218 // document property widgets
219 DocumentTypesWidget *documentTypesWidget = new DocumentTypesWidget(this);
220 connect(this, &MainWindow::graphDocumentChanged, documentTypesWidget, &DocumentTypesWidget::setDocument);
221 sideDock->addDock(documentTypesWidget, i18n("Element Types"), QIcon::fromTheme("document-properties"));
222 if (m_currentProject && m_currentProject->activeGraphDocument()) {
223 documentTypesWidget->setDocument(m_currentProject->activeGraphDocument());
224 }
225
226 // Project Journal
227 m_journalWidget = new JournalEditorWidget(this);
228 sideDock->addDock(m_journalWidget, i18nc("@title", "Journal"), QIcon::fromTheme("story-editor"));
229
230 // Rocs scripting API documentation
231 ScriptApiWidget* apiDoc = new ScriptApiWidget(this);
232 sideDock->addDock(apiDoc, i18nc("@title", "Scripting API"), QIcon::fromTheme("documentation"));
233
234 return sideDock;
235 }
236
setProject(Project * project)237 void MainWindow::setProject(Project *project)
238 {
239 m_codeEditorWidget->setProject(project);
240 m_graphEditorWidget->setProject(project);
241 m_journalWidget->openJournal(project);
242 updateCaption();
243
244 if (m_currentProject) {
245 m_currentProject->disconnect(this);
246 m_currentProject->deleteLater();
247 }
248
249 connect(project, static_cast<void (Project::*)(GraphTheory::GraphDocumentPtr)>(&Project::activeGraphDocumentChanged),
250 this, &MainWindow::graphDocumentChanged);
251 connect(project, &Project::modifiedChanged,
252 this, &MainWindow::updateCaption);
253 m_currentProject = project;
254 Q_EMIT graphDocumentChanged(m_currentProject->activeGraphDocument());
255 }
256
setupActions()257 void MainWindow::setupActions()
258 {
259 KStandardAction::quit(this, SLOT(quit()), actionCollection());
260 KStandardAction::preferences(this, SLOT(showConfigurationDialog()), actionCollection());
261
262 // setup graph visual editor actions and add them to mainwindow action collection
263 // m_graphEditor->setupActions(actionCollection()); //FIXME add editor actions to main action collection
264
265 // Menu actions
266 QAction *newProjectAction = new QAction(QIcon::fromTheme("document-new"), i18nc("@action:inmenu", "New Project"), this);
267 newProjectAction->setShortcutContext(Qt::ApplicationShortcut);
268 actionCollection()->addAction("new-project", newProjectAction);
269 actionCollection()->setDefaultShortcut(newProjectAction, QKeySequence::New);
270 connect(newProjectAction, &QAction::triggered, this, &MainWindow::createProject);
271
272 QAction *projectSaveAction = new QAction(QIcon::fromTheme("document-save"), i18nc("@action:inmenu", "Save Project"), this);
273 projectSaveAction->setShortcutContext(Qt::ApplicationShortcut);
274 actionCollection()->addAction("save-project", projectSaveAction);
275 actionCollection()->setDefaultShortcut(projectSaveAction, QKeySequence::Save);
276 connect(projectSaveAction, &QAction::triggered, this, &MainWindow::saveProject);
277
278 QAction *projectOpenAction = new QAction(QIcon::fromTheme("document-open"), i18nc("@action:inmenu", "Open Project..."), this);
279 projectOpenAction->setShortcutContext(Qt::ApplicationShortcut);
280 actionCollection()->addAction("open-project", projectOpenAction);
281 actionCollection()->setDefaultShortcut(projectOpenAction, QKeySequence::Open);
282 connect(projectOpenAction, &QAction::triggered, this, [=] () { openProject(); });
283
284 m_recentProjects = new KRecentFilesAction(QIcon ("document-open"), i18nc("@action:inmenu","Recent Projects"), this);
285 connect(m_recentProjects, &KRecentFilesAction::urlSelected,
286 this, &MainWindow::openProject);
287 actionCollection()->addAction("recent-project", m_recentProjects);
288 m_recentProjects->loadEntries(Settings::self()->config()->group("RecentFiles"));
289
290 createAction("document-save-as", i18nc("@action:inmenu", "Save Project As..."), "save-project-as", SLOT(saveProjectAs()), this);
291 createAction("document-new", i18nc("@action:inmenu", "New Graph Document"), "new-graph", SLOT(createGraphDocument()), this);
292 createAction("document-new", i18nc("@action:inmenu", "New Script File..."), "new-script", SLOT(tryToCreateCodeDocument()), this);
293 createAction("document-import", i18nc("@action:inmenu", "Import Graph..."), "import-graph", SLOT(importGraphDocument()), this);
294 createAction("document-export", i18nc("@action:inmenu", "Export Graph As..."), "export-graph-as", SLOT(exportGraphDocument()), this);
295 createAction("document-import", i18nc("@action:inmenu", "Import Script..."), "add-script", SLOT(importCodeDocument()), this);
296 createAction("document-export", i18nc("@action:inmenu", "Export Script..."), "export-script", SLOT(exportCodeDocument()), this);
297 }
298
createAction(const QByteArray & iconName,const QString & actionTitle,const QString & actionName,const char * slot,QObject * parent)299 void MainWindow::createAction(const QByteArray& iconName, const QString& actionTitle, const QString& actionName,
300 const char* slot, QObject *parent)
301 {
302 QAction* action = new QAction(QIcon::fromTheme(iconName), actionTitle, parent);
303 actionCollection()->addAction(actionName, action);
304 connect(action, SIGNAL(triggered(bool)), parent, slot);
305 }
306
showConfigurationDialog()307 void MainWindow::showConfigurationDialog()
308 {
309 QPointer<KConfigDialog> dialog = new KConfigDialog(this, "settings", Settings::self());
310 KTextEditor::Editor *editor = KTextEditor::Editor::instance();
311 for (int index = 0; index < editor->configPages(); ++index) {
312 KTextEditor::ConfigPage *page = editor->configPage(index, dialog);
313 dialog->addPage(page,
314 page->name(),
315 page->icon().name(),
316 page->fullName());
317 }
318 dialog->exec();
319 }
320
setupToolsPluginsAction()321 void MainWindow::setupToolsPluginsAction()
322 {
323 QList<EditorPluginInterface*> availablePlugins = m_graphEditorPluginManager.plugins();
324 QList<QAction*> actions;
325 int count = 0;
326 for (auto plugin : availablePlugins) {
327 auto *action = new QAction(plugin->displayName(), this);
328 action->setData(count++);
329 connect(action, &QAction::triggered,
330 this, &MainWindow::showEditorPluginDialog);
331 actions << action;
332 }
333 unplugActionList("tools_plugins");
334 plugActionList("tools_plugins", actions);
335 }
336
importCodeDocument()337 void MainWindow::importCodeDocument()
338 {
339 QString startDirectory = Settings::lastOpenedDirectory();
340 QUrl fileUrl = QUrl::fromLocalFile(QFileDialog::getOpenFileName(this,
341 i18nc("@title:window", "Import Script into Project"),
342 startDirectory));
343
344 if (fileUrl.isEmpty()) {
345 return;
346 }
347
348 m_currentProject->importCodeDocument(fileUrl);
349 Settings::setLastOpenedDirectory(startDirectory);
350 }
351
exportCodeDocument()352 void MainWindow::exportCodeDocument()
353 {
354 QString startDirectory = Settings::lastOpenedDirectory();
355 QUrl fileUrl = QUrl::fromLocalFile(QFileDialog::getSaveFileName(this,
356 i18nc("@title:window", "Export Script"),
357 startDirectory,
358 i18n("JavaScript (*.js)")));
359 m_codeEditorWidget->activeDocument()->saveAs(fileUrl);
360 }
361
createProject()362 void MainWindow::createProject()
363 {
364 if (!queryClose()) {
365 return;
366 }
367
368 Project *project = new Project(m_graphEditor);
369 project->createCodeDocument(i18n("untitled"));
370 project->addGraphDocument(m_graphEditor->createDocument());
371 project->setModified(false);
372
373 setProject(project);
374 }
375
saveProject()376 void MainWindow::saveProject()
377 {
378 if (m_currentProject->projectUrl().isEmpty()) {
379 saveProjectAs();
380 return;
381 }
382
383 m_currentProject->projectSave();
384 m_recentProjects->addUrl(m_currentProject->projectUrl());
385
386 updateCaption();
387 }
388
saveProjectAs()389 void MainWindow::saveProjectAs()
390 {
391 QString startDirectory = Settings::lastOpenedDirectory();
392 QString file = QFileDialog::getSaveFileName(this,
393 i18nc("@title:window", "Save Project As"),
394 startDirectory,
395 i18n("Rocs Projects (*.rocs)"));
396
397 if (file.isEmpty()) {
398 qCritical() << "Filename is empty and no script file was created.";
399 return;
400 }
401 QFileInfo fi(file);
402 if (fi.exists()) {
403 const int btnCode = KMessageBox::warningContinueCancel(
404 this,
405 i18nc("@info", "A file named \"%1\" already exists. Are you sure you want to overwrite it?", fi.fileName()),
406 i18nc("@title:window", "Overwrite File?"),
407 KStandardGuiItem::overwrite());
408 if (btnCode == KMessageBox::Cancel) {
409 return; // cancel saving
410 }
411 }
412 Settings::setLastOpenedDirectory(m_currentProject->projectUrl().path());
413 m_currentProject->projectSaveAs(QUrl::fromLocalFile(file));
414 m_recentProjects->addUrl(QUrl::fromLocalFile(file));
415 updateCaption();
416 }
417
openProject(const QUrl & fileName)418 void MainWindow::openProject(const QUrl &fileName)
419 {
420 if (!queryClose()) {
421 return;
422 }
423
424 QString startDirectory = Settings::lastOpenedDirectory();
425 QUrl file = fileName;
426 if (file.isEmpty()) {
427 // show open dialog
428 file = QUrl::fromLocalFile(QFileDialog::getOpenFileName(this,
429 i18nc("@title:window", "Open Project Files"),
430 startDirectory,
431 i18n("Rocs projects (*.rocs)")));
432 }
433 if (file.isEmpty()) {
434 return;
435 }
436 Project *project = new Project(file, m_graphEditor);
437 setProject(project);
438 m_recentProjects->addUrl(file);
439 updateCaption();
440
441 Settings::setLastOpenedDirectory(file.path());
442 }
443
updateCaption()444 void MainWindow::updateCaption()
445 {
446 if (!m_currentProject) {
447 return;
448 }
449
450 QString modified;
451 if (m_currentProject->isModified()) {
452 modified = '*';
453 }
454
455 if (m_currentProject->projectUrl().isEmpty()) {
456 setCaption(i18nc("caption text for temporary project", "[ untitled ]%1", modified));
457 } else {
458 setCaption(QString("[ %1 ]%2").arg(m_currentProject->projectUrl().toLocalFile()).arg(modified));
459 }
460 }
461
uniqueFilename(const QString & basePrefix,const QString & suffix)462 QString MainWindow::uniqueFilename(const QString &basePrefix, const QString &suffix) {
463 QFile targetFile;
464 QString basePath = m_currentProject->projectUrl().path();
465 QString fullSuffix = '.' + suffix;
466 QString fullPrefix = basePrefix;
467
468 if (fullPrefix.isNull()) {
469 fullPrefix = m_currentProject->projectUrl().fileName().remove(QRegExp(".rocs*$"));
470 } else if (fullPrefix.endsWith(fullSuffix)) {
471 fullPrefix.remove(QRegExp(fullSuffix + '$'));
472 }
473
474 targetFile.setFileName(basePath + fullPrefix + fullSuffix);
475 for(int i = 1; targetFile.exists(); i++) {
476 targetFile.setFileName(basePath + fullPrefix + QString::number(i) + fullSuffix);
477 }
478
479 return targetFile.fileName();
480 }
481
tryToCreateCodeDocument()482 void MainWindow::tryToCreateCodeDocument()
483 {
484 QString basePrefix = QInputDialog::getText(this,
485 i18n("ScriptName"),
486 i18n("Enter the name of your new script"));
487 if (basePrefix.isNull()) {
488 qDebug() << "Filename is empty and no script file was created.";
489 return;
490 }
491
492 QString fullPath = m_currentProject->workingDir() + QLatin1Char('/') + basePrefix + QStringLiteral(".js");
493 QFileInfo file(fullPath);
494 if (file.exists()) {
495 KMessageBox::error(this, i18n("File already exists."));
496 return;
497 }
498
499 m_currentProject->createCodeDocument(basePrefix);
500 }
501
createGraphDocument()502 void MainWindow::createGraphDocument()
503 {
504 GraphDocumentPtr document = m_graphEditor->createDocument();
505 m_currentProject->addGraphDocument(document);
506 }
507
queryClose()508 bool MainWindow::queryClose()
509 {
510 if (!m_currentProject) {
511 return true;
512 }
513 if (!m_currentProject->isModified()) {
514 return true;
515 }
516 const int btnCode = KMessageBox::warningYesNoCancel(this, i18nc(
517 "@info",
518 "Changes on your project are unsaved. Do you want to save your changes?"));
519
520 if (btnCode == KMessageBox::Cancel) {
521 return false;
522 }
523
524 if (btnCode == KMessageBox::Yes) {
525 saveProject();
526 }
527
528 return true;
529 }
530
quit()531 void MainWindow::quit()
532 {
533 if (queryClose()) {
534 QApplication::quit();
535 }
536 }
537
importGraphDocument()538 void MainWindow::importGraphDocument()
539 {
540 FileFormatDialog importer(this);
541 GraphDocumentPtr document = importer.importFile();
542 if (!document) {
543 qWarning() << "No graph document was imported.";
544 return;
545 }
546 m_currentProject->addGraphDocument(document);
547 }
548
exportGraphDocument()549 void MainWindow::exportGraphDocument()
550 {
551 FileFormatDialog exporter(this);
552 exporter.exportFile(m_currentProject->activeGraphDocument());
553 }
554
showEditorPluginDialog()555 void MainWindow::showEditorPluginDialog()
556 {
557 QAction *action = qobject_cast<QAction *> (sender());
558
559 if (!action) {
560 return;
561 }
562 if (EditorPluginInterface *plugin = m_graphEditorPluginManager.plugins().value(action->data().toInt())) {
563 plugin->showDialog(m_currentProject->activeGraphDocument());
564 }
565 }
566
executeScript()567 void MainWindow::executeScript()
568 {
569 if (m_outputWidget->isOutputClearEnabled()) {
570 m_outputWidget->clear();
571 }
572 QString script = m_codeEditorWidget->activeDocument()->text();
573 enableStopAction();
574
575 if (m_openDebugger->isChecked()) {
576 m_kernel->triggerInterruptAction();
577 }
578
579 m_kernel->execute(m_currentProject->activeGraphDocument(), script);
580 }
581
stopScript()582 void MainWindow::stopScript()
583 {
584 m_kernel->stop();
585 disableStopAction();
586 }
587
checkDebugger()588 void MainWindow::checkDebugger()
589 {
590 if (m_openDebugger->isChecked()) {
591 m_kernel->attachDebugger();
592 } else {
593 m_kernel->detachDebugger();
594 }
595 }
596
enableStopAction()597 void MainWindow::enableStopAction()
598 {
599 m_stopScript->setEnabled(true);
600 }
601
disableStopAction()602 void MainWindow::disableStopAction()
603 {
604 m_stopScript->setEnabled(false);
605 }
606