diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 2b7c3d4..4a668b7 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -315,6 +316,16 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event) return QMainWindow::eventFilter(watched, event); } +void MainWindow::closeEvent(QCloseEvent *event) +{ + if (!confirmSaveBeforeDestructiveAction()) + { + event->ignore(); + return; + } + QMainWindow::closeEvent(event); +} + const std::string &MainWindow::currentHmiPageId() const { return current_hmi_page_id_; @@ -1035,6 +1046,44 @@ void MainWindow::refreshProjectUi() { project_workspace_controller_->refresh(); updateEditActions(); + updateWindowTitle(); +} + +void MainWindow::updateWindowTitle() +{ + QString title = tr("综合平台编程器"); + if (project_service_.isModified()) + { + title += QStringLiteral("*"); + } + setWindowTitle(title); +} + +bool MainWindow::confirmSaveBeforeDestructiveAction() +{ + if (!project_service_.isModified()) + { + return true; + } + + const QString project_name = fromUtf8(project_service_.project().metadata.name); + const QMessageBox::StandardButton choice = QMessageBox::warning( + this, + tr("工程尚未保存"), + tr("工程“%1”有未保存的修改,是否先保存?").arg(project_name), + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, + QMessageBox::Save); + if (choice == QMessageBox::Cancel) + { + return false; + } + if (choice == QMessageBox::Discard) + { + return true; + } + + saveProject(); + return !project_service_.isModified(); } void MainWindow::updateProjectTreeActions() @@ -1245,6 +1294,10 @@ void MainWindow::deleteSelectedLogicObject() void MainWindow::createNewProject() { + if (!confirmSaveBeforeDestructiveAction()) + { + return; + } const QString name = QInputDialog::getText( this, tr("新建工程"), tr("工程名称")); if (name.isEmpty()) @@ -1273,12 +1326,18 @@ void MainWindow::createNewProject() void MainWindow::saveProject() { + if (!project_service_.hasCurrentFile()) + { + saveProjectAs(); + return; + } const ProjectOperationResult result = project_service_.save(); if (!result.succeeded) { - saveProjectAs(); + showProjectResult(tr("保存工程"), fromUtf8(result.message), false); return; } + updateWindowTitle(); showProjectResult(tr("保存工程"), tr("工程已保存"), true); } @@ -1291,6 +1350,10 @@ void MainWindow::saveProjectAs() return; } const ProjectOperationResult result = project_service_.saveAs(toUtf8(path)); + if (result.succeeded) + { + updateWindowTitle(); + } showProjectResult( tr("保存工程"), result.succeeded ? tr("工程已保存") : fromUtf8(result.message), @@ -1299,6 +1362,10 @@ void MainWindow::saveProjectAs() void MainWindow::loadProject() { + if (!confirmSaveBeforeDestructiveAction()) + { + return; + } const QString path = QFileDialog::getOpenFileName( this, tr("加载工程"), {}, tr("工程文件 (*.json)")); if (path.isEmpty()) diff --git a/app/src/ui/main_window.h b/app/src/ui/main_window.h index b677526..0a457e9 100644 --- a/app/src/ui/main_window.h +++ b/app/src/ui/main_window.h @@ -20,6 +20,7 @@ QT_BEGIN_NAMESPACE class QAction; class QActionGroup; +class QCloseEvent; class QEvent; class QLabel; @@ -94,6 +95,7 @@ public: protected: bool eventFilter(QObject *watched, QEvent *event) override; + void closeEvent(QCloseEvent *event) override; private: // 配置菜单和工具栏动作 @@ -117,6 +119,8 @@ private: void clearActiveSelection(); void updateEditActions(); void clearEditorHistories(); + void updateWindowTitle(); + bool confirmSaveBeforeDestructiveAction(); // 刷新工程树和当前 HMI 页面信息 void refreshProjectUi(); void updateProjectTreeActions(); diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index ffd9ac3..23c3014 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -560,6 +560,51 @@ void testRuntimeAlarmListInteraction() "AlarmList must become visible again after returning to editing"); } +void testWindowTitleTracksUnsavedProjectChanges() +{ + TestProjectStorage storage; + ProjectService project_service(storage); + HmiEditorService editor_service(project_service); + LogicEditorService logic_editor_service(project_service); + require(editor_service.ensureDefaultPage().succeeded, + "window-title test must create a default HMI page"); + require(logic_editor_service.ensureDefaultLogic().succeeded, + "window-title test must create a default control logic"); + require(project_service.saveAs("window-title-test.json").succeeded, + "window-title fixture must start from a saved project"); + + VirtualRegisterRepository repository; + HmiRuntimeService runtime_service(repository); + AlarmEditorService alarm_editor_service(project_service); + AlarmService alarm_service(project_service, repository); + RegisterCommentService register_comment_service(project_service); + OfflineSimulationService simulation_service(repository); + RuntimeModeService mode_service(project_service, simulation_service); + RegisterMonitorService monitor_service(repository); + MainWindow window( + mode_service, + project_service, + editor_service, + logic_editor_service, + runtime_service, + alarm_editor_service, + alarm_service, + register_comment_service, + monitor_service); + + require(!window.windowTitle().endsWith(QLatin1Char('*')), + "a saved project title must not show an unsaved marker"); + requiredChild(window, "addLabelAction")->trigger(); + require(project_service.isModified() + && window.windowTitle().endsWith(QLatin1Char('*')), + "editing the project must immediately add an unsaved marker"); + + requiredChild(window, "saveProjectAction")->trigger(); + require(!project_service.isModified() + && !window.windowTitle().endsWith(QLatin1Char('*')), + "saving the project must immediately remove the unsaved marker"); +} + void testModeActionsControlEditingAvailability() { // 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择 @@ -1482,6 +1527,7 @@ int main(int argc, char *argv[]) testRuntimeProgressBarRendering(); testRuntimePageJumpDoesNotRequireRegisterWritePermission(); testRuntimeAlarmListInteraction(); + testWindowTitleTracksUnsavedProjectChanges(); testModeActionsControlEditingAvailability(); testEdgeTimerPropertyEditingAndParallelMenu(); testPlcConfigurationUsesDialog();