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