综合平台编程器项目的远程存储
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 

1867 Zeilen
65 KiB

  1. #include "main_window.h"
  2. #include "hmi_editor_widget.h"
  3. #include "alarm_configuration_dialog.h"
  4. #include "register_comment_dialog.h"
  5. #include "logic_editor_widget.h"
  6. #include "logic_instruction_dialog.h"
  7. #include "plc_connection_dialog.h"
  8. #include "runtime_monitor_widget.h"
  9. #include "toolbar_icon_factory.h"
  10. #include "project_workspace_controller.h"
  11. #include "property_panel_controller.h"
  12. #include "runtime_panel_controller.h"
  13. #include "services/hmi_editor_service.h"
  14. #include "services/hmi_navigation_service.h"
  15. #include "services/hmi_runtime_service.h"
  16. #include "services/alarm_editor_service.h"
  17. #include "services/alarm_service.h"
  18. #include "services/logic_editor_service.h"
  19. #include "services/project_service.h"
  20. #include "services/runtime_mode_service.h"
  21. #include "services/register_monitor_service.h"
  22. #include "services/register_comment_service.h"
  23. #include "ui_main_window.h"
  24. #include "domain/project_limits.h"
  25. #include <QActionGroup>
  26. #include <QAbstractSpinBox>
  27. #include <QApplication>
  28. #include <QCloseEvent>
  29. #include <QDialogButtonBox>
  30. #include <QEvent>
  31. #include <QFileDialog>
  32. #include <QGraphicsScene>
  33. #include <QInputDialog>
  34. #include <QIcon>
  35. #include <QLabel>
  36. #include <QLineEdit>
  37. #include <QKeyEvent>
  38. #include <QKeySequence>
  39. #include <QMessageBox>
  40. #include <QMenu>
  41. #include <QPlainTextEdit>
  42. #include <QPushButton>
  43. #include <QStatusBar>
  44. #include <QTabWidget>
  45. #include <QToolBar>
  46. #include <QToolButton>
  47. #include <QTextEdit>
  48. #include <algorithm>
  49. namespace {
  50. bool isTextEditingObject(QObject *object)
  51. {
  52. QWidget *widget = qobject_cast<QWidget *>(object);
  53. while (widget != nullptr)
  54. {
  55. if (qobject_cast<QLineEdit *>(widget) != nullptr
  56. || qobject_cast<QTextEdit *>(widget) != nullptr
  57. || qobject_cast<QPlainTextEdit *>(widget) != nullptr
  58. || qobject_cast<QAbstractSpinBox *>(widget) != nullptr)
  59. {
  60. return true;
  61. }
  62. widget = widget->parentWidget();
  63. }
  64. return false;
  65. }
  66. QString suggestedProjectFileName(QString project_name)
  67. {
  68. project_name = project_name.trimmed();
  69. const QString invalid_characters = QStringLiteral("<>:\"/\\|?*");
  70. for (QChar &character : project_name)
  71. {
  72. if (character.unicode() < 0x20U || invalid_characters.contains(character))
  73. {
  74. character = QLatin1Char('_');
  75. }
  76. }
  77. constexpr int kMaximumSuggestedBaseNameLength = 240;
  78. if (project_name.size() > kMaximumSuggestedBaseNameLength)
  79. {
  80. project_name.truncate(kMaximumSuggestedBaseNameLength);
  81. if (!project_name.isEmpty()
  82. && project_name.at(project_name.size() - 1).isHighSurrogate())
  83. {
  84. project_name.chop(1);
  85. }
  86. }
  87. while (project_name.endsWith(QLatin1Char(' '))
  88. || project_name.endsWith(QLatin1Char('.')))
  89. {
  90. project_name.chop(1);
  91. }
  92. if (project_name.isEmpty())
  93. {
  94. project_name = QStringLiteral("未命名工程");
  95. }
  96. const QString device_name = project_name.section(QLatin1Char('.'), 0, 0).toUpper();
  97. const bool is_reserved_device_name = device_name == QStringLiteral("CON")
  98. || device_name == QStringLiteral("PRN")
  99. || device_name == QStringLiteral("AUX")
  100. || device_name == QStringLiteral("NUL")
  101. || (device_name.size() == 4
  102. && (device_name.startsWith(QStringLiteral("COM"))
  103. || device_name.startsWith(QStringLiteral("LPT")))
  104. && device_name.back() >= QLatin1Char('1')
  105. && device_name.back() <= QLatin1Char('9'));
  106. if (is_reserved_device_name)
  107. {
  108. project_name.prepend(QLatin1Char('_'));
  109. }
  110. return project_name + QStringLiteral(".json");
  111. }
  112. bool isEditorShortcut(const QKeyEvent &event)
  113. {
  114. return event.matches(QKeySequence::Undo)
  115. || event.matches(QKeySequence::Redo)
  116. || event.matches(QKeySequence::Copy)
  117. || event.matches(QKeySequence::Paste)
  118. || (event.modifiers() == Qt::NoModifier
  119. && (event.key() == Qt::Key_Delete || event.key() == Qt::Key_Escape));
  120. }
  121. QString modeText(ApplicationMode mode)
  122. {
  123. switch (mode)
  124. {
  125. case ApplicationMode::Editing:
  126. {
  127. return MainWindow::tr("编辑态");
  128. }
  129. case ApplicationMode::OfflineRunning:
  130. {
  131. return MainWindow::tr("离线运行态");
  132. }
  133. case ApplicationMode::OnlineRunning:
  134. {
  135. return MainWindow::tr("真机运行态");
  136. }
  137. default:
  138. {
  139. return MainWindow::tr("未知状态");
  140. }
  141. }
  142. }
  143. QString transitionErrorText(ModeTransitionError error)
  144. {
  145. switch (error)
  146. {
  147. case ModeTransitionError::MustReturnToEditing:
  148. {
  149. return MainWindow::tr("请先返回编辑态,再切换运行模式");
  150. }
  151. case ModeTransitionError::InitialPlcReadRequired:
  152. {
  153. return MainWindow::tr("真机运行前必须连接 PLC 并完成首次读取");
  154. }
  155. case ModeTransitionError::AlreadyInRequestedMode:
  156. {
  157. return MainWindow::tr("当前已处于所选模式");
  158. }
  159. case ModeTransitionError::ProjectNotReady:
  160. {
  161. return MainWindow::tr("工程存在未配置或未完成的 HMI/梯形图节点,暂不能进入离线运行态");
  162. }
  163. case ModeTransitionError::SimulationStartFailed:
  164. {
  165. return MainWindow::tr("离线逻辑执行器预检或启动失败");
  166. }
  167. case ModeTransitionError::None:
  168. default:
  169. {
  170. return MainWindow::tr("模式切换失败");
  171. }
  172. }
  173. }
  174. std::string toUtf8(const QString &value)
  175. {
  176. const QByteArray bytes = value.toUtf8();
  177. return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size()));
  178. }
  179. QString fromUtf8(const std::string &value)
  180. {
  181. return QString::fromUtf8(value.data(), static_cast<int>(value.size()));
  182. }
  183. QString plcStatusText(const RuntimeModeService &service)
  184. {
  185. switch (service.plcConnectionState())
  186. {
  187. case PlcConnectionState::Connecting:
  188. return MainWindow::tr("PLC 正在连接");
  189. case PlcConnectionState::Connected:
  190. return service.initialPlcReadCompleted()
  191. ? MainWindow::tr("PLC 已连接,首次读取完成")
  192. : MainWindow::tr("PLC 已连接,正在读取工程使用的 M/D 地址");
  193. case PlcConnectionState::Recovering:
  194. return MainWindow::tr("PLC 通信已恢复,正在重新读取工程使用的 M/D 地址");
  195. case PlcConnectionState::Faulted:
  196. {
  197. const QString error = fromUtf8(service.plcError());
  198. return error.isEmpty()
  199. ? MainWindow::tr("PLC 通信故障")
  200. : MainWindow::tr("PLC 通信故障:%1").arg(error);
  201. }
  202. case PlcConnectionState::Disconnected:
  203. {
  204. const QString error = fromUtf8(service.plcError());
  205. return error.isEmpty()
  206. ? MainWindow::tr("PLC 已断开")
  207. : MainWindow::tr("PLC 已断开:%1").arg(error);
  208. }
  209. default:
  210. {
  211. return MainWindow::tr("PLC 已断开");
  212. }
  213. }
  214. }
  215. QToolButton *addToolbarMenu(
  216. QToolBar *toolbar,
  217. const QString &text,
  218. const QString &object_name,
  219. const QIcon &icon,
  220. const QList<QAction *> &actions)
  221. {
  222. auto *button = new QToolButton(toolbar);
  223. button->setObjectName(object_name);
  224. button->setText(text);
  225. button->setIcon(icon);
  226. button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
  227. button->setPopupMode(QToolButton::InstantPopup);
  228. auto *menu = new QMenu(button);
  229. menu->addActions(actions);
  230. button->setMenu(menu);
  231. toolbar->addWidget(button);
  232. return button;
  233. }
  234. } // namespace
  235. MainWindow::MainWindow(
  236. RuntimeModeService &runtime_mode_service,
  237. ProjectService &project_service,
  238. HmiEditorService &hmi_editor_service,
  239. LogicEditorService &logic_editor_service,
  240. HmiRuntimeService &hmi_runtime_service,
  241. AlarmEditorService &alarm_editor_service,
  242. AlarmService &alarm_service,
  243. RegisterCommentService &register_comment_service,
  244. RegisterMonitorService &register_monitor_service,
  245. PlcDiscoveryGateway &plc_discovery_gateway,
  246. QWidget *parent)
  247. : QMainWindow(parent),
  248. ui_(std::make_unique<Ui::MainWindow>()),
  249. runtime_mode_service_(runtime_mode_service),
  250. project_service_(project_service),
  251. hmi_editor_service_(hmi_editor_service),
  252. logic_editor_service_(logic_editor_service),
  253. hmi_runtime_service_(hmi_runtime_service),
  254. alarm_editor_service_(alarm_editor_service),
  255. alarm_service_(alarm_service),
  256. register_comment_service_(register_comment_service),
  257. register_monitor_service_(register_monitor_service),
  258. plc_discovery_gateway_(plc_discovery_gateway),
  259. owned_hmi_navigation_service_(
  260. std::make_unique<HmiNavigationService>(project_service)),
  261. hmi_navigation_service_(owned_hmi_navigation_service_.get())
  262. {
  263. initializeUi();
  264. }
  265. MainWindow::MainWindow(
  266. RuntimeModeService &runtime_mode_service,
  267. ProjectService &project_service,
  268. HmiEditorService &hmi_editor_service,
  269. LogicEditorService &logic_editor_service,
  270. HmiRuntimeService &hmi_runtime_service,
  271. AlarmEditorService &alarm_editor_service,
  272. AlarmService &alarm_service,
  273. HmiNavigationService &hmi_navigation_service,
  274. RegisterCommentService &register_comment_service,
  275. RegisterMonitorService &register_monitor_service,
  276. PlcDiscoveryGateway &plc_discovery_gateway,
  277. QWidget *parent)
  278. : QMainWindow(parent),
  279. ui_(std::make_unique<Ui::MainWindow>()),
  280. runtime_mode_service_(runtime_mode_service),
  281. project_service_(project_service),
  282. hmi_editor_service_(hmi_editor_service),
  283. logic_editor_service_(logic_editor_service),
  284. hmi_runtime_service_(hmi_runtime_service),
  285. alarm_editor_service_(alarm_editor_service),
  286. alarm_service_(alarm_service),
  287. register_comment_service_(register_comment_service),
  288. register_monitor_service_(register_monitor_service),
  289. plc_discovery_gateway_(plc_discovery_gateway),
  290. hmi_navigation_service_(&hmi_navigation_service)
  291. {
  292. initializeUi();
  293. }
  294. void MainWindow::initializeUi()
  295. {
  296. ui_->setupUi(this);
  297. qApp->installEventFilter(this);
  298. configureAppearance();
  299. property_panel_controller_ = std::make_unique<PropertyPanelController>(
  300. *this,
  301. *ui_,
  302. project_service_,
  303. hmi_editor_service_,
  304. logic_editor_service_,
  305. [this] { return current_hmi_page_id_; },
  306. [this] { return current_logic_id_; },
  307. selected_control_id_,
  308. selected_logic_node_id_,
  309. [this] { refreshProjectUi(); },
  310. [this](const QString &action, const QString &message, bool succeeded)
  311. {
  312. showProjectResult(action, message, succeeded);
  313. },
  314. [this](const QString &message, int timeout_ms)
  315. {
  316. statusBar()->showMessage(message, timeout_ms);
  317. });
  318. configureActions();
  319. configurePropertyEditor();
  320. configurePlcConnection();
  321. configureAlarms();
  322. configureRegisterComments();
  323. configureHmiEditor();
  324. configureLogicEditor();
  325. configureRuntimeMonitor();
  326. configureProjectTree();
  327. runtime_mode_service_.setPlcStatusChangedCallback(
  328. [this] { schedulePlcStatusUpdate(); });
  329. hmi_editor_service_.ensureDefaultPage();
  330. logic_editor_service_.ensureDefaultLogic();
  331. clearEditorHistories();
  332. current_hmi_page_id_ = hmi_editor_service_.firstPageId();
  333. current_logic_id_ = logic_editor_service_.firstLogicId();
  334. showControlProperties({});
  335. refreshProjectUi();
  336. updateModeUi(tr("系统已进入编辑态"));
  337. }
  338. MainWindow::~MainWindow()
  339. {
  340. qApp->removeEventFilter(this);
  341. runtime_mode_service_.setPlcStatusChangedCallback({});
  342. }
  343. bool MainWindow::eventFilter(QObject *watched, QEvent *event)
  344. {
  345. if (event->type() == QEvent::ShortcutOverride
  346. && isTextEditingObject(watched))
  347. {
  348. auto *key_event = static_cast<QKeyEvent *>(event);
  349. if (isEditorShortcut(*key_event))
  350. {
  351. event->accept();
  352. return true;
  353. }
  354. }
  355. return QMainWindow::eventFilter(watched, event);
  356. }
  357. void MainWindow::closeEvent(QCloseEvent *event)
  358. {
  359. if (!confirmSaveBeforeDestructiveAction())
  360. {
  361. event->ignore();
  362. return;
  363. }
  364. if (runtime_panel_controller_ != nullptr)
  365. {
  366. runtime_panel_controller_->closeForApplicationExit();
  367. }
  368. QMainWindow::closeEvent(event);
  369. }
  370. const std::string &MainWindow::currentHmiPageId() const
  371. {
  372. return current_hmi_page_id_;
  373. }
  374. const std::string &MainWindow::currentLogicId() const
  375. {
  376. return current_logic_id_;
  377. }
  378. void MainWindow::configureActions()
  379. {
  380. mode_action_group_ = new QActionGroup(this);
  381. mode_action_group_->setExclusive(true);
  382. mode_action_group_->addAction(ui_->editingModeAction);
  383. mode_action_group_->addAction(ui_->offlineModeAction);
  384. mode_action_group_->addAction(ui_->onlineModeAction);
  385. connect(ui_->editingModeAction, &QAction::triggered, this,
  386. [this] { requestMode(ApplicationMode::Editing); });
  387. connect(ui_->offlineModeAction, &QAction::triggered, this,
  388. [this] { requestMode(ApplicationMode::OfflineRunning); });
  389. connect(ui_->onlineModeAction, &QAction::triggered, this,
  390. [this] { requestMode(ApplicationMode::OnlineRunning); });
  391. connect(ui_->exitAction, &QAction::triggered, this, [this] { close(); });
  392. connect(ui_->newProjectAction, &QAction::triggered, this, &MainWindow::createNewProject);
  393. connect(ui_->saveProjectAction, &QAction::triggered, this, &MainWindow::saveProject);
  394. connect(ui_->saveAsProjectAction, &QAction::triggered, this, &MainWindow::saveProjectAs);
  395. connect(ui_->loadProjectAction, &QAction::triggered, this, &MainWindow::loadProject);
  396. connect(ui_->undoAction, &QAction::triggered,
  397. this, &MainWindow::undoActiveEditor);
  398. connect(ui_->redoAction, &QAction::triggered,
  399. this, &MainWindow::redoActiveEditor);
  400. connect(ui_->copyAction, &QAction::triggered,
  401. this, &MainWindow::copyActiveSelection);
  402. connect(ui_->pasteAction, &QAction::triggered,
  403. this, &MainWindow::pasteActiveSelection);
  404. connect(ui_->deleteSelectionAction, &QAction::triggered,
  405. this, &MainWindow::deleteActiveSelection);
  406. connect(ui_->clearSelectionAction, &QAction::triggered,
  407. this, &MainWindow::clearActiveSelection);
  408. connect(ui_->addButtonAction, &QAction::triggered, this,
  409. [this] { addHmiControl(HmiControlType::Button); });
  410. connect(ui_->addIndicatorAction, &QAction::triggered, this,
  411. [this] { addHmiControl(HmiControlType::Indicator); });
  412. connect(ui_->addNumericDisplayAction, &QAction::triggered, this,
  413. [this] { addHmiControl(HmiControlType::NumericDisplay); });
  414. connect(ui_->addNumericInputAction, &QAction::triggered, this,
  415. [this] { addHmiControl(HmiControlType::NumericInput); });
  416. connect(ui_->addLabelAction, &QAction::triggered, this,
  417. [this] { addHmiControl(HmiControlType::Label); });
  418. connect(ui_->addPageJumpAction, &QAction::triggered, this,
  419. [this] { addHmiControl(HmiControlType::PageJump); });
  420. connect(ui_->addAlarmListAction, &QAction::triggered, this,
  421. [this] { addHmiControl(HmiControlType::AlarmList); });
  422. connect(ui_->deleteControlAction, &QAction::triggered,
  423. this, &MainWindow::deleteSelectedControl);
  424. addToolbarMenu(
  425. ui_->hmiToolBar,
  426. tr("更多控件"),
  427. QStringLiteral("hmiMoreControlsButton"),
  428. makeUiIcon(UiIcon::More),
  429. QList<QAction *>{
  430. ui_->addPageJumpAction,
  431. ui_->addAlarmListAction,
  432. ui_->configureAlarmsAction});
  433. connect(ui_->addRungAction, &QAction::triggered,
  434. this, &MainWindow::addLogicRung);
  435. connect(ui_->insertHorizontalWireAction, &QAction::triggered,
  436. this, &MainWindow::addLogicHorizontalWire);
  437. connect(ui_->insertVerticalWireAction, &QAction::triggered,
  438. this, &MainWindow::addLogicVerticalWire);
  439. connect(ui_->deleteHorizontalWireAction, &QAction::triggered,
  440. this, &MainWindow::deleteLogicHorizontalWire);
  441. connect(ui_->deleteVerticalWireAction, &QAction::triggered,
  442. this, &MainWindow::deleteLogicVerticalWire);
  443. connect(ui_->parallelInsertAction, &QAction::triggered,
  444. this,
  445. [this]
  446. {
  447. addLogicParallelBranch(ContactNodeConfig{
  448. RegisterAddress{RegisterArea::M, 0},
  449. ContactMode::NormallyOpen});
  450. });
  451. QMenu *parallel_menu = new QMenu(this);
  452. QAction *parallel_open = parallel_menu->addAction(tr("并联常开触点"));
  453. QAction *parallel_closed = parallel_menu->addAction(tr("并联常闭触点"));
  454. QAction *parallel_rising = parallel_menu->addAction(tr("并联上升沿触点"));
  455. QAction *parallel_falling = parallel_menu->addAction(tr("并联下降沿触点"));
  456. QAction *parallel_compare = parallel_menu->addAction(tr("并联比较条件"));
  457. connect(parallel_open, &QAction::triggered,
  458. this,
  459. [this]
  460. {
  461. addLogicParallelBranch(ContactNodeConfig{
  462. RegisterAddress{RegisterArea::M, 0},
  463. ContactMode::NormallyOpen});
  464. });
  465. connect(parallel_closed, &QAction::triggered,
  466. this,
  467. [this]
  468. {
  469. addLogicParallelBranch(ContactNodeConfig{
  470. RegisterAddress{RegisterArea::M, 0},
  471. ContactMode::NormallyClosed});
  472. });
  473. connect(parallel_rising, &QAction::triggered,
  474. this,
  475. [this]
  476. {
  477. addLogicParallelBranch(EdgeContactNodeConfig{
  478. RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising});
  479. });
  480. connect(parallel_falling, &QAction::triggered,
  481. this,
  482. [this]
  483. {
  484. addLogicParallelBranch(EdgeContactNodeConfig{
  485. RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling});
  486. });
  487. connect(parallel_compare, &QAction::triggered,
  488. this,
  489. [this]
  490. {
  491. addLogicParallelBranch(CompareNodeConfig{
  492. RegisterAddress{RegisterArea::D, 0},
  493. ComparisonOperator::Equal,
  494. 0});
  495. });
  496. ui_->parallelInsertAction->setMenu(parallel_menu);
  497. if (QToolButton *parallel_button = qobject_cast<QToolButton *>(
  498. ui_->logicToolBar->widgetForAction(ui_->parallelInsertAction)))
  499. {
  500. parallel_button->setPopupMode(QToolButton::MenuButtonPopup);
  501. }
  502. connect(ui_->addNormallyOpenAction, &QAction::triggered,
  503. this,
  504. [this]
  505. {
  506. addLogicCondition(ContactNodeConfig{
  507. RegisterAddress{RegisterArea::M, 0},
  508. ContactMode::NormallyOpen});
  509. });
  510. connect(ui_->addNormallyClosedAction, &QAction::triggered,
  511. this,
  512. [this]
  513. {
  514. addLogicCondition(ContactNodeConfig{
  515. RegisterAddress{RegisterArea::M, 0},
  516. ContactMode::NormallyClosed});
  517. });
  518. connect(ui_->addRisingEdgeAction, &QAction::triggered,
  519. this,
  520. [this]
  521. {
  522. addLogicCondition(EdgeContactNodeConfig{
  523. RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising});
  524. });
  525. connect(ui_->addFallingEdgeAction, &QAction::triggered,
  526. this,
  527. [this]
  528. {
  529. addLogicCondition(EdgeContactNodeConfig{
  530. RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling});
  531. });
  532. connect(ui_->addNormalCoilAction, &QAction::triggered,
  533. this,
  534. [this]
  535. {
  536. setLogicOutput(CoilNodeConfig{
  537. RegisterAddress{RegisterArea::M, 0},
  538. CoilMode::Normal});
  539. });
  540. connect(ui_->addSetCoilAction, &QAction::triggered,
  541. this,
  542. [this]
  543. {
  544. setLogicOutput(CoilNodeConfig{
  545. RegisterAddress{RegisterArea::M, 0},
  546. CoilMode::Set});
  547. });
  548. connect(ui_->addResetCoilAction, &QAction::triggered,
  549. this,
  550. [this]
  551. {
  552. setLogicOutput(CoilNodeConfig{
  553. RegisterAddress{RegisterArea::M, 0},
  554. CoilMode::Reset});
  555. });
  556. connect(ui_->addMoveAction, &QAction::triggered,
  557. this,
  558. [this]
  559. {
  560. configureAndSetLogicOutput(MoveNodeConfig{
  561. WordOperand{
  562. WordOperandKind::Constant,
  563. RegisterAddress{RegisterArea::D, 0},
  564. 0},
  565. RegisterAddress{RegisterArea::D, 0}});
  566. });
  567. connect(ui_->addAddAction, &QAction::triggered,
  568. this,
  569. [this]
  570. {
  571. configureAndSetLogicOutput(ArithmeticNodeConfig{
  572. ArithmeticOperation::Add,
  573. WordOperand{
  574. WordOperandKind::Register,
  575. RegisterAddress{RegisterArea::D, 0},
  576. 0},
  577. WordOperand{
  578. WordOperandKind::Constant,
  579. RegisterAddress{RegisterArea::D, 0},
  580. 1},
  581. RegisterAddress{RegisterArea::D, 0}});
  582. });
  583. connect(ui_->addSubAction, &QAction::triggered,
  584. this,
  585. [this]
  586. {
  587. configureAndSetLogicOutput(ArithmeticNodeConfig{
  588. ArithmeticOperation::Subtract,
  589. WordOperand{
  590. WordOperandKind::Register,
  591. RegisterAddress{RegisterArea::D, 0},
  592. 0},
  593. WordOperand{
  594. WordOperandKind::Constant,
  595. RegisterAddress{RegisterArea::D, 0},
  596. 1},
  597. RegisterAddress{RegisterArea::D, 0}});
  598. });
  599. connect(ui_->addCompareAction, &QAction::triggered,
  600. this,
  601. [this]
  602. {
  603. addLogicCondition(CompareNodeConfig{
  604. RegisterAddress{RegisterArea::D, 0},
  605. ComparisonOperator::Equal,
  606. 0});
  607. });
  608. connect(ui_->deleteLogicAction, &QAction::triggered,
  609. this, &MainWindow::deleteSelectedLogicObject);
  610. connect(ui_->editRungCommentAction, &QAction::triggered,
  611. this, &MainWindow::editSelectedRungComment);
  612. addToolbarMenu(
  613. ui_->logicToolBar,
  614. tr("更多触点"),
  615. QStringLiteral("logicContactMenuButton"),
  616. makeUiIcon(UiIcon::NormallyOpenContact),
  617. QList<QAction *>{
  618. ui_->addRisingEdgeAction,
  619. ui_->addFallingEdgeAction});
  620. addToolbarMenu(
  621. ui_->logicToolBar,
  622. tr("更多输出"),
  623. QStringLiteral("logicOutputMenuButton"),
  624. makeUiIcon(UiIcon::Coil),
  625. QList<QAction *>{ui_->addSetCoilAction, ui_->addResetCoilAction});
  626. addToolbarMenu(
  627. ui_->logicToolBar,
  628. tr("数据运算"),
  629. QStringLiteral("logicDataMenuButton"),
  630. makeUiIcon(UiIcon::Move),
  631. QList<QAction *>{
  632. ui_->addMoveAction,
  633. ui_->addAddAction,
  634. ui_->addSubAction,
  635. ui_->addCompareAction});
  636. connect(ui_->editorTabWidget, &QTabWidget::currentChanged,
  637. this,
  638. [this](int index)
  639. {
  640. ui_->hmiToolBar->setVisible(index == 0);
  641. ui_->logicToolBar->setVisible(index == 1);
  642. if (index == 0)
  643. {
  644. showControlProperties(selected_control_id_);
  645. }
  646. else if (index == 1)
  647. {
  648. showLogicNodeProperties(selected_logic_node_id_);
  649. }
  650. if (index == 0 || index == 1)
  651. {
  652. refreshProjectUi();
  653. }
  654. });
  655. ui_->hmiToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 0);
  656. ui_->logicToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 1);
  657. ui_->viewMenu->addAction(ui_->projectDock->toggleViewAction());
  658. ui_->viewMenu->addAction(ui_->propertiesDock->toggleViewAction());
  659. ui_->viewMenu->addAction(ui_->outputDock->toggleViewAction());
  660. ui_->viewMenu->addAction(ui_->hmiToolBar->toggleViewAction());
  661. ui_->viewMenu->addAction(ui_->logicToolBar->toggleViewAction());
  662. ui_->viewMenu->addSeparator();
  663. ui_->viewMenu->addAction(ui_->modeToolBar->toggleViewAction());
  664. }
  665. void MainWindow::configureAppearance()
  666. {
  667. setDockNestingEnabled(true);
  668. setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea);
  669. setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea);
  670. ui_->editingModeAction->setIcon(makeUiIcon(UiIcon::Edit));
  671. ui_->offlineModeAction->setIcon(makeUiIcon(UiIcon::RunOffline));
  672. ui_->onlineModeAction->setIcon(makeUiIcon(UiIcon::RunOnline));
  673. ui_->newProjectAction->setIcon(makeUiIcon(UiIcon::NewDocument));
  674. ui_->saveProjectAction->setIcon(makeUiIcon(UiIcon::Save));
  675. ui_->saveAsProjectAction->setIcon(makeUiIcon(UiIcon::SaveAs));
  676. ui_->loadProjectAction->setIcon(makeUiIcon(UiIcon::Open));
  677. ui_->undoAction->setIcon(makeUiIcon(UiIcon::Undo));
  678. ui_->redoAction->setIcon(makeUiIcon(UiIcon::Redo));
  679. ui_->deleteSelectionAction->setIcon(makeUiIcon(UiIcon::Delete));
  680. ui_->clearSelectionAction->setIcon(makeUiIcon(UiIcon::ClearList));
  681. ui_->exitAction->setIcon(makeUiIcon(UiIcon::Exit));
  682. ui_->configurePlcAction->setIcon(makeUiIcon(UiIcon::PlcSettings));
  683. ui_->disconnectPlcAction->setIcon(makeUiIcon(UiIcon::Disconnect));
  684. ui_->configureRegisterCommentsAction->setIcon(
  685. makeUiIcon(UiIcon::RegisterComments));
  686. ui_->addButtonAction->setIcon(makeUiIcon(UiIcon::HmiButton));
  687. ui_->addIndicatorAction->setIcon(makeUiIcon(UiIcon::Indicator));
  688. ui_->addNumericDisplayAction->setIcon(makeUiIcon(UiIcon::NumericDisplay));
  689. ui_->addNumericInputAction->setIcon(makeUiIcon(UiIcon::NumericInput));
  690. ui_->addLabelAction->setIcon(makeUiIcon(UiIcon::Text));
  691. ui_->addPageJumpAction->setIcon(makeUiIcon(UiIcon::PageJump));
  692. ui_->addAlarmListAction->setIcon(makeUiIcon(UiIcon::AlarmList));
  693. ui_->configureAlarmsAction->setIcon(makeUiIcon(UiIcon::AlarmSettings));
  694. ui_->deleteControlAction->setIcon(makeUiIcon(UiIcon::Delete));
  695. ui_->addRungAction->setIcon(makeUiIcon(UiIcon::AddRung));
  696. ui_->parallelInsertAction->setIcon(makeUiIcon(UiIcon::ParallelBranch));
  697. ui_->insertHorizontalWireAction->setIcon(makeUiIcon(UiIcon::HorizontalWire));
  698. ui_->insertVerticalWireAction->setIcon(makeUiIcon(UiIcon::VerticalWire));
  699. ui_->deleteHorizontalWireAction->setIcon(makeUiIcon(UiIcon::Delete));
  700. ui_->deleteVerticalWireAction->setIcon(makeUiIcon(UiIcon::Delete));
  701. ui_->addNormallyOpenAction->setIcon(makeUiIcon(UiIcon::NormallyOpenContact));
  702. ui_->addNormallyClosedAction->setIcon(makeUiIcon(UiIcon::NormallyClosedContact));
  703. ui_->addRisingEdgeAction->setIcon(makeUiIcon(UiIcon::RisingEdgeContact));
  704. ui_->addFallingEdgeAction->setIcon(makeUiIcon(UiIcon::FallingEdgeContact));
  705. ui_->addNormalCoilAction->setIcon(makeUiIcon(UiIcon::Coil));
  706. ui_->addSetCoilAction->setIcon(makeUiIcon(UiIcon::SetCoil));
  707. ui_->addResetCoilAction->setIcon(makeUiIcon(UiIcon::ResetCoil));
  708. ui_->addMoveAction->setIcon(makeUiIcon(UiIcon::Move));
  709. ui_->addAddAction->setIcon(makeUiIcon(UiIcon::Add));
  710. ui_->addSubAction->setIcon(makeUiIcon(UiIcon::Subtract));
  711. ui_->addCompareAction->setIcon(makeUiIcon(UiIcon::Compare));
  712. ui_->editRungCommentAction->setIcon(makeUiIcon(UiIcon::Comment));
  713. ui_->deleteLogicAction->setIcon(makeUiIcon(UiIcon::Delete));
  714. ui_->editorTabWidget->setTabIcon(0, makeUiIcon(UiIcon::HmiPage));
  715. ui_->editorTabWidget->setTabIcon(1, makeUiIcon(UiIcon::Logic));
  716. ui_->outputDock->setMaximumHeight(220);
  717. mode_status_label_ = new QLabel(this);
  718. mode_status_label_->setObjectName(QStringLiteral("modeStatusLabel"));
  719. mode_status_label_->setMinimumWidth(88);
  720. mode_status_label_->setAlignment(Qt::AlignCenter);
  721. register_status_label_ = new QLabel(this);
  722. register_status_label_->setObjectName(QStringLiteral("registerStatusLabel"));
  723. register_status_label_->setMinimumWidth(132);
  724. executor_status_label_ = new QLabel(this);
  725. executor_status_label_->setObjectName(QStringLiteral("executorStatusLabel"));
  726. executor_status_label_->setMinimumWidth(132);
  727. statusBar()->addPermanentWidget(mode_status_label_);
  728. statusBar()->addPermanentWidget(register_status_label_);
  729. statusBar()->addPermanentWidget(executor_status_label_);
  730. setStyleSheet(QStringLiteral(
  731. "QMainWindow { background: #f3f5f7; }"
  732. "QToolBar { background: #ffffff; border: 0; border-bottom: 1px solid #cbd2d9;"
  733. " padding: 4px; spacing: 3px; }"
  734. "QToolButton { min-height: 26px; padding: 3px 9px; border: 1px solid transparent; }"
  735. "QToolButton:hover { background: #edf2f6; border-color: #c5ced6; }"
  736. "QToolButton:checked { background: #dcece3; border-color: #75a58a; }"
  737. "QDockWidget { color: #24313b; font-weight: 600; }"
  738. "QDockWidget::title { background: #e9edf0; padding: 6px;"
  739. " border-bottom: 1px solid #cbd2d9; }"
  740. "QTreeWidget, QListWidget, QScrollArea { background: #ffffff; border: 1px solid #d5dbe0; }"
  741. "QTabWidget::pane { border: 0; background: #f3f5f7; }"
  742. "QTabBar::tab { background: #e5e9ec; padding: 7px 14px; border-right: 1px solid #cbd2d9; }"
  743. "QTabBar::tab:selected { background: #ffffff; color: #15232d; }"
  744. "QFrame#hmiCanvasPlaceholder, QFrame#logicCanvasPlaceholder { background: #ffffff;"
  745. " border: 1px solid #cbd2d9; }"
  746. "QLabel#hmiEmptyLabel, QLabel#logicEmptyLabel { color: #75838d; }"
  747. "QLabel#hmiPageTitleLabel { color: #1d2a33; font-weight: 600; }"
  748. "QLabel#hmiPageSizeLabel { color: #66747e; }"));
  749. }
  750. void MainWindow::configureHmiEditor()
  751. {
  752. QLayout *layout = ui_->hmiCanvasPlaceholder->layout();
  753. delete ui_->hmiEmptyLabel;
  754. hmi_editor_widget_ = new HmiEditorWidget(
  755. hmi_editor_service_,
  756. hmi_runtime_service_,
  757. alarm_service_,
  758. ui_->hmiCanvasPlaceholder);
  759. hmi_editor_widget_->setMinimumHeight(320);
  760. layout->addWidget(hmi_editor_widget_);
  761. }
  762. void MainWindow::configureLogicEditor()
  763. {
  764. QLayout *layout = ui_->logicCanvasPlaceholder->layout();
  765. delete ui_->logicEmptyLabel;
  766. logic_editor_widget_ = new LogicEditorWidget(
  767. logic_editor_service_, ui_->logicCanvasPlaceholder);
  768. logic_editor_widget_->setObjectName(QStringLiteral("logicEditorWidget"));
  769. logic_editor_widget_->setMinimumHeight(320);
  770. layout->addWidget(logic_editor_widget_);
  771. property_panel_controller_->bindEditorWidgets(
  772. *hmi_editor_widget_, *logic_editor_widget_);
  773. }
  774. void MainWindow::configureRuntimeMonitor()
  775. {
  776. runtime_panel_controller_ = std::make_unique<RuntimePanelController>(
  777. *this,
  778. runtime_mode_service_,
  779. project_service_,
  780. hmi_editor_service_,
  781. hmi_runtime_service_,
  782. logic_editor_service_,
  783. *hmi_navigation_service_,
  784. alarm_service_,
  785. register_monitor_service_,
  786. *hmi_editor_widget_,
  787. *logic_editor_widget_,
  788. *executor_status_label_,
  789. [this] { return current_logic_id_; },
  790. [this](const std::string &logic_id) { current_logic_id_ = logic_id; },
  791. [this](const std::string &node_id)
  792. {
  793. showLogicNodeProperties(node_id);
  794. },
  795. [this](const QString &message, int timeout_ms)
  796. {
  797. statusBar()->showMessage(message, timeout_ms);
  798. },
  799. [this](const QString &message)
  800. {
  801. appendOutputMessage(message);
  802. },
  803. [this]
  804. {
  805. requestMode(ApplicationMode::Editing);
  806. });
  807. runtime_panel_controller_->configure();
  808. runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget();
  809. }
  810. void MainWindow::configureProjectTree()
  811. {
  812. project_workspace_controller_ = std::make_unique<ProjectWorkspaceController>(
  813. *this,
  814. *ui_,
  815. project_service_,
  816. hmi_editor_service_,
  817. logic_editor_service_,
  818. runtime_mode_service_,
  819. *hmi_navigation_service_,
  820. *hmi_editor_widget_,
  821. *logic_editor_widget_,
  822. *runtime_monitor_widget_,
  823. current_hmi_page_id_,
  824. current_logic_id_,
  825. [this](const std::string &control_id)
  826. {
  827. showControlProperties(control_id);
  828. },
  829. [this](const std::string &node_id)
  830. {
  831. showLogicNodeProperties(node_id);
  832. },
  833. [this](const QString &action, const QString &message, bool succeeded)
  834. {
  835. showProjectResult(action, message, succeeded);
  836. },
  837. [this](const QString &message, int timeout_ms)
  838. {
  839. statusBar()->showMessage(message, timeout_ms);
  840. },
  841. [this]
  842. {
  843. updateEditActions();
  844. });
  845. project_workspace_controller_->configure();
  846. }
  847. void MainWindow::configurePropertyEditor()
  848. {
  849. property_panel_controller_->configure();
  850. }
  851. void MainWindow::configurePlcConnection()
  852. {
  853. connect(ui_->configurePlcAction, &QAction::triggered, this, &MainWindow::connectPlc);
  854. connect(ui_->disconnectPlcAction, &QAction::triggered, this, &MainWindow::disconnectPlc);
  855. }
  856. void MainWindow::configureAlarms()
  857. {
  858. connect(ui_->configureAlarmsAction, &QAction::triggered,
  859. this,
  860. [this]
  861. {
  862. AlarmConfigurationDialog dialog(alarm_editor_service_, this);
  863. dialog.exec();
  864. refreshProjectUi();
  865. });
  866. }
  867. void MainWindow::configureRegisterComments()
  868. {
  869. connect(
  870. ui_->configureRegisterCommentsAction,
  871. &QAction::triggered,
  872. this,
  873. [this]
  874. {
  875. RegisterCommentDialog dialog(register_comment_service_, this);
  876. dialog.exec();
  877. logic_editor_widget_->reloadLogic();
  878. refreshProjectUi();
  879. });
  880. }
  881. void MainWindow::undoActiveEditor()
  882. {
  883. if (!runtime_mode_service_.policy().allowsProjectEditing)
  884. {
  885. return;
  886. }
  887. if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab)
  888. {
  889. if (!hmi_editor_service_.undo().succeeded)
  890. {
  891. return;
  892. }
  893. selected_control_id_.clear();
  894. showControlProperties({});
  895. refreshProjectUi();
  896. hmi_editor_widget_->reloadPage();
  897. statusBar()->showMessage(tr("已撤销 HMI 编辑操作"), 3000);
  898. }
  899. else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab)
  900. {
  901. if (!logic_editor_service_.undo().succeeded)
  902. {
  903. return;
  904. }
  905. selected_logic_node_id_.clear();
  906. showLogicNodeProperties({});
  907. refreshProjectUi();
  908. logic_editor_widget_->reloadLogic();
  909. statusBar()->showMessage(tr("已撤销梯形图编辑操作"), 3000);
  910. }
  911. updateEditActions();
  912. }
  913. void MainWindow::redoActiveEditor()
  914. {
  915. if (!runtime_mode_service_.policy().allowsProjectEditing)
  916. {
  917. return;
  918. }
  919. if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab)
  920. {
  921. if (!hmi_editor_service_.redo().succeeded)
  922. {
  923. return;
  924. }
  925. selected_control_id_.clear();
  926. showControlProperties({});
  927. refreshProjectUi();
  928. hmi_editor_widget_->reloadPage();
  929. statusBar()->showMessage(tr("已重做 HMI 编辑操作"), 3000);
  930. }
  931. else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab)
  932. {
  933. if (!logic_editor_service_.redo().succeeded)
  934. {
  935. return;
  936. }
  937. selected_logic_node_id_.clear();
  938. showLogicNodeProperties({});
  939. refreshProjectUi();
  940. logic_editor_widget_->reloadLogic();
  941. statusBar()->showMessage(tr("已重做梯形图编辑操作"), 3000);
  942. }
  943. updateEditActions();
  944. }
  945. void MainWindow::copyActiveSelection()
  946. {
  947. if (!runtime_mode_service_.policy().allowsProjectEditing
  948. || isTextEditingObject(qApp->focusWidget()))
  949. {
  950. return;
  951. }
  952. if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab)
  953. {
  954. const std::vector<std::string> ids = hmi_editor_widget_->selectedControlIds();
  955. std::vector<HmiControl> controls;
  956. controls.reserve(ids.size());
  957. for (const std::string &id : ids)
  958. {
  959. const HmiControl *control = hmi_editor_service_.findControl(
  960. current_hmi_page_id_, id);
  961. if (control != nullptr)
  962. {
  963. controls.push_back(*control);
  964. }
  965. }
  966. if (controls.empty())
  967. {
  968. return;
  969. }
  970. editor_clipboard_ = HmiClipboardData{std::move(controls), 0};
  971. statusBar()->showMessage(tr("已复制 %1 个 HMI 控件").arg(ids.size()), 3000);
  972. }
  973. else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab)
  974. {
  975. const std::vector<std::string> ids = logic_editor_widget_->selectedNodeIds();
  976. if (!ids.empty())
  977. {
  978. const std::string rung_id = logic_editor_widget_->selectedRungId();
  979. if (rung_id.empty())
  980. {
  981. statusBar()->showMessage(tr("只能复制同一网络中的梯形图指令"), 3000);
  982. return;
  983. }
  984. std::vector<LogicNode> nodes;
  985. nodes.reserve(ids.size());
  986. bool all_conditions = true;
  987. for (const std::string &id : ids)
  988. {
  989. const LogicNode *node = logic_editor_service_.findNode(
  990. current_logic_id_, id);
  991. if (node == nullptr)
  992. {
  993. return;
  994. }
  995. all_conditions = all_conditions && node->isCondition();
  996. nodes.push_back(*node);
  997. }
  998. if (all_conditions
  999. && logic_editor_service_.areConditionNodesContiguous(
  1000. current_logic_id_, rung_id, ids))
  1001. {
  1002. editor_clipboard_ = LogicNodesClipboardData{std::move(nodes)};
  1003. statusBar()->showMessage(tr("已复制 %1 个梯形图条件").arg(ids.size()), 3000);
  1004. }
  1005. else if (ids.size() == 1U && nodes.front().isOutput())
  1006. {
  1007. editor_clipboard_ = LogicOutputClipboardData{nodes.front()};
  1008. statusBar()->showMessage(tr("已复制梯形图输出指令"), 3000);
  1009. }
  1010. else
  1011. {
  1012. statusBar()->showMessage(
  1013. tr("只能复制同一串联层级中连续的条件,或单个输出指令"), 3000);
  1014. }
  1015. updateEditActions();
  1016. return;
  1017. }
  1018. const std::string rung_id = logic_editor_widget_->selectedRungId();
  1019. const LadderRung *rung = logic_editor_service_.findRung(
  1020. current_logic_id_, rung_id);
  1021. if (rung != nullptr)
  1022. {
  1023. editor_clipboard_ = LogicRungClipboardData{*rung};
  1024. statusBar()->showMessage(tr("已复制整条梯形图网络"), 3000);
  1025. }
  1026. }
  1027. updateEditActions();
  1028. }
  1029. void MainWindow::pasteActiveSelection()
  1030. {
  1031. if (!runtime_mode_service_.policy().allowsProjectEditing
  1032. || isTextEditingObject(qApp->focusWidget()))
  1033. {
  1034. return;
  1035. }
  1036. if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab)
  1037. {
  1038. auto *data = std::get_if<HmiClipboardData>(&editor_clipboard_);
  1039. if (data == nullptr)
  1040. {
  1041. return;
  1042. }
  1043. const int offset = 20 * std::min(data->pasteCount + 1, 10);
  1044. const HmiEditorResult result = hmi_editor_service_.pasteControls(
  1045. current_hmi_page_id_, data->controls, offset, offset);
  1046. if (!result.succeeded)
  1047. {
  1048. showProjectResult(tr("粘贴 HMI 控件"), fromUtf8(result.message), false);
  1049. return;
  1050. }
  1051. ++data->pasteCount;
  1052. refreshProjectUi();
  1053. hmi_editor_widget_->reloadPage();
  1054. hmi_editor_widget_->selectControl(result.id);
  1055. showControlProperties(result.id);
  1056. statusBar()->showMessage(tr("已粘贴 HMI 控件"), 3000);
  1057. }
  1058. else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab)
  1059. {
  1060. LogicEditorResult result;
  1061. if (const auto *data = std::get_if<LogicNodesClipboardData>(&editor_clipboard_))
  1062. {
  1063. result = logic_editor_widget_->pasteConditionNodes(data->nodes);
  1064. if (result.succeeded)
  1065. {
  1066. selected_logic_node_id_ = result.id;
  1067. showLogicNodeProperties(result.id);
  1068. }
  1069. }
  1070. else if (const auto *data = std::get_if<LogicOutputClipboardData>(&editor_clipboard_))
  1071. {
  1072. result = logic_editor_widget_->setOutput(
  1073. data->output.config, data->output.configured);
  1074. }
  1075. else if (const auto *data = std::get_if<LogicRungClipboardData>(&editor_clipboard_))
  1076. {
  1077. result = logic_editor_service_.pasteRung(current_logic_id_, data->rung);
  1078. if (result.succeeded)
  1079. {
  1080. logic_editor_widget_->reloadLogic();
  1081. }
  1082. }
  1083. if (!result.succeeded)
  1084. {
  1085. if (!result.message.empty())
  1086. {
  1087. showProjectResult(tr("粘贴梯形图"), fromUtf8(result.message), false);
  1088. }
  1089. return;
  1090. }
  1091. refreshProjectUi();
  1092. statusBar()->showMessage(tr("已粘贴梯形图对象"), 3000);
  1093. }
  1094. updateEditActions();
  1095. }
  1096. void MainWindow::deleteActiveSelection()
  1097. {
  1098. if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab)
  1099. {
  1100. deleteSelectedControl();
  1101. }
  1102. else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab)
  1103. {
  1104. deleteSelectedLogicObject();
  1105. }
  1106. }
  1107. void MainWindow::clearActiveSelection()
  1108. {
  1109. if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab)
  1110. {
  1111. hmi_editor_widget_->scene()->clearSelection();
  1112. selected_control_id_.clear();
  1113. showControlProperties({});
  1114. }
  1115. else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab)
  1116. {
  1117. logic_editor_widget_->scene()->clearSelection();
  1118. selected_logic_node_id_.clear();
  1119. showLogicNodeProperties({});
  1120. }
  1121. }
  1122. void MainWindow::updateEditActions()
  1123. {
  1124. const bool editable = runtime_mode_service_.policy().allowsProjectEditing;
  1125. const bool hmi_active = ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab;
  1126. const bool logic_active =
  1127. ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab;
  1128. ui_->undoAction->setEnabled(
  1129. editable && ((hmi_active && hmi_editor_service_.canUndo())
  1130. || (logic_active && logic_editor_service_.canUndo())));
  1131. ui_->redoAction->setEnabled(
  1132. editable && ((hmi_active && hmi_editor_service_.canRedo())
  1133. || (logic_active && logic_editor_service_.canRedo())));
  1134. ui_->copyAction->setEnabled(editable && (hmi_active || logic_active));
  1135. const bool hmi_clipboard = std::holds_alternative<HmiClipboardData>(editor_clipboard_);
  1136. const bool logic_clipboard = std::holds_alternative<LogicNodesClipboardData>(editor_clipboard_)
  1137. || std::holds_alternative<LogicOutputClipboardData>(editor_clipboard_)
  1138. || std::holds_alternative<LogicRungClipboardData>(editor_clipboard_);
  1139. ui_->pasteAction->setEnabled(editable && ((hmi_active && hmi_clipboard)
  1140. || (logic_active && logic_clipboard)));
  1141. ui_->deleteSelectionAction->setEnabled(editable && (hmi_active || logic_active));
  1142. ui_->clearSelectionAction->setEnabled(editable && (hmi_active || logic_active));
  1143. ui_->insertHorizontalWireAction->setEnabled(editable && logic_active);
  1144. ui_->insertVerticalWireAction->setEnabled(editable && logic_active);
  1145. ui_->deleteHorizontalWireAction->setEnabled(editable && logic_active);
  1146. ui_->deleteVerticalWireAction->setEnabled(editable && logic_active);
  1147. }
  1148. void MainWindow::clearEditorHistories()
  1149. {
  1150. hmi_editor_service_.clearHistory();
  1151. logic_editor_service_.clearHistory();
  1152. if (ui_->undoAction != nullptr)
  1153. {
  1154. updateEditActions();
  1155. }
  1156. }
  1157. void MainWindow::refreshProjectUi()
  1158. {
  1159. project_workspace_controller_->refresh();
  1160. updateEditActions();
  1161. updateWindowTitle();
  1162. }
  1163. void MainWindow::updateWindowTitle()
  1164. {
  1165. QString title = tr("综合平台编程器");
  1166. if (project_service_.isModified())
  1167. {
  1168. title += QStringLiteral("*");
  1169. }
  1170. setWindowTitle(title);
  1171. }
  1172. bool MainWindow::confirmSaveBeforeDestructiveAction()
  1173. {
  1174. if (!project_service_.isModified())
  1175. {
  1176. return true;
  1177. }
  1178. const QString project_name = fromUtf8(project_service_.project().metadata.name);
  1179. const QMessageBox::StandardButton choice = QMessageBox::warning(
  1180. this,
  1181. tr("工程尚未保存"),
  1182. tr("工程“%1”有未保存的修改,是否先保存?").arg(project_name),
  1183. QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
  1184. QMessageBox::Save);
  1185. if (choice == QMessageBox::Cancel)
  1186. {
  1187. return false;
  1188. }
  1189. if (choice == QMessageBox::Discard)
  1190. {
  1191. return true;
  1192. }
  1193. saveProject();
  1194. return !project_service_.isModified();
  1195. }
  1196. void MainWindow::updateProjectTreeActions()
  1197. {
  1198. project_workspace_controller_->updateActions();
  1199. }
  1200. void MainWindow::connectPlc()
  1201. {
  1202. PlcConnectionDialog dialog(
  1203. plc_configuration_,
  1204. plc_discovery_gateway_,
  1205. [this] { runtime_mode_service_.disconnectPlc(); },
  1206. this);
  1207. if (dialog.exec() != QDialog::Accepted)
  1208. {
  1209. return;
  1210. }
  1211. plc_configuration_ = dialog.configuration();
  1212. if (plc_configuration_.portName.empty())
  1213. {
  1214. showProjectResult(tr("连接 PLC"), tr("请选择或输入串口端口"), false);
  1215. return;
  1216. }
  1217. const PlcCommunicationResult result = runtime_mode_service_.connectPlc(
  1218. plc_configuration_);
  1219. if (!result.succeeded)
  1220. {
  1221. const QString message = fromUtf8(result.message);
  1222. if (result.message == runtime_mode_service_.plcError())
  1223. {
  1224. statusBar()->showMessage(message, 5000);
  1225. QMessageBox::warning(this, tr("连接 PLC"), message);
  1226. }
  1227. else
  1228. {
  1229. showProjectResult(tr("连接 PLC"), message, false);
  1230. }
  1231. return;
  1232. }
  1233. statusBar()->showMessage(tr("PLC 正在连接,等待首次 M/D 读取"), 5000);
  1234. }
  1235. void MainWindow::disconnectPlc()
  1236. {
  1237. runtime_mode_service_.disconnectPlc();
  1238. }
  1239. void MainWindow::schedulePlcStatusUpdate()
  1240. {
  1241. if (plc_status_update_pending_)
  1242. {
  1243. return;
  1244. }
  1245. plc_status_update_pending_ = true;
  1246. QMetaObject::invokeMethod(
  1247. this,
  1248. [this]
  1249. {
  1250. plc_status_update_pending_ = false;
  1251. updateModeUi(plcStatusText(runtime_mode_service_));
  1252. },
  1253. Qt::QueuedConnection);
  1254. }
  1255. // 根据控件ID加载控件属性到右侧属性面板
  1256. void MainWindow::showControlProperties(const std::string &control_id)
  1257. {
  1258. property_panel_controller_->showControlProperties(control_id);
  1259. }
  1260. void MainWindow::showLogicNodeProperties(const std::string &node_id)
  1261. {
  1262. property_panel_controller_->showLogicNodeProperties(node_id);
  1263. }
  1264. void MainWindow::addHmiControl(HmiControlType type)
  1265. {
  1266. property_panel_controller_->addHmiControl(type);
  1267. }
  1268. void MainWindow::deleteSelectedControl()
  1269. {
  1270. property_panel_controller_->deleteSelectedControl();
  1271. }
  1272. void MainWindow::addLogicCondition(const LogicNodeConfig &config)
  1273. {
  1274. const LogicEditorResult result = logic_editor_widget_->addCondition(config);
  1275. if (!result.succeeded)
  1276. {
  1277. showProjectResult(tr("添加逻辑条件"), fromUtf8(result.message), false);
  1278. return;
  1279. }
  1280. selected_logic_node_id_ = result.id;
  1281. showLogicNodeProperties(result.id);
  1282. refreshProjectUi();
  1283. statusBar()->showMessage(tr("已添加逻辑条件"), 3000);
  1284. }
  1285. void MainWindow::addLogicParallelBranch(const LogicNodeConfig &config)
  1286. {
  1287. const LogicEditorResult result = logic_editor_widget_->addParallelBranch(config);
  1288. if (!result.succeeded)
  1289. {
  1290. showProjectResult(tr("建立并联支路"), fromUtf8(result.message), false);
  1291. return;
  1292. }
  1293. selected_logic_node_id_ = result.id;
  1294. showLogicNodeProperties(result.id);
  1295. refreshProjectUi();
  1296. statusBar()->showMessage(tr("已建立并联支路,请配置新触点"), 3000);
  1297. }
  1298. void MainWindow::addLogicHorizontalWire()
  1299. {
  1300. const LogicEditorResult result = logic_editor_widget_->addHorizontalWire();
  1301. if (!result.succeeded)
  1302. {
  1303. showProjectResult(tr("插入横线"), fromUtf8(result.message), false);
  1304. return;
  1305. }
  1306. selected_logic_node_id_.clear();
  1307. showLogicNodeProperties({});
  1308. refreshProjectUi();
  1309. statusBar()->showMessage(tr("横线已插入,可直接用触点替换"), 3000);
  1310. }
  1311. void MainWindow::addLogicVerticalWire()
  1312. {
  1313. const LogicEditorResult result = logic_editor_widget_->addVerticalWire();
  1314. if (!result.succeeded)
  1315. {
  1316. showProjectResult(tr("插入竖线"), fromUtf8(result.message), false);
  1317. return;
  1318. }
  1319. selected_logic_node_id_.clear();
  1320. showLogicNodeProperties({});
  1321. refreshProjectUi();
  1322. statusBar()->showMessage(tr("已建立横线旁路和竖线连接"), 3000);
  1323. }
  1324. void MainWindow::deleteLogicHorizontalWire()
  1325. {
  1326. const LogicEditorResult result = logic_editor_widget_->deleteHorizontalWire();
  1327. if (!result.succeeded)
  1328. {
  1329. showProjectResult(tr("删除横线"), fromUtf8(result.message), false);
  1330. return;
  1331. }
  1332. selected_logic_node_id_.clear();
  1333. showLogicNodeProperties({});
  1334. refreshProjectUi();
  1335. statusBar()->showMessage(tr("横线已删除"), 3000);
  1336. }
  1337. void MainWindow::deleteLogicVerticalWire()
  1338. {
  1339. const LogicEditorResult result = logic_editor_widget_->deleteVerticalWire();
  1340. if (!result.succeeded)
  1341. {
  1342. showProjectResult(tr("删除竖线"), fromUtf8(result.message), false);
  1343. return;
  1344. }
  1345. selected_logic_node_id_.clear();
  1346. showLogicNodeProperties({});
  1347. refreshProjectUi();
  1348. statusBar()->showMessage(tr("竖线及对应并联支路已删除"), 3000);
  1349. }
  1350. void MainWindow::setLogicOutput(const LogicNodeConfig &config)
  1351. {
  1352. const LogicEditorResult result = logic_editor_widget_->setOutput(config);
  1353. if (!result.succeeded)
  1354. {
  1355. showProjectResult(tr("设置逻辑输出"), fromUtf8(result.message), false);
  1356. return;
  1357. }
  1358. selected_logic_node_id_ = result.id;
  1359. showLogicNodeProperties(result.id);
  1360. refreshProjectUi();
  1361. statusBar()->showMessage(tr("逻辑输出已设置"), 3000);
  1362. }
  1363. void MainWindow::configureAndSetLogicOutput(const LogicNodeConfig &config)
  1364. {
  1365. LogicInstructionDialog dialog(config, this);
  1366. if (dialog.exec() != QDialog::Accepted)
  1367. {
  1368. return;
  1369. }
  1370. const LogicNodeConfig configured = dialog.config();
  1371. LogicEditorResult result = logic_editor_widget_->setOutput(configured, true);
  1372. if (!result.succeeded)
  1373. {
  1374. showProjectResult(tr("设置逻辑输出"), fromUtf8(result.message), false);
  1375. return;
  1376. }
  1377. const std::string node_id = result.id;
  1378. selected_logic_node_id_ = node_id;
  1379. showLogicNodeProperties(node_id);
  1380. refreshProjectUi();
  1381. statusBar()->showMessage(tr("逻辑输出已配置"), 3000);
  1382. }
  1383. void MainWindow::addLogicRung()
  1384. {
  1385. const LogicEditorResult result = logic_editor_widget_->addRung();
  1386. if (!result.succeeded)
  1387. {
  1388. showProjectResult(tr("新建网络"), fromUtf8(result.message), false);
  1389. return;
  1390. }
  1391. selected_logic_node_id_.clear();
  1392. showLogicNodeProperties({});
  1393. refreshProjectUi();
  1394. statusBar()->showMessage(tr("已新建网络"), 3000);
  1395. }
  1396. void MainWindow::editSelectedRungComment()
  1397. {
  1398. const std::string rung_id = logic_editor_widget_->selectedRungId();
  1399. if (rung_id.empty())
  1400. {
  1401. showProjectResult(tr("网络注释"), tr("请先选择一个梯形图网络"), false);
  1402. return;
  1403. }
  1404. const LadderRung *rung = logic_editor_service_.findRung(
  1405. current_logic_id_, rung_id);
  1406. if (rung == nullptr)
  1407. {
  1408. return;
  1409. }
  1410. bool accepted = false;
  1411. const QString comment = QInputDialog::getText(
  1412. this,
  1413. tr("网络注释"),
  1414. tr("说明"),
  1415. QLineEdit::Normal,
  1416. fromUtf8(rung->comment),
  1417. &accepted);
  1418. if (!accepted)
  1419. {
  1420. return;
  1421. }
  1422. const LogicEditorResult result = logic_editor_service_.updateRungComment(
  1423. current_logic_id_, rung_id, toUtf8(comment));
  1424. if (!result.succeeded)
  1425. {
  1426. showProjectResult(tr("网络注释"), fromUtf8(result.message), false);
  1427. return;
  1428. }
  1429. logic_editor_widget_->reloadLogic();
  1430. refreshProjectUi();
  1431. statusBar()->showMessage(tr("网络注释已更新"), 3000);
  1432. }
  1433. void MainWindow::deleteSelectedLogicObject()
  1434. {
  1435. const LogicEditorResult result = logic_editor_widget_->deleteSelected();
  1436. if (!result.succeeded)
  1437. {
  1438. return;
  1439. }
  1440. selected_logic_node_id_.clear();
  1441. showLogicNodeProperties({});
  1442. refreshProjectUi();
  1443. }
  1444. void MainWindow::createNewProject()
  1445. {
  1446. if (!confirmSaveBeforeDestructiveAction())
  1447. {
  1448. return;
  1449. }
  1450. QInputDialog name_dialog(this);
  1451. name_dialog.setWindowTitle(tr("新建工程"));
  1452. name_dialog.setLabelText(tr("工程名称"));
  1453. name_dialog.setInputMode(QInputDialog::TextInput);
  1454. QDialogButtonBox *button_box = name_dialog.findChild<QDialogButtonBox *>();
  1455. QPushButton *ok_button = button_box != nullptr
  1456. ? button_box->button(QDialogButtonBox::Ok)
  1457. : nullptr;
  1458. if (ok_button != nullptr)
  1459. {
  1460. ok_button->setEnabled(false);
  1461. connect(&name_dialog,
  1462. &QInputDialog::textValueChanged,
  1463. &name_dialog,
  1464. [ok_button](const QString &text) {
  1465. ok_button->setEnabled(!text.trimmed().isEmpty());
  1466. });
  1467. }
  1468. if (name_dialog.exec() != QDialog::Accepted)
  1469. {
  1470. return;
  1471. }
  1472. const QString name = name_dialog.textValue().trimmed();
  1473. const ProjectOperationResult result = project_service_.createNewProject(toUtf8(name));
  1474. if (!result.succeeded)
  1475. {
  1476. showProjectResult(tr("新建工程"), fromUtf8(result.message), false);
  1477. return;
  1478. }
  1479. hmi_editor_service_.ensureDefaultPage();
  1480. logic_editor_service_.ensureDefaultLogic();
  1481. clearEditorHistories();
  1482. selected_control_id_.clear();
  1483. selected_logic_node_id_.clear();
  1484. editor_clipboard_ = std::monostate{};
  1485. current_hmi_page_id_ = project_service_.project().initialHmiPageId;
  1486. current_logic_id_ = logic_editor_service_.firstLogicId();
  1487. refreshProjectUi();
  1488. hmi_editor_widget_->reloadPage();
  1489. logic_editor_widget_->reloadLogic();
  1490. showControlProperties({});
  1491. statusBar()->showMessage(tr("已创建新工程"), 3000);
  1492. }
  1493. void MainWindow::saveProject()
  1494. {
  1495. if (!project_service_.hasCurrentFile())
  1496. {
  1497. saveProjectAs();
  1498. return;
  1499. }
  1500. const ProjectOperationResult result = project_service_.save();
  1501. if (!result.succeeded)
  1502. {
  1503. showProjectResult(tr("保存工程"), fromUtf8(result.message), false);
  1504. return;
  1505. }
  1506. updateWindowTitle();
  1507. showProjectResult(tr("保存工程"), tr("工程已保存"), true);
  1508. }
  1509. void MainWindow::saveProjectAs()
  1510. {
  1511. const QString suggested_file_name = suggestedProjectFileName(
  1512. fromUtf8(project_service_.project().metadata.name));
  1513. const QString path = QFileDialog::getSaveFileName(
  1514. this,
  1515. tr("工程另存为"),
  1516. suggested_file_name,
  1517. tr("工程文件 (*.json)"));
  1518. if (path.isEmpty())
  1519. {
  1520. return;
  1521. }
  1522. const ProjectOperationResult result = project_service_.saveAs(toUtf8(path));
  1523. if (result.succeeded)
  1524. {
  1525. updateWindowTitle();
  1526. }
  1527. showProjectResult(
  1528. tr("保存工程"),
  1529. result.succeeded ? tr("工程已保存") : fromUtf8(result.message),
  1530. result.succeeded);
  1531. }
  1532. void MainWindow::loadProject()
  1533. {
  1534. if (!confirmSaveBeforeDestructiveAction())
  1535. {
  1536. return;
  1537. }
  1538. const QString path = QFileDialog::getOpenFileName(
  1539. this, tr("加载工程"), {}, tr("工程文件 (*.json)"));
  1540. if (path.isEmpty())
  1541. {
  1542. return;
  1543. }
  1544. const ProjectOperationResult result = project_service_.load(toUtf8(path));
  1545. if (!result.succeeded)
  1546. {
  1547. showProjectResult(tr("加载工程"), fromUtf8(result.message), false);
  1548. return;
  1549. }
  1550. clearEditorHistories();
  1551. selected_control_id_.clear();
  1552. selected_logic_node_id_.clear();
  1553. editor_clipboard_ = std::monostate{};
  1554. current_hmi_page_id_ = project_service_.project().initialHmiPageId;
  1555. current_logic_id_ = logic_editor_service_.firstLogicId();
  1556. refreshProjectUi();
  1557. hmi_editor_widget_->reloadPage();
  1558. logic_editor_widget_->reloadLogic();
  1559. showControlProperties({});
  1560. showProjectResult(tr("加载工程"), tr("工程已加载"), true);
  1561. }
  1562. void MainWindow::showProjectResult(
  1563. const QString &action, const QString &message, bool succeeded)
  1564. {
  1565. const QString output = action + QStringLiteral(": ") + message;
  1566. statusBar()->showMessage(output, 5000);
  1567. appendOutputMessage(output);
  1568. if (!succeeded)
  1569. {
  1570. QMessageBox::warning(this, action, message);
  1571. }
  1572. }
  1573. void MainWindow::appendOutputMessage(const QString &message)
  1574. {
  1575. while (ui_->outputList->count() >= ProjectLimits::kMaximumOutputMessages)
  1576. {
  1577. delete ui_->outputList->takeItem(0);
  1578. }
  1579. ui_->outputList->addItem(message);
  1580. ui_->outputList->scrollToBottom();
  1581. }
  1582. void MainWindow::requestMode(ApplicationMode requested_mode)
  1583. {
  1584. // UI 动作只提出目标模式,所有前置条件和仓库切换由 RuntimeModeService 决定
  1585. // UI 仅转发模式意图,合法性由服务层和领域状态机决定
  1586. ModeTransitionResult result;
  1587. switch (requested_mode)
  1588. {
  1589. case ApplicationMode::Editing:
  1590. {
  1591. result = runtime_mode_service_.enterEditing();
  1592. break;
  1593. }
  1594. case ApplicationMode::OfflineRunning:
  1595. {
  1596. result = runtime_mode_service_.enterOfflineRunning();
  1597. break;
  1598. }
  1599. case ApplicationMode::OnlineRunning:
  1600. {
  1601. result = runtime_mode_service_.enterOnlineRunning();
  1602. break;
  1603. }
  1604. default:
  1605. {
  1606. restoreCurrentModeAction();
  1607. statusBar()->showMessage(tr("不支持的运行模式"), 5000);
  1608. return;
  1609. }
  1610. }
  1611. if (!result.succeeded)
  1612. {
  1613. restoreCurrentModeAction();
  1614. QString message = transitionErrorText(result.error);
  1615. if (result.error == ModeTransitionError::InitialPlcReadRequired)
  1616. {
  1617. message += tr(";当前状态:%1").arg(plcStatusText(runtime_mode_service_));
  1618. }
  1619. if (result.error == ModeTransitionError::SimulationStartFailed)
  1620. {
  1621. const LogicScanResult &error = runtime_mode_service_.simulationError();
  1622. if (!error.message.empty())
  1623. {
  1624. message += QStringLiteral(": ") + fromUtf8(error.message);
  1625. }
  1626. }
  1627. statusBar()->showMessage(message, 5000);
  1628. return;
  1629. }
  1630. updateModeUi(tr("已进入%1").arg(modeText(runtime_mode_service_.mode())));
  1631. }
  1632. void MainWindow::updateModeUi(const QString &message)
  1633. {
  1634. // 按服务层策略统一启用/禁用编辑入口,避免单个按钮遗漏状态同步
  1635. const ApplicationMode mode = runtime_mode_service_.mode();
  1636. const ModePolicy policy = runtime_mode_service_.policy();
  1637. restoreCurrentModeAction();
  1638. const bool running = mode != ApplicationMode::Editing;
  1639. if (running)
  1640. {
  1641. if (hmi_navigation_service_->currentPageId().empty())
  1642. {
  1643. const HmiNavigationResult navigation = hmi_navigation_service_->start();
  1644. if (!navigation.succeeded)
  1645. {
  1646. statusBar()->showMessage(fromUtf8(navigation.message), 5000);
  1647. }
  1648. }
  1649. runtime_panel_controller_->enterRuntime(
  1650. hmi_navigation_service_->currentPageId(),
  1651. current_logic_id_,
  1652. mode,
  1653. runtime_mode_service_.plcConnectionState());
  1654. }
  1655. else
  1656. {
  1657. hmi_navigation_service_->stop();
  1658. runtime_panel_controller_->leaveRuntime(
  1659. mode, runtime_mode_service_.plcConnectionState());
  1660. }
  1661. // 将同一份模式策略同步到所有可编辑入口,避免只禁用部分操作
  1662. ui_->projectDock->setEnabled(policy.allowsProjectEditing);
  1663. ui_->propertiesDock->setEnabled(policy.allowsProjectEditing);
  1664. hmi_editor_widget_->setEditingEnabled(policy.allowsProjectEditing);
  1665. hmi_editor_widget_->setRuntimeActive(
  1666. policy.usesVirtualRegisters || policy.usesPlcRegisters);
  1667. logic_editor_widget_->setEditingEnabled(policy.allowsProjectEditing);
  1668. if (mode == ApplicationMode::Editing)
  1669. {
  1670. alarm_service_.reset();
  1671. logic_editor_widget_->clearRuntimeTrace();
  1672. }
  1673. ui_->hmiToolBar->setEnabled(policy.allowsProjectEditing);
  1674. ui_->logicToolBar->setEnabled(policy.allowsProjectEditing);
  1675. ui_->addButtonAction->setEnabled(policy.allowsProjectEditing);
  1676. ui_->addIndicatorAction->setEnabled(policy.allowsProjectEditing);
  1677. ui_->addNumericDisplayAction->setEnabled(policy.allowsProjectEditing);
  1678. ui_->addNumericInputAction->setEnabled(policy.allowsProjectEditing);
  1679. ui_->addLabelAction->setEnabled(policy.allowsProjectEditing);
  1680. ui_->addPageJumpAction->setEnabled(policy.allowsProjectEditing);
  1681. ui_->addAlarmListAction->setEnabled(policy.allowsProjectEditing);
  1682. ui_->configureAlarmsAction->setEnabled(policy.allowsProjectEditing);
  1683. ui_->configureRegisterCommentsAction->setEnabled(policy.allowsProjectEditing);
  1684. ui_->deleteControlAction->setEnabled(policy.allowsProjectEditing);
  1685. ui_->addNormallyOpenAction->setEnabled(policy.allowsProjectEditing);
  1686. ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing);
  1687. ui_->addRisingEdgeAction->setEnabled(policy.allowsProjectEditing);
  1688. ui_->addFallingEdgeAction->setEnabled(policy.allowsProjectEditing);
  1689. ui_->addNormalCoilAction->setEnabled(policy.allowsProjectEditing);
  1690. ui_->addSetCoilAction->setEnabled(policy.allowsProjectEditing);
  1691. ui_->addResetCoilAction->setEnabled(policy.allowsProjectEditing);
  1692. ui_->addMoveAction->setEnabled(policy.allowsProjectEditing);
  1693. ui_->addAddAction->setEnabled(policy.allowsProjectEditing);
  1694. ui_->addSubAction->setEnabled(policy.allowsProjectEditing);
  1695. ui_->addCompareAction->setEnabled(policy.allowsProjectEditing);
  1696. ui_->editRungCommentAction->setEnabled(policy.allowsProjectEditing);
  1697. ui_->deleteLogicAction->setEnabled(policy.allowsProjectEditing);
  1698. ui_->newProjectAction->setEnabled(policy.allowsProjectEditing);
  1699. ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing);
  1700. ui_->saveAsProjectAction->setEnabled(policy.allowsProjectEditing);
  1701. ui_->loadProjectAction->setEnabled(policy.allowsProjectEditing);
  1702. updateProjectTreeActions();
  1703. updateEditActions();
  1704. mode_status_label_->setText(modeText(mode));
  1705. if (mode == ApplicationMode::Editing)
  1706. {
  1707. mode_status_label_->setStyleSheet(QStringLiteral(
  1708. "border: 1px solid #75a58a; background: #e3f0e8; color: #205c3b; padding: 2px 8px;"));
  1709. }
  1710. else if (mode == ApplicationMode::OfflineRunning)
  1711. {
  1712. mode_status_label_->setStyleSheet(QStringLiteral(
  1713. "border: 1px solid #bd8739; background: #fff0d2; color: #744b0e; padding: 2px 8px;"));
  1714. }
  1715. else
  1716. {
  1717. mode_status_label_->setStyleSheet(QStringLiteral(
  1718. "border: 1px solid #4b88a8; background: #deeff7; color: #174f6c; padding: 2px 8px;"));
  1719. }
  1720. register_status_label_->setText(
  1721. policy.usesVirtualRegisters ? tr("数据源:虚拟 M/D")
  1722. : policy.usesPlcRegisters ? tr("数据源:PLC 缓存") : tr("数据源:未启用"));
  1723. const PlcConnectionState plc_state = runtime_mode_service_.plcConnectionState();
  1724. const bool plc_configuration_available = plc_state == PlcConnectionState::Disconnected
  1725. || plc_state == PlcConnectionState::Faulted;
  1726. ui_->configurePlcAction->setEnabled(
  1727. policy.allowsProjectEditing && plc_configuration_available);
  1728. ui_->configurePlcAction->setText(
  1729. plc_state == PlcConnectionState::Faulted ? tr("PLC 重新配置") : tr("PLC 配置"));
  1730. ui_->configurePlcAction->setToolTip(
  1731. plc_state == PlcConnectionState::Faulted
  1732. ? tr("清理故障会话后重新配置并连接真实 PLC")
  1733. : tr("配置参数并连接真实 PLC"));
  1734. ui_->disconnectPlcAction->setEnabled(plc_state != PlcConnectionState::Disconnected);
  1735. if (plc_state == PlcConnectionState::Connecting)
  1736. {
  1737. register_status_label_->setText(tr("PLC:正在连接"));
  1738. }
  1739. else if (plc_state == PlcConnectionState::Connected)
  1740. {
  1741. register_status_label_->setText(
  1742. runtime_mode_service_.initialPlcReadCompleted()
  1743. ? tr("PLC:已连接,首读完成") : tr("PLC:已连接,正在首读"));
  1744. }
  1745. else if (plc_state == PlcConnectionState::Recovering)
  1746. {
  1747. register_status_label_->setText(tr("PLC:通信已恢复,正在重新首读"));
  1748. }
  1749. else if (plc_state == PlcConnectionState::Faulted)
  1750. {
  1751. register_status_label_->setText(tr("PLC:通信故障"));
  1752. }
  1753. updateSimulationUi(false);
  1754. statusBar()->showMessage(message, 4000);
  1755. const bool duplicate = ui_->outputList->count() > 0
  1756. && ui_->outputList->item(ui_->outputList->count() - 1)->text() == message;
  1757. if (!duplicate)
  1758. {
  1759. appendOutputMessage(message);
  1760. }
  1761. }
  1762. void MainWindow::updateSimulationUi(bool report_fault)
  1763. {
  1764. runtime_panel_controller_->updateSimulationUi(report_fault);
  1765. }
  1766. // 根据当前真实运行模式,同步更新模式工具栏单选按钮选中状态
  1767. void MainWindow::restoreCurrentModeAction()
  1768. {
  1769. ui_->editingModeAction->setChecked(runtime_mode_service_.mode()
  1770. == ApplicationMode::Editing);
  1771. ui_->offlineModeAction->setChecked(runtime_mode_service_.mode()
  1772. == ApplicationMode::OfflineRunning);
  1773. ui_->onlineModeAction->setChecked(runtime_mode_service_.mode()
  1774. == ApplicationMode::OnlineRunning);
  1775. }