| @@ -26,6 +26,7 @@ | |||
| #include <QActionGroup> | |||
| #include <QAbstractSpinBox> | |||
| #include <QApplication> | |||
| #include <QCloseEvent> | |||
| #include <QEvent> | |||
| #include <QFileDialog> | |||
| #include <QGraphicsScene> | |||
| @@ -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()) | |||
| @@ -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(); | |||
| @@ -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<QAction>(window, "addLabelAction")->trigger(); | |||
| require(project_service.isModified() | |||
| && window.windowTitle().endsWith(QLatin1Char('*')), | |||
| "editing the project must immediately add an unsaved marker"); | |||
| requiredChild<QAction>(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(); | |||