| @@ -22,7 +22,7 @@ SOURCES += \ | |||||
| src/ui/register_comment_dialog.cpp \ | src/ui/register_comment_dialog.cpp \ | ||||
| src/ui/plc_connection_dialog.cpp \ | src/ui/plc_connection_dialog.cpp \ | ||||
| src/ui/free_monitor_widget.cpp \ | src/ui/free_monitor_widget.cpp \ | ||||
| src/ui/hmi_runtime_window.cpp \ | |||||
| src/ui/runtime_monitor_window.cpp \ | |||||
| src/ui/runtime_monitor_widget.cpp \ | src/ui/runtime_monitor_widget.cpp \ | ||||
| src/ui/toolbar_icon_factory.cpp \ | src/ui/toolbar_icon_factory.cpp \ | ||||
| src/ui/project_workspace_controller.cpp \ | src/ui/project_workspace_controller.cpp \ | ||||
| @@ -64,7 +64,7 @@ HEADERS += \ | |||||
| src/ui/register_comment_dialog.h \ | src/ui/register_comment_dialog.h \ | ||||
| src/ui/plc_connection_dialog.h \ | src/ui/plc_connection_dialog.h \ | ||||
| src/ui/free_monitor_widget.h \ | src/ui/free_monitor_widget.h \ | ||||
| src/ui/hmi_runtime_window.h \ | |||||
| src/ui/runtime_monitor_window.h \ | |||||
| src/ui/runtime_monitor_widget.h \ | src/ui/runtime_monitor_widget.h \ | ||||
| src/ui/toolbar_icon_factory.h \ | src/ui/toolbar_icon_factory.h \ | ||||
| src/ui/project_workspace_controller.h \ | src/ui/project_workspace_controller.h \ | ||||
| @@ -110,5 +110,5 @@ FORMS += \ | |||||
| src/ui/register_comment_dialog.ui \ | src/ui/register_comment_dialog.ui \ | ||||
| src/ui/plc_connection_dialog.ui \ | src/ui/plc_connection_dialog.ui \ | ||||
| src/ui/free_monitor_widget.ui \ | src/ui/free_monitor_widget.ui \ | ||||
| src/ui/hmi_runtime_window.ui \ | |||||
| src/ui/runtime_monitor_window.ui \ | |||||
| src/ui/runtime_monitor_widget.ui | src/ui/runtime_monitor_widget.ui | ||||
| @@ -66,7 +66,7 @@ constexpr std::array kControlDescriptors = { | |||||
| "报警列表", | "报警列表", | ||||
| "alarm-list", | "alarm-list", | ||||
| "报警信息", | "报警信息", | ||||
| {0, 0, 360, 180}, | |||||
| {0, 0, 360, 136}, | |||||
| HmiBindingKind::None, | HmiBindingKind::None, | ||||
| HmiRuntimeValueKind::None, | HmiRuntimeValueKind::None, | ||||
| false}, | false}, | ||||
| @@ -25,6 +25,8 @@ static_assert(kMaximumLadderColumns == kMaximumConditionColumns + 1); | |||||
| constexpr std::size_t kMaximumHmiProperties = 64U; | constexpr std::size_t kMaximumHmiProperties = 64U; | ||||
| constexpr std::size_t kMaximumIdBytes = 128U; | constexpr std::size_t kMaximumIdBytes = 128U; | ||||
| constexpr std::size_t kMaximumTextBytes = 4096U; | constexpr std::size_t kMaximumTextBytes = 4096U; | ||||
| constexpr std::size_t kMaximumAlarmTitleCharacters = 12U; | |||||
| constexpr std::size_t kMaximumAlarmMessageCharacters = 20U; | |||||
| constexpr std::size_t kMaximumPropertyKeyBytes = 128U; | constexpr std::size_t kMaximumPropertyKeyBytes = 128U; | ||||
| constexpr std::size_t kMaximumPropertyValueBytes = 4096U; | constexpr std::size_t kMaximumPropertyValueBytes = 4096U; | ||||
| constexpr std::size_t kMaximumRegisterCommentBytes = 64U; | constexpr std::size_t kMaximumRegisterCommentBytes = 64U; | ||||
| @@ -36,6 +38,8 @@ constexpr int kMaximumHmiControlWidth = 8192; | |||||
| constexpr int kMaximumHmiControlHeight = 8192; | constexpr int kMaximumHmiControlHeight = 8192; | ||||
| constexpr int kMinimumHmiFontPointSize = 6; | constexpr int kMinimumHmiFontPointSize = 6; | ||||
| constexpr int kMaximumHmiFontPointSize = 72; | constexpr int kMaximumHmiFontPointSize = 72; | ||||
| constexpr int kAlarmDefaultFontPointReduction = 2; | |||||
| constexpr int kMaximumVisibleAlarmRows = 5; | |||||
| constexpr std::size_t kMaximumPollAddresses = 1024U; | constexpr std::size_t kMaximumPollAddresses = 1024U; | ||||
| constexpr std::size_t kMaximumPollBlocks = 64U; | constexpr std::size_t kMaximumPollBlocks = 64U; | ||||
| @@ -1,5 +1,6 @@ | |||||
| #include "alarm_configuration_dialog.h" | #include "alarm_configuration_dialog.h" | ||||
| #include "domain/project_limits.h" | |||||
| #include "services/alarm_editor_service.h" | #include "services/alarm_editor_service.h" | ||||
| #include "ui_alarm_configuration_dialog.h" | #include "ui_alarm_configuration_dialog.h" | ||||
| @@ -53,6 +54,8 @@ AlarmConfigurationDialog::AlarmConfigurationDialog( | |||||
| service_(service) | service_(service) | ||||
| { | { | ||||
| ui_->setupUi(this); | ui_->setupUi(this); | ||||
| ui_->messageEdit->setMaxLength( | |||||
| static_cast<int>(ProjectLimits::kMaximumAlarmMessageCharacters)); | |||||
| ui_->alarmTable->horizontalHeader()->setSectionResizeMode( | ui_->alarmTable->horizontalHeader()->setSectionResizeMode( | ||||
| 0, QHeaderView::ResizeToContents); | 0, QHeaderView::ResizeToContents); | ||||
| ui_->alarmTable->horizontalHeader()->setSectionResizeMode( | ui_->alarmTable->horizontalHeader()->setSectionResizeMode( | ||||
| @@ -5,6 +5,7 @@ | |||||
| #include "services/alarm_service.h" | #include "services/alarm_service.h" | ||||
| #include "domain/project_limits.h" | #include "domain/project_limits.h" | ||||
| #include <QApplication> | |||||
| #include <QDateTime> | #include <QDateTime> | ||||
| #include <QFont> | #include <QFont> | ||||
| #include <QGraphicsItem> | #include <QGraphicsItem> | ||||
| @@ -15,6 +16,8 @@ | |||||
| #include <QInputDialog> | #include <QInputDialog> | ||||
| #include <QPainter> | #include <QPainter> | ||||
| #include <QResizeEvent> | #include <QResizeEvent> | ||||
| #include <QStyle> | |||||
| #include <QStyleOption> | |||||
| #include <QStyleOptionGraphicsItem> | #include <QStyleOptionGraphicsItem> | ||||
| #include <algorithm> | #include <algorithm> | ||||
| @@ -27,6 +30,13 @@ namespace { | |||||
| constexpr qreal kAlarmHeaderHeight = 24.0; | constexpr qreal kAlarmHeaderHeight = 24.0; | ||||
| constexpr qreal kAlarmRowHeight = 22.0; | constexpr qreal kAlarmRowHeight = 22.0; | ||||
| constexpr qreal kAlarmCellLeftPadding = 5.0; | |||||
| constexpr qreal kAlarmCellRightPadding = 5.0; | |||||
| constexpr qreal kAlarmColumnSpacing = 3.0; | |||||
| constexpr qreal kAlarmTimeColumnWidth = 76.0; | |||||
| constexpr qreal kAlarmStateColumnWidth = 52.0; | |||||
| constexpr qreal kAlarmPageButtonSize = 16.0; | |||||
| constexpr qreal kAlarmPageIndicatorWidth = 34.0; | |||||
| QString alarmTimeText(const std::chrono::system_clock::time_point &time) | QString alarmTimeText(const std::chrono::system_clock::time_point &time) | ||||
| { | { | ||||
| @@ -73,9 +83,9 @@ public: | |||||
| { | { | ||||
| if (!control_.binding.has_value()) | if (!control_.binding.has_value()) | ||||
| { | { | ||||
| return controlRect(); | |||||
| return visibleControlRect(); | |||||
| } | } | ||||
| return controlRect().united(addressRect()); | |||||
| return visibleControlRect().united(addressRect()); | |||||
| } | } | ||||
| void paint( | void paint( | ||||
| @@ -84,7 +94,7 @@ public: | |||||
| QWidget *) override | QWidget *) override | ||||
| { | { | ||||
| // 留出一个像素边距,避免描边被图元边界裁剪 | // 留出一个像素边距,避免描边被图元边界裁剪 | ||||
| const QRectF rect = controlRect().adjusted(1, 1, -1, -1); | |||||
| const QRectF rect = visibleControlRect().adjusted(1, 1, -1, -1); | |||||
| painter->setRenderHint(QPainter::Antialiasing, true); | painter->setRenderHint(QPainter::Antialiasing, true); | ||||
| painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1)); | painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1)); | ||||
| @@ -247,18 +257,51 @@ public: | |||||
| rect.left(), rect.top(), rect.width(), kAlarmHeaderHeight); | rect.left(), rect.top(), rect.width(), kAlarmHeaderHeight); | ||||
| painter->fillRect(header, QColor(QStringLiteral("#a63f3f"))); | painter->fillRect(header, QColor(QStringLiteral("#a63f3f"))); | ||||
| painter->setPen(Qt::white); | painter->setPen(Qt::white); | ||||
| QRectF title_rect = header.adjusted(7, 0, -7, 0); | |||||
| const std::size_t page_count = alarmPageCount(); | |||||
| if (runtime_active_ && page_count > 1U) | |||||
| { | |||||
| const QRectF previous_rect = alarmPreviousPageRect(header); | |||||
| const QRectF next_rect = alarmNextPageRect(header); | |||||
| const QRectF indicator_rect = alarmPageIndicatorRect(header); | |||||
| title_rect.setRight(previous_rect.left() - kAlarmColumnSpacing); | |||||
| QStyleOption previous_option; | |||||
| previous_option.rect = previous_rect.toAlignedRect(); | |||||
| previous_option.state = alarm_page_ > 0U | |||||
| ? QStyle::State_Enabled : QStyle::State_None; | |||||
| QApplication::style()->drawPrimitive( | |||||
| QStyle::PE_IndicatorArrowLeft, | |||||
| &previous_option, | |||||
| painter); | |||||
| QStyleOption next_option; | |||||
| next_option.rect = next_rect.toAlignedRect(); | |||||
| next_option.state = alarm_page_ + 1U < page_count | |||||
| ? QStyle::State_Enabled : QStyle::State_None; | |||||
| QApplication::style()->drawPrimitive( | |||||
| QStyle::PE_IndicatorArrowRight, | |||||
| &next_option, | |||||
| painter); | |||||
| painter->setPen(Qt::white); | |||||
| painter->drawText( | |||||
| indicator_rect, | |||||
| Qt::AlignCenter, | |||||
| QStringLiteral("%1/%2") | |||||
| .arg(static_cast<qulonglong>(alarm_page_ + 1U)) | |||||
| .arg(static_cast<qulonglong>(page_count))); | |||||
| } | |||||
| const QString title = QString::fromUtf8( | |||||
| control_.text.data(), static_cast<int>(control_.text.size())); | |||||
| painter->drawText( | painter->drawText( | ||||
| header.adjusted(7, 0, -7, 0), | |||||
| title_rect, | |||||
| Qt::AlignVCenter | Qt::AlignLeft, | Qt::AlignVCenter | Qt::AlignLeft, | ||||
| QString::fromUtf8( | |||||
| control_.text.data(), static_cast<int>(control_.text.size()))); | |||||
| painter->fontMetrics().elidedText( | |||||
| title, | |||||
| Qt::ElideRight, | |||||
| std::max(0, static_cast<int>(title_rect.width())))); | |||||
| const int maximum_rows = std::max( | |||||
| 0, | |||||
| static_cast<int>((rect.height() - kAlarmHeaderHeight) | |||||
| / kAlarmRowHeight)); | |||||
| const int visible_rows = std::min( | |||||
| maximum_rows, static_cast<int>(alarm_records_.size())); | |||||
| const int visible_rows = static_cast<int>(visibleAlarmRecordCount()); | |||||
| if (visible_rows == 0) | if (visible_rows == 0) | ||||
| { | { | ||||
| painter->setPen(configuredTextColor(QColor(QStringLiteral("#6f7a82")))); | painter->setPen(configuredTextColor(QColor(QStringLiteral("#6f7a82")))); | ||||
| @@ -273,9 +316,11 @@ public: | |||||
| : QObject::tr("运行时显示当前报警")); | : QObject::tr("运行时显示当前报警")); | ||||
| break; | break; | ||||
| } | } | ||||
| const std::size_t first_record = alarmFirstRecordIndex(); | |||||
| for (int row = 0; row < visible_rows; ++row) | for (int row = 0; row < visible_rows; ++row) | ||||
| { | { | ||||
| const AlarmRecord &record = alarm_records_[static_cast<std::size_t>(row)]; | |||||
| const AlarmRecord &record = alarm_records_[ | |||||
| first_record + static_cast<std::size_t>(row)]; | |||||
| const QRectF row_rect( | const QRectF row_rect( | ||||
| rect.left(), | rect.left(), | ||||
| header.bottom() + row * kAlarmRowHeight, | header.bottom() + row * kAlarmRowHeight, | ||||
| @@ -289,23 +334,43 @@ public: | |||||
| painter->setPen(configuredTextColor(QColor(QStringLiteral("#8d1f1f")))); | painter->setPen(configuredTextColor(QColor(QStringLiteral("#8d1f1f")))); | ||||
| const QString state = record.acknowledged | const QString state = record.acknowledged | ||||
| ? QObject::tr("已确认") : QObject::tr("未确认"); | ? QObject::tr("已确认") : QObject::tr("未确认"); | ||||
| const QRectF time_rect( | |||||
| row_rect.left() + kAlarmCellLeftPadding, | |||||
| row_rect.top(), | |||||
| kAlarmTimeColumnWidth, | |||||
| row_rect.height()); | |||||
| const QRectF state_rect( | |||||
| time_rect.right() + kAlarmColumnSpacing, | |||||
| row_rect.top(), | |||||
| kAlarmStateColumnWidth, | |||||
| row_rect.height()); | |||||
| const qreal message_left = | |||||
| state_rect.right() + kAlarmColumnSpacing; | |||||
| const QRectF message_rect( | |||||
| message_left, | |||||
| row_rect.top(), | |||||
| std::max( | |||||
| 0.0, | |||||
| row_rect.right() - kAlarmCellRightPadding | |||||
| - message_left), | |||||
| row_rect.height()); | |||||
| painter->drawText( | painter->drawText( | ||||
| QRectF(row_rect.left() + 5, row_rect.top(), 58, row_rect.height()), | |||||
| time_rect, | |||||
| Qt::AlignVCenter | Qt::AlignLeft, | Qt::AlignVCenter | Qt::AlignLeft, | ||||
| alarmTimeText(record.occurredAt)); | alarmTimeText(record.occurredAt)); | ||||
| painter->drawText( | painter->drawText( | ||||
| QRectF(row_rect.left() + 66, row_rect.top(), 52, row_rect.height()), | |||||
| state_rect, | |||||
| Qt::AlignVCenter | Qt::AlignLeft, | Qt::AlignVCenter | Qt::AlignLeft, | ||||
| state); | state); | ||||
| painter->drawText( | painter->drawText( | ||||
| QRectF( | |||||
| row_rect.left() + 120, | |||||
| row_rect.top(), | |||||
| std::max(0.0, row_rect.width() - 125), | |||||
| row_rect.height()), | |||||
| message_rect, | |||||
| Qt::AlignVCenter | Qt::AlignLeft, | Qt::AlignVCenter | Qt::AlignLeft, | ||||
| QString::fromUtf8( | |||||
| record.message.data(), static_cast<int>(record.message.size()))); | |||||
| painter->fontMetrics().elidedText( | |||||
| QString::fromUtf8( | |||||
| record.message.data(), | |||||
| static_cast<int>(record.message.size())), | |||||
| Qt::ElideRight, | |||||
| std::max(0, static_cast<int>(message_rect.width())))); | |||||
| } | } | ||||
| break; | break; | ||||
| } | } | ||||
| @@ -340,6 +405,14 @@ public: | |||||
| void setInteractionState( | void setInteractionState( | ||||
| bool editable, bool runtime_active, bool runtime_write_enabled) | bool editable, bool runtime_active, bool runtime_write_enabled) | ||||
| { | { | ||||
| if (control_.type == HmiControlType::AlarmList) | |||||
| { | |||||
| prepareGeometryChange(); | |||||
| if (!runtime_active) | |||||
| { | |||||
| alarm_page_ = 0U; | |||||
| } | |||||
| } | |||||
| editing_enabled_ = editable; | editing_enabled_ = editable; | ||||
| runtime_active_ = runtime_active; | runtime_active_ = runtime_active; | ||||
| runtime_write_enabled_ = runtime_active && runtime_write_enabled; | runtime_write_enabled_ = runtime_active && runtime_write_enabled; | ||||
| @@ -396,7 +469,14 @@ public: | |||||
| void setAlarmRecords(const std::vector<AlarmRecord> &records) | void setAlarmRecords(const std::vector<AlarmRecord> &records) | ||||
| { | { | ||||
| if (control_.type == HmiControlType::AlarmList) | |||||
| { | |||||
| prepareGeometryChange(); | |||||
| } | |||||
| alarm_records_ = records; | alarm_records_ = records; | ||||
| const std::size_t page_count = alarmPageCount(); | |||||
| alarm_page_ = page_count == 0U | |||||
| ? 0U : std::min(alarm_page_, page_count - 1U); | |||||
| updateAlarmVisibility(); | updateAlarmVisibility(); | ||||
| update(); | update(); | ||||
| } | } | ||||
| @@ -428,14 +508,40 @@ protected: | |||||
| { | { | ||||
| if (event->pos().y() < kAlarmHeaderHeight) | if (event->pos().y() < kAlarmHeaderHeight) | ||||
| { | { | ||||
| const QRectF content_rect = | |||||
| visibleControlRect().adjusted(1, 1, -1, -1); | |||||
| const QRectF header( | |||||
| content_rect.left(), | |||||
| content_rect.top(), | |||||
| content_rect.width(), | |||||
| kAlarmHeaderHeight); | |||||
| const std::size_t page_count = alarmPageCount(); | |||||
| if (alarmPreviousPageRect(header).contains(event->pos()) | |||||
| && alarm_page_ > 0U) | |||||
| { | |||||
| prepareGeometryChange(); | |||||
| --alarm_page_; | |||||
| update(); | |||||
| } | |||||
| else if (alarmNextPageRect(header).contains(event->pos()) | |||||
| && alarm_page_ + 1U < page_count) | |||||
| { | |||||
| prepareGeometryChange(); | |||||
| ++alarm_page_; | |||||
| update(); | |||||
| } | |||||
| event->accept(); | event->accept(); | ||||
| return; | return; | ||||
| } | } | ||||
| const int row = static_cast<int>( | const int row = static_cast<int>( | ||||
| (event->pos().y() - kAlarmHeaderHeight) / kAlarmRowHeight); | (event->pos().y() - kAlarmHeaderHeight) / kAlarmRowHeight); | ||||
| if (row >= 0 && row < static_cast<int>(alarm_records_.size())) | |||||
| const std::size_t record_index = alarmFirstRecordIndex() | |||||
| + static_cast<std::size_t>(std::max(0, row)); | |||||
| if (row >= 0 | |||||
| && row < static_cast<int>(visibleAlarmRecordCount()) | |||||
| && record_index < alarm_records_.size()) | |||||
| { | { | ||||
| const AlarmRecord &record = alarm_records_[static_cast<std::size_t>(row)]; | |||||
| const AlarmRecord &record = alarm_records_[record_index]; | |||||
| if (!record.acknowledged && alarm_acknowledge_) | if (!record.acknowledged && alarm_acknowledge_) | ||||
| { | { | ||||
| alarm_acknowledge_(record.definitionId); | alarm_acknowledge_(record.definitionId); | ||||
| @@ -543,6 +649,86 @@ protected: | |||||
| } | } | ||||
| private: | private: | ||||
| std::size_t alarmPageSize() const | |||||
| { | |||||
| const QRectF rect = controlRect().adjusted(1, 1, -1, -1); | |||||
| const int rows_by_height = std::max( | |||||
| 0, | |||||
| static_cast<int>((rect.height() - kAlarmHeaderHeight) | |||||
| / kAlarmRowHeight)); | |||||
| return static_cast<std::size_t>(std::min( | |||||
| rows_by_height, ProjectLimits::kMaximumVisibleAlarmRows)); | |||||
| } | |||||
| std::size_t alarmPageCount() const | |||||
| { | |||||
| const std::size_t page_size = alarmPageSize(); | |||||
| return page_size == 0U || alarm_records_.empty() | |||||
| ? 0U | |||||
| : (alarm_records_.size() + page_size - 1U) / page_size; | |||||
| } | |||||
| std::size_t alarmFirstRecordIndex() const | |||||
| { | |||||
| return alarm_page_ * alarmPageSize(); | |||||
| } | |||||
| std::size_t visibleAlarmRecordCount() const | |||||
| { | |||||
| const std::size_t first_record = alarmFirstRecordIndex(); | |||||
| if (first_record >= alarm_records_.size()) | |||||
| { | |||||
| return 0U; | |||||
| } | |||||
| return std::min( | |||||
| alarmPageSize(), alarm_records_.size() - first_record); | |||||
| } | |||||
| QRectF visibleControlRect() const | |||||
| { | |||||
| const QRectF full_rect = controlRect(); | |||||
| if (control_.type != HmiControlType::AlarmList || !runtime_active_) | |||||
| { | |||||
| return full_rect; | |||||
| } | |||||
| const qreal content_height = kAlarmHeaderHeight | |||||
| + static_cast<qreal>(visibleAlarmRecordCount()) * kAlarmRowHeight | |||||
| + 2.0; | |||||
| return {full_rect.left(), | |||||
| full_rect.top(), | |||||
| full_rect.width(), | |||||
| std::min(full_rect.height(), content_height)}; | |||||
| } | |||||
| QRectF alarmNextPageRect(const QRectF &header) const | |||||
| { | |||||
| return { | |||||
| header.right() - kAlarmCellRightPadding - kAlarmPageButtonSize, | |||||
| header.center().y() - kAlarmPageButtonSize / 2.0, | |||||
| kAlarmPageButtonSize, | |||||
| kAlarmPageButtonSize}; | |||||
| } | |||||
| QRectF alarmPageIndicatorRect(const QRectF &header) const | |||||
| { | |||||
| const QRectF next_rect = alarmNextPageRect(header); | |||||
| return { | |||||
| next_rect.left() - kAlarmPageIndicatorWidth, | |||||
| header.top(), | |||||
| kAlarmPageIndicatorWidth, | |||||
| header.height()}; | |||||
| } | |||||
| QRectF alarmPreviousPageRect(const QRectF &header) const | |||||
| { | |||||
| const QRectF indicator_rect = alarmPageIndicatorRect(header); | |||||
| return { | |||||
| indicator_rect.left() - kAlarmPageButtonSize, | |||||
| header.center().y() - kAlarmPageButtonSize / 2.0, | |||||
| kAlarmPageButtonSize, | |||||
| kAlarmPageButtonSize}; | |||||
| } | |||||
| void updateAlarmVisibility() | void updateAlarmVisibility() | ||||
| { | { | ||||
| if (control_.type != HmiControlType::AlarmList) | if (control_.type != HmiControlType::AlarmList) | ||||
| @@ -616,6 +802,14 @@ private: | |||||
| font.setPointSize(point_size); | font.setPointSize(point_size); | ||||
| } | } | ||||
| } | } | ||||
| else if (control_.type == HmiControlType::AlarmList | |||||
| && font.pointSize() > 0) | |||||
| { | |||||
| font.setPointSize(std::max( | |||||
| ProjectLimits::kMinimumHmiFontPointSize, | |||||
| font.pointSize() | |||||
| - ProjectLimits::kAlarmDefaultFontPointReduction)); | |||||
| } | |||||
| const auto font_bold = control_.properties.find( | const auto font_bold = control_.properties.find( | ||||
| HmiAppearanceProperty::kFontBold); | HmiAppearanceProperty::kFontBold); | ||||
| if (font_bold != control_.properties.cend()) | if (font_bold != control_.properties.cend()) | ||||
| @@ -661,6 +855,7 @@ private: | |||||
| std::function<void(const std::string &)> page_navigation_; | std::function<void(const std::string &)> page_navigation_; | ||||
| std::function<void(const std::string &)> alarm_acknowledge_; | std::function<void(const std::string &)> alarm_acknowledge_; | ||||
| std::vector<AlarmRecord> alarm_records_; | std::vector<AlarmRecord> alarm_records_; | ||||
| std::size_t alarm_page_ = 0U; | |||||
| bool editing_enabled_ = true; | bool editing_enabled_ = true; | ||||
| bool runtime_active_ = false; | bool runtime_active_ = false; | ||||
| bool runtime_write_enabled_ = false; | bool runtime_write_enabled_ = false; | ||||
| @@ -1,134 +0,0 @@ | |||||
| #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 <QCloseEvent> | |||||
| #include <QToolButton> | |||||
| #include <algorithm> | |||||
| namespace { | |||||
| QString fromUtf8(const std::string &value) | |||||
| { | |||||
| return QString::fromUtf8(value.data(), static_cast<int>(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<Ui::HmiRuntimeWindow>()), | |||||
| 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); | |||||
| } | |||||
| @@ -1,63 +0,0 @@ | |||||
| /** | |||||
| * @file hmi_runtime_window.h | |||||
| * @brief 定义面向操作员的独立 HMI 运行窗口 | |||||
| * @author suyu | |||||
| */ | |||||
| #pragma once | |||||
| #include "domain/runtime_state.h" | |||||
| #include "services/plc_communication_gateway.h" | |||||
| #include <QMainWindow> | |||||
| #include <memory> | |||||
| #include <string> | |||||
| 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::HmiRuntimeWindow> ui_; | |||||
| ProjectService &project_service_; | |||||
| HmiEditorWidget *hmi_view_ = nullptr; | |||||
| bool runtime_active_ = false; | |||||
| }; | |||||
| @@ -1,49 +0,0 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <ui version="4.0"> | |||||
| <class>HmiRuntimeWindow</class> | |||||
| <widget class="QMainWindow" name="HmiRuntimeWindow"> | |||||
| <property name="geometry"> | |||||
| <rect><x>0</x><y>0</y><width>1280</width><height>800</height></rect> | |||||
| </property> | |||||
| <property name="windowTitle"><string>HMI 运行</string></property> | |||||
| <widget class="QWidget" name="centralWidget"> | |||||
| <layout class="QVBoxLayout" name="centralLayout"> | |||||
| <property name="spacing"><number>0</number></property> | |||||
| <property name="leftMargin"><number>0</number></property> | |||||
| <property name="topMargin"><number>0</number></property> | |||||
| <property name="rightMargin"><number>0</number></property> | |||||
| <property name="bottomMargin"><number>0</number></property> | |||||
| <item> | |||||
| <widget class="QFrame" name="runtimeHeaderFrame"> | |||||
| <property name="minimumSize"><size><width>0</width><height>42</height></size></property> | |||||
| <property name="maximumSize"><size><width>16777215</width><height>42</height></size></property> | |||||
| <property name="frameShape"><enum>QFrame::StyledPanel</enum></property> | |||||
| <layout class="QHBoxLayout" name="runtimeHeaderLayout"> | |||||
| <property name="leftMargin"><number>12</number></property> | |||||
| <property name="topMargin"><number>4</number></property> | |||||
| <property name="rightMargin"><number>8</number></property> | |||||
| <property name="bottomMargin"><number>4</number></property> | |||||
| <item><widget class="QLabel" name="titleLabel"><property name="text"><string>HMI 运行</string></property><property name="styleSheet"><string notr="true">font-weight: 600;</string></property></widget></item> | |||||
| <item><spacer name="headerSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item> | |||||
| <item><widget class="QLabel" name="runtimePageLabel"><property name="text"><string>未选择页面</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item> | |||||
| <item><widget class="QLabel" name="modeLabel"><property name="minimumSize"><size><width>170</width><height>0</height></size></property><property name="text"><string>当前未运行</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item> | |||||
| <item><widget class="QToolButton" name="exitRuntimeButton"><property name="toolTip"><string>退出运行</string></property><property name="accessibleName"><string>退出运行</string></property><property name="text"><string>退出运行</string></property><property name="toolButtonStyle"><enum>Qt::ToolButtonIconOnly</enum></property></widget></item> | |||||
| </layout> | |||||
| </widget> | |||||
| </item> | |||||
| <item> | |||||
| <widget class="QWidget" name="hmiViewContainer" native="true"> | |||||
| <layout class="QVBoxLayout" name="hmiViewLayout"> | |||||
| <property name="leftMargin"><number>0</number></property> | |||||
| <property name="topMargin"><number>0</number></property> | |||||
| <property name="rightMargin"><number>0</number></property> | |||||
| <property name="bottomMargin"><number>0</number></property> | |||||
| </layout> | |||||
| </widget> | |||||
| </item> | |||||
| </layout> | |||||
| </widget> | |||||
| </widget> | |||||
| <resources/> | |||||
| <connections/> | |||||
| </ui> | |||||
| @@ -475,12 +475,17 @@ public: | |||||
| configured_ ? counterAddressText(counter->address) | configured_ ? counterAddressText(counter->address) | ||||
| : tr("< C 地址 >")); | : tr("< C 地址 >")); | ||||
| painter->drawText( | painter->drawText( | ||||
| QRectF(-kCellWidth / 2.0, 22, kCellWidth, 18), | |||||
| Qt::AlignCenter, | |||||
| configured_ ? QStringLiteral("CV %1 / PV %2") | |||||
| QRectF(-kCellWidth / 2.0 + 4.0, 20, kCellWidth - 8.0, 18), | |||||
| Qt::AlignCenter | Qt::TextSingleLine, | |||||
| configured_ ? QStringLiteral("CV %1") | |||||
| .arg(registerAddressText(counter->currentValueAddress)) | .arg(registerAddressText(counter->currentValueAddress)) | ||||
| : tr("< CV >")); | |||||
| painter->drawText( | |||||
| QRectF(-kCellWidth / 2.0 + 4.0, 38, kCellWidth - 8.0, 18), | |||||
| Qt::AlignCenter | Qt::TextSingleLine, | |||||
| configured_ ? QStringLiteral("PV %1") | |||||
| .arg(wordOperandText(counter->preset)) | .arg(wordOperandText(counter->preset)) | ||||
| : tr("< CV / PV >")); | |||||
| : tr("< PV >")); | |||||
| } | } | ||||
| else if (const auto *move = std::get_if<MoveNodeConfig>(&config_)) | else if (const auto *move = std::get_if<MoveNodeConfig>(&config_)) | ||||
| { | { | ||||
| @@ -356,7 +356,7 @@ void MainWindow::configureActions() | |||||
| [this] { requestMode(ApplicationMode::OfflineRunning); }); | [this] { requestMode(ApplicationMode::OfflineRunning); }); | ||||
| connect(ui_->onlineModeAction, &QAction::triggered, this, | connect(ui_->onlineModeAction, &QAction::triggered, this, | ||||
| [this] { requestMode(ApplicationMode::OnlineRunning); }); | [this] { requestMode(ApplicationMode::OnlineRunning); }); | ||||
| connect(ui_->exitAction, &QAction::triggered, qApp, &QApplication::closeAllWindows); | |||||
| connect(ui_->exitAction, &QAction::triggered, this, [this] { close(); }); | |||||
| connect(ui_->newProjectAction, &QAction::triggered, this, &MainWindow::createNewProject); | connect(ui_->newProjectAction, &QAction::triggered, this, &MainWindow::createNewProject); | ||||
| connect(ui_->saveProjectAction, &QAction::triggered, this, &MainWindow::saveProject); | connect(ui_->saveProjectAction, &QAction::triggered, this, &MainWindow::saveProject); | ||||
| connect(ui_->saveAsProjectAction, &QAction::triggered, this, &MainWindow::saveProjectAs); | connect(ui_->saveAsProjectAction, &QAction::triggered, this, &MainWindow::saveProjectAs); | ||||
| @@ -691,7 +691,6 @@ void MainWindow::configureActions() | |||||
| } | } | ||||
| if (index == 0 || index == 1) | if (index == 0 || index == 1) | ||||
| { | { | ||||
| last_editing_tab_index_ = index; | |||||
| refreshProjectUi(); | refreshProjectUi(); | ||||
| } | } | ||||
| }); | }); | ||||
| @@ -765,7 +764,6 @@ void MainWindow::configureAppearance() | |||||
| ui_->deleteLogicAction->setIcon(makeUiIcon(UiIcon::Delete)); | ui_->deleteLogicAction->setIcon(makeUiIcon(UiIcon::Delete)); | ||||
| ui_->editorTabWidget->setTabIcon(0, makeUiIcon(UiIcon::HmiPage)); | ui_->editorTabWidget->setTabIcon(0, makeUiIcon(UiIcon::HmiPage)); | ||||
| ui_->editorTabWidget->setTabIcon(1, makeUiIcon(UiIcon::Logic)); | ui_->editorTabWidget->setTabIcon(1, makeUiIcon(UiIcon::Logic)); | ||||
| ui_->editorTabWidget->setTabIcon(2, makeUiIcon(UiIcon::Runtime)); | |||||
| ui_->outputDock->setMaximumHeight(220); | ui_->outputDock->setMaximumHeight(220); | ||||
| mode_status_label_ = new QLabel(this); | mode_status_label_ = new QLabel(this); | ||||
| @@ -833,7 +831,6 @@ void MainWindow::configureRuntimeMonitor() | |||||
| { | { | ||||
| runtime_panel_controller_ = std::make_unique<RuntimePanelController>( | runtime_panel_controller_ = std::make_unique<RuntimePanelController>( | ||||
| *this, | *this, | ||||
| *ui_, | |||||
| runtime_mode_service_, | runtime_mode_service_, | ||||
| project_service_, | project_service_, | ||||
| hmi_editor_service_, | hmi_editor_service_, | ||||
| @@ -1550,7 +1547,6 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| const ModePolicy policy = runtime_mode_service_.policy(); | const ModePolicy policy = runtime_mode_service_.policy(); | ||||
| restoreCurrentModeAction(); | restoreCurrentModeAction(); | ||||
| const bool running = mode != ApplicationMode::Editing; | const bool running = mode != ApplicationMode::Editing; | ||||
| ui_->editorTabWidget->setTabVisible(2, running); | |||||
| if (running) | if (running) | ||||
| { | { | ||||
| if (hmi_navigation_service_->currentPageId().empty()) | if (hmi_navigation_service_->currentPageId().empty()) | ||||
| @@ -1566,17 +1562,12 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| current_logic_id_, | current_logic_id_, | ||||
| mode, | mode, | ||||
| runtime_mode_service_.plcConnectionState()); | runtime_mode_service_.plcConnectionState()); | ||||
| ui_->editorTabWidget->setCurrentWidget(ui_->runtimeMonitorTab); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| hmi_navigation_service_->stop(); | hmi_navigation_service_->stop(); | ||||
| runtime_panel_controller_->leaveRuntime( | runtime_panel_controller_->leaveRuntime( | ||||
| mode, runtime_mode_service_.plcConnectionState()); | mode, runtime_mode_service_.plcConnectionState()); | ||||
| if (ui_->editorTabWidget->currentWidget() == ui_->runtimeMonitorTab) | |||||
| { | |||||
| ui_->editorTabWidget->setCurrentIndex(last_editing_tab_index_); | |||||
| } | |||||
| } | } | ||||
| // 将同一份模式策略同步到所有可编辑入口,避免只禁用部分操作 | // 将同一份模式策略同步到所有可编辑入口,避免只禁用部分操作 | ||||
| ui_->projectDock->setEnabled(policy.allowsProjectEditing); | ui_->projectDock->setEnabled(policy.allowsProjectEditing); | ||||
| @@ -220,5 +220,4 @@ private: | |||||
| std::string current_hmi_page_id_; | std::string current_hmi_page_id_; | ||||
| std::string current_logic_id_; | std::string current_logic_id_; | ||||
| bool plc_status_update_pending_ = false; | bool plc_status_update_pending_ = false; | ||||
| int last_editing_tab_index_ = 0; | |||||
| }; | }; | ||||
| @@ -207,18 +207,7 @@ | |||||
| </item> | </item> | ||||
| </layout> | </layout> | ||||
| </widget> | </widget> | ||||
| <widget class="QWidget" name="runtimeMonitorTab"> | |||||
| <attribute name="title"> | |||||
| <string>运行监控</string> | |||||
| </attribute> | |||||
| <layout class="QVBoxLayout" name="runtimeMonitorLayout"> | |||||
| <property name="leftMargin"><number>0</number></property> | |||||
| <property name="topMargin"><number>0</number></property> | |||||
| <property name="rightMargin"><number>0</number></property> | |||||
| <property name="bottomMargin"><number>0</number></property> | |||||
| </layout> | |||||
| </widget> | </widget> | ||||
| </widget> | |||||
| </item> | </item> | ||||
| </layout> | </layout> | ||||
| </widget> | </widget> | ||||
| @@ -310,7 +299,7 @@ | |||||
| <enum>TopToolBarArea</enum> | <enum>TopToolBarArea</enum> | ||||
| </attribute> | </attribute> | ||||
| <attribute name="toolBarBreak"> | <attribute name="toolBarBreak"> | ||||
| <bool>false</bool> | |||||
| <bool>true</bool> | |||||
| </attribute> | </attribute> | ||||
| <addaction name="addButtonAction"/> | <addaction name="addButtonAction"/> | ||||
| <addaction name="addIndicatorAction"/> | <addaction name="addIndicatorAction"/> | ||||
| @@ -330,7 +319,7 @@ | |||||
| <enum>TopToolBarArea</enum> | <enum>TopToolBarArea</enum> | ||||
| </attribute> | </attribute> | ||||
| <attribute name="toolBarBreak"> | <attribute name="toolBarBreak"> | ||||
| <bool>false</bool> | |||||
| <bool>true</bool> | |||||
| </attribute> | </attribute> | ||||
| <addaction name="addRungAction"/> | <addaction name="addRungAction"/> | ||||
| <addaction name="insertHorizontalWireAction"/> | <addaction name="insertHorizontalWireAction"/> | ||||
| @@ -15,6 +15,7 @@ | |||||
| #include <QColorDialog> | #include <QColorDialog> | ||||
| #include <QComboBox> | #include <QComboBox> | ||||
| #include <QLabel> | #include <QLabel> | ||||
| #include <QLineEdit> | |||||
| #include <QPushButton> | #include <QPushButton> | ||||
| #include <QDialog> | #include <QDialog> | ||||
| #include <QObject> | #include <QObject> | ||||
| @@ -40,6 +41,19 @@ std::string toUtf8(const QString &value) | |||||
| return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size())); | return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size())); | ||||
| } | } | ||||
| int defaultFontPointSize(HmiControlType type) | |||||
| { | |||||
| int point_size = QApplication::font().pointSize(); | |||||
| if (type == HmiControlType::AlarmList) | |||||
| { | |||||
| point_size -= ProjectLimits::kAlarmDefaultFontPointReduction; | |||||
| } | |||||
| return std::clamp( | |||||
| point_size, | |||||
| ProjectLimits::kMinimumHmiFontPointSize, | |||||
| ProjectLimits::kMaximumHmiFontPointSize); | |||||
| } | |||||
| } // namespace | } // namespace | ||||
| PropertyPanelController::PropertyPanelController( | PropertyPanelController::PropertyPanelController( | ||||
| @@ -181,6 +195,7 @@ void PropertyPanelController::showControlProperties(const std::string &control_i | |||||
| if (!has_control) | if (!has_control) | ||||
| { | { | ||||
| ui_.controlTextEdit->setMaxLength(32767); | |||||
| ui_.bindingAreaLabel->setVisible(false); | ui_.bindingAreaLabel->setVisible(false); | ||||
| ui_.bindingAreaComboBox->setVisible(false); | ui_.bindingAreaComboBox->setVisible(false); | ||||
| ui_.bindingIndexLabel->setVisible(false); | ui_.bindingIndexLabel->setVisible(false); | ||||
| @@ -207,6 +222,10 @@ void PropertyPanelController::showControlProperties(const std::string &control_i | |||||
| return; | return; | ||||
| } | } | ||||
| ui_.controlTextEdit->setMaxLength( | |||||
| control->type == HmiControlType::AlarmList | |||||
| ? static_cast<int>(ProjectLimits::kMaximumAlarmTitleCharacters) | |||||
| : 32767); | |||||
| ui_.controlIdEdit->setText(fromUtf8(control->id)); | ui_.controlIdEdit->setText(fromUtf8(control->id)); | ||||
| ui_.controlTextEdit->setText(fromUtf8(control->text)); | ui_.controlTextEdit->setText(fromUtf8(control->text)); | ||||
| ui_.controlXSpinBox->setValue(control->bounds.x); | ui_.controlXSpinBox->setValue(control->bounds.x); | ||||
| @@ -278,10 +297,7 @@ void PropertyPanelController::showControlProperties(const std::string &control_i | |||||
| swatch_color.name(QColor::HexRgb)) | swatch_color.name(QColor::HexRgb)) | ||||
| : QString{}); | : QString{}); | ||||
| const int default_font_size = std::clamp( | |||||
| QApplication::font().pointSize(), | |||||
| ProjectLimits::kMinimumHmiFontPointSize, | |||||
| ProjectLimits::kMaximumHmiFontPointSize); | |||||
| const int default_font_size = defaultFontPointSize(control->type); | |||||
| int font_size = default_font_size; | int font_size = default_font_size; | ||||
| const auto font_size_property = control->properties.find( | const auto font_size_property = control->properties.find( | ||||
| HmiAppearanceProperty::kFontSize); | HmiAppearanceProperty::kFontSize); | ||||
| @@ -6,11 +6,13 @@ | |||||
| #include "services/hmi_navigation_service.h" | #include "services/hmi_navigation_service.h" | ||||
| #include "services/alarm_service.h" | #include "services/alarm_service.h" | ||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include "toolbar_icon_factory.h" | |||||
| #include "ui_runtime_monitor_widget.h" | #include "ui_runtime_monitor_widget.h" | ||||
| #include <QSplitter> | #include <QSplitter> | ||||
| #include <QComboBox> | #include <QComboBox> | ||||
| #include <QSignalBlocker> | #include <QSignalBlocker> | ||||
| #include <QToolButton> | |||||
| #include <algorithm> | #include <algorithm> | ||||
| @@ -44,6 +46,7 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( | |||||
| hmi_navigation_service_(hmi_navigation_service) | hmi_navigation_service_(hmi_navigation_service) | ||||
| { | { | ||||
| ui_->setupUi(this); | ui_->setupUi(this); | ||||
| ui_->exitRuntimeButton->setIcon(makeUiIcon(UiIcon::Exit)); | |||||
| hmi_view_ = new HmiEditorWidget( | hmi_view_ = new HmiEditorWidget( | ||||
| hmi_editor_service, | hmi_editor_service, | ||||
| hmi_runtime_service, | hmi_runtime_service, | ||||
| @@ -77,8 +80,9 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( | |||||
| return; | return; | ||||
| } | } | ||||
| showRuntimePage(result.pageId); | showRuntimePage(result.pageId); | ||||
| emit runtimePageChanged(fromUtf8(result.pageId)); | |||||
| }); | }); | ||||
| connect(ui_->exitRuntimeButton, &QToolButton::clicked, | |||||
| this, &RuntimeMonitorWidget::exitRequested); | |||||
| connect(ui_->runtimeLogicComboBox, | connect(ui_->runtimeLogicComboBox, | ||||
| QOverload<int>::of(&QComboBox::currentIndexChanged), | QOverload<int>::of(&QComboBox::currentIndexChanged), | ||||
| this, | this, | ||||
| @@ -132,8 +136,10 @@ void RuntimeMonitorWidget::setMode( | |||||
| const bool online = mode == ApplicationMode::OnlineRunning; | const bool online = mode == ApplicationMode::OnlineRunning; | ||||
| ui_->logicFrame->setVisible(offline); | ui_->logicFrame->setVisible(offline); | ||||
| hmi_view_->setRuntimeActive(offline || online); | hmi_view_->setRuntimeActive(offline || online); | ||||
| // 工程师诊断视图只投影 HMI,实际运行输入统一由独立 HMI 窗口处理 | |||||
| setHmiWriteEnabled(false); | |||||
| if (!offline && !online) | |||||
| { | |||||
| setHmiWriteEnabled(false); | |||||
| } | |||||
| setFreeMonitorWriteEnabled(offline | setFreeMonitorWriteEnabled(offline | ||||
| || (online && plc_state == PlcConnectionState::Connected)); | || (online && plc_state == PlcConnectionState::Connected)); | ||||
| if (!offline) | if (!offline) | ||||
| @@ -52,7 +52,7 @@ public: | |||||
| signals: | signals: | ||||
| void navigationFailed(const QString &message); | void navigationFailed(const QString &message); | ||||
| void runtimePageChanged(const QString &page_id); | |||||
| void exitRequested(); | |||||
| private: | private: | ||||
| void showRuntimePage(const std::string &page_id); | void showRuntimePage(const std::string &page_id); | ||||
| @@ -13,6 +13,7 @@ | |||||
| <item><widget class="QLabel" name="titleLabel"><property name="text"><string>运行监控</string></property><property name="styleSheet"><string notr="true">font-weight: 600;</string></property></widget></item> | <item><widget class="QLabel" name="titleLabel"><property name="text"><string>运行监控</string></property><property name="styleSheet"><string notr="true">font-weight: 600;</string></property></widget></item> | ||||
| <item><spacer name="headerSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item> | <item><spacer name="headerSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item> | ||||
| <item><widget class="QLabel" name="modeLabel"><property name="text"><string>当前未运行</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item> | <item><widget class="QLabel" name="modeLabel"><property name="text"><string>当前未运行</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item> | ||||
| <item><widget class="QToolButton" name="exitRuntimeButton"><property name="toolTip"><string>返回编辑态</string></property><property name="accessibleName"><string>返回编辑态</string></property><property name="text"><string>返回编辑态</string></property><property name="toolButtonStyle"><enum>Qt::ToolButtonIconOnly</enum></property></widget></item> | |||||
| </layout> | </layout> | ||||
| </item> | </item> | ||||
| <item> | <item> | ||||
| @@ -0,0 +1,54 @@ | |||||
| #include "runtime_monitor_window.h" | |||||
| #include "ui_runtime_monitor_window.h" | |||||
| #include <QCloseEvent> | |||||
| #include <QWidget> | |||||
| RuntimeMonitorWindow::RuntimeMonitorWindow(QWidget *parent) | |||||
| : QMainWindow(parent, Qt::Window), | |||||
| ui_(std::make_unique<Ui::RuntimeMonitorWindow>()) | |||||
| { | |||||
| ui_->setupUi(this); | |||||
| } | |||||
| RuntimeMonitorWindow::~RuntimeMonitorWindow() = default; | |||||
| void RuntimeMonitorWindow::setMonitorWidget(QWidget &widget) | |||||
| { | |||||
| widget.setParent(ui_->centralWidget); | |||||
| ui_->runtimeMonitorWindowLayout->addWidget(&widget); | |||||
| } | |||||
| void RuntimeMonitorWindow::showForRuntime() | |||||
| { | |||||
| application_exit_ = false; | |||||
| runtime_active_ = true; | |||||
| showMaximized(); | |||||
| raise(); | |||||
| activateWindow(); | |||||
| } | |||||
| void RuntimeMonitorWindow::hideForEditing() | |||||
| { | |||||
| runtime_active_ = false; | |||||
| hide(); | |||||
| } | |||||
| void RuntimeMonitorWindow::closeForApplicationExit() | |||||
| { | |||||
| application_exit_ = true; | |||||
| runtime_active_ = false; | |||||
| close(); | |||||
| } | |||||
| void RuntimeMonitorWindow::closeEvent(QCloseEvent *event) | |||||
| { | |||||
| if (runtime_active_ && !application_exit_) | |||||
| { | |||||
| emit exitRequested(); | |||||
| event->ignore(); | |||||
| return; | |||||
| } | |||||
| QMainWindow::closeEvent(event); | |||||
| } | |||||
| @@ -0,0 +1,38 @@ | |||||
| #pragma once | |||||
| #include <QMainWindow> | |||||
| #include <memory> | |||||
| namespace Ui { | |||||
| class RuntimeMonitorWindow; | |||||
| } | |||||
| class QCloseEvent; | |||||
| class QWidget; | |||||
| // 承载唯一工程师运行监控投影的独立顶层窗口 | |||||
| class RuntimeMonitorWindow final : public QMainWindow | |||||
| { | |||||
| Q_OBJECT | |||||
| public: | |||||
| explicit RuntimeMonitorWindow(QWidget *parent = nullptr); | |||||
| ~RuntimeMonitorWindow() override; | |||||
| void setMonitorWidget(QWidget &widget); | |||||
| void showForRuntime(); | |||||
| void hideForEditing(); | |||||
| void closeForApplicationExit(); | |||||
| signals: | |||||
| void exitRequested(); | |||||
| protected: | |||||
| void closeEvent(QCloseEvent *event) override; | |||||
| private: | |||||
| std::unique_ptr<Ui::RuntimeMonitorWindow> ui_; | |||||
| bool runtime_active_ = false; | |||||
| bool application_exit_ = false; | |||||
| }; | |||||
| @@ -0,0 +1,44 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <ui version="4.0"> | |||||
| <class>RuntimeMonitorWindow</class> | |||||
| <widget class="QMainWindow" name="RuntimeMonitorWindow"> | |||||
| <property name="geometry"> | |||||
| <rect> | |||||
| <x>0</x> | |||||
| <y>0</y> | |||||
| <width>1280</width> | |||||
| <height>800</height> | |||||
| </rect> | |||||
| </property> | |||||
| <property name="minimumSize"> | |||||
| <size> | |||||
| <width>960</width> | |||||
| <height>640</height> | |||||
| </size> | |||||
| </property> | |||||
| <property name="windowTitle"> | |||||
| <string>运行监控 - 综合平台编程器</string> | |||||
| </property> | |||||
| <widget class="QWidget" name="centralWidget"> | |||||
| <layout class="QVBoxLayout" name="runtimeMonitorWindowLayout"> | |||||
| <property name="spacing"> | |||||
| <number>0</number> | |||||
| </property> | |||||
| <property name="leftMargin"> | |||||
| <number>0</number> | |||||
| </property> | |||||
| <property name="topMargin"> | |||||
| <number>0</number> | |||||
| </property> | |||||
| <property name="rightMargin"> | |||||
| <number>0</number> | |||||
| </property> | |||||
| <property name="bottomMargin"> | |||||
| <number>0</number> | |||||
| </property> | |||||
| </layout> | |||||
| </widget> | |||||
| </widget> | |||||
| <resources/> | |||||
| <connections/> | |||||
| </ui> | |||||
| @@ -2,9 +2,9 @@ | |||||
| #include "free_monitor_widget.h" | #include "free_monitor_widget.h" | ||||
| #include "hmi_editor_widget.h" | #include "hmi_editor_widget.h" | ||||
| #include "hmi_runtime_window.h" | |||||
| #include "logic_editor_widget.h" | #include "logic_editor_widget.h" | ||||
| #include "runtime_monitor_widget.h" | #include "runtime_monitor_widget.h" | ||||
| #include "runtime_monitor_window.h" | |||||
| #include "services/alarm_service.h" | #include "services/alarm_service.h" | ||||
| #include "services/hmi_editor_service.h" | #include "services/hmi_editor_service.h" | ||||
| #include "services/hmi_navigation_service.h" | #include "services/hmi_navigation_service.h" | ||||
| @@ -14,7 +14,6 @@ | |||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| #include "services/register_monitor_service.h" | #include "services/register_monitor_service.h" | ||||
| #include "services/runtime_mode_service.h" | #include "services/runtime_mode_service.h" | ||||
| #include "ui_main_window.h" | |||||
| #include <QListWidget> | #include <QListWidget> | ||||
| #include <QLabel> | #include <QLabel> | ||||
| @@ -35,7 +34,6 @@ QString fromUtf8(const std::string &value) | |||||
| RuntimePanelController::RuntimePanelController( | RuntimePanelController::RuntimePanelController( | ||||
| QWidget &parent, | QWidget &parent, | ||||
| Ui::MainWindow &ui, | |||||
| RuntimeModeService &runtime_mode_service, | RuntimeModeService &runtime_mode_service, | ||||
| ProjectService &project_service, | ProjectService &project_service, | ||||
| HmiEditorService &hmi_editor_service, | HmiEditorService &hmi_editor_service, | ||||
| @@ -54,7 +52,6 @@ RuntimePanelController::RuntimePanelController( | |||||
| OutputReporter output_reporter, | OutputReporter output_reporter, | ||||
| RuntimeExitRequester runtime_exit_requester) | RuntimeExitRequester runtime_exit_requester) | ||||
| : parent_(parent), | : parent_(parent), | ||||
| ui_(ui), | |||||
| runtime_mode_service_(runtime_mode_service), | runtime_mode_service_(runtime_mode_service), | ||||
| project_service_(project_service), | project_service_(project_service), | ||||
| hmi_editor_service_(hmi_editor_service), | hmi_editor_service_(hmi_editor_service), | ||||
| @@ -79,6 +76,9 @@ RuntimePanelController::~RuntimePanelController() = default; | |||||
| void RuntimePanelController::configure() | void RuntimePanelController::configure() | ||||
| { | { | ||||
| runtime_monitor_window_ = std::make_unique<RuntimeMonitorWindow>(&parent_); | |||||
| runtime_monitor_window_->setObjectName( | |||||
| QStringLiteral("runtimeMonitorWindow")); | |||||
| runtime_monitor_widget_ = new RuntimeMonitorWidget( | runtime_monitor_widget_ = new RuntimeMonitorWidget( | ||||
| hmi_editor_service_, | hmi_editor_service_, | ||||
| hmi_runtime_service_, | hmi_runtime_service_, | ||||
| @@ -87,31 +87,19 @@ void RuntimePanelController::configure() | |||||
| hmi_navigation_service_, | hmi_navigation_service_, | ||||
| alarm_service_, | alarm_service_, | ||||
| register_monitor_service_, | register_monitor_service_, | ||||
| ui_.runtimeMonitorTab); | |||||
| runtime_monitor_window_.get()); | |||||
| runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget")); | runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget")); | ||||
| ui_.runtimeMonitorLayout->addWidget(runtime_monitor_widget_); | |||||
| hmi_runtime_window_ = std::make_unique<HmiRuntimeWindow>( | |||||
| 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) | |||||
| runtime_monitor_window_->setMonitorWidget(*runtime_monitor_widget_); | |||||
| QObject::connect(runtime_monitor_widget_, &RuntimeMonitorWidget::exitRequested, | |||||
| &parent_, [this] | |||||
| { | { | ||||
| const HmiNavigationResult result = hmi_navigation_service_.navigateTo( | |||||
| target_page_id.toUtf8().toStdString()); | |||||
| if (!result.succeeded) | |||||
| if (runtime_exit_requester_) | |||||
| { | { | ||||
| const QString message = fromUtf8(result.message); | |||||
| status_reporter_(message, 5000); | |||||
| output_reporter_(QObject::tr("页面导航失败:%1").arg(message)); | |||||
| return; | |||||
| runtime_exit_requester_(); | |||||
| } | } | ||||
| runtime_monitor_widget_->setRuntimePage(result.pageId); | |||||
| hmi_runtime_window_->setRuntimePage(result.pageId); | |||||
| }); | }); | ||||
| QObject::connect(hmi_runtime_window_.get(), &HmiRuntimeWindow::exitRequested, | |||||
| QObject::connect(runtime_monitor_window_.get(), | |||||
| &RuntimeMonitorWindow::exitRequested, | |||||
| &parent_, [this] | &parent_, [this] | ||||
| { | { | ||||
| if (runtime_exit_requester_) | if (runtime_exit_requester_) | ||||
| @@ -141,17 +129,6 @@ void RuntimePanelController::configure() | |||||
| status_reporter_(message, 5000); | status_reporter_(message, 5000); | ||||
| output_reporter_(QObject::tr("页面导航失败:%1").arg(message)); | 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_); | runtime_refresh_timer_ = new QTimer(&parent_); | ||||
| runtime_refresh_timer_->setInterval(150); | runtime_refresh_timer_->setInterval(150); | ||||
| QObject::connect(runtime_refresh_timer_, &QTimer::timeout, | QObject::connect(runtime_refresh_timer_, &QTimer::timeout, | ||||
| @@ -172,11 +149,6 @@ RuntimeMonitorWidget *RuntimePanelController::runtimeMonitorWidget() const | |||||
| return runtime_monitor_widget_; | return runtime_monitor_widget_; | ||||
| } | } | ||||
| HmiRuntimeWindow *RuntimePanelController::hmiRuntimeWindow() const | |||||
| { | |||||
| return hmi_runtime_window_.get(); | |||||
| } | |||||
| void RuntimePanelController::enterRuntime( | void RuntimePanelController::enterRuntime( | ||||
| const std::string &page_id, | const std::string &page_id, | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| @@ -186,13 +158,10 @@ void RuntimePanelController::enterRuntime( | |||||
| if (!runtime_session_active_) | if (!runtime_session_active_) | ||||
| { | { | ||||
| runtime_monitor_widget_->setProjectObjects(page_id, logic_id); | runtime_monitor_widget_->setProjectObjects(page_id, logic_id); | ||||
| hmi_runtime_window_->setRuntimePage(page_id); | |||||
| hmi_runtime_window_->reloadRuntimePage(); | |||||
| runtime_session_active_ = true; | runtime_session_active_ = true; | ||||
| } | } | ||||
| runtime_monitor_widget_->setMode(mode, plc_state); | runtime_monitor_widget_->setMode(mode, plc_state); | ||||
| hmi_runtime_window_->setMode(mode, plc_state); | |||||
| hmi_runtime_window_->showForRuntime(); | |||||
| runtime_monitor_window_->showForRuntime(); | |||||
| } | } | ||||
| void RuntimePanelController::leaveRuntime( | void RuntimePanelController::leaveRuntime( | ||||
| @@ -200,15 +169,14 @@ void RuntimePanelController::leaveRuntime( | |||||
| { | { | ||||
| runtime_session_active_ = false; | runtime_session_active_ = false; | ||||
| runtime_monitor_widget_->setMode(mode, plc_state); | runtime_monitor_widget_->setMode(mode, plc_state); | ||||
| hmi_runtime_window_->setMode(mode, plc_state); | |||||
| hmi_runtime_window_->hideForEditing(); | |||||
| runtime_monitor_window_->hideForEditing(); | |||||
| } | } | ||||
| void RuntimePanelController::closeForApplicationExit() | void RuntimePanelController::closeForApplicationExit() | ||||
| { | { | ||||
| if (hmi_runtime_window_ != nullptr) | |||||
| if (runtime_monitor_window_ != nullptr) | |||||
| { | { | ||||
| hmi_runtime_window_->closeForApplicationExit(); | |||||
| runtime_monitor_window_->closeForApplicationExit(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -223,22 +191,16 @@ void RuntimePanelController::updateSimulationUi(bool report_fault) | |||||
| const SimulationState state = runtime_mode_service_.simulationState(); | const SimulationState state = runtime_mode_service_.simulationState(); | ||||
| const bool write_enabled = (online && plc_connected) | const bool write_enabled = (online && plc_connected) | ||||
| || (offline && state == SimulationState::Running); | || (offline && state == SimulationState::Running); | ||||
| // 操作员输入只允许从独立 HMI 窗口发出,主窗口的画布全部只读 | |||||
| // 编辑画布始终只读,运行操作统一由运行监控中的 HMI 发出 | |||||
| hmi_editor_widget_.setRuntimeWriteEnabled(false); | hmi_editor_widget_.setRuntimeWriteEnabled(false); | ||||
| if (runtime_monitor_widget_ != nullptr) | if (runtime_monitor_widget_ != nullptr) | ||||
| { | { | ||||
| runtime_monitor_widget_->setHmiWriteEnabled(false); | |||||
| runtime_monitor_widget_->setHmiWriteEnabled(write_enabled); | |||||
| runtime_monitor_widget_->setFreeMonitorWriteEnabled(write_enabled); | runtime_monitor_widget_->setFreeMonitorWriteEnabled(write_enabled); | ||||
| runtime_monitor_widget_->refreshValues( | runtime_monitor_widget_->refreshValues( | ||||
| runtime_mode_service_.mode(), | runtime_mode_service_.mode(), | ||||
| runtime_mode_service_.plcConnectionState()); | runtime_mode_service_.plcConnectionState()); | ||||
| } | } | ||||
| if (hmi_runtime_window_ != nullptr) | |||||
| { | |||||
| hmi_runtime_window_->setHmiWriteEnabled(write_enabled); | |||||
| hmi_runtime_window_->refreshValues(); | |||||
| } | |||||
| switch (state) | switch (state) | ||||
| { | { | ||||
| case SimulationState::Running: | case SimulationState::Running: | ||||
| @@ -268,11 +230,6 @@ void RuntimePanelController::updateSimulationUi(bool report_fault) | |||||
| runtime_monitor_widget_->setProjectObjects( | runtime_monitor_widget_->setProjectObjects( | ||||
| hmi_navigation_service_.currentPageId(), logic_id); | hmi_navigation_service_.currentPageId(), logic_id); | ||||
| } | } | ||||
| if (hmi_runtime_window_ != nullptr) | |||||
| { | |||||
| hmi_runtime_window_->setRuntimePage( | |||||
| hmi_navigation_service_.currentPageId()); | |||||
| } | |||||
| logic_editor_widget_.setRuntimeTrace( | logic_editor_widget_.setRuntimeTrace( | ||||
| runtime_mode_service_.offlineSimulationService() | runtime_mode_service_.offlineSimulationService() | ||||
| .traceSnapshot().forLogic(logic_id), | .traceSnapshot().forLogic(logic_id), | ||||
| @@ -328,10 +285,6 @@ void RuntimePanelController::handleRuntimeTimer() | |||||
| runtime_mode_service_.mode(), | runtime_mode_service_.mode(), | ||||
| runtime_mode_service_.plcConnectionState()); | runtime_mode_service_.plcConnectionState()); | ||||
| } | } | ||||
| if (hmi_runtime_window_ != nullptr) | |||||
| { | |||||
| hmi_runtime_window_->refreshValues(); | |||||
| } | |||||
| updateSimulationUi(false); | updateSimulationUi(false); | ||||
| } | } | ||||
| @@ -12,7 +12,6 @@ | |||||
| class AlarmService; | class AlarmService; | ||||
| class HmiEditorService; | class HmiEditorService; | ||||
| class HmiEditorWidget; | class HmiEditorWidget; | ||||
| class HmiRuntimeWindow; | |||||
| class HmiNavigationService; | class HmiNavigationService; | ||||
| class HmiRuntimeService; | class HmiRuntimeService; | ||||
| class LogicEditorService; | class LogicEditorService; | ||||
| @@ -22,13 +21,10 @@ class ProjectService; | |||||
| class RegisterMonitorService; | class RegisterMonitorService; | ||||
| class RuntimeModeService; | class RuntimeModeService; | ||||
| class RuntimeMonitorWidget; | class RuntimeMonitorWidget; | ||||
| class RuntimeMonitorWindow; | |||||
| class QTimer; | class QTimer; | ||||
| class QWidget; | class QWidget; | ||||
| namespace Ui { | |||||
| class MainWindow; | |||||
| } | |||||
| // 组织运行监控工作台、运行刷新定时器和离线执行器反馈 | // 组织运行监控工作台、运行刷新定时器和离线执行器反馈 | ||||
| class RuntimePanelController final | class RuntimePanelController final | ||||
| { | { | ||||
| @@ -40,7 +36,6 @@ public: | |||||
| RuntimePanelController( | RuntimePanelController( | ||||
| QWidget &parent, | QWidget &parent, | ||||
| Ui::MainWindow &ui, | |||||
| RuntimeModeService &runtime_mode_service, | RuntimeModeService &runtime_mode_service, | ||||
| ProjectService &project_service, | ProjectService &project_service, | ||||
| HmiEditorService &hmi_editor_service, | HmiEditorService &hmi_editor_service, | ||||
| @@ -70,7 +65,6 @@ public: | |||||
| void closeForApplicationExit(); | void closeForApplicationExit(); | ||||
| void updateSimulationUi(bool report_fault); | void updateSimulationUi(bool report_fault); | ||||
| RuntimeMonitorWidget *runtimeMonitorWidget() const; | RuntimeMonitorWidget *runtimeMonitorWidget() const; | ||||
| HmiRuntimeWindow *hmiRuntimeWindow() const; | |||||
| private: | private: | ||||
| void handleRuntimeTimer(); | void handleRuntimeTimer(); | ||||
| @@ -78,7 +72,6 @@ private: | |||||
| void handleScanCompleted(); | void handleScanCompleted(); | ||||
| QWidget &parent_; | QWidget &parent_; | ||||
| Ui::MainWindow &ui_; | |||||
| RuntimeModeService &runtime_mode_service_; | RuntimeModeService &runtime_mode_service_; | ||||
| ProjectService &project_service_; | ProjectService &project_service_; | ||||
| HmiEditorService &hmi_editor_service_; | HmiEditorService &hmi_editor_service_; | ||||
| @@ -96,8 +89,8 @@ private: | |||||
| StatusReporter status_reporter_; | StatusReporter status_reporter_; | ||||
| OutputReporter output_reporter_; | OutputReporter output_reporter_; | ||||
| RuntimeExitRequester runtime_exit_requester_; | RuntimeExitRequester runtime_exit_requester_; | ||||
| std::unique_ptr<RuntimeMonitorWindow> runtime_monitor_window_; | |||||
| RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; | RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; | ||||
| std::unique_ptr<HmiRuntimeWindow> hmi_runtime_window_; | |||||
| QTimer *runtime_refresh_timer_ = nullptr; | QTimer *runtime_refresh_timer_ = nullptr; | ||||
| bool runtime_session_active_ = false; | bool runtime_session_active_ = false; | ||||
| }; | }; | ||||
| @@ -13,13 +13,14 @@ | |||||
| #include "services/runtime_mode_service.h" | #include "services/runtime_mode_service.h" | ||||
| #include "services/register_monitor_service.h" | #include "services/register_monitor_service.h" | ||||
| #include "services/register_comment_service.h" | #include "services/register_comment_service.h" | ||||
| #include "ui/alarm_configuration_dialog.h" | |||||
| #include "ui/hmi_editor_widget.h" | #include "ui/hmi_editor_widget.h" | ||||
| #include "ui/free_monitor_widget.h" | #include "ui/free_monitor_widget.h" | ||||
| #include "ui/hmi_runtime_window.h" | |||||
| #include "ui/logic_editor_widget.h" | #include "ui/logic_editor_widget.h" | ||||
| #include "ui/main_window.h" | #include "ui/main_window.h" | ||||
| #include "ui/plc_connection_dialog.h" | #include "ui/plc_connection_dialog.h" | ||||
| #include "ui/runtime_monitor_widget.h" | #include "ui/runtime_monitor_widget.h" | ||||
| #include "ui/runtime_monitor_window.h" | |||||
| #include <QAction> | #include <QAction> | ||||
| #include <QApplication> | #include <QApplication> | ||||
| @@ -39,6 +40,7 @@ | |||||
| #include <QPushButton> | #include <QPushButton> | ||||
| #include <QStringList> | #include <QStringList> | ||||
| #include <QTableWidget> | #include <QTableWidget> | ||||
| #include <QTabBar> | |||||
| #include <QTabWidget> | #include <QTabWidget> | ||||
| #include <QTimer> | #include <QTimer> | ||||
| #include <QTest> | #include <QTest> | ||||
| @@ -280,19 +282,6 @@ ObjectType *requiredChild(MainWindow &window, const char *name) | |||||
| return child; | return child; | ||||
| } | } | ||||
| HmiRuntimeWindow *findHmiRuntimeWindow() | |||||
| { | |||||
| for (QWidget *widget : QApplication::topLevelWidgets()) | |||||
| { | |||||
| auto *runtime_window = qobject_cast<HmiRuntimeWindow *>(widget); | |||||
| if (runtime_window != nullptr) | |||||
| { | |||||
| return runtime_window; | |||||
| } | |||||
| } | |||||
| return nullptr; | |||||
| } | |||||
| QColor renderedColorAt(HmiEditorWidget &view, const QPoint &viewport_position) | QColor renderedColorAt(HmiEditorWidget &view, const QPoint &viewport_position) | ||||
| { | { | ||||
| QImage image(view.viewport()->size(), QImage::Format_ARGB32_Premultiplied); | QImage image(view.viewport()->size(), QImage::Format_ARGB32_Premultiplied); | ||||
| @@ -591,14 +580,28 @@ void testRuntimeAlarmListInteraction() | |||||
| require(alarm_list != nullptr && !alarm_list->binding.has_value(), | require(alarm_list != nullptr && !alarm_list->binding.has_value(), | ||||
| "AlarmList must not require a register binding"); | "AlarmList must not require a register binding"); | ||||
| AlarmDefinition definition; | |||||
| definition.address = RegisterAddress{RegisterArea::M, 0}; | |||||
| definition.condition = AlarmCondition::MOn; | |||||
| definition.message = "Emergency stop"; | |||||
| const AlarmEditorResult alarm_result = | |||||
| alarm_editor_service.addDefinition(definition); | |||||
| require(alarm_result.succeeded, | |||||
| "runtime alarm test must create an M alarm definition"); | |||||
| std::vector<std::string> alarm_ids; | |||||
| for (int index = 0; index <= ProjectLimits::kMaximumVisibleAlarmRows; ++index) | |||||
| { | |||||
| AlarmDefinition definition; | |||||
| definition.address = RegisterAddress{RegisterArea::M, index}; | |||||
| definition.condition = AlarmCondition::MOn; | |||||
| definition.message = "Alarm " + std::to_string(index); | |||||
| const AlarmEditorResult alarm_result = | |||||
| alarm_editor_service.addDefinition(definition); | |||||
| require(alarm_result.succeeded, | |||||
| "runtime alarm test must create paged M alarm definitions"); | |||||
| alarm_ids.push_back(alarm_result.id); | |||||
| } | |||||
| AlarmConfigurationDialog configuration_dialog(alarm_editor_service); | |||||
| QLineEdit *message_edit = configuration_dialog.findChild<QLineEdit *>( | |||||
| QStringLiteral("messageEdit")); | |||||
| require(message_edit != nullptr | |||||
| && message_edit->maxLength() | |||||
| == static_cast<int>( | |||||
| ProjectLimits::kMaximumAlarmMessageCharacters), | |||||
| "alarm configuration must limit messages to 20 characters"); | |||||
| HmiEditorWidget view(editor_service, runtime_service, alarm_service); | HmiEditorWidget view(editor_service, runtime_service, alarm_service); | ||||
| view.setPageId(page_id); | view.setPageId(page_id); | ||||
| @@ -632,6 +635,8 @@ void testRuntimeAlarmListInteraction() | |||||
| "an active M alarm must be available to AlarmList"); | "an active M alarm must be available to AlarmList"); | ||||
| require(alarm_item->isVisible() && alarm_item->zValue() > 0.0, | require(alarm_item->isVisible() && alarm_item->zValue() > 0.0, | ||||
| "AlarmList must appear above the page for an active alarm"); | "AlarmList must appear above the page for an active alarm"); | ||||
| require(alarm_item->boundingRect().height() < alarm_list->bounds.height, | |||||
| "a runtime AlarmList with one record must remove unused row space"); | |||||
| const QPoint header_position = view.mapFromScene(QPointF( | const QPoint header_position = view.mapFromScene(QPointF( | ||||
| alarm_list->bounds.x + 20.0, alarm_list->bounds.y + 10.0)); | alarm_list->bounds.x + 20.0, alarm_list->bounds.y + 10.0)); | ||||
| @@ -647,7 +652,40 @@ void testRuntimeAlarmListInteraction() | |||||
| require(alarm_service.records().front().acknowledged, | require(alarm_service.records().front().acknowledged, | ||||
| "clicking an active AlarmList row must acknowledge the alarm"); | "clicking an active AlarmList row must acknowledge the alarm"); | ||||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, false); | |||||
| for (int index = 1; index <= ProjectLimits::kMaximumVisibleAlarmRows; ++index) | |||||
| { | |||||
| repository.writeBit(RegisterAddress{RegisterArea::M, index}, true); | |||||
| } | |||||
| alarm_service.reset(); | |||||
| alarm_service.refresh(); | |||||
| view.refreshRuntimeValues(); | |||||
| QApplication::processEvents(); | |||||
| require(alarm_service.records().size() | |||||
| == static_cast<std::size_t>( | |||||
| ProjectLimits::kMaximumVisibleAlarmRows + 1), | |||||
| "AlarmList pagination must retain records beyond the visible row limit"); | |||||
| require(alarm_item->boundingRect().height() == alarm_list->bounds.height, | |||||
| "a full AlarmList page must use its configured maximum height"); | |||||
| const QPoint next_page_position = view.mapFromScene(QPointF( | |||||
| alarm_list->bounds.x + alarm_list->bounds.width - 14.0, | |||||
| alarm_list->bounds.y + 10.0)); | |||||
| QTest::mouseClick( | |||||
| view.viewport(), Qt::LeftButton, Qt::NoModifier, next_page_position); | |||||
| QApplication::processEvents(); | |||||
| require(alarm_item->boundingRect().height() < alarm_list->bounds.height, | |||||
| "the final partial alarm page must shrink to its visible rows"); | |||||
| QTest::mouseClick( | |||||
| view.viewport(), Qt::LeftButton, Qt::NoModifier, first_row_position); | |||||
| require(alarm_service.records().back().definitionId == alarm_ids.front() | |||||
| && alarm_service.records().back().acknowledged, | |||||
| "the next AlarmList page must acknowledge its own first record"); | |||||
| for (int index = 0; index <= ProjectLimits::kMaximumVisibleAlarmRows; ++index) | |||||
| { | |||||
| repository.writeBit(RegisterAddress{RegisterArea::M, index}, false); | |||||
| } | |||||
| alarm_service.refresh(); | alarm_service.refresh(); | ||||
| view.refreshRuntimeValues(); | view.refreshRuntimeValues(); | ||||
| QApplication::processEvents(); | QApplication::processEvents(); | ||||
| @@ -963,6 +1001,7 @@ void testModeActionsControlEditingAvailability() | |||||
| window, "deleteVerticalWireAction"); | window, "deleteVerticalWireAction"); | ||||
| QDockWidget *project_dock = requiredChild<QDockWidget>(window, "projectDock"); | QDockWidget *project_dock = requiredChild<QDockWidget>(window, "projectDock"); | ||||
| QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock"); | QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock"); | ||||
| QDockWidget *output_dock = requiredChild<QDockWidget>(window, "outputDock"); | |||||
| QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel"); | QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel"); | ||||
| QLineEdit *text_edit = requiredChild<QLineEdit>(window, "controlTextEdit"); | QLineEdit *text_edit = requiredChild<QLineEdit>(window, "controlTextEdit"); | ||||
| QLineEdit *text_color_edit = requiredChild<QLineEdit>(window, "textColorEdit"); | QLineEdit *text_color_edit = requiredChild<QLineEdit>(window, "textColorEdit"); | ||||
| @@ -988,14 +1027,21 @@ void testModeActionsControlEditingAvailability() | |||||
| LogicEditorWidget *logic_editor = requiredChild<LogicEditorWidget>( | LogicEditorWidget *logic_editor = requiredChild<LogicEditorWidget>( | ||||
| window, "logicEditorWidget"); | window, "logicEditorWidget"); | ||||
| QLabel *executor_status = requiredChild<QLabel>(window, "executorStatusLabel"); | QLabel *executor_status = requiredChild<QLabel>(window, "executorStatusLabel"); | ||||
| QWidget *runtime_tab = requiredChild<QWidget>(window, "runtimeMonitorTab"); | |||||
| RuntimeMonitorWindow *runtime_window = requiredChild<RuntimeMonitorWindow>( | |||||
| window, "runtimeMonitorWindow"); | |||||
| QWidget *runtime_hmi = requiredChild<QWidget>(window, "runtimeHmiView"); | QWidget *runtime_hmi = requiredChild<QWidget>(window, "runtimeHmiView"); | ||||
| QWidget *runtime_logic = requiredChild<QWidget>(window, "runtimeLogicView"); | QWidget *runtime_logic = requiredChild<QWidget>(window, "runtimeLogicView"); | ||||
| QWidget *free_monitor = requiredChild<QWidget>(window, "freeMonitorWidget"); | QWidget *free_monitor = requiredChild<QWidget>(window, "freeMonitorWidget"); | ||||
| QTabWidget *editor_tabs = requiredChild<QTabWidget>(window, "editorTabWidget"); | QTabWidget *editor_tabs = requiredChild<QTabWidget>(window, "editorTabWidget"); | ||||
| QTreeWidget *project_tree = requiredChild<QTreeWidget>(window, "projectTree"); | QTreeWidget *project_tree = requiredChild<QTreeWidget>(window, "projectTree"); | ||||
| require(!runtime_tab->isVisible(), | |||||
| "runtime monitor workspace must be hidden while editing"); | |||||
| const bool initially_maximized = window.isMaximized(); | |||||
| const bool project_dock_initially_visible = project_dock->isVisible(); | |||||
| const bool properties_dock_initially_visible = properties_dock->isVisible(); | |||||
| const bool output_dock_initially_visible = output_dock->isVisible(); | |||||
| require(!runtime_window->isVisible() | |||||
| && window.findChild<QWidget *>(QStringLiteral("runtimeMonitorTab")) | |||||
| == nullptr, | |||||
| "runtime monitor window must be hidden while editing and absent from editor tabs"); | |||||
| for (const char *action_name : { | for (const char *action_name : { | ||||
| "exitAction", | "exitAction", | ||||
| "newProjectAction", | "newProjectAction", | ||||
| @@ -1056,11 +1102,10 @@ void testModeActionsControlEditingAvailability() | |||||
| && !logic_timer_counter_menu->icon().isNull() | && !logic_timer_counter_menu->icon().isNull() | ||||
| && !logic_data_menu->icon().isNull(), | && !logic_data_menu->icon().isNull(), | ||||
| "every grouped toolbar menu must have an icon"); | "every grouped toolbar menu must have an icon"); | ||||
| require(editor_tabs->count() == 3 | |||||
| require(editor_tabs->count() == 2 | |||||
| && !editor_tabs->tabIcon(0).isNull() | && !editor_tabs->tabIcon(0).isNull() | ||||
| && !editor_tabs->tabIcon(1).isNull() | |||||
| && !editor_tabs->tabIcon(2).isNull(), | |||||
| "every editor workspace tab must have an icon"); | |||||
| && !editor_tabs->tabIcon(1).isNull(), | |||||
| "the two editor tabs must remain present and have icons"); | |||||
| QToolButton *monitor_remove = free_monitor->findChild<QToolButton *>( | QToolButton *monitor_remove = free_monitor->findChild<QToolButton *>( | ||||
| QStringLiteral("removeButton")); | QStringLiteral("removeButton")); | ||||
| QToolButton *monitor_clear = free_monitor->findChild<QToolButton *>( | QToolButton *monitor_clear = free_monitor->findChild<QToolButton *>( | ||||
| @@ -1237,6 +1282,26 @@ void testModeActionsControlEditingAvailability() | |||||
| require(editor_service.findPage(page_id)->controls.empty(), | require(editor_service.findPage(page_id)->controls.empty(), | ||||
| "deleting a ProgressBar must remove it from the HMI page"); | "deleting a ProgressBar must remove it from the HMI page"); | ||||
| requiredChild<QAction>(window, "addAlarmListAction")->trigger(); | |||||
| const HmiControl *default_alarm_list = editor_service.findControl( | |||||
| page_id, "alarm-list-1"); | |||||
| const int expected_alarm_font_size = std::clamp( | |||||
| QApplication::font().pointSize() | |||||
| - ProjectLimits::kAlarmDefaultFontPointReduction, | |||||
| ProjectLimits::kMinimumHmiFontPointSize, | |||||
| ProjectLimits::kMaximumHmiFontPointSize); | |||||
| require(default_alarm_list != nullptr | |||||
| && default_alarm_list->bounds.width == 360 | |||||
| && default_alarm_list->bounds.height == 136 | |||||
| && text_edit->maxLength() | |||||
| == static_cast<int>( | |||||
| ProjectLimits::kMaximumAlarmTitleCharacters) | |||||
| && font_size->value() == expected_alarm_font_size, | |||||
| "AlarmList defaults must expose five rows and the reduced font size"); | |||||
| delete_control_action->trigger(); | |||||
| require(editor_service.findPage(page_id)->controls.empty(), | |||||
| "deleting an AlarmList must remove it from the HMI page"); | |||||
| add_indicator_action->trigger(); | add_indicator_action->trigger(); | ||||
| HmiControl runtime_indicator = *editor_service.findControl(page_id, "indicator-1"); | HmiControl runtime_indicator = *editor_service.findControl(page_id, "indicator-1"); | ||||
| runtime_indicator.binding = RegisterAddress{RegisterArea::M, 3}; | runtime_indicator.binding = RegisterAddress{RegisterArea::M, 3}; | ||||
| @@ -1415,6 +1480,14 @@ void testModeActionsControlEditingAvailability() | |||||
| "project dock must be disabled while running"); | "project dock must be disabled while running"); | ||||
| require(!properties_dock->isEnabled(), | require(!properties_dock->isEnabled(), | ||||
| "properties dock must be disabled while running"); | "properties dock must be disabled while running"); | ||||
| require(runtime_window->isVisible() && runtime_window->isMaximized() | |||||
| && project_dock->isVisible() == project_dock_initially_visible | |||||
| && properties_dock->isVisible() | |||||
| == properties_dock_initially_visible | |||||
| && output_dock->isVisible() == output_dock_initially_visible | |||||
| && editor_tabs->tabBar()->isVisible() | |||||
| && window.isMaximized() == initially_maximized, | |||||
| "runtime monitoring must open maximized without changing the editor layout"); | |||||
| require(!add_button_action->isEnabled(), | require(!add_button_action->isEnabled(), | ||||
| "HMI add controls must be disabled while running"); | "HMI add controls must be disabled while running"); | ||||
| require(!add_progress_bar_action->isEnabled(), | require(!add_progress_bar_action->isEnabled(), | ||||
| @@ -1424,7 +1497,7 @@ void testModeActionsControlEditingAvailability() | |||||
| require(!undo_action->isEnabled() && !redo_action->isEnabled() | require(!undo_action->isEnabled() && !redo_action->isEnabled() | ||||
| && !delete_selection_action->isEnabled(), | && !delete_selection_action->isEnabled(), | ||||
| "undo, redo and delete must be disabled while running"); | "undo, redo and delete must be disabled while running"); | ||||
| require(runtime_tab->isVisible() && runtime_hmi->isVisible() | |||||
| require(runtime_window->isVisible() && runtime_hmi->isVisible() | |||||
| && runtime_logic->isVisible() && free_monitor->isVisible(), | && runtime_logic->isVisible() && free_monitor->isVisible(), | ||||
| "offline running must show HMI, ladder trace and free monitor together"); | "offline running must show HMI, ladder trace and free monitor together"); | ||||
| auto *runtime_hmi_view = qobject_cast<HmiEditorWidget *>(runtime_hmi); | auto *runtime_hmi_view = qobject_cast<HmiEditorWidget *>(runtime_hmi); | ||||
| @@ -1461,14 +1534,21 @@ void testModeActionsControlEditingAvailability() | |||||
| "project dock must be restored after returning to editing"); | "project dock must be restored after returning to editing"); | ||||
| require(properties_dock->isEnabled(), | require(properties_dock->isEnabled(), | ||||
| "properties dock must be restored after returning to editing"); | "properties dock must be restored after returning to editing"); | ||||
| require(project_dock->isVisible() == project_dock_initially_visible | |||||
| && properties_dock->isVisible() | |||||
| == properties_dock_initially_visible | |||||
| && output_dock->isVisible() == output_dock_initially_visible | |||||
| && editor_tabs->tabBar()->isVisible() | |||||
| && window.isMaximized() == initially_maximized, | |||||
| "returning to editing must restore the previous workspace layout"); | |||||
| require(add_button_action->isEnabled(), | require(add_button_action->isEnabled(), | ||||
| "HMI add controls must be restored after returning to editing"); | "HMI add controls must be restored after returning to editing"); | ||||
| require(add_progress_bar_action->isEnabled(), | require(add_progress_bar_action->isEnabled(), | ||||
| "ProgressBar creation must be restored after returning to editing"); | "ProgressBar creation must be restored after returning to editing"); | ||||
| require(add_normally_open_action->isEnabled(), | require(add_normally_open_action->isEnabled(), | ||||
| "logic add nodes must be restored after returning to editing"); | "logic add nodes must be restored after returning to editing"); | ||||
| require(!runtime_tab->isVisible(), | |||||
| "returning to editing must hide the runtime monitor workspace"); | |||||
| require(!runtime_window->isVisible(), | |||||
| "returning to editing must hide the runtime monitor window"); | |||||
| online_action->trigger(); | online_action->trigger(); | ||||
| require(mode_service.mode() == ApplicationMode::Editing, | require(mode_service.mode() == ApplicationMode::Editing, | ||||
| @@ -1477,7 +1557,7 @@ void testModeActionsControlEditingAvailability() | |||||
| "rejected online running must restore the editing action"); | "rejected online running must restore the editing action"); | ||||
| } | } | ||||
| void testIndependentHmiRuntimeWindowLifecycle() | |||||
| void testRuntimeMonitorWindowLifecycle() | |||||
| { | { | ||||
| TestProjectStorage storage; | TestProjectStorage storage; | ||||
| ProjectService project_service(storage); | ProjectService project_service(storage); | ||||
| @@ -1491,7 +1571,17 @@ void testIndependentHmiRuntimeWindowLifecycle() | |||||
| jump.pageJump = HmiPageJumpConfig{settings_page_id}; | jump.pageJump = HmiPageJumpConfig{settings_page_id}; | ||||
| require(editor_service.updateControl( | require(editor_service.updateControl( | ||||
| main_page_id, jump_result.id, jump).succeeded, | main_page_id, jump_result.id, jump).succeeded, | ||||
| "runtime-window fixture must configure a PageJump target"); | |||||
| "runtime-workspace fixture must configure a PageJump target"); | |||||
| const HmiEditorResult button_result = editor_service.addControl( | |||||
| main_page_id, HmiControlType::Button); | |||||
| HmiControl button = *editor_service.findControl( | |||||
| main_page_id, button_result.id); | |||||
| button.binding = RegisterAddress{RegisterArea::M, 0}; | |||||
| button.buttonOperation = HmiButtonOperation::Toggle; | |||||
| button.bounds = HmiRect{200, 20, 120, 40}; | |||||
| require(editor_service.updateControl( | |||||
| main_page_id, button_result.id, button).succeeded, | |||||
| "runtime-workspace fixture must configure an interactive HMI button"); | |||||
| const HmiEditorResult return_jump_result = editor_service.addControl( | const HmiEditorResult return_jump_result = editor_service.addControl( | ||||
| settings_page_id, HmiControlType::PageJump); | settings_page_id, HmiControlType::PageJump); | ||||
| HmiControl return_jump = *editor_service.findControl( | HmiControl return_jump = *editor_service.findControl( | ||||
| @@ -1499,7 +1589,7 @@ void testIndependentHmiRuntimeWindowLifecycle() | |||||
| return_jump.pageJump = HmiPageJumpConfig{main_page_id}; | return_jump.pageJump = HmiPageJumpConfig{main_page_id}; | ||||
| require(editor_service.updateControl( | require(editor_service.updateControl( | ||||
| settings_page_id, return_jump_result.id, return_jump).succeeded, | settings_page_id, return_jump_result.id, return_jump).succeeded, | ||||
| "runtime-window fixture must configure a PageJump back to the initial page"); | |||||
| "runtime-workspace fixture must configure a PageJump back to the initial page"); | |||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| HmiRuntimeService runtime_service(repository); | HmiRuntimeService runtime_service(repository); | ||||
| AlarmEditorService alarm_editor_service(project_service); | AlarmEditorService alarm_editor_service(project_service); | ||||
| @@ -1525,92 +1615,114 @@ void testIndependentHmiRuntimeWindowLifecycle() | |||||
| requiredChild<QAction>(window, "offlineModeAction")->trigger(); | requiredChild<QAction>(window, "offlineModeAction")->trigger(); | ||||
| QApplication::processEvents(); | QApplication::processEvents(); | ||||
| HmiRuntimeWindow *runtime_window = findHmiRuntimeWindow(); | |||||
| require(mode_service.mode() == ApplicationMode::OfflineRunning, | require(mode_service.mode() == ApplicationMode::OfflineRunning, | ||||
| "offline mode must remain the authoritative runtime state"); | "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<HmiEditorWidget *>( | |||||
| QStringLiteral("hmiRuntimeHmiView")) != nullptr, | |||||
| "independent HMI window must contain a runtime-only HMI view"); | |||||
| HmiEditorWidget *runtime_hmi = runtime_window->findChild<HmiEditorWidget *>( | |||||
| 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"); | |||||
| RuntimeMonitorWindow *runtime_window = requiredChild<RuntimeMonitorWindow>( | |||||
| window, "runtimeMonitorWindow"); | |||||
| HmiEditorWidget *runtime_hmi = requiredChild<HmiEditorWidget>( | |||||
| window, "runtimeHmiView"); | |||||
| require(runtime_window->isWindow() && runtime_window->isVisible() | |||||
| && runtime_window->isMaximized(), | |||||
| "offline running must show a maximized runtime monitor window"); | |||||
| require(window.findChildren<RuntimeMonitorWidget *>().size() == 1, | |||||
| "the application must keep exactly one runtime monitor projection"); | |||||
| require(window.findChild<QWidget *>(QStringLiteral("runtimeMonitorTab")) == nullptr, | |||||
| "the editor must not retain an embedded runtime monitor tab"); | |||||
| QTest::mouseClick( | |||||
| runtime_hmi->viewport(), Qt::LeftButton, Qt::NoModifier, | |||||
| runtime_hmi->mapFromScene(QPointF( | |||||
| button.bounds.x + button.bounds.width / 2.0, | |||||
| button.bounds.y + button.bounds.height / 2.0))); | |||||
| QApplication::processEvents(); | |||||
| require(repository.readBit({RegisterArea::M, 0}).succeeded | |||||
| && repository.readBit({RegisterArea::M, 0}).value, | |||||
| "the unified runtime HMI must write to the active virtual repository"); | |||||
| QTest::mouseClick(runtime_hmi->viewport(), Qt::LeftButton, Qt::NoModifier, | QTest::mouseClick(runtime_hmi->viewport(), Qt::LeftButton, Qt::NoModifier, | ||||
| runtime_hmi->mapFromScene( | |||||
| jump_item->mapToScene(jump_item->boundingRect().center()))); | |||||
| runtime_hmi->mapFromScene(QPointF( | |||||
| jump.bounds.x + jump.bounds.width / 2.0, | |||||
| jump.bounds.y + jump.bounds.height / 2.0))); | |||||
| QApplication::processEvents(); | QApplication::processEvents(); | ||||
| require(navigation_service.currentPageId() == settings_page_id, | require(navigation_service.currentPageId() == settings_page_id, | ||||
| "PageJump in the independent window must update the shared navigation session"); | |||||
| require(runtime_window->findChild<QLabel *>(QStringLiteral("runtimePageLabel")) | |||||
| ->text() == QStringLiteral("Settings"), | |||||
| "independent window title must follow the navigated runtime page"); | |||||
| "PageJump in the unified runtime HMI must update the navigation session"); | |||||
| require(requiredChild<QLabel>(window, "runtimePageLabel")->text() | require(requiredChild<QLabel>(window, "runtimePageLabel")->text() | ||||
| == QStringLiteral("Settings"), | == 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"); | |||||
| "the runtime workspace header must follow the navigated HMI page"); | |||||
| QTest::mouseClick(runtime_hmi->viewport(), Qt::LeftButton, Qt::NoModifier, | QTest::mouseClick(runtime_hmi->viewport(), Qt::LeftButton, Qt::NoModifier, | ||||
| runtime_hmi->mapFromScene( | |||||
| return_jump_item->mapToScene( | |||||
| return_jump_item->boundingRect().center()))); | |||||
| runtime_hmi->mapFromScene(QPointF( | |||||
| return_jump.bounds.x + return_jump.bounds.width / 2.0, | |||||
| return_jump.bounds.y + return_jump.bounds.height / 2.0))); | |||||
| QApplication::processEvents(); | QApplication::processEvents(); | ||||
| require(navigation_service.currentPageId() == main_page_id, | require(navigation_service.currentPageId() == main_page_id, | ||||
| "the independent HMI must return to the initial page before editing"); | |||||
| require(runtime_window->findChild<QLabel *>(QStringLiteral("runtimePageLabel")) | |||||
| ->text() == QStringLiteral("主操作页面"), | |||||
| "the independent window must keep the initial page selected across sessions"); | |||||
| "the unified runtime HMI must return to the initial page before editing"); | |||||
| requiredChild<QAction>(window, "editingModeAction")->trigger(); | |||||
| requiredChild<QToolButton>(window, "exitRuntimeButton")->click(); | |||||
| QApplication::processEvents(); | QApplication::processEvents(); | ||||
| require(mode_service.mode() == ApplicationMode::Editing, | require(mode_service.mode() == ApplicationMode::Editing, | ||||
| "editing action must return the runtime mode to editing"); | |||||
| "the runtime monitor exit button must return to editing"); | |||||
| require(!runtime_window->isVisible(), | require(!runtime_window->isVisible(), | ||||
| "returning to editing must hide the independent HMI window"); | |||||
| "returning to editing must hide the runtime monitor window"); | |||||
| requiredChild<QAction>(window, "addLabelAction")->trigger(); | requiredChild<QAction>(window, "addLabelAction")->trigger(); | ||||
| QApplication::processEvents(); | QApplication::processEvents(); | ||||
| require(editor_service.findPage(main_page_id)->controls.size() == 2U, | |||||
| require(editor_service.findPage(main_page_id)->controls.size() == 3U, | |||||
| "editing after a runtime session must add the new control to the page"); | "editing after a runtime session must add the new control to the page"); | ||||
| requiredChild<QAction>(window, "offlineModeAction")->trigger(); | requiredChild<QAction>(window, "offlineModeAction")->trigger(); | ||||
| QApplication::processEvents(); | 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<HmiEditorWidget *>( | |||||
| 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<RuntimeMonitorWidget>(window, "runtimeMonitorWidget") | |||||
| ->findChild<HmiEditorWidget *>(QStringLiteral("runtimeHmiView")) | |||||
| ->scene()->items().size() == 3, | |||||
| "the diagnostic HMI must keep projecting the edited page"); | |||||
| runtime_hmi = requiredChild<HmiEditorWidget>(window, "runtimeHmiView"); | |||||
| require(runtime_window->isVisible() && runtime_window->isMaximized() | |||||
| && runtime_hmi->scene()->items().size() == 4, | |||||
| "a later runtime session must reopen and refresh the monitor window"); | |||||
| runtime_window->close(); | runtime_window->close(); | ||||
| QApplication::processEvents(); | QApplication::processEvents(); | ||||
| require(mode_service.mode() == ApplicationMode::Editing, | require(mode_service.mode() == ApplicationMode::Editing, | ||||
| "closing the HMI window must request a transition to editing"); | |||||
| "closing the runtime monitor window must request editing mode"); | |||||
| require(!runtime_window->isVisible(), | require(!runtime_window->isVisible(), | ||||
| "closing the HMI window must not leave a visible runtime surface"); | |||||
| "closing the runtime monitor window must leave no visible runtime surface"); | |||||
| } | |||||
| void testApplicationExitClosesRuntimeMonitorWindow() | |||||
| { | |||||
| TestProjectStorage storage; | |||||
| ProjectService project_service(storage); | |||||
| HmiEditorService editor_service(project_service); | |||||
| LogicEditorService logic_editor_service(project_service); | |||||
| 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); | |||||
| window.show(); | |||||
| QApplication::processEvents(); | |||||
| require(project_service.saveAs("runtime-window-exit-test.json").succeeded, | |||||
| "application-exit fixture must clear the unsaved-project prompt"); | |||||
| requiredChild<QAction>(window, "offlineModeAction")->trigger(); | |||||
| QApplication::processEvents(); | |||||
| RuntimeMonitorWindow *runtime_window = requiredChild<RuntimeMonitorWindow>( | |||||
| window, "runtimeMonitorWindow"); | |||||
| require(runtime_window->isVisible(), | |||||
| "application-exit fixture must start with a visible runtime window"); | |||||
| requiredChild<QAction>(window, "exitAction")->trigger(); | |||||
| QApplication::processEvents(); | |||||
| require(!window.isVisible() && !runtime_window->isVisible(), | |||||
| "application exit must close both programmer and runtime windows"); | |||||
| } | } | ||||
| void testEdgeTimerPropertyEditingAndParallelMenu() | void testEdgeTimerPropertyEditingAndParallelMenu() | ||||
| @@ -1881,6 +1993,15 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||||
| ProjectService project_service(storage); | ProjectService project_service(storage); | ||||
| HmiEditorService editor_service(project_service); | HmiEditorService editor_service(project_service); | ||||
| LogicEditorService logic_editor_service(project_service); | LogicEditorService logic_editor_service(project_service); | ||||
| const std::string page_id = editor_service.ensureDefaultPage().id; | |||||
| const HmiEditorResult button_result = editor_service.addControl( | |||||
| page_id, HmiControlType::Button); | |||||
| HmiControl button = *editor_service.findControl(page_id, button_result.id); | |||||
| button.binding = RegisterAddress{RegisterArea::M, 5}; | |||||
| button.buttonOperation = HmiButtonOperation::Toggle; | |||||
| require(editor_service.updateControl( | |||||
| page_id, button_result.id, button).succeeded, | |||||
| "online workspace fixture must configure an interactive HMI button"); | |||||
| VirtualRegisterRepository virtual_repository; | VirtualRegisterRepository virtual_repository; | ||||
| VirtualRegisterRepository plc_repository; | VirtualRegisterRepository plc_repository; | ||||
| ActiveRegisterRepository active_repository(virtual_repository); | ActiveRegisterRepository active_repository(virtual_repository); | ||||
| @@ -1916,17 +2037,31 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||||
| online_action->trigger(); | online_action->trigger(); | ||||
| QApplication::processEvents(); | QApplication::processEvents(); | ||||
| QWidget *runtime_tab = requiredChild<QWidget>(window, "runtimeMonitorTab"); | |||||
| RuntimeMonitorWindow *runtime_window = requiredChild<RuntimeMonitorWindow>( | |||||
| window, "runtimeMonitorWindow"); | |||||
| QWidget *runtime_hmi = requiredChild<QWidget>(window, "runtimeHmiView"); | QWidget *runtime_hmi = requiredChild<QWidget>(window, "runtimeHmiView"); | ||||
| QWidget *runtime_logic = requiredChild<QWidget>(window, "runtimeLogicView"); | QWidget *runtime_logic = requiredChild<QWidget>(window, "runtimeLogicView"); | ||||
| QWidget *free_monitor = requiredChild<QWidget>(window, "freeMonitorWidget"); | QWidget *free_monitor = requiredChild<QWidget>(window, "freeMonitorWidget"); | ||||
| require(mode_service.mode() == ApplicationMode::OnlineRunning, | require(mode_service.mode() == ApplicationMode::OnlineRunning, | ||||
| "completed PLC initial read must allow online running"); | "completed PLC initial read must allow online running"); | ||||
| require(runtime_tab->isVisible() && runtime_hmi->isVisible() | |||||
| require(runtime_window->isVisible() && runtime_window->isMaximized() | |||||
| && runtime_hmi->isVisible() | |||||
| && free_monitor->isVisible(), | && free_monitor->isVisible(), | ||||
| "online running must show HMI and free monitor together"); | |||||
| "online running must show HMI and free monitor in the maximized window"); | |||||
| require(!runtime_logic->isVisible(), | require(!runtime_logic->isVisible(), | ||||
| "online running must not show a misleading local ladder runtime trace"); | "online running must not show a misleading local ladder runtime trace"); | ||||
| auto *runtime_hmi_view = qobject_cast<HmiEditorWidget *>(runtime_hmi); | |||||
| require(runtime_hmi_view != nullptr, | |||||
| "online running must expose the unified interactive HMI view"); | |||||
| QTest::mouseClick( | |||||
| runtime_hmi_view->viewport(), Qt::LeftButton, Qt::NoModifier, | |||||
| runtime_hmi_view->mapFromScene(QPointF( | |||||
| button.bounds.x + button.bounds.width / 2.0, | |||||
| button.bounds.y + button.bounds.height / 2.0))); | |||||
| QApplication::processEvents(); | |||||
| require(plc_repository.readBit({RegisterArea::M, 5}).succeeded | |||||
| && plc_repository.readBit({RegisterArea::M, 5}).value, | |||||
| "the unified online HMI must write through the active PLC repository"); | |||||
| QLineEdit *monitor_address = requiredChild<QLineEdit>(window, "addressEdit"); | QLineEdit *monitor_address = requiredChild<QLineEdit>(window, "addressEdit"); | ||||
| QPushButton *monitor_add = requiredChild<QPushButton>(window, "addButton"); | QPushButton *monitor_add = requiredChild<QPushButton>(window, "addButton"); | ||||
| QTableWidget *monitor_table = requiredChild<QTableWidget>(window, "monitorTable"); | QTableWidget *monitor_table = requiredChild<QTableWidget>(window, "monitorTable"); | ||||
| @@ -1950,6 +2085,8 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||||
| require(mode_service.mode() == ApplicationMode::Editing, | require(mode_service.mode() == ApplicationMode::Editing, | ||||
| "a PLC timeout must return online running to editing"); | "a PLC timeout must return online running to editing"); | ||||
| require(!runtime_window->isVisible(), | |||||
| "a PLC timeout must hide the runtime monitor window"); | |||||
| require(!monitor_write->isEnabled(), | require(!monitor_write->isEnabled(), | ||||
| "a PLC communication fault must disable free monitor writes"); | "a PLC communication fault must disable free monitor writes"); | ||||
| require(disconnect_action->isEnabled(), | require(disconnect_action->isEnabled(), | ||||
| @@ -2141,13 +2278,14 @@ int main(int argc, char *argv[]) | |||||
| testLogicContinuousInsertionConsumesFullWire(); | testLogicContinuousInsertionConsumesFullWire(); | ||||
| testWindowTitleTracksUnsavedProjectChanges(); | testWindowTitleTracksUnsavedProjectChanges(); | ||||
| testModeActionsControlEditingAvailability(); | testModeActionsControlEditingAvailability(); | ||||
| testIndependentHmiRuntimeWindowLifecycle(); | |||||
| testRuntimeMonitorWindowLifecycle(); | |||||
| testEdgeTimerPropertyEditingAndParallelMenu(); | testEdgeTimerPropertyEditingAndParallelMenu(); | ||||
| testPlcConfigurationUsesDialog(); | testPlcConfigurationUsesDialog(); | ||||
| testRepeatedPlcStatusNotificationsAreCoalesced(); | testRepeatedPlcStatusNotificationsAreCoalesced(); | ||||
| testFreeMonitorWritesSingleValues(); | testFreeMonitorWritesSingleValues(); | ||||
| testOnlineWorkspaceShowsHmiAndFreeMonitorOnly(); | testOnlineWorkspaceShowsHmiAndFreeMonitorOnly(); | ||||
| testMultiPageAndLogicMainWindowIntegration(); | testMultiPageAndLogicMainWindowIntegration(); | ||||
| testApplicationExitClosesRuntimeMonitorWindow(); | |||||
| } | } | ||||
| catch (const std::exception &error) | catch (const std::exception &error) | ||||
| { | { | ||||
| @@ -15,7 +15,7 @@ SOURCES += \ | |||||
| ../src/ui/register_comment_dialog.cpp \ | ../src/ui/register_comment_dialog.cpp \ | ||||
| ../src/ui/plc_connection_dialog.cpp \ | ../src/ui/plc_connection_dialog.cpp \ | ||||
| ../src/ui/free_monitor_widget.cpp \ | ../src/ui/free_monitor_widget.cpp \ | ||||
| ../src/ui/hmi_runtime_window.cpp \ | |||||
| ../src/ui/runtime_monitor_window.cpp \ | |||||
| ../src/ui/runtime_monitor_widget.cpp \ | ../src/ui/runtime_monitor_widget.cpp \ | ||||
| ../src/ui/toolbar_icon_factory.cpp \ | ../src/ui/toolbar_icon_factory.cpp \ | ||||
| ../src/ui/project_workspace_controller.cpp \ | ../src/ui/project_workspace_controller.cpp \ | ||||
| @@ -53,7 +53,7 @@ HEADERS += \ | |||||
| ../src/ui/register_comment_dialog.h \ | ../src/ui/register_comment_dialog.h \ | ||||
| ../src/ui/plc_connection_dialog.h \ | ../src/ui/plc_connection_dialog.h \ | ||||
| ../src/ui/free_monitor_widget.h \ | ../src/ui/free_monitor_widget.h \ | ||||
| ../src/ui/hmi_runtime_window.h \ | |||||
| ../src/ui/runtime_monitor_window.h \ | |||||
| ../src/ui/runtime_monitor_widget.h \ | ../src/ui/runtime_monitor_widget.h \ | ||||
| ../src/ui/toolbar_icon_factory.h \ | ../src/ui/toolbar_icon_factory.h \ | ||||
| ../src/ui/project_workspace_controller.h \ | ../src/ui/project_workspace_controller.h \ | ||||
| @@ -96,6 +96,6 @@ FORMS += \ | |||||
| ../src/ui/register_comment_dialog.ui \ | ../src/ui/register_comment_dialog.ui \ | ||||
| ../src/ui/plc_connection_dialog.ui \ | ../src/ui/plc_connection_dialog.ui \ | ||||
| ../src/ui/free_monitor_widget.ui \ | ../src/ui/free_monitor_widget.ui \ | ||||
| ../src/ui/hmi_runtime_window.ui \ | |||||
| ../src/ui/runtime_monitor_window.ui \ | |||||
| ../src/ui/runtime_monitor_widget.ui \ | ../src/ui/runtime_monitor_widget.ui \ | ||||
| ../src/ui/logic_instruction_dialog.ui | ../src/ui/logic_instruction_dialog.ui | ||||