diff --git a/src/appproject.cpp b/src/appproject.cpp index fd5584f2..5556f6dc 100644 --- a/src/appproject.cpp +++ b/src/appproject.cpp @@ -606,6 +606,12 @@ void AppProject::closeProject() // Close any remaining windows (for example split auto-clones). _mdiArea->closeAllSubWindows(); + // Force immediate destruction of MDI subwindows (WA_DeleteOnClose uses + // deleteLater, so without this their destructors run after new forms are + // already created, causing deviceIdAdded/unitMapAdded signals to be + // suppressed for the newly opened project). + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + const auto deleteClosedForms = [this](auto&& shouldDelete) { const auto snapshot = _closedForms; for (auto* frm : snapshot) { @@ -628,6 +634,8 @@ void AppProject::closeProject() _closedForms.clear(); _mbServer.clearAddressSpace(); + _mbServer.clearDescriptions(); + _mbServer.clearTimestamps(); _dataCounter = 0; _trafficCounter = 0; _scriptCounter = 0; @@ -1687,11 +1695,11 @@ void AppProject::loadProject(const QString& filename) if(!file.open(QFile::ReadOnly)) return; - _projectFilename = QFileInfo(filename).absoluteFilePath(); - emit projectOpened(_projectFilename); - - _mbServer.clearDescriptions(); - _mbServer.clearTimestamps(); + if (_projectFilename.isEmpty()) { + setSavePath(QFileInfo(filename).absoluteDir().absolutePath()); + _projectFilename = QFileInfo(filename).absoluteFilePath(); + emit projectOpened(_projectFilename); + } ModbusDefinitions defs; QList conns; @@ -1773,20 +1781,6 @@ void AppProject::loadProject(const QString& filename) _mdiArea->setSplitViewEnabled(splitView); viewPreparedForForms = true; } - - _mdiArea->closeAllSubWindows(); - // Force immediate destruction of MDI subwindows (WA_DeleteOnClose uses - // deleteLater, so without this their destructors run after new forms are - // already created, causing deviceIdAdded/unitMapAdded signals to be - // suppressed for the newly opened project). - QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - // Clean up forms that were already closed (hidden) - const auto closed = _closedForms; - for (auto&& frm : closed) { - _projectTree->removeForm(frm); - delete frm; - } - _closedForms.clear(); while (xml.readNextStartElement()) { ProjectFormKind kind; bool isForm = true; @@ -2103,6 +2097,7 @@ bool AppProject::saveProject(const QString& filename) return false; } + setSavePath(QFileInfo(filename).absoluteDir().absolutePath()); _projectFilename = absoluteFilename; QXmlStreamWriter w(&file); diff --git a/src/appproject.h b/src/appproject.h index c5fb6083..b9e7f157 100644 --- a/src/appproject.h +++ b/src/appproject.h @@ -97,6 +97,7 @@ class AppProject : public QObject void loadProject(const QString& filename); bool saveProject(const QString& filename); void restoreActiveWindows(); + const QString & filePath() const noexcept { return _projectFilename; } // Called from MainWindow::~MainWindow() before delete ui. // Closes MDI windows and deletes forms/scripts without touching the project tree UI diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index cebf3694..1a1151d8 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -293,6 +293,7 @@ MainWindow::MainWindow(const QString& profile, bool useSession, const QString& s setWindowTitle(APP_PRODUCT_NAME); setUnifiedTitleAndToolBarOnMac(true); setStatusBar(new MainStatusBar(_mbMultiServer, this)); + setAcceptDrops(true); if (auto* newButton = qobject_cast(ui->toolBarMain->widgetForAction(ui->actionNew))) { newButton->setMenu(ui->menuNew); @@ -522,9 +523,6 @@ void MainWindow::on_outputDockVisibilityChanged(bool visible) /// void MainWindow::on_mdiSubWindowActivated(QMdiSubWindow* wnd) { - if(wnd) - markModified(); - QMdiSubWindow* stableWnd = ui->mdiArea->activeSubWindow(); if(!stableWnd) stableWnd = ui->mdiArea->currentSubWindow(); @@ -693,12 +691,10 @@ void MainWindow::changeEvent(QEvent* event) /// void MainWindow::closeEvent(QCloseEvent *event) { - const bool shouldAskToSave = hasProjectContext() && (_isModified || _projectFilePath.isEmpty()); - if(shouldAskToSave) { - if(!confirmSaveOnClose()) { - event->ignore(); - return; - } + if (!confirmSaveOnClose()) { + // User canceled + event->ignore(); + return; } saveAppSettings(); @@ -748,6 +744,47 @@ bool MainWindow::eventFilter(QObject* obj, QEvent* e) return QObject::eventFilter(obj, e); } +void MainWindow::dragEnterEvent(QDragEnterEvent *event) +{ + const auto urls = event->mimeData()->urls(); + if (!urls.empty()) { + if (QFile::exists(urls.front().toLocalFile())) { + event->acceptProposedAction(); + } + } +} + +void MainWindow::dropEvent(QDropEvent *event) +{ + const auto urls = event->mimeData()->urls(); + if (!urls.empty()) { + auto replace = true; + if (hasProjectContext()) { + switch (QMessageBox::question(this, APP_PRODUCT_NAME, + tr("Would you like to combine the file(s) with the project?"), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No)) { + case QMessageBox::Yes: + replace = false; + break; + case QMessageBox::No: + replace = true; + break; + default: + // User canceled + return; + } + } + for (const auto& url : urls) { + if (!loadProject(url.toLocalFile(), replace)) { + // User canceled + return; + } + replace = false; + } + event->acceptProposedAction(); + } +} + /// /// \brief MainWindow::on_awake /// @@ -931,11 +968,14 @@ void MainWindow::on_actionOpenProject_triggered() filters << tr("Project files (*.omsim)"); filters << tr("All files (*)"); - const auto filename = QFileDialog::getOpenFileName(this, QString(), _project->savePath(), filters.join(";;")); - if(filename.isEmpty()) return; - - _project->setSavePath(QFileInfo(filename).absoluteDir().absolutePath()); - loadProject(filename); + const auto filenames = QFileDialog::getOpenFileNames(this, QString(), _project->savePath(), filters.join(";;")); + auto replace = true; + for (const auto& filename : filenames) { + if (!loadProject(filename, replace)) { + return; + } + replace = false; + } } /// @@ -943,13 +983,12 @@ void MainWindow::on_actionOpenProject_triggered() /// void MainWindow::on_actionSaveProject_triggered() { - if(_projectFilePath.isEmpty()) { + if (_project->filePath().isEmpty()) { on_actionSaveProjectAs_triggered(); return; } - if(saveProject(_projectFilePath)) { - addRecentProject(_projectFilePath); + if (saveProject(_project->filePath())) { return; } @@ -969,18 +1008,7 @@ void MainWindow::on_actionSaveProjectAs_triggered() /// void MainWindow::on_actionCloseProject_triggered() { - const bool shouldAskToSave = hasProjectContext() && (_isModified || _projectFilePath.isEmpty()); - if(shouldAskToSave) { - if(!confirmSaveOnClose()) - return; - } - - _project->closeProject(); - _projectFilePath.clear(); - _lastProjectPath.clear(); - _isModified = false; - updateProjectWindowTitle(); - updateMainToolbarState(); + closeProject(); } /// @@ -1524,29 +1552,29 @@ void MainWindow::presetRegs(QModbusDataUnit::RegisterType type) /// /// \brief MainWindow::loadProject /// \param filename +/// \param replace +/// \return /// -void MainWindow::loadProject(const QString& filename) +bool MainWindow::loadProject(const QString& filename, bool replace) { - if (hasProjectContext()) { - _project->closeProject(); - _projectFilePath.clear(); - _isModified = false; - updateProjectWindowTitle(); + if (replace) { + if (!closeProject()) { + // User canceled + return false; + } + AppLogger::clear(); } - AppLogger::clear(); - _project->loadProject(filename); applyGlobalAddressBase(AppPreferences::instance().globalAddressBase(), false); applyGlobalHexView(AppPreferences::instance().globalHexView(), false); syncGlobalViewControls(); - _projectFilePath = QFileInfo(filename).absoluteFilePath(); - _lastProjectPath = _projectFilePath; - _project->setSavePath(QFileInfo(filename).absoluteDir().absolutePath()); - _isModified = false; + _isModified = !replace; updateProjectWindowTitle(); updateMainToolbarState(); - addRecentProject(_projectFilePath); + addRecentProject(_project->filePath()); + + return true; } /// @@ -1558,11 +1586,28 @@ bool MainWindow::saveProject(const QString& filename) if (!_project->saveProject(filename)) return false; - _projectFilePath = QFileInfo(filename).absoluteFilePath(); - _lastProjectPath = _projectFilePath; - _project->setSavePath(QFileInfo(filename).absoluteDir().absolutePath()); _isModified = false; updateProjectWindowTitle(); + addRecentProject(_project->filePath()); + return true; +} + +/// +/// \brief MainWindow::closeProject +/// \return +/// +bool MainWindow::closeProject() +{ + if (!confirmSaveOnClose()) { + // User canceled + return false; + } + + _project->closeProject(); + _isModified = false; + updateProjectWindowTitle(); + updateMainToolbarState(); + return true; } @@ -1572,8 +1617,8 @@ bool MainWindow::saveProject(const QString& filename) /// QString MainWindow::projectName() const { - if(!_projectFilePath.isEmpty()) - return QFileInfo(_projectFilePath).completeBaseName(); + if (!_project->filePath().isEmpty()) + return QFileInfo(_project->filePath()).completeBaseName(); if(hasProjectContext()) return tr("Untitled"); @@ -1592,7 +1637,7 @@ void MainWindow::updateProjectWindowTitle() if(name.isEmpty()) setWindowTitle(modifiedMark + APP_PRODUCT_NAME); else - setWindowTitle(QString("%1%2 - %3").arg(modifiedMark, APP_PRODUCT_NAME, name)); + setWindowTitle(QString("%1%2 - %3").arg(modifiedMark, name, APP_PRODUCT_NAME)); } /// @@ -1750,7 +1795,7 @@ void MainWindow::saveAppSettings() m.setValue("SavePath", _project->savePath()); m.setValue(kNewFormKindKey, newFormKindToSetting(_newFormKind)); m.setValue(kRecentProjectsKey, _recentProjects); - m.setValue(kLastProjectPathKey, _lastProjectPath); + m.setValue(kLastProjectPathKey, _project->filePath()); } /// @@ -1921,6 +1966,11 @@ void MainWindow::applyGlobalHexView(bool enabled, bool persist) /// bool MainWindow::confirmSaveOnClose() { + const auto shouldAskToSave = hasProjectContext() && (_isModified || _project->filePath().isEmpty()); + if (!shouldAskToSave) { + return true; + } + const auto button = QMessageBox::question(this, tr("Save Project"), tr("Save project before closing?"), @@ -1932,9 +1982,8 @@ bool MainWindow::confirmSaveOnClose() if(button != QMessageBox::Save) return true; - if(!_projectFilePath.isEmpty()) { - if(saveProject(_projectFilePath)) { - addRecentProject(_projectFilePath); + if (!_project->filePath().isEmpty()) { + if (saveProject(_project->filePath())) { return true; } @@ -1954,9 +2003,9 @@ bool MainWindow::promptSaveProjectAs(const QString& initialPath) QStringList filters; filters << tr("Project files (*.omsim)"); - const QString defaultPath = _projectFilePath.isEmpty() + const QString defaultPath = _project->filePath().isEmpty() ? _project->savePath() + "/" + projectName() + ".omsim" - : _projectFilePath; + : _project->filePath(); const QString dialogPath = initialPath.isEmpty() ? defaultPath : initialPath; auto filename = QFileDialog::getSaveFileName(this, QString(), dialogPath, filters.join(";;")); @@ -1966,11 +2015,9 @@ bool MainWindow::promptSaveProjectAs(const QString& initialPath) if(!filename.endsWith(".omsim", Qt::CaseInsensitive)) filename.append(".omsim"); - _project->setSavePath(QFileInfo(filename).absoluteDir().absolutePath()); if(!saveProject(filename)) return false; - addRecentProject(QFileInfo(filename).absoluteFilePath()); return true; } @@ -1997,7 +2044,7 @@ QString MainWindow::projectSavePathInProfileDir() const /// bool MainWindow::hasProjectContext() const { - return !_projectFilePath.isEmpty() + return !_project->filePath().isEmpty() || _project->firstMdiChild() != nullptr || !_project->closedForms().isEmpty(); } diff --git a/src/mainwindow.h b/src/mainwindow.h index 0330747c..17468330 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -58,8 +58,9 @@ class MainWindow : public QMainWindow void applyGlobalAddressBase(AddressBase base, bool persist = true); void applyGlobalHexView(bool enabled, bool persist = true); - void loadProject(const QString& filename); + bool loadProject(const QString& filename, bool replace = true); bool saveProject(const QString& filename); + bool closeProject(); void appendConsoleMessage(const QString& source, const QString& text, ConsoleOutput::MessageType type); void showOutputConsole(); @@ -79,6 +80,8 @@ class MainWindow : public QMainWindow void changeEvent(QEvent* event) override; void closeEvent(QCloseEvent *event) override; bool eventFilter(QObject * obj, QEvent * e) override; + void dragEnterEvent(QDragEnterEvent *event) override; + void dropEvent(QDropEvent *event) override; public slots: void windowActivate(QMdiSubWindow* wnd); @@ -206,7 +209,6 @@ private slots: QSharedPointer _selectedPrinter; DataSimulator* _dataSimulator = nullptr; QString _profile; - QString _projectFilePath; ProjectFormKind _newFormKind = ProjectFormKind::Data; QStringList _recentProjects; QString _lastProjectPath;