diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index 4c8ffa9..26113e2 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -22,6 +22,7 @@ SOURCES += \ src/ui/register_comment_dialog.cpp \ src/ui/plc_connection_dialog.cpp \ src/ui/free_monitor_widget.cpp \ + src/ui/hmi_runtime_window.cpp \ src/ui/runtime_monitor_widget.cpp \ src/ui/toolbar_icon_factory.cpp \ src/ui/project_workspace_controller.cpp \ @@ -63,6 +64,7 @@ HEADERS += \ src/ui/register_comment_dialog.h \ src/ui/plc_connection_dialog.h \ src/ui/free_monitor_widget.h \ + src/ui/hmi_runtime_window.h \ src/ui/runtime_monitor_widget.h \ src/ui/toolbar_icon_factory.h \ src/ui/project_workspace_controller.h \ @@ -108,4 +110,5 @@ FORMS += \ src/ui/register_comment_dialog.ui \ src/ui/plc_connection_dialog.ui \ src/ui/free_monitor_widget.ui \ + src/ui/hmi_runtime_window.ui \ src/ui/runtime_monitor_widget.ui diff --git a/app/src/ui/hmi_runtime_window.cpp b/app/src/ui/hmi_runtime_window.cpp new file mode 100644 index 0000000..678b36c --- /dev/null +++ b/app/src/ui/hmi_runtime_window.cpp @@ -0,0 +1,134 @@ +#include "hmi_runtime_window.h" + +#include "hmi_editor_widget.h" +#include "services/alarm_service.h" +#include "services/hmi_editor_service.h" +#include "services/hmi_runtime_service.h" +#include "services/project_service.h" +#include "toolbar_icon_factory.h" +#include "ui_hmi_runtime_window.h" + +#include +#include + +#include + +namespace { + +QString fromUtf8(const std::string &value) +{ + return QString::fromUtf8(value.data(), static_cast(value.size())); +} + +} // namespace + +HmiRuntimeWindow::HmiRuntimeWindow( + HmiEditorService &hmi_editor_service, + HmiRuntimeService &hmi_runtime_service, + AlarmService &alarm_service, + ProjectService &project_service, + QWidget *parent) + : QMainWindow(parent), + ui_(std::make_unique()), + project_service_(project_service) +{ + ui_->setupUi(this); + setObjectName(QStringLiteral("hmiRuntimeWindow")); + ui_->exitRuntimeButton->setIcon(makeUiIcon(UiIcon::Exit)); + hmi_view_ = new HmiEditorWidget( + hmi_editor_service, + hmi_runtime_service, + alarm_service, + ui_->hmiViewContainer); + hmi_view_->setObjectName(QStringLiteral("hmiRuntimeHmiView")); + hmi_view_->setEditingEnabled(false); + ui_->hmiViewLayout->addWidget(hmi_view_); + + connect(hmi_view_, &HmiEditorWidget::pageNavigationRequested, + this, &HmiRuntimeWindow::pageNavigationRequested); + connect(ui_->exitRuntimeButton, &QToolButton::clicked, + this, &HmiRuntimeWindow::exitRequested); +} + +HmiRuntimeWindow::~HmiRuntimeWindow() = default; + +void HmiRuntimeWindow::setRuntimePage(const std::string &page_id) +{ + hmi_view_->setPageId(page_id); + const auto page = std::find_if( + project_service_.project().hmiPages.cbegin(), + project_service_.project().hmiPages.cend(), + [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }); + ui_->runtimePageLabel->setText( + page == project_service_.project().hmiPages.cend() + ? tr("未选择页面") : fromUtf8(page->name)); +} + +void HmiRuntimeWindow::reloadRuntimePage() +{ + hmi_view_->reloadPage(); +} + +void HmiRuntimeWindow::setMode( + ApplicationMode mode, PlcConnectionState plc_state) +{ + const bool offline = mode == ApplicationMode::OfflineRunning; + const bool online = mode == ApplicationMode::OnlineRunning; + runtime_active_ = offline || online; + hmi_view_->setRuntimeActive(runtime_active_); + if (!runtime_active_) + { + hmi_view_->setRuntimeWriteEnabled(false); + } + ui_->modeLabel->setText( + offline ? tr("离线仿真 · 虚拟 M/D") + : online && plc_state == PlcConnectionState::Connected + ? tr("真机运行 · PLC 已连接") + : online ? tr("真机运行 · PLC 通信不可用") + : tr("当前未运行")); +} + +void HmiRuntimeWindow::setHmiWriteEnabled(bool enabled) +{ + hmi_view_->setRuntimeWriteEnabled(runtime_active_ && enabled); +} + +void HmiRuntimeWindow::refreshValues() +{ + hmi_view_->refreshRuntimeValues(); +} + +void HmiRuntimeWindow::showForRuntime() +{ + if (!isVisible()) + { + showMaximized(); + } + else + { + raise(); + activateWindow(); + } +} + +void HmiRuntimeWindow::hideForEditing() +{ + hide(); +} + +void HmiRuntimeWindow::closeForApplicationExit() +{ + runtime_active_ = false; + close(); +} + +void HmiRuntimeWindow::closeEvent(QCloseEvent *event) +{ + if (runtime_active_) + { + emit exitRequested(); + event->ignore(); + return; + } + QMainWindow::closeEvent(event); +} diff --git a/app/src/ui/hmi_runtime_window.h b/app/src/ui/hmi_runtime_window.h new file mode 100644 index 0000000..36d8f9d --- /dev/null +++ b/app/src/ui/hmi_runtime_window.h @@ -0,0 +1,63 @@ +/** + * @file hmi_runtime_window.h + * @brief 定义面向操作员的独立 HMI 运行窗口 + * @author suyu + */ + +#pragma once + +#include "domain/runtime_state.h" +#include "services/plc_communication_gateway.h" + +#include + +#include +#include + +namespace Ui { +class HmiRuntimeWindow; +} + +class AlarmService; +class HmiEditorService; +class HmiEditorWidget; +class HmiRuntimeService; +class ProjectService; +class QCloseEvent; + +// 独立显示运行 HMI,工程编辑和运行模式切换仍由外层控制器负责 +class HmiRuntimeWindow final : public QMainWindow +{ + Q_OBJECT + +public: + HmiRuntimeWindow( + HmiEditorService &hmi_editor_service, + HmiRuntimeService &hmi_runtime_service, + AlarmService &alarm_service, + ProjectService &project_service, + QWidget *parent = nullptr); + ~HmiRuntimeWindow() override; + + void setRuntimePage(const std::string &page_id); + void reloadRuntimePage(); + void setMode(ApplicationMode mode, PlcConnectionState plc_state); + void setHmiWriteEnabled(bool enabled); + void refreshValues(); + void showForRuntime(); + void hideForEditing(); + void closeForApplicationExit(); + +signals: + void pageNavigationRequested(const QString &target_page_id); + void exitRequested(); + +protected: + void closeEvent(QCloseEvent *event) override; + +private: + std::unique_ptr ui_; + ProjectService &project_service_; + HmiEditorWidget *hmi_view_ = nullptr; + bool runtime_active_ = false; +}; diff --git a/app/src/ui/hmi_runtime_window.ui b/app/src/ui/hmi_runtime_window.ui new file mode 100644 index 0000000..89b557f --- /dev/null +++ b/app/src/ui/hmi_runtime_window.ui @@ -0,0 +1,49 @@ + + + HmiRuntimeWindow + + + 001280800 + + HMI 运行 + + + 0 + 0 + 0 + 0 + 0 + + + 042 + 1677721542 + QFrame::StyledPanel + + 12 + 4 + 8 + 4 + HMI 运行font-weight: 600; + Qt::Horizontal4020 + 未选择页面Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + 1700当前未运行Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + 退出运行退出运行退出运行Qt::ToolButtonIconOnly + + + + + + + 0 + 0 + 0 + 0 + + + + + + + + + diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 042e816..822f517 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -325,6 +325,10 @@ void MainWindow::closeEvent(QCloseEvent *event) event->ignore(); return; } + if (runtime_panel_controller_ != nullptr) + { + runtime_panel_controller_->closeForApplicationExit(); + } QMainWindow::closeEvent(event); } @@ -854,6 +858,10 @@ void MainWindow::configureRuntimeMonitor() [this](const QString &message) { appendOutputMessage(message); + }, + [this] + { + requestMode(ApplicationMode::Editing); }); runtime_panel_controller_->configure(); runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget(); @@ -1552,15 +1560,17 @@ void MainWindow::updateModeUi(const QString &message) statusBar()->showMessage(fromUtf8(navigation.message), 5000); } } - runtime_monitor_widget_->setProjectObjects( - hmi_navigation_service_->currentPageId(), current_logic_id_); - runtime_monitor_widget_->setMode(mode, runtime_mode_service_.plcConnectionState()); + runtime_panel_controller_->enterRuntime( + hmi_navigation_service_->currentPageId(), + current_logic_id_, + mode, + runtime_mode_service_.plcConnectionState()); ui_->editorTabWidget->setCurrentWidget(ui_->runtimeMonitorTab); } else { hmi_navigation_service_->stop(); - runtime_monitor_widget_->setMode( + runtime_panel_controller_->leaveRuntime( mode, runtime_mode_service_.plcConnectionState()); if (ui_->editorTabWidget->currentWidget() == ui_->runtimeMonitorTab) { diff --git a/app/src/ui/runtime_monitor_widget.cpp b/app/src/ui/runtime_monitor_widget.cpp index f00224d..2d73cb2 100644 --- a/app/src/ui/runtime_monitor_widget.cpp +++ b/app/src/ui/runtime_monitor_widget.cpp @@ -97,7 +97,7 @@ RuntimeMonitorWidget::~RuntimeMonitorWidget() = default; void RuntimeMonitorWidget::setProjectObjects( const std::string &page_id, const std::string &logic_id) { - showRuntimePage(page_id); + setRuntimePage(page_id); const QSignalBlocker blocker(ui_->runtimeLogicComboBox); ui_->runtimeLogicComboBox->clear(); @@ -120,6 +120,11 @@ void RuntimeMonitorWidget::setProjectObjects( : std::string{}); } +void RuntimeMonitorWidget::setRuntimePage(const std::string &page_id) +{ + showRuntimePage(page_id); +} + void RuntimeMonitorWidget::setMode( ApplicationMode mode, PlcConnectionState plc_state) { @@ -127,7 +132,10 @@ void RuntimeMonitorWidget::setMode( const bool online = mode == ApplicationMode::OnlineRunning; ui_->logicFrame->setVisible(offline); hmi_view_->setRuntimeActive(offline || online); - setHmiWriteEnabled(offline || (online && plc_state == PlcConnectionState::Connected)); + // 工程师诊断视图只投影 HMI,实际运行输入统一由独立 HMI 窗口处理 + setHmiWriteEnabled(false); + setFreeMonitorWriteEnabled(offline + || (online && plc_state == PlcConnectionState::Connected)); if (!offline) { logic_view_->clearRuntimeTrace(); @@ -144,6 +152,10 @@ void RuntimeMonitorWidget::setMode( void RuntimeMonitorWidget::setHmiWriteEnabled(bool enabled) { hmi_view_->setRuntimeWriteEnabled(enabled); +} + +void RuntimeMonitorWidget::setFreeMonitorWriteEnabled(bool enabled) +{ free_monitor_widget_->setWriteEnabled(enabled); } diff --git a/app/src/ui/runtime_monitor_widget.h b/app/src/ui/runtime_monitor_widget.h index 19c1c23..0313dfd 100644 --- a/app/src/ui/runtime_monitor_widget.h +++ b/app/src/ui/runtime_monitor_widget.h @@ -41,9 +41,11 @@ public: ~RuntimeMonitorWidget() override; void setProjectObjects(const std::string &page_id, const std::string &logic_id); + void setRuntimePage(const std::string &page_id); void setMode(ApplicationMode mode, PlcConnectionState plc_state); void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); void setHmiWriteEnabled(bool enabled); + void setFreeMonitorWriteEnabled(bool enabled); void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {}); FreeMonitorWidget *freeMonitorWidget() const; std::string selectedLogicId() const; diff --git a/app/src/ui/runtime_panel_controller.cpp b/app/src/ui/runtime_panel_controller.cpp index 3563db2..7ecf37e 100644 --- a/app/src/ui/runtime_panel_controller.cpp +++ b/app/src/ui/runtime_panel_controller.cpp @@ -2,6 +2,7 @@ #include "free_monitor_widget.h" #include "hmi_editor_widget.h" +#include "hmi_runtime_window.h" #include "logic_editor_widget.h" #include "runtime_monitor_widget.h" #include "services/alarm_service.h" @@ -50,7 +51,8 @@ RuntimePanelController::RuntimePanelController( std::function select_logic, std::function show_logic_properties, StatusReporter status_reporter, - OutputReporter output_reporter) + OutputReporter output_reporter, + RuntimeExitRequester runtime_exit_requester) : parent_(parent), ui_(ui), runtime_mode_service_(runtime_mode_service), @@ -68,10 +70,13 @@ RuntimePanelController::RuntimePanelController( select_logic_(std::move(select_logic)), show_logic_properties_(std::move(show_logic_properties)), status_reporter_(std::move(status_reporter)), - output_reporter_(std::move(output_reporter)) + output_reporter_(std::move(output_reporter)), + runtime_exit_requester_(std::move(runtime_exit_requester)) { } +RuntimePanelController::~RuntimePanelController() = default; + void RuntimePanelController::configure() { runtime_monitor_widget_ = new RuntimeMonitorWidget( @@ -85,6 +90,35 @@ void RuntimePanelController::configure() ui_.runtimeMonitorTab); runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget")); ui_.runtimeMonitorLayout->addWidget(runtime_monitor_widget_); + hmi_runtime_window_ = std::make_unique( + hmi_editor_service_, + hmi_runtime_service_, + alarm_service_, + project_service_); + QObject::connect(hmi_runtime_window_.get(), + &HmiRuntimeWindow::pageNavigationRequested, + &parent_, [this](const QString &target_page_id) + { + const HmiNavigationResult result = hmi_navigation_service_.navigateTo( + target_page_id.toUtf8().toStdString()); + if (!result.succeeded) + { + const QString message = fromUtf8(result.message); + status_reporter_(message, 5000); + output_reporter_(QObject::tr("页面导航失败:%1").arg(message)); + return; + } + runtime_monitor_widget_->setRuntimePage(result.pageId); + hmi_runtime_window_->setRuntimePage(result.pageId); + }); + QObject::connect(hmi_runtime_window_.get(), &HmiRuntimeWindow::exitRequested, + &parent_, [this] + { + if (runtime_exit_requester_) + { + runtime_exit_requester_(); + } + }); QObject::connect(runtime_monitor_widget_->freeMonitorWidget(), &FreeMonitorWidget::monitorAddressesChanged, &parent_, [this] @@ -107,6 +141,15 @@ void RuntimePanelController::configure() status_reporter_(message, 5000); output_reporter_(QObject::tr("页面导航失败:%1").arg(message)); }); + QObject::connect(runtime_monitor_widget_, &RuntimeMonitorWidget::runtimePageChanged, + &parent_, [this](const QString &page_id) + { + if (hmi_runtime_window_ != nullptr) + { + hmi_runtime_window_->setRuntimePage( + page_id.toUtf8().toStdString()); + } + }); ui_.editorTabWidget->setTabVisible(2, false); runtime_refresh_timer_ = new QTimer(&parent_); @@ -129,6 +172,46 @@ RuntimeMonitorWidget *RuntimePanelController::runtimeMonitorWidget() const return runtime_monitor_widget_; } +HmiRuntimeWindow *RuntimePanelController::hmiRuntimeWindow() const +{ + return hmi_runtime_window_.get(); +} + +void RuntimePanelController::enterRuntime( + const std::string &page_id, + const std::string &logic_id, + ApplicationMode mode, + PlcConnectionState plc_state) +{ + if (!runtime_session_active_) + { + runtime_monitor_widget_->setProjectObjects(page_id, logic_id); + hmi_runtime_window_->setRuntimePage(page_id); + hmi_runtime_window_->reloadRuntimePage(); + runtime_session_active_ = true; + } + runtime_monitor_widget_->setMode(mode, plc_state); + hmi_runtime_window_->setMode(mode, plc_state); + hmi_runtime_window_->showForRuntime(); +} + +void RuntimePanelController::leaveRuntime( + ApplicationMode mode, PlcConnectionState plc_state) +{ + runtime_session_active_ = false; + runtime_monitor_widget_->setMode(mode, plc_state); + hmi_runtime_window_->setMode(mode, plc_state); + hmi_runtime_window_->hideForEditing(); +} + +void RuntimePanelController::closeForApplicationExit() +{ + if (hmi_runtime_window_ != nullptr) + { + hmi_runtime_window_->closeForApplicationExit(); + } +} + void RuntimePanelController::updateSimulationUi(bool report_fault) { const bool offline = runtime_mode_service_.mode() @@ -140,14 +223,21 @@ void RuntimePanelController::updateSimulationUi(bool report_fault) const SimulationState state = runtime_mode_service_.simulationState(); const bool write_enabled = (online && plc_connected) || (offline && state == SimulationState::Running); - hmi_editor_widget_.setRuntimeWriteEnabled(write_enabled); + // 操作员输入只允许从独立 HMI 窗口发出,主窗口的画布全部只读 + hmi_editor_widget_.setRuntimeWriteEnabled(false); if (runtime_monitor_widget_ != nullptr) { - runtime_monitor_widget_->setHmiWriteEnabled(write_enabled); + runtime_monitor_widget_->setHmiWriteEnabled(false); + runtime_monitor_widget_->setFreeMonitorWriteEnabled(write_enabled); runtime_monitor_widget_->refreshValues( runtime_mode_service_.mode(), runtime_mode_service_.plcConnectionState()); } + if (hmi_runtime_window_ != nullptr) + { + hmi_runtime_window_->setHmiWriteEnabled(write_enabled); + hmi_runtime_window_->refreshValues(); + } switch (state) { @@ -178,6 +268,11 @@ void RuntimePanelController::updateSimulationUi(bool report_fault) runtime_monitor_widget_->setProjectObjects( hmi_navigation_service_.currentPageId(), logic_id); } + if (hmi_runtime_window_ != nullptr) + { + hmi_runtime_window_->setRuntimePage( + hmi_navigation_service_.currentPageId()); + } logic_editor_widget_.setRuntimeTrace( runtime_mode_service_.offlineSimulationService() .traceSnapshot().forLogic(logic_id), @@ -233,6 +328,10 @@ void RuntimePanelController::handleRuntimeTimer() runtime_mode_service_.mode(), runtime_mode_service_.plcConnectionState()); } + if (hmi_runtime_window_ != nullptr) + { + hmi_runtime_window_->refreshValues(); + } updateSimulationUi(false); } diff --git a/app/src/ui/runtime_panel_controller.h b/app/src/ui/runtime_panel_controller.h index 491fd95..cc8b853 100644 --- a/app/src/ui/runtime_panel_controller.h +++ b/app/src/ui/runtime_panel_controller.h @@ -12,6 +12,7 @@ class AlarmService; class HmiEditorService; class HmiEditorWidget; +class HmiRuntimeWindow; class HmiNavigationService; class HmiRuntimeService; class LogicEditorService; @@ -35,6 +36,7 @@ public: using StatusReporter = std::function; using OutputReporter = std::function; + using RuntimeExitRequester = std::function; RuntimePanelController( QWidget &parent, @@ -54,11 +56,21 @@ public: std::function select_logic, std::function show_logic_properties, StatusReporter status_reporter, - OutputReporter output_reporter); + OutputReporter output_reporter, + RuntimeExitRequester runtime_exit_requester); + ~RuntimePanelController(); void configure(); + void enterRuntime( + const std::string &page_id, + const std::string &logic_id, + ApplicationMode mode, + PlcConnectionState plc_state); + void leaveRuntime(ApplicationMode mode, PlcConnectionState plc_state); + void closeForApplicationExit(); void updateSimulationUi(bool report_fault); RuntimeMonitorWidget *runtimeMonitorWidget() const; + HmiRuntimeWindow *hmiRuntimeWindow() const; private: void handleRuntimeTimer(); @@ -83,6 +95,9 @@ private: std::function show_logic_properties_; StatusReporter status_reporter_; OutputReporter output_reporter_; + RuntimeExitRequester runtime_exit_requester_; RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; + std::unique_ptr hmi_runtime_window_; QTimer *runtime_refresh_timer_ = nullptr; + bool runtime_session_active_ = false; }; diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index 2d46a75..419d52f 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -14,6 +14,7 @@ #include "services/register_comment_service.h" #include "ui/hmi_editor_widget.h" #include "ui/free_monitor_widget.h" +#include "ui/hmi_runtime_window.h" #include "ui/logic_editor_widget.h" #include "ui/main_window.h" #include "ui/plc_connection_dialog.h" @@ -278,6 +279,19 @@ ObjectType *requiredChild(MainWindow &window, const char *name) return child; } +HmiRuntimeWindow *findHmiRuntimeWindow() +{ + for (QWidget *widget : QApplication::topLevelWidgets()) + { + auto *runtime_window = qobject_cast(widget); + if (runtime_window != nullptr) + { + return runtime_window; + } + } + return nullptr; +} + QColor renderedColorAt(HmiEditorWidget &view, const QPoint &viewport_position) { QImage image(view.viewport()->size(), QImage::Format_ARGB32_Premultiplied); @@ -1207,6 +1221,142 @@ void testModeActionsControlEditingAvailability() "rejected online running must restore the editing action"); } +void testIndependentHmiRuntimeWindowLifecycle() +{ + TestProjectStorage storage; + ProjectService project_service(storage); + HmiEditorService editor_service(project_service); + LogicEditorService logic_editor_service(project_service); + const std::string main_page_id = editor_service.ensureDefaultPage().id; + const std::string settings_page_id = editor_service.addPage("Settings").id; + const HmiEditorResult jump_result = editor_service.addControl( + main_page_id, HmiControlType::PageJump); + HmiControl jump = *editor_service.findControl(main_page_id, jump_result.id); + jump.pageJump = HmiPageJumpConfig{settings_page_id}; + require(editor_service.updateControl( + main_page_id, jump_result.id, jump).succeeded, + "runtime-window fixture must configure a PageJump target"); + const HmiEditorResult return_jump_result = editor_service.addControl( + settings_page_id, HmiControlType::PageJump); + HmiControl return_jump = *editor_service.findControl( + settings_page_id, return_jump_result.id); + return_jump.pageJump = HmiPageJumpConfig{main_page_id}; + require(editor_service.updateControl( + settings_page_id, return_jump_result.id, return_jump).succeeded, + "runtime-window fixture must configure a PageJump back to the initial page"); + 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); + HmiNavigationService navigation_service(project_service); + MainWindow window( + mode_service, + project_service, + editor_service, + logic_editor_service, + runtime_service, + alarm_editor_service, + alarm_service, + navigation_service, + register_comment_service, + monitor_service); + window.show(); + QApplication::processEvents(); + + requiredChild(window, "offlineModeAction")->trigger(); + QApplication::processEvents(); + HmiRuntimeWindow *runtime_window = findHmiRuntimeWindow(); + require(mode_service.mode() == ApplicationMode::OfflineRunning, + "offline mode must remain the authoritative runtime state"); + require(runtime_window != nullptr && runtime_window->isVisible(), + "entering offline running must show an independent HMI window"); + require(runtime_window->findChild( + QStringLiteral("hmiRuntimeHmiView")) != nullptr, + "independent HMI window must contain a runtime-only HMI view"); + HmiEditorWidget *runtime_hmi = runtime_window->findChild( + QStringLiteral("hmiRuntimeHmiView")); + QGraphicsItem *jump_item = nullptr; + for (QGraphicsItem *item : runtime_hmi->scene()->items()) + { + if (item->zValue() >= 0.0) + { + jump_item = item; + break; + } + } + require(jump_item != nullptr, + "independent HMI window must project the configured PageJump"); + QTest::mouseClick(runtime_hmi->viewport(), Qt::LeftButton, Qt::NoModifier, + runtime_hmi->mapFromScene( + jump_item->mapToScene(jump_item->boundingRect().center()))); + QApplication::processEvents(); + require(navigation_service.currentPageId() == settings_page_id, + "PageJump in the independent window must update the shared navigation session"); + require(runtime_window->findChild(QStringLiteral("runtimePageLabel")) + ->text() == QStringLiteral("Settings"), + "independent window title must follow the navigated runtime page"); + require(requiredChild(window, "runtimePageLabel")->text() + == QStringLiteral("Settings"), + "diagnostic runtime view must stay synchronized with the independent HMI page"); + QGraphicsItem *return_jump_item = nullptr; + for (QGraphicsItem *item : runtime_hmi->scene()->items()) + { + if (item->zValue() >= 0.0) + { + return_jump_item = item; + break; + } + } + require(return_jump_item != nullptr, + "the destination page must expose a PageJump back to the initial page"); + QTest::mouseClick(runtime_hmi->viewport(), Qt::LeftButton, Qt::NoModifier, + runtime_hmi->mapFromScene( + return_jump_item->mapToScene( + return_jump_item->boundingRect().center()))); + QApplication::processEvents(); + require(navigation_service.currentPageId() == main_page_id, + "the independent HMI must return to the initial page before editing"); + require(runtime_window->findChild(QStringLiteral("runtimePageLabel")) + ->text() == QStringLiteral("主操作页面"), + "the independent window must keep the initial page selected across sessions"); + + requiredChild(window, "editingModeAction")->trigger(); + QApplication::processEvents(); + require(mode_service.mode() == ApplicationMode::Editing, + "editing action must return the runtime mode to editing"); + require(!runtime_window->isVisible(), + "returning to editing must hide the independent HMI window"); + + requiredChild(window, "addLabelAction")->trigger(); + QApplication::processEvents(); + require(editor_service.findPage(main_page_id)->controls.size() == 2U, + "editing after a runtime session must add the new control to the page"); + + requiredChild(window, "offlineModeAction")->trigger(); + QApplication::processEvents(); + runtime_window = findHmiRuntimeWindow(); + require(runtime_window != nullptr && runtime_window->isVisible(), + "the HMI window must be reusable for a later runtime session"); + runtime_hmi = runtime_window->findChild( + QStringLiteral("hmiRuntimeHmiView")); + require(runtime_hmi != nullptr && runtime_hmi->scene()->items().size() == 3, + "a later runtime session must refresh the independent HMI page after an edit"); + require(requiredChild(window, "runtimeMonitorWidget") + ->findChild(QStringLiteral("runtimeHmiView")) + ->scene()->items().size() == 3, + "the diagnostic HMI must keep projecting the edited page"); + runtime_window->close(); + QApplication::processEvents(); + require(mode_service.mode() == ApplicationMode::Editing, + "closing the HMI window must request a transition to editing"); + require(!runtime_window->isVisible(), + "closing the HMI window must not leave a visible runtime surface"); +} + void testEdgeTimerPropertyEditingAndParallelMenu() { TestProjectStorage storage; @@ -1733,6 +1883,7 @@ int main(int argc, char *argv[]) testRuntimeAlarmListInteraction(); testWindowTitleTracksUnsavedProjectChanges(); testModeActionsControlEditingAvailability(); + testIndependentHmiRuntimeWindowLifecycle(); testEdgeTimerPropertyEditingAndParallelMenu(); testPlcConfigurationUsesDialog(); testRepeatedPlcStatusNotificationsAreCoalesced(); diff --git a/app/tests/main_window_tests.pro b/app/tests/main_window_tests.pro index b43e90c..638720b 100644 --- a/app/tests/main_window_tests.pro +++ b/app/tests/main_window_tests.pro @@ -15,6 +15,7 @@ SOURCES += \ ../src/ui/register_comment_dialog.cpp \ ../src/ui/plc_connection_dialog.cpp \ ../src/ui/free_monitor_widget.cpp \ + ../src/ui/hmi_runtime_window.cpp \ ../src/ui/runtime_monitor_widget.cpp \ ../src/ui/toolbar_icon_factory.cpp \ ../src/ui/project_workspace_controller.cpp \ @@ -52,6 +53,7 @@ HEADERS += \ ../src/ui/register_comment_dialog.h \ ../src/ui/plc_connection_dialog.h \ ../src/ui/free_monitor_widget.h \ + ../src/ui/hmi_runtime_window.h \ ../src/ui/runtime_monitor_widget.h \ ../src/ui/toolbar_icon_factory.h \ ../src/ui/project_workspace_controller.h \ @@ -94,5 +96,6 @@ FORMS += \ ../src/ui/register_comment_dialog.ui \ ../src/ui/plc_connection_dialog.ui \ ../src/ui/free_monitor_widget.ui \ + ../src/ui/hmi_runtime_window.ui \ ../src/ui/runtime_monitor_widget.ui \ ../src/ui/logic_instruction_dialog.ui