#include "MainWindow.hh" #include "PropertyModel.hh" #include "VivyDocumentView.hh" #include "AboutWindow.hh" #include "VivyFileIconProvider.hh" #include "../Lib/Utils.hh" #include "../VivyApplication.hh" #define DCL_MENU(menu, name) [[maybe_unused]] QMenu *menu##Menu = menuBar()->addMenu(name); #define DCL_ACTION(method, name, tip, menu) \ [[maybe_unused]] QAction *method##Act = new QAction(tr(name), this); \ method##Act->setStatusTip(tr(tip)); \ menu##Menu->addAction(method##Act); \ connect(method##Act, &QAction::triggered, this, &MainWindow::method); #define ACTION_ADD_ICON(action, icon) action##Act->setIcon(QIcon(icon)); #define ACTION_ADD_SHORTCUT(action, shortcut) action##Act->setShortcut(shortcut); using namespace Vivy; MainWindow::MainWindow() noexcept : QMainWindow() { setWindowIcon(QIcon(VIVY_ICON_APP)); setWindowTitle("Vivy"); setDocumentMode(true); setDockNestingEnabled(true); setTabShape(QTabWidget::Rounded); /* Some declarations */ DCL_MENU(file, "&File"); DCL_MENU(edit, "&Edit"); DCL_MENU(viewTmp, "&View"); viewMenu = viewTmpMenu; // Save the view menu DCL_MENU(simulate, "&Simulate"); DCL_MENU(help, "&Help"); DCL_ACTION(newDocument, "&New document", "Create a new document", file); DCL_ACTION(openDocument, "&Open document", "Open a document", file); DCL_ACTION(saveFile, "&Save file", "Save the current document", file); DCL_ACTION(saveFileAs, "&Save file as", "Save the current document as", file); DCL_ACTION(renameFile, "&Rename the file", "Rename the current document", file); fileMenu->addSeparator(); DCL_ACTION(loadSubDocumentAudio, "&Load audio", "Load an audio file as a sub-document", file); DCL_ACTION(loadSubDocumentVideo, "&Load video", "Load a vide file as a sub-document", file); DCL_ACTION(loadSubDocumentAss, "&Load ASS", "Load an ASS file as a sub-document", file); DCL_ACTION(openDialogHelp, "&About", "Open the help dialog", help); ACTION_ADD_ICON(newDocument, VIVY_ICON_NEW); ACTION_ADD_ICON(openDocument, VIVY_ICON_OPEN); ACTION_ADD_ICON(saveFile, VIVY_ICON_SAVE); ACTION_ADD_ICON(saveFileAs, VIVY_ICON_SAVE_AS); ACTION_ADD_ICON(renameFile, VIVY_ICON_RENAME); ACTION_ADD_ICON(openDialogHelp, VIVY_ICON_ABOUT); ACTION_ADD_SHORTCUT(newDocument, QKeySequence::New); ACTION_ADD_SHORTCUT(openDocument, QKeySequence::Open); ACTION_ADD_SHORTCUT(saveFile, QKeySequence::Save); // Custom useFakeVim action { editMenu->addSeparator(); QAction *useFakeVim = new QAction(tr("Use FakeVim"), this); useFakeVim->setStatusTip("Use FakeVim with integrated text editors"); useFakeVim->setCheckable(true); editMenu->addAction(useFakeVim); connect(useFakeVim, &QAction::toggled, this, [this](bool checked) noexcept -> void { const bool oldState = vivyApp->getUseFakeVimEditor(); if (oldState != checked) { vivyApp->setUseFakeVimEditor(checked); updateFakeVimUsage(checked); } }); } // Setup the tabs to display the documents documents = new QTabWidget(this); documents->setMovable(true); documents->setTabsClosable(true); documents->setElideMode(Qt::ElideRight); documents->setUsesScrollButtons(true); documents->setDocumentMode(true); documents->setTabBarAutoHide(true); connect(documents, &QTabWidget::tabCloseRequested, this, [this](int index) noexcept -> void { closeDocument(index); }); connect(documents, &QTabWidget::tabBarDoubleClicked, this, &MainWindow::openProperties); setCentralWidget(documents); centralWidget()->setContentsMargins(0, 0, 0, 0); setContentsMargins(0, 0, 0, 0); // Enable/disable actions depending on the context saveFileAct->setEnabled(false); saveFileAsAct->setEnabled(false); loadSubDocumentAssAct->setEnabled(false); loadSubDocumentVideoAct->setEnabled(false); loadSubDocumentAudioAct->setEnabled(false); // Enable "Save As" action auto enableSaveAsOnDocument = [this](auto *widget, int index) noexcept -> void { if (index >= 0) { const auto doc = static_cast<AbstractDocumentView *>(documents->widget(index)); const auto type = doc->getType(); widget->setEnabled(type == AbstractDocumentView::Type::Vivy || type == AbstractDocumentView::Type::Script); } else { widget->setEnabled(false); } }; // Enable actions if the document is save-able auto enableSaveOnDocument = [this](auto *widget, int index) noexcept -> void { if (index >= 0) { const auto doc = static_cast<AbstractDocumentView *>(documents->widget(index)); const auto type = doc->getType(); const bool isVivyMemoryDoc = (type == AbstractDocumentView::Type::Vivy) && dynamic_cast<VivyDocument *>(doc->getDocument()) ->checkDocumentOption(VivyDocument::MemoryDocumentCreation); widget->setEnabled((!isVivyMemoryDoc) && (type == AbstractDocumentView::Type::Vivy || type == AbstractDocumentView::Type::Script)); } else { widget->setEnabled(false); } }; // Enable load sub-document on sub-document able objects auto enableLoadSubOnDocument = [this](auto *widget, int index) noexcept -> void { if (index >= 0) { auto type = static_cast<AbstractDocumentView *>(documents->widget(index))->getType(); widget->setEnabled(type == AbstractDocumentView::Type::Vivy); } else { widget->setEnabled(false); } }; { #define CONNECT_ENABLE(act, func) \ connect(documents, &QTabWidget::currentChanged, act, std::bind_front(func, act)); connect(documents, &QTabWidget::currentChanged, this, [this](int) noexcept -> void { documentViewActionsChanged(); }); CONNECT_ENABLE(saveFileAct, enableSaveOnDocument); CONNECT_ENABLE(saveFileAsAct, enableSaveAsOnDocument); CONNECT_ENABLE(loadSubDocumentAssAct, enableLoadSubOnDocument); CONNECT_ENABLE(loadSubDocumentVideoAct, enableLoadSubOnDocument); CONNECT_ENABLE(loadSubDocumentAudioAct, enableLoadSubOnDocument); #undef CONNECT_ENABLE } // Main window has finished its construction statusBar()->showMessage("QSimulate has started"); // Minimal size... setMinimumHeight(400); setMinimumWidth(600); // Always a new empty document newDocument(); } void MainWindow::closeEvent(QCloseEvent *event) noexcept { qDebug() << "Closing the main window!"; forEachViews<VivyDocumentView>( [](VivyDocumentView *view, int) { view->closeDocument(); }); // XXX // event->accept(); QMainWindow::closeEvent(event); } void MainWindow::updateFakeVimUsage(bool yes) noexcept { forEachViews<AbstractDocumentView>([yes](AbstractDocumentView *docView, int) noexcept -> void { const bool documentExists = docView && docView->getDocument(); const bool isScriptEditor = documentExists && (docView->getDocument()->getType() == AbstractDocument::Type::Script); if (isScriptEditor) static_cast<ScriptDocumentView *>(docView)->setUseFakeVimEditor(yes); }); } void MainWindow::openProperties(int index) noexcept { if (index < 0) { // TODO: May may want to do something when the user is clicking where // no tab bar is openned (like open document?) return; } qDebug().nospace() << "Tab n°" << index << " was double clicked"; AbstractDocumentView *current = getTab(index); current->openProperties(); } void MainWindow::openDialogHelp() noexcept { if (aboutWindowMutex.try_lock() && aboutWindow == nullptr) { aboutWindow = new AboutWindow(this); QEventLoop loop; connect(aboutWindow, &AboutWindow::closed, &loop, &QEventLoop::quit); aboutWindow->show(); loop.exec(); delete aboutWindow; aboutWindow = nullptr; aboutWindowMutex.unlock(); } } AbstractDocument * MainWindow::getCurrentDocument() const { return getCurrentDocumentView()->getDocument(); } void MainWindow::saveFile() noexcept { try { const auto document = getCurrentDocument(); document->save(); } catch (const std::runtime_error &e) { qCritical() << "Failed to save current document:" << e.what(); } } void MainWindow::renameFile() noexcept { try { const auto docView = getCurrentDocumentView(); auto document = docView->getDocument(); const QString filename = dialogSaveFileName("Select the target file to rename the current file to", QDir::homePath(), Utils::getVivyDocumentFileSuffixFilter()); if (filename.isEmpty()) throw std::runtime_error("No filename passed"); else document->rename(filename); // Kubat: Save is called by rename! } catch (const std::runtime_error &e) { qCritical() << "Failed to save current document:" << e.what(); } } void MainWindow::saveFileAs() noexcept { try { const auto docView = getCurrentDocumentView(); auto document = docView->getDocument(); const QString filename = dialogSaveFileName("Select the target file to save into", QDir::homePath(), Utils::getVivyDocumentFileSuffixFilter()); if (filename.isEmpty()) throw std::runtime_error("No filename passed"); else document->copy(filename); // Kubat: Save is called by copy! } catch (const std::runtime_error &e) { qCritical() << "Failed to save current document:" << e.what(); } } void MainWindow::closeDocument(AbstractDocumentView *const view) noexcept { if (view == nullptr || !view->getDocument()) return; forEachViews<AbstractDocumentView>( [this, view](AbstractDocumentView *documentView, int index) noexcept -> void { const bool documentExists = documentView && documentView->getDocument(); if (documentExists && (*documentView->getDocument()) == (*view->getDocument())) closeDocument(index); }); } void MainWindow::closeDocument(int index) noexcept { if (index < 0) return; auto *documentToClose = static_cast<AbstractDocumentView *>(documents->widget(index)); documents->removeTab(index); disconnect(documentToClose, &AbstractDocumentView::viewActionsChanged, this, &MainWindow::documentViewActionsChanged); if (documentToClose) { qDebug() << "Delete document view" << documentToClose->getDocumentTabName(); documentToClose->closeDocument(); delete documentToClose; } } void MainWindow::newDocument() noexcept { try { addTab(new VivyDocumentView( vivyApp->documentStore.newDocument(VivyDocument::UntouchedByDefault), documents)); } catch (const std::runtime_error &e) { qCritical() << "Failed to create a new empty document:" << e.what(); } } void MainWindow::openDocument() noexcept { const QString filename = dialogOpenFileName("Select a document to open", QDir::homePath(), Utils::getAnyTopLevelDocumentFileSuffixFilter()); if (filename.isEmpty()) { qWarning() << "Found an empty filename, don't open a file"; return; } const QFileInfo fileInfo(filename); Utils::DocumentType fileType; if (!Utils::detectDocumentType(fileInfo, &fileType)) { qWarning() << "Failed to detect file type for" << filename; return; } // Handle the different types here try { if (fileType == Utils::DocumentType::Vivy) addTab(new VivyDocumentView(vivyApp->documentStore.loadDocument(filename), documents)); else if (fileType == Utils::DocumentType::VivyScript) { auto scriptDocument = vivyApp->scriptStore.loadDocument(filename); auto errorTuple = vivyApp->scriptStore.executeScript(scriptDocument->getUuid()); ScriptDocumentView *newView = new ScriptDocumentView(scriptDocument, documents); if (errorTuple.has_value()) { const auto &[line, desc] = errorTuple.value(); emit newView->luaErrorFound(line, QString::fromUtf8(desc.c_str())); } addTab(newView); } } catch (const std::runtime_error &e) { qCritical() << "Failed to load document" << filename << "with error:" << e.what(); } } void MainWindow::loadSubDocumentAss() noexcept { withOpenFileNameDialog<VivyDocumentView, VivyDocument>( "Select an ASS document to load", Utils::getAssFileSuffixFilter(), [](VivyDocumentView *view, VivyDocument *doc, const QString &filename) noexcept -> void { doc->setAssSubDocument(filename); view->loadAssView(); }); } void MainWindow::loadSubDocumentVideo() noexcept { withOpenFileNameDialog<VivyDocumentView, VivyDocument>( "Select a video document to load", Utils::getVideoFileSuffixFilter(), [](VivyDocumentView *view, VivyDocument *doc, const QString &filename) noexcept -> void { doc->setVideoSubDocument(filename); view->loadVideoView(); }); } void MainWindow::loadSubDocumentAudio() noexcept { withOpenFileNameDialog<VivyDocumentView, VivyDocument>( "Select an audio document to load", Utils::getAudioFileSuffixFilter() + QStringLiteral(";;") + Utils::getVideoFileSuffixFilter(), [](VivyDocumentView *view, VivyDocument *doc, const QString &filename) noexcept -> void { doc->setAudioSubDocument(filename); view->loadAudioView(); }); } void MainWindow::addTab(AbstractDocumentView *const tab) { int index = -1; if (const int untouched_index = findFirstUntouchedDocument(); untouched_index >= 0) { closeDocument(untouched_index); index = documents->insertTab(untouched_index, tab, tab->getDocumentTabIcon(), tab->getDocumentTabName()); } else { index = documents->addTab(tab, tab->getDocumentTabIcon(), tab->getDocumentTabName()); } documents->setTabToolTip(index, tab->getDocumentTabToolTip()); documents->setCurrentIndex(index); connect(tab, &AbstractDocumentView::viewActionsChanged, this, &MainWindow::documentViewActionsChanged); connect(tab, &AbstractDocumentView::documentPropertyChanged, this, [this, tab]() noexcept -> void { int tabIndex = getIndexOfTab(tab); if (tabIndex < 0) { qDebug() << "Tab was not found!"; } else { qDebug() << "Tab was found at index" << tabIndex; documents->setTabToolTip(tabIndex, tab->getDocumentTabToolTip()); documents->setTabText(tabIndex, tab->getDocumentTabName()); } }); documentViewActionsChanged(); qDebug() << "View constructed successfully"; } int MainWindow::getIndexOfTab(const AbstractDocumentView *const viewA) const noexcept { if (viewA == nullptr) return -1; const AbstractDocument *const docA = viewA->getDocument(); if (docA == nullptr) return -1; int returnIndex = -1; forEachViews<AbstractDocumentView>( [docA, &returnIndex](AbstractDocumentView *viewB, int index) noexcept -> void { if (viewB == nullptr) return; const AbstractDocument *const docB = viewB->getDocument(); if (docB == nullptr) return; if (*docB == *docA) returnIndex = index; }); return returnIndex; } AbstractDocumentView * MainWindow::getCurrentDocumentView() const { if (AbstractDocumentView *currentView = static_cast<AbstractDocumentView *>(documents->currentWidget())) { return currentView; } else { throw std::runtime_error("no current document"); } } AbstractDocumentView * MainWindow::getTab(const int index) const noexcept { if (index > documents->count()) return nullptr; return static_cast<AbstractDocumentView *>(documents->widget(index)); } int MainWindow::findFirstUntouchedDocument() const noexcept { int returnIndex = -1; forEachViews<VivyDocumentView>( [&returnIndex](VivyDocumentView *view, int index) noexcept -> void { if (view->getDocument()->checkDocumentOption(VivyDocument::UntouchedByDefault)) returnIndex = index; }); return returnIndex; } void MainWindow::documentViewActionsChanged() noexcept { qInfo() << "Document view action changed"; viewMenu->clear(); // Change document view menu if we have a document try { viewMenu->addActions(getCurrentDocumentView()->getViewsActions()); } catch (const std::runtime_error &e) { qInfo() << "No view to display:" << e.what(); } } static inline QString executeDialog(MainWindow *const self, QFileDialog *const dialog) noexcept { bool dialogAccepted = false; std::unique_ptr<VivyFileIconProvider> iconProvider(new VivyFileIconProvider()); dialog->setIconProvider(iconProvider.get()); QFileDialog::connect(dialog, &QFileDialog::accepted, self, [&dialogAccepted]() noexcept -> void { dialogAccepted = true; }); dialog->exec(); if (!dialogAccepted) return QStringLiteral(""); const QStringList resList = dialog->selectedFiles(); if (resList.size() != 1) { qCritical() << "You must select only one file"; return QStringLiteral(""); } return resList.at(0); } QString MainWindow::dialogOpenFileName(const QString &title, const QString &folder, const QString &filter) noexcept { QFileDialog dialog(this, title, folder, filter); dialog.setOption(QFileDialog::ReadOnly); dialog.setAcceptMode(QFileDialog::AcceptOpen); dialog.setFileMode(QFileDialog::ExistingFile); return executeDialog(this, &dialog); } QString MainWindow::dialogSaveFileName(const QString &title, const QString &folder, const QString &filter) noexcept { QFileDialog dialog(this, title, folder, filter); dialog.setOption(QFileDialog::ReadOnly, false); dialog.setAcceptMode(QFileDialog::AcceptSave); return executeDialog(this, &dialog); }