综合平台编程器项目的远程存储
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 

2645 wiersze
87 KiB

  1. #include "logic_editor_widget.h"
  2. #include <QApplication>
  3. #include <QBrush>
  4. #include <QCompleter>
  5. #include <QEvent>
  6. #include <QGraphicsItem>
  7. #include <QGraphicsLineItem>
  8. #include <QGraphicsRectItem>
  9. #include <QGraphicsScene>
  10. #include <QHeaderView>
  11. #include <QKeyEvent>
  12. #include <QLineEdit>
  13. #include <QMouseEvent>
  14. #include <QPainter>
  15. #include <QPainterPathStroker>
  16. #include <QPen>
  17. #include <QRegularExpression>
  18. #include <QResizeEvent>
  19. #include <QRubberBand>
  20. #include <QStandardItemModel>
  21. #include <QStyleOptionGraphicsItem>
  22. #include <QTimer>
  23. #include <QTreeView>
  24. #include <algorithm>
  25. #include <type_traits>
  26. namespace {
  27. constexpr qreal kLabelWidth = 60.0;
  28. constexpr qreal kCellWidth = 96.0;
  29. constexpr qreal kOutputWidth = 224.0;
  30. constexpr qreal kRowHeight = 78.0;
  31. constexpr qreal kCommentBandHeight = 28.0;
  32. constexpr qreal kTop = 18.0;
  33. constexpr qreal kLeftBus = kLabelWidth;
  34. constexpr qreal kConditionRight = kLabelWidth
  35. + ProjectLimits::kMaximumConditionColumns * kCellWidth;
  36. constexpr qreal kRightBus = kConditionRight + kOutputWidth;
  37. constexpr qreal kSceneRightMargin = 24.0;
  38. const QColor kCanvasBackground(QStringLiteral("#ffffff"));
  39. const QColor kGridColor(QStringLiteral("#d8e0e4"));
  40. const QColor kLadderColor(QStringLiteral("#263842"));
  41. const QColor kActiveColor(QStringLiteral("#16854f"));
  42. const QColor kFaultColor(QStringLiteral("#c5362e"));
  43. const QColor kCommentColor(QStringLiteral("#16854f"));
  44. const QColor kSelectionFill(QStringLiteral("#dfeef5"));
  45. const QColor kSelectionBorder(QStringLiteral("#277da1"));
  46. QString registerAddressText(const RegisterAddress &address)
  47. {
  48. return QString::fromStdString(address.toString());
  49. }
  50. QString wordOperandText(const WordOperand &operand)
  51. {
  52. return operand.kind == WordOperandKind::Register
  53. ? registerAddressText(operand.address)
  54. : QString::number(operand.constant);
  55. }
  56. QString comparisonText(ComparisonOperator comparison)
  57. {
  58. switch (comparison)
  59. {
  60. case ComparisonOperator::Equal: return QStringLiteral("=");
  61. case ComparisonOperator::NotEqual: return QStringLiteral("<>");
  62. case ComparisonOperator::LessThan: return QStringLiteral("<");
  63. case ComparisonOperator::LessThanOrEqual: return QStringLiteral("<=");
  64. case ComparisonOperator::GreaterThan: return QStringLiteral(">");
  65. case ComparisonOperator::GreaterThanOrEqual: return QStringLiteral(">=");
  66. }
  67. return QStringLiteral("?");
  68. }
  69. QString logicCommandText(const LogicNodeConfig &config)
  70. {
  71. return std::visit(
  72. [](const auto &value) -> QString
  73. {
  74. using Config = std::decay_t<decltype(value)>;
  75. if constexpr (std::is_same_v<Config, ContactNodeConfig>)
  76. {
  77. return QStringLiteral("%1 %2")
  78. .arg(value.mode == ContactMode::NormallyOpen
  79. ? QStringLiteral("LD") : QStringLiteral("LDI"))
  80. .arg(registerAddressText(value.address));
  81. }
  82. else if constexpr (std::is_same_v<Config, EdgeContactNodeConfig>)
  83. {
  84. return QStringLiteral("%1 %2")
  85. .arg(value.mode == EdgeMode::Rising
  86. ? QStringLiteral("LDP") : QStringLiteral("LDF"))
  87. .arg(registerAddressText(value.address));
  88. }
  89. else if constexpr (std::is_same_v<Config, CoilNodeConfig>)
  90. {
  91. const QString mnemonic = value.mode == CoilMode::Set
  92. ? QStringLiteral("SET")
  93. : value.mode == CoilMode::Reset
  94. ? QStringLiteral("RST") : QStringLiteral("OUT");
  95. return QStringLiteral("%1 %2")
  96. .arg(mnemonic)
  97. .arg(registerAddressText(value.address));
  98. }
  99. else if constexpr (std::is_same_v<Config, CompareNodeConfig>)
  100. {
  101. return QStringLiteral("LD%1 %2 %3")
  102. .arg(comparisonText(value.comparison))
  103. .arg(registerAddressText(value.address))
  104. .arg(value.value);
  105. }
  106. else if constexpr (std::is_same_v<Config, MoveNodeConfig>)
  107. {
  108. return QStringLiteral("MOV %1 %2")
  109. .arg(wordOperandText(value.source))
  110. .arg(registerAddressText(value.destination));
  111. }
  112. else
  113. {
  114. return QStringLiteral("%1 %2 %3 %4")
  115. .arg(value.operation == ArithmeticOperation::Add
  116. ? QStringLiteral("ADD") : QStringLiteral("SUB"))
  117. .arg(wordOperandText(value.left))
  118. .arg(wordOperandText(value.right))
  119. .arg(registerAddressText(value.destination));
  120. }
  121. },
  122. config);
  123. }
  124. QString commandCompletionPrefix(const QString &text)
  125. {
  126. return text.section(QRegularExpression(QStringLiteral("\\s+")), 0, 0)
  127. .toUpper();
  128. }
  129. QPen ladderPen(bool active, bool faulted = false)
  130. {
  131. QPen pen(faulted ? kFaultColor : active ? kActiveColor : kLadderColor);
  132. pen.setWidthF(active || faulted ? 2.5 : 1.8);
  133. pen.setJoinStyle(Qt::MiterJoin);
  134. pen.setCapStyle(Qt::SquareCap);
  135. return pen;
  136. }
  137. bool containsId(
  138. const std::vector<std::string> &ids, const std::string &candidate)
  139. {
  140. return std::find(ids.cbegin(), ids.cend(), candidate) != ids.cend();
  141. }
  142. bool containsCell(
  143. const std::vector<std::pair<std::string, int>> &cells,
  144. const std::pair<std::string, int> &candidate)
  145. {
  146. return std::find(cells.cbegin(), cells.cend(), candidate) != cells.cend();
  147. }
  148. struct GridRow
  149. {
  150. qreal top = 0.0;
  151. };
  152. class GridLayerItem final : public QGraphicsItem
  153. {
  154. public:
  155. explicit GridLayerItem(std::vector<GridRow> rows)
  156. : rows_(std::move(rows))
  157. {
  158. setZValue(0.0);
  159. }
  160. QRectF boundingRect() const override
  161. {
  162. if (rows_.empty())
  163. {
  164. return {};
  165. }
  166. return {
  167. kLeftBus,
  168. rows_.front().top,
  169. kRightBus - kLeftBus,
  170. rows_.back().top + kRowHeight - rows_.front().top};
  171. }
  172. void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
  173. {
  174. painter->setRenderHint(QPainter::Antialiasing, false);
  175. QPen grid_pen(kGridColor);
  176. grid_pen.setWidthF(1.0);
  177. grid_pen.setCosmetic(true);
  178. painter->setPen(grid_pen);
  179. for (const GridRow &row : rows_)
  180. {
  181. painter->fillRect(
  182. QRectF(
  183. kLeftBus,
  184. row.top,
  185. kRightBus - kLeftBus,
  186. kRowHeight),
  187. kCanvasBackground);
  188. painter->drawRect(
  189. QRectF(
  190. kLeftBus,
  191. row.top,
  192. kRightBus - kLeftBus,
  193. kRowHeight));
  194. for (int boundary = 1;
  195. boundary <= ProjectLimits::kMaximumConditionColumns;
  196. ++boundary)
  197. {
  198. const qreal x = kLeftBus
  199. + static_cast<qreal>(boundary) * kCellWidth;
  200. painter->drawLine(
  201. QPointF(x, row.top),
  202. QPointF(x, row.top + kRowHeight));
  203. }
  204. }
  205. }
  206. private:
  207. std::vector<GridRow> rows_;
  208. };
  209. class CellContentItem final : public QGraphicsItem
  210. {
  211. public:
  212. CellContentItem(
  213. const LadderCell &cell,
  214. const std::string &rung_id,
  215. int column,
  216. const QPointF &top_left,
  217. bool input_active,
  218. bool output_active,
  219. bool faulted)
  220. : cell_(cell), input_active_(input_active),
  221. output_active_(output_active), faulted_(faulted)
  222. {
  223. setPos(top_left);
  224. setZValue(10.0);
  225. setData(0, QStringLiteral("cell"));
  226. setData(1, QString::fromStdString(rung_id));
  227. setData(2, column);
  228. setData(3, QString::fromStdString(
  229. cell.node.has_value() ? cell.node->id : cell.id));
  230. setData(4, static_cast<int>(cell.kind));
  231. setData(5, QString::fromStdString(cell.id));
  232. setData(6, input_active_);
  233. setData(7, output_active_);
  234. }
  235. QRectF boundingRect() const override
  236. {
  237. return {0.0, 0.0, kCellWidth, kRowHeight};
  238. }
  239. void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
  240. {
  241. if (cell_.kind == LadderCellKind::Gap)
  242. {
  243. return;
  244. }
  245. painter->setRenderHint(QPainter::Antialiasing, true);
  246. const auto use_pen = [this, painter](bool active)
  247. {
  248. painter->setPen(ladderPen(active, faulted_));
  249. };
  250. const qreal center_x = kCellWidth / 2.0;
  251. const qreal center_y = kRowHeight / 2.0 + 6.0;
  252. if (cell_.kind == LadderCellKind::Wire || !cell_.node.has_value())
  253. {
  254. use_pen(input_active_);
  255. painter->drawLine(
  256. QPointF(0.0, center_y), QPointF(center_x, center_y));
  257. use_pen(output_active_);
  258. painter->drawLine(
  259. QPointF(center_x, center_y), QPointF(kCellWidth, center_y));
  260. return;
  261. }
  262. const LogicNode &node = *cell_.node;
  263. QFont font = painter->font();
  264. font.setPointSizeF(8.5);
  265. painter->setFont(font);
  266. std::visit(
  267. [&](const auto &config)
  268. {
  269. using Config = std::decay_t<decltype(config)>;
  270. if constexpr (std::is_same_v<Config, ContactNodeConfig>
  271. || std::is_same_v<Config, EdgeContactNodeConfig>)
  272. {
  273. const qreal left = center_x - 15.0;
  274. const qreal right = center_x + 15.0;
  275. use_pen(input_active_);
  276. painter->drawLine(QPointF(0.0, center_y), QPointF(left, center_y));
  277. use_pen(output_active_);
  278. painter->drawLine(QPointF(right, center_y), QPointF(kCellWidth, center_y));
  279. painter->drawLine(QPointF(left, center_y - 14.0), QPointF(left, center_y + 14.0));
  280. painter->drawLine(QPointF(right, center_y - 14.0), QPointF(right, center_y + 14.0));
  281. if constexpr (std::is_same_v<Config, ContactNodeConfig>)
  282. {
  283. if (config.mode == ContactMode::NormallyClosed)
  284. {
  285. painter->drawLine(
  286. QPointF(left - 4.0, center_y + 17.0),
  287. QPointF(right + 4.0, center_y - 17.0));
  288. }
  289. }
  290. else
  291. {
  292. painter->drawText(
  293. QRectF(center_x - 13.0, center_y - 12.0, 26.0, 24.0),
  294. Qt::AlignCenter,
  295. config.mode == EdgeMode::Rising
  296. ? QStringLiteral("P") : QStringLiteral("N"));
  297. }
  298. painter->drawText(
  299. QRectF(2.0, 3.0, kCellWidth - 4.0, 20.0),
  300. Qt::AlignCenter,
  301. node.configured
  302. ? registerAddressText(config.address)
  303. : QStringLiteral("<M>"));
  304. }
  305. else if constexpr (std::is_same_v<Config, CompareNodeConfig>)
  306. {
  307. const QRectF box(7.0, center_y - 15.0, kCellWidth - 14.0, 30.0);
  308. use_pen(input_active_);
  309. painter->drawLine(QPointF(0.0, center_y), QPointF(box.left(), center_y));
  310. use_pen(output_active_);
  311. painter->drawLine(QPointF(box.right(), center_y), QPointF(kCellWidth, center_y));
  312. painter->drawRect(box);
  313. QFont compare_font = painter->font();
  314. compare_font.setPointSizeF(7.5);
  315. painter->setFont(compare_font);
  316. painter->drawText(
  317. box.adjusted(2.0, 0.0, -2.0, 0.0),
  318. Qt::AlignCenter,
  319. node.configured
  320. ? QStringLiteral("%1 %2 %3")
  321. .arg(registerAddressText(config.address))
  322. .arg(comparisonText(config.comparison))
  323. .arg(config.value)
  324. : QStringLiteral("<比较>"));
  325. }
  326. else
  327. {
  328. use_pen(input_active_);
  329. painter->drawLine(
  330. QPointF(0.0, center_y), QPointF(center_x, center_y));
  331. use_pen(output_active_);
  332. painter->drawLine(
  333. QPointF(center_x, center_y), QPointF(kCellWidth, center_y));
  334. }
  335. },
  336. node.config);
  337. }
  338. private:
  339. LadderCell cell_;
  340. bool input_active_ = false;
  341. bool output_active_ = false;
  342. bool faulted_ = false;
  343. };
  344. class OutputContentItem final : public QGraphicsItem
  345. {
  346. public:
  347. OutputContentItem(
  348. const LadderRung &rung,
  349. const QPointF &top_left,
  350. bool input_active,
  351. bool symbol_active,
  352. bool faulted)
  353. : rung_id_(rung.id), output_(rung.output),
  354. input_active_(input_active), symbol_active_(symbol_active),
  355. faulted_(faulted)
  356. {
  357. setPos(top_left);
  358. setZValue(10.0);
  359. setData(0, QStringLiteral("output"));
  360. setData(1, QString::fromStdString(rung.id));
  361. setData(2, QString::fromStdString(
  362. rung.output.has_value() ? rung.output->id : std::string{}));
  363. setData(3, input_active_);
  364. setData(4, symbol_active_);
  365. }
  366. QRectF boundingRect() const override
  367. {
  368. return {0.0, 0.0, kOutputWidth, kRowHeight};
  369. }
  370. void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
  371. {
  372. if (!output_.has_value())
  373. {
  374. return;
  375. }
  376. painter->setRenderHint(QPainter::Antialiasing, true);
  377. const auto use_pen = [this, painter](bool active)
  378. {
  379. painter->setPen(ladderPen(active, faulted_));
  380. };
  381. const qreal center_y = kRowHeight / 2.0 + 6.0;
  382. const LogicNode &node = *output_;
  383. QFont font = painter->font();
  384. font.setPointSizeF(8.5);
  385. painter->setFont(font);
  386. std::visit(
  387. [&](const auto &config)
  388. {
  389. using Config = std::decay_t<decltype(config)>;
  390. if constexpr (std::is_same_v<Config, CoilNodeConfig>)
  391. {
  392. const qreal center_x = kOutputWidth / 2.0;
  393. use_pen(input_active_);
  394. painter->drawLine(QPointF(0.0, center_y), QPointF(center_x - 22.0, center_y));
  395. painter->drawLine(QPointF(center_x + 22.0, center_y), QPointF(kOutputWidth, center_y));
  396. use_pen(symbol_active_);
  397. QPainterPath left;
  398. left.moveTo(center_x - 3.0, center_y - 18.0);
  399. left.cubicTo(
  400. center_x - 25.0, center_y - 14.0,
  401. center_x - 25.0, center_y + 14.0,
  402. center_x - 3.0, center_y + 18.0);
  403. painter->drawPath(left);
  404. QPainterPath right;
  405. right.moveTo(center_x + 3.0, center_y - 18.0);
  406. right.cubicTo(
  407. center_x + 25.0, center_y - 14.0,
  408. center_x + 25.0, center_y + 14.0,
  409. center_x + 3.0, center_y + 18.0);
  410. painter->drawPath(right);
  411. if (config.mode != CoilMode::Normal)
  412. {
  413. painter->drawText(
  414. QRectF(center_x - 13.0, center_y - 13.0, 26.0, 26.0),
  415. Qt::AlignCenter,
  416. config.mode == CoilMode::Set
  417. ? QStringLiteral("S") : QStringLiteral("R"));
  418. }
  419. painter->drawText(
  420. QRectF(2.0, 3.0, kOutputWidth - 4.0, 20.0),
  421. Qt::AlignCenter,
  422. node.configured
  423. ? registerAddressText(config.address)
  424. : QStringLiteral("<M>"));
  425. }
  426. else if constexpr (std::is_same_v<Config, MoveNodeConfig>)
  427. {
  428. const QRectF box(26.0, 11.0, kOutputWidth - 52.0, kRowHeight - 22.0);
  429. use_pen(input_active_);
  430. painter->drawLine(QPointF(0.0, center_y), QPointF(box.left(), center_y));
  431. painter->drawLine(QPointF(box.right(), center_y), QPointF(kOutputWidth, center_y));
  432. use_pen(symbol_active_);
  433. painter->drawRect(box);
  434. QFont mnemonic_font = painter->font();
  435. mnemonic_font.setBold(true);
  436. painter->setFont(mnemonic_font);
  437. painter->drawText(
  438. QRectF(box.left(), box.top() + 2.0, box.width(), 20.0),
  439. Qt::AlignCenter,
  440. QStringLiteral("MOV"));
  441. mnemonic_font.setBold(false);
  442. mnemonic_font.setPointSizeF(8.0);
  443. painter->setFont(mnemonic_font);
  444. painter->drawText(
  445. QRectF(box.left() + 4.0, box.top() + 23.0, box.width() - 8.0, 20.0),
  446. Qt::AlignCenter,
  447. node.configured
  448. ? QStringLiteral("%1 -> %2")
  449. .arg(wordOperandText(config.source))
  450. .arg(registerAddressText(config.destination))
  451. : QStringLiteral("<源> -> <目标>"));
  452. }
  453. else if constexpr (std::is_same_v<Config, ArithmeticNodeConfig>)
  454. {
  455. const QRectF box(18.0, 11.0, kOutputWidth - 36.0, kRowHeight - 22.0);
  456. use_pen(input_active_);
  457. painter->drawLine(QPointF(0.0, center_y), QPointF(box.left(), center_y));
  458. painter->drawLine(QPointF(box.right(), center_y), QPointF(kOutputWidth, center_y));
  459. use_pen(symbol_active_);
  460. painter->drawRect(box);
  461. QFont mnemonic_font = painter->font();
  462. mnemonic_font.setBold(true);
  463. painter->setFont(mnemonic_font);
  464. painter->drawText(
  465. QRectF(box.left(), box.top() + 2.0, box.width(), 20.0),
  466. Qt::AlignCenter,
  467. config.operation == ArithmeticOperation::Add
  468. ? QStringLiteral("ADD") : QStringLiteral("SUB"));
  469. mnemonic_font.setBold(false);
  470. mnemonic_font.setPointSizeF(8.0);
  471. painter->setFont(mnemonic_font);
  472. painter->drawText(
  473. QRectF(box.left() + 4.0, box.top() + 23.0, box.width() - 8.0, 20.0),
  474. Qt::AlignCenter,
  475. node.configured
  476. ? QStringLiteral("%1, %2 -> %3")
  477. .arg(wordOperandText(config.left))
  478. .arg(wordOperandText(config.right))
  479. .arg(registerAddressText(config.destination))
  480. : QStringLiteral("<左>, <右> -> <目标>"));
  481. }
  482. else
  483. {
  484. use_pen(input_active_);
  485. painter->drawLine(
  486. QPointF(0.0, center_y), QPointF(kOutputWidth, center_y));
  487. }
  488. },
  489. node.config);
  490. }
  491. private:
  492. std::string rung_id_;
  493. std::optional<LogicNode> output_;
  494. bool input_active_ = false;
  495. bool symbol_active_ = false;
  496. bool faulted_ = false;
  497. };
  498. class VerticalConnectionItem final : public QGraphicsItem
  499. {
  500. public:
  501. VerticalConnectionItem(
  502. const VerticalConnection &connection,
  503. qreal x,
  504. qreal top,
  505. qreal bottom,
  506. bool active)
  507. : height_(bottom - top), active_(active)
  508. {
  509. setPos(x, top);
  510. setZValue(20.0);
  511. setData(0, QStringLiteral("vertical"));
  512. setData(1, QString::fromStdString(connection.id));
  513. setData(2, QString::fromStdString(connection.upperRungId));
  514. setData(3, connection.columnBoundary);
  515. setData(4, QString::fromStdString(connection.lowerRungId));
  516. setData(5, active_);
  517. }
  518. QRectF boundingRect() const override
  519. {
  520. return {-7.0, 0.0, 14.0, height_};
  521. }
  522. QPainterPath shape() const override
  523. {
  524. QPainterPath path;
  525. path.moveTo(0.0, 0.0);
  526. path.lineTo(0.0, height_);
  527. QPainterPathStroker stroker;
  528. stroker.setWidth(12.0);
  529. return stroker.createStroke(path);
  530. }
  531. void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
  532. {
  533. painter->setPen(ladderPen(active_));
  534. painter->drawLine(QPointF(0.0, 0.0), QPointF(0.0, height_));
  535. }
  536. private:
  537. qreal height_ = 0.0;
  538. bool active_ = false;
  539. };
  540. class SelectionOverlayItem final : public QGraphicsItem
  541. {
  542. public:
  543. explicit SelectionOverlayItem(std::vector<QRectF> rectangles)
  544. : rectangles_(std::move(rectangles))
  545. {
  546. for (const QRectF &rectangle : rectangles_)
  547. {
  548. bounds_ = bounds_.isNull() ? rectangle : bounds_.united(rectangle);
  549. }
  550. bounds_.adjust(-2.0, -2.0, 2.0, 2.0);
  551. setZValue(100.0);
  552. }
  553. QRectF boundingRect() const override
  554. {
  555. return bounds_;
  556. }
  557. void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
  558. {
  559. painter->setRenderHint(QPainter::Antialiasing, false);
  560. QPen pen(kSelectionBorder, 1.4, Qt::DashLine);
  561. pen.setCosmetic(true);
  562. painter->setPen(pen);
  563. painter->setBrush(QColor(
  564. kSelectionFill.red(),
  565. kSelectionFill.green(),
  566. kSelectionFill.blue(),
  567. 90));
  568. for (const QRectF &rectangle : rectangles_)
  569. {
  570. painter->drawRect(rectangle.adjusted(2.5, 2.5, -2.5, -2.5));
  571. }
  572. }
  573. private:
  574. std::vector<QRectF> rectangles_;
  575. QRectF bounds_;
  576. };
  577. } // namespace
  578. LogicEditorWidget::LogicEditorWidget(
  579. LogicEditorService &editor_service,
  580. QWidget *parent)
  581. : QGraphicsView(parent),
  582. editor_service_(editor_service),
  583. command_service_(editor_service),
  584. scene_(new QGraphicsScene(this))
  585. {
  586. setScene(scene_);
  587. setRenderHint(QPainter::Antialiasing, true);
  588. setBackgroundBrush(kCanvasBackground);
  589. setDragMode(QGraphicsView::NoDrag);
  590. setAlignment(Qt::AlignLeft | Qt::AlignTop);
  591. selection_band_ = new QRubberBand(QRubberBand::Rectangle, viewport());
  592. selection_band_->hide();
  593. command_editor_ = new QLineEdit(viewport());
  594. command_editor_->setObjectName(QStringLiteral("logicCommandInput"));
  595. command_editor_->setPlaceholderText(
  596. tr("输入 PLC 指令,例如 LD M0、AND M1、OUT M10"));
  597. command_editor_->setClearButtonEnabled(true);
  598. command_editor_->setVisible(false);
  599. command_editor_->setToolTip(
  600. tr("触点使用 M 地址,比较和数据指令使用 D 地址"));
  601. command_completer_ = new QCompleter(command_editor_);
  602. auto *command_model = new QStandardItemModel(0, 3, command_completer_);
  603. command_model->setHeaderData(0, Qt::Horizontal, tr("指令"));
  604. command_model->setHeaderData(1, Qt::Horizontal, tr("操作数"));
  605. command_model->setHeaderData(2, Qt::Horizontal, tr("说明"));
  606. for (const LogicCommandSuggestion &suggestion
  607. : LogicCommandService::suggestions())
  608. {
  609. QList<QStandardItem *> row{
  610. new QStandardItem(QString::fromStdString(suggestion.mnemonic)),
  611. new QStandardItem(QString::fromStdString(suggestion.operand_hint)),
  612. new QStandardItem(QString::fromStdString(suggestion.description))};
  613. for (QStandardItem *item : row)
  614. {
  615. item->setEnabled(suggestion.supported);
  616. item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
  617. }
  618. command_model->appendRow(row);
  619. }
  620. command_completer_->setModel(command_model);
  621. command_completer_->setCompletionColumn(0);
  622. command_completer_->setCaseSensitivity(Qt::CaseInsensitive);
  623. command_completer_->setCompletionMode(QCompleter::PopupCompletion);
  624. auto *command_popup = new QTreeView;
  625. command_popup->setRootIsDecorated(false);
  626. command_popup->setItemsExpandable(false);
  627. command_popup->setHeaderHidden(false);
  628. command_popup->setUniformRowHeights(true);
  629. command_popup->setAlternatingRowColors(true);
  630. command_popup->setMinimumSize(620, 300);
  631. command_completer_->setPopup(command_popup);
  632. command_popup->header()->setStretchLastSection(true);
  633. command_popup->setColumnWidth(0, 96);
  634. command_popup->setColumnWidth(1, 190);
  635. command_editor_->setCompleter(command_completer_);
  636. connect(
  637. command_editor_, &QLineEdit::textEdited,
  638. this,
  639. [this](const QString &text)
  640. {
  641. command_completer_->setCompletionPrefix(
  642. commandCompletionPrefix(text));
  643. command_completer_->complete(command_editor_->rect());
  644. });
  645. connect(
  646. command_editor_, &QLineEdit::returnPressed,
  647. this, &LogicEditorWidget::commitCommandInput);
  648. command_editor_->installEventFilter(this);
  649. }
  650. void LogicEditorWidget::setLogicId(const std::string &logic_id)
  651. {
  652. const bool logic_changed = logic_id_ != logic_id;
  653. logic_id_ = logic_id;
  654. if (logic_changed)
  655. {
  656. cancelCommandInput();
  657. selected_rung_id_.clear();
  658. selected_node_ids_.clear();
  659. selected_cells_.clear();
  660. selected_output_rung_ids_.clear();
  661. selected_vertical_connection_ids_.clear();
  662. selected_row_ids_.clear();
  663. selected_column_ = -1;
  664. selected_cell_ = false;
  665. selected_output_ = false;
  666. selected_boundary_ = false;
  667. selected_vertical_connection_id_.clear();
  668. }
  669. else if (!selected_rung_id_.empty()
  670. && editor_service_.findRung(logic_id_, selected_rung_id_) == nullptr)
  671. {
  672. selected_rung_id_.clear();
  673. selected_node_ids_.clear();
  674. selected_cells_.clear();
  675. selected_output_rung_ids_.clear();
  676. selected_vertical_connection_ids_.clear();
  677. selected_row_ids_.clear();
  678. selected_column_ = -1;
  679. selected_cell_ = false;
  680. selected_output_ = false;
  681. selected_boundary_ = false;
  682. selected_vertical_connection_id_.clear();
  683. }
  684. else
  685. {
  686. if (!selected_vertical_connection_id_.empty()
  687. && editor_service_.findConnection(
  688. logic_id_, selected_vertical_connection_id_) == nullptr)
  689. {
  690. selected_vertical_connection_id_.clear();
  691. selected_column_ = -1;
  692. selected_cell_ = false;
  693. selected_output_ = false;
  694. selected_boundary_ = false;
  695. }
  696. selected_cells_.erase(
  697. std::remove_if(
  698. selected_cells_.begin(),
  699. selected_cells_.end(),
  700. [this](const auto &position)
  701. {
  702. const LadderCell *cell = editor_service_.findCell(
  703. logic_id_, position.first, position.second);
  704. return cell == nullptr || cell->kind == LadderCellKind::Gap;
  705. }),
  706. selected_cells_.end());
  707. selected_output_rung_ids_.erase(
  708. std::remove_if(
  709. selected_output_rung_ids_.begin(),
  710. selected_output_rung_ids_.end(),
  711. [this](const std::string &rung_id)
  712. {
  713. const LadderRung *rung = editor_service_.findRung(
  714. logic_id_, rung_id);
  715. return rung == nullptr || !rung->output.has_value();
  716. }),
  717. selected_output_rung_ids_.end());
  718. selected_vertical_connection_ids_.erase(
  719. std::remove_if(
  720. selected_vertical_connection_ids_.begin(),
  721. selected_vertical_connection_ids_.end(),
  722. [this](const std::string &connection_id)
  723. {
  724. return editor_service_.findConnection(
  725. logic_id_, connection_id) == nullptr;
  726. }),
  727. selected_vertical_connection_ids_.end());
  728. selected_row_ids_.erase(
  729. std::remove_if(
  730. selected_row_ids_.begin(),
  731. selected_row_ids_.end(),
  732. [this](const std::string &rung_id)
  733. {
  734. return editor_service_.findRung(logic_id_, rung_id) == nullptr;
  735. }),
  736. selected_row_ids_.end());
  737. synchronizeSelectedNodes();
  738. selected_vertical_connection_id_ =
  739. selected_vertical_connection_ids_.empty()
  740. ? std::string{} : selected_vertical_connection_ids_.back();
  741. if (selected_cell_
  742. && editor_service_.findCell(
  743. logic_id_, selected_rung_id_, selected_column_)
  744. == nullptr)
  745. {
  746. selected_column_ = -1;
  747. selected_cell_ = false;
  748. }
  749. }
  750. rebuildScene();
  751. }
  752. void LogicEditorWidget::setEditingEnabled(bool enabled)
  753. {
  754. editing_enabled_ = enabled;
  755. if (!enabled)
  756. {
  757. cancelCommandInput();
  758. mouse_wire_mode_ = MouseWireMode::Select;
  759. selection_pressed_ = false;
  760. selection_dragging_ = false;
  761. selection_band_->hide();
  762. }
  763. rebuildScene();
  764. }
  765. void LogicEditorWidget::setMouseWireMode(MouseWireMode mode)
  766. {
  767. mouse_wire_mode_ = editing_enabled_ ? mode : MouseWireMode::Select;
  768. selection_pressed_ = false;
  769. selection_dragging_ = false;
  770. selection_band_->hide();
  771. viewport()->setCursor(mouse_wire_mode_ == MouseWireMode::Select
  772. ? Qt::ArrowCursor : Qt::CrossCursor);
  773. }
  774. LogicEditorWidget::MouseWireMode LogicEditorWidget::mouseWireMode() const
  775. {
  776. return mouse_wire_mode_;
  777. }
  778. void LogicEditorWidget::setRuntimeTrace(
  779. const LogicTraceSnapshot &trace,
  780. const std::string &fault_node_id)
  781. {
  782. trace_ = trace.forLogic(logic_id_);
  783. fault_node_id_ = fault_node_id;
  784. runtime_trace_enabled_ = true;
  785. rebuildScene();
  786. }
  787. void LogicEditorWidget::clearRuntimeTrace()
  788. {
  789. trace_.clear();
  790. fault_node_id_.clear();
  791. runtime_trace_enabled_ = false;
  792. rebuildScene();
  793. }
  794. bool LogicEditorWidget::runtimeTraceEnabled() const
  795. {
  796. return runtime_trace_enabled_;
  797. }
  798. void LogicEditorWidget::reloadLogic()
  799. {
  800. setLogicId(logic_id_);
  801. }
  802. void LogicEditorWidget::clearSelection()
  803. {
  804. selected_rung_id_.clear();
  805. selected_node_ids_.clear();
  806. selected_cells_.clear();
  807. selected_output_rung_ids_.clear();
  808. selected_vertical_connection_ids_.clear();
  809. selected_row_ids_.clear();
  810. selected_column_ = -1;
  811. selected_cell_ = false;
  812. selected_output_ = false;
  813. selected_boundary_ = false;
  814. selected_vertical_connection_id_.clear();
  815. rebuildScene();
  816. emit nodeSelected(QString{});
  817. }
  818. void LogicEditorWidget::selectNode(const std::string &node_id)
  819. {
  820. selected_node_ids_.clear();
  821. selected_cells_.clear();
  822. selected_output_rung_ids_.clear();
  823. selected_vertical_connection_ids_.clear();
  824. selected_row_ids_.clear();
  825. selected_column_ = -1;
  826. selected_cell_ = false;
  827. selected_output_ = false;
  828. selected_boundary_ = false;
  829. selected_vertical_connection_id_.clear();
  830. if (!node_id.empty())
  831. {
  832. selected_node_ids_.push_back(node_id);
  833. const std::string rung_id = editor_service_.rungIdForNode(
  834. logic_id_, node_id);
  835. const LadderRung *rung = editor_service_.findRung(logic_id_, rung_id);
  836. if (rung != nullptr)
  837. {
  838. selected_rung_id_ = rung_id;
  839. for (std::size_t index = 0U; index < rung->cells.size(); ++index)
  840. {
  841. if (rung->cells[index].node.has_value()
  842. && rung->cells[index].node->id == node_id)
  843. {
  844. selected_column_ = static_cast<int>(index);
  845. selected_cell_ = true;
  846. selected_cells_.push_back({
  847. rung_id, static_cast<int>(index)});
  848. break;
  849. }
  850. }
  851. if (!selected_cell_ && rung->output.has_value()
  852. && rung->output->id == node_id)
  853. {
  854. selected_output_rung_ids_.push_back(rung_id);
  855. }
  856. }
  857. }
  858. rebuildScene();
  859. emit nodeSelected(QString::fromStdString(node_id));
  860. }
  861. void LogicEditorWidget::focusSyntaxLocation(
  862. const std::string &rung_id, int column)
  863. {
  864. const LadderRung *rung = editor_service_.findRung(logic_id_, rung_id);
  865. if (rung == nullptr)
  866. {
  867. return;
  868. }
  869. cancelCommandInput();
  870. selected_node_ids_.clear();
  871. selected_cells_.clear();
  872. selected_output_rung_ids_.clear();
  873. selected_vertical_connection_ids_.clear();
  874. selected_row_ids_.clear();
  875. selected_vertical_connection_id_.clear();
  876. selected_rung_id_ = rung_id;
  877. selected_boundary_ = false;
  878. const bool output = column >= ProjectLimits::kMaximumLadderColumns;
  879. selected_output_ = output;
  880. selected_cell_ = !output;
  881. selected_column_ = output
  882. ? ProjectLimits::kMaximumConditionColumns
  883. : std::max(0, std::min(
  884. column - 1, ProjectLimits::kMaximumConditionColumns - 1));
  885. if (output)
  886. {
  887. selected_output_rung_ids_.push_back(rung_id);
  888. if (rung->output.has_value())
  889. {
  890. selected_node_ids_.push_back(rung->output->id);
  891. }
  892. }
  893. else
  894. {
  895. const LadderCell &cell = rung->cells[
  896. static_cast<std::size_t>(selected_column_)];
  897. if (cell.kind != LadderCellKind::Gap)
  898. {
  899. selected_cells_.push_back({rung_id, selected_column_});
  900. }
  901. if (cell.node.has_value())
  902. {
  903. selected_node_ids_.push_back(cell.node->id);
  904. }
  905. }
  906. rebuildScene();
  907. notifySelectionChanged();
  908. const RowLayout *layout = layoutForRung(rung_id);
  909. if (layout != nullptr)
  910. {
  911. const qreal x = output
  912. ? kConditionRight
  913. : kLeftBus + static_cast<qreal>(selected_column_) * kCellWidth;
  914. ensureVisible(
  915. QRectF(
  916. x,
  917. layout->gridTop,
  918. output ? kOutputWidth : kCellWidth,
  919. kRowHeight),
  920. 24,
  921. 24);
  922. }
  923. setFocus(Qt::OtherFocusReason);
  924. }
  925. std::string LogicEditorWidget::selectedNodeId() const
  926. {
  927. return selected_node_ids_.empty() ? std::string{} : selected_node_ids_.front();
  928. }
  929. std::vector<std::string> LogicEditorWidget::selectedNodeIds() const
  930. {
  931. return selected_node_ids_;
  932. }
  933. std::string LogicEditorWidget::selectedRungId() const
  934. {
  935. return !selected_rung_id_.empty()
  936. && editor_service_.findRung(logic_id_, selected_rung_id_) != nullptr
  937. ? selected_rung_id_ : std::string{};
  938. }
  939. bool LogicEditorWidget::hasCopyableSelection() const
  940. {
  941. return !selected_cells_.empty()
  942. || !selected_output_rung_ids_.empty()
  943. || !selected_vertical_connection_ids_.empty()
  944. || !selected_row_ids_.empty();
  945. }
  946. LogicClipboardCopyResult LogicEditorWidget::copySelection() const
  947. {
  948. LogicSelectionCopyRequest selection;
  949. selection.cells = selected_cells_;
  950. selection.outputRungIds = selected_output_rung_ids_;
  951. selection.verticalConnectionIds = selected_vertical_connection_ids_;
  952. selection.wholeRungIds = selected_row_ids_;
  953. return editor_service_.copySelection(logic_id_, selection);
  954. }
  955. LogicPasteTarget LogicEditorWidget::pasteTarget() const
  956. {
  957. LogicPasteTarget target;
  958. target.rungId = selected_rung_id_.empty()
  959. ? editor_service_.firstRungId(logic_id_)
  960. : selected_rung_id_;
  961. target.output = selected_output_;
  962. target.boundary = selected_boundary_;
  963. target.column = selected_output_
  964. ? ProjectLimits::kMaximumConditionColumns
  965. : std::max(selected_column_, 0);
  966. return target;
  967. }
  968. std::string LogicEditorWidget::currentRungId() const
  969. {
  970. if (!selected_rung_id_.empty()
  971. && editor_service_.findRung(logic_id_, selected_rung_id_) != nullptr)
  972. {
  973. return selected_rung_id_;
  974. }
  975. return editor_service_.firstRungId(logic_id_);
  976. }
  977. int LogicEditorWidget::rowAt(const std::string &rung_id) const
  978. {
  979. const ControlLogic *logic = editor_service_.findLogic(logic_id_);
  980. if (logic == nullptr)
  981. {
  982. return -1;
  983. }
  984. for (std::size_t index = 0U; index < logic->rungs.size(); ++index)
  985. {
  986. if (logic->rungs[index].id == rung_id)
  987. {
  988. return static_cast<int>(index);
  989. }
  990. }
  991. return -1;
  992. }
  993. void LogicEditorWidget::rebuildRowLayout(const ControlLogic &logic)
  994. {
  995. row_layouts_.clear();
  996. qreal next_top = kTop;
  997. for (std::size_t row = 0U; row < logic.rungs.size(); ++row)
  998. {
  999. bool connected_to_previous = false;
  1000. if (row > 0U)
  1001. {
  1002. const std::string &upper_id = logic.rungs[row - 1U].id;
  1003. const std::string &lower_id = logic.rungs[row].id;
  1004. connected_to_previous = std::any_of(
  1005. logic.verticalConnections.cbegin(),
  1006. logic.verticalConnections.cend(),
  1007. [&upper_id, &lower_id](const VerticalConnection &connection)
  1008. {
  1009. return connection.upperRungId == upper_id
  1010. && connection.lowerRungId == lower_id;
  1011. });
  1012. }
  1013. const bool network_head = row == 0U || !connected_to_previous;
  1014. if (network_head)
  1015. {
  1016. next_top += kCommentBandHeight;
  1017. }
  1018. RowLayout layout;
  1019. layout.rungId = logic.rungs[row].id;
  1020. layout.row = static_cast<int>(row);
  1021. layout.gridTop = next_top;
  1022. layout.centerY = next_top + kRowHeight / 2.0 + 6.0;
  1023. layout.bottom = next_top + kRowHeight;
  1024. layout.networkHead = network_head;
  1025. row_layouts_.push_back(std::move(layout));
  1026. next_top += kRowHeight;
  1027. }
  1028. }
  1029. const LogicEditorWidget::RowLayout *LogicEditorWidget::layoutForRung(
  1030. const std::string &rung_id) const
  1031. {
  1032. const auto found = std::find_if(
  1033. row_layouts_.cbegin(),
  1034. row_layouts_.cend(),
  1035. [&rung_id](const RowLayout &layout)
  1036. {
  1037. return layout.rungId == rung_id;
  1038. });
  1039. return found == row_layouts_.cend() ? nullptr : &*found;
  1040. }
  1041. LogicEditorWidget::Hit LogicEditorWidget::hitAt(
  1042. const QPointF &scene_position) const
  1043. {
  1044. const QList<QGraphicsItem *> items = scene_->items(scene_position);
  1045. const auto hit_for_type = [&items](const QString &wanted) -> Hit
  1046. {
  1047. for (QGraphicsItem *item : items)
  1048. {
  1049. if (item->data(0).toString() != wanted)
  1050. {
  1051. continue;
  1052. }
  1053. Hit hit;
  1054. if (wanted == QStringLiteral("vertical"))
  1055. {
  1056. hit.rungId = item->data(2).toString().toStdString();
  1057. hit.column = item->data(3).toInt();
  1058. hit.vertical = true;
  1059. hit.lowerRungId = item->data(4).toString().toStdString();
  1060. hit.objectId = item->data(1).toString().toStdString();
  1061. }
  1062. else if (wanted == QStringLiteral("boundary"))
  1063. {
  1064. hit.rungId = item->data(1).toString().toStdString();
  1065. hit.column = item->data(2).toInt();
  1066. hit.boundary = true;
  1067. }
  1068. else if (wanted == QStringLiteral("cell"))
  1069. {
  1070. hit.rungId = item->data(1).toString().toStdString();
  1071. hit.column = item->data(2).toInt();
  1072. hit.objectId = item->data(3).toString().toStdString();
  1073. hit.cellKind = static_cast<LadderCellKind>(item->data(4).toInt());
  1074. }
  1075. else if (wanted == QStringLiteral("output"))
  1076. {
  1077. hit.rungId = item->data(1).toString().toStdString();
  1078. hit.column = ProjectLimits::kMaximumConditionColumns;
  1079. hit.output = true;
  1080. hit.objectId = item->data(2).toString().toStdString();
  1081. }
  1082. else if (wanted == QStringLiteral("rowHeader"))
  1083. {
  1084. hit.rungId = item->data(1).toString().toStdString();
  1085. hit.rowHeader = true;
  1086. }
  1087. return hit;
  1088. }
  1089. return {};
  1090. };
  1091. Hit hit = hit_for_type(QStringLiteral("rowHeader"));
  1092. if (!hit.rungId.empty())
  1093. {
  1094. return hit;
  1095. }
  1096. hit = hit_for_type(QStringLiteral("vertical"));
  1097. if (!hit.rungId.empty())
  1098. {
  1099. return hit;
  1100. }
  1101. hit = hit_for_type(QStringLiteral("boundary"));
  1102. if (!hit.rungId.empty())
  1103. {
  1104. return hit;
  1105. }
  1106. hit = hit_for_type(QStringLiteral("cell"));
  1107. if (!hit.rungId.empty())
  1108. {
  1109. return hit;
  1110. }
  1111. hit = hit_for_type(QStringLiteral("output"));
  1112. if (!hit.rungId.empty())
  1113. {
  1114. return hit;
  1115. }
  1116. return {};
  1117. }
  1118. void LogicEditorWidget::rebuildScene()
  1119. {
  1120. scene_->clear();
  1121. row_layouts_.clear();
  1122. const ControlLogic *logic = editor_service_.findLogic(logic_id_);
  1123. if (logic == nullptr)
  1124. {
  1125. scene_->setSceneRect(0.0, 0.0, kRightBus + kSceneRightMargin, 120.0);
  1126. return;
  1127. }
  1128. rebuildRowLayout(*logic);
  1129. const qreal height = row_layouts_.empty()
  1130. ? 120.0 : row_layouts_.back().bottom + 24.0;
  1131. scene_->setSceneRect(
  1132. 0.0, 0.0, kRightBus + kSceneRightMargin, height);
  1133. if (row_layouts_.empty())
  1134. {
  1135. return;
  1136. }
  1137. std::vector<GridRow> grid_rows;
  1138. grid_rows.reserve(logic->rungs.size());
  1139. for (std::size_t row = 0U; row < logic->rungs.size(); ++row)
  1140. {
  1141. const RowLayout &layout = row_layouts_[row];
  1142. GridRow grid_row;
  1143. grid_row.top = layout.gridTop;
  1144. grid_rows.push_back(std::move(grid_row));
  1145. }
  1146. scene_->addItem(new GridLayerItem(std::move(grid_rows)));
  1147. QPen bus_pen(kLadderColor);
  1148. bus_pen.setWidthF(2.2);
  1149. bus_pen.setCosmetic(true);
  1150. QGraphicsLineItem *left_bus = scene_->addLine(
  1151. kLeftBus,
  1152. row_layouts_.front().gridTop,
  1153. kLeftBus,
  1154. row_layouts_.back().bottom,
  1155. bus_pen);
  1156. left_bus->setZValue(5.0);
  1157. QGraphicsLineItem *right_bus = scene_->addLine(
  1158. kRightBus,
  1159. row_layouts_.front().gridTop,
  1160. kRightBus,
  1161. row_layouts_.back().bottom,
  1162. bus_pen);
  1163. right_bus->setZValue(5.0);
  1164. for (std::size_t row = 0U; row < logic->rungs.size(); ++row)
  1165. {
  1166. const LadderRung &rung = logic->rungs[row];
  1167. const RowLayout &layout = row_layouts_[row];
  1168. QGraphicsRectItem *row_header = scene_->addRect(
  1169. QRectF(0.0, layout.gridTop, kLeftBus - 4.0, kRowHeight),
  1170. QPen(Qt::NoPen),
  1171. QBrush(Qt::transparent));
  1172. row_header->setData(0, QStringLiteral("rowHeader"));
  1173. row_header->setData(1, QString::fromStdString(rung.id));
  1174. row_header->setZValue(29.0);
  1175. QGraphicsSimpleTextItem *label = scene_->addSimpleText(
  1176. QStringLiteral("%1")
  1177. .arg(static_cast<int>(row), 3, 10, QLatin1Char('0')));
  1178. label->setBrush(QColor(QStringLiteral("#66757d")));
  1179. label->setPos(10.0, layout.centerY - 10.0);
  1180. label->setZValue(6.0);
  1181. if (layout.networkHead && !rung.comment.empty())
  1182. {
  1183. QGraphicsSimpleTextItem *comment = scene_->addSimpleText(
  1184. QString::fromStdString(rung.comment));
  1185. QFont comment_font = comment->font();
  1186. comment_font.setPointSizeF(9.0);
  1187. comment->setFont(comment_font);
  1188. comment->setBrush(kCommentColor);
  1189. comment->setPos(
  1190. kLeftBus + 7.0,
  1191. layout.gridTop - kCommentBandHeight + 4.0);
  1192. comment->setZValue(6.0);
  1193. }
  1194. for (int column = 0;
  1195. column < ProjectLimits::kMaximumConditionColumns;
  1196. ++column)
  1197. {
  1198. const LadderCell &cell = rung.cells[static_cast<std::size_t>(column)];
  1199. const qreal x = kLeftBus + static_cast<qreal>(column) * kCellWidth;
  1200. const auto input_power = trace_.cellInputPowerValues.find(cell.id);
  1201. const bool input_active = runtime_trace_enabled_
  1202. && input_power != trace_.cellInputPowerValues.end()
  1203. && input_power->second;
  1204. const auto output_power = trace_.cellPowerValues.find(cell.id);
  1205. const bool output_active = runtime_trace_enabled_
  1206. && output_power != trace_.cellPowerValues.end()
  1207. && output_power->second;
  1208. const bool faulted = cell.node.has_value()
  1209. && cell.node->id == fault_node_id_;
  1210. scene_->addItem(new CellContentItem(
  1211. cell,
  1212. rung.id,
  1213. column,
  1214. QPointF(x, layout.gridTop),
  1215. input_active,
  1216. output_active,
  1217. faulted));
  1218. }
  1219. for (int boundary = 0;
  1220. boundary <= ProjectLimits::kMaximumConditionColumns;
  1221. ++boundary)
  1222. {
  1223. const qreal x = kLeftBus + static_cast<qreal>(boundary) * kCellWidth;
  1224. QPen boundary_pen(Qt::transparent);
  1225. boundary_pen.setWidthF(12.0);
  1226. QGraphicsLineItem *hit_line = scene_->addLine(
  1227. x,
  1228. layout.gridTop + 8.0,
  1229. x,
  1230. layout.bottom - 8.0,
  1231. boundary_pen);
  1232. hit_line->setData(0, QStringLiteral("boundary"));
  1233. hit_line->setData(1, QString::fromStdString(rung.id));
  1234. hit_line->setData(2, boundary);
  1235. hit_line->setZValue(30.0);
  1236. }
  1237. const auto rung_power = trace_.rungValues.find(rung.id);
  1238. const bool input_active = runtime_trace_enabled_
  1239. && rung_power != trace_.rungValues.end() && rung_power->second;
  1240. bool symbol_active = false;
  1241. if (rung.output.has_value())
  1242. {
  1243. const auto output_value = trace_.nodeValues.find(rung.output->id);
  1244. symbol_active = runtime_trace_enabled_
  1245. && output_value != trace_.nodeValues.end()
  1246. && output_value->second;
  1247. }
  1248. const bool faulted = rung.output.has_value()
  1249. && rung.output->id == fault_node_id_;
  1250. scene_->addItem(new OutputContentItem(
  1251. rung,
  1252. QPointF(kConditionRight, layout.gridTop),
  1253. input_active,
  1254. symbol_active,
  1255. faulted));
  1256. }
  1257. for (const VerticalConnection &connection : logic->verticalConnections)
  1258. {
  1259. const RowLayout *upper = layoutForRung(connection.upperRungId);
  1260. const RowLayout *lower = layoutForRung(connection.lowerRungId);
  1261. if (upper == nullptr || lower == nullptr)
  1262. {
  1263. continue;
  1264. }
  1265. const qreal x = kLeftBus
  1266. + static_cast<qreal>(connection.columnBoundary) * kCellWidth;
  1267. const auto power = trace_.verticalConnectionValues.find(connection.id);
  1268. const bool active = runtime_trace_enabled_
  1269. && power != trace_.verticalConnectionValues.end() && power->second;
  1270. scene_->addItem(new VerticalConnectionItem(
  1271. connection, x, upper->centerY, lower->centerY, active));
  1272. }
  1273. std::vector<QRectF> selection_rectangles;
  1274. for (const std::string &rung_id : selected_row_ids_)
  1275. {
  1276. const RowLayout *layout = layoutForRung(rung_id);
  1277. if (layout != nullptr)
  1278. {
  1279. selection_rectangles.emplace_back(
  1280. 0.0,
  1281. layout->gridTop,
  1282. kRightBus,
  1283. kRowHeight);
  1284. }
  1285. }
  1286. for (const auto &position : selected_cells_)
  1287. {
  1288. const RowLayout *layout = layoutForRung(position.first);
  1289. if (layout != nullptr && position.second >= 0
  1290. && position.second < ProjectLimits::kMaximumConditionColumns)
  1291. {
  1292. selection_rectangles.emplace_back(
  1293. kLeftBus + static_cast<qreal>(position.second) * kCellWidth,
  1294. layout->gridTop,
  1295. kCellWidth,
  1296. kRowHeight);
  1297. }
  1298. }
  1299. for (const std::string &rung_id : selected_output_rung_ids_)
  1300. {
  1301. const RowLayout *layout = layoutForRung(rung_id);
  1302. if (layout != nullptr)
  1303. {
  1304. selection_rectangles.emplace_back(
  1305. kConditionRight,
  1306. layout->gridTop,
  1307. kOutputWidth,
  1308. kRowHeight);
  1309. }
  1310. }
  1311. for (const std::string &connection_id : selected_vertical_connection_ids_)
  1312. {
  1313. const VerticalConnection *connection = editor_service_.findConnection(
  1314. logic_id_, connection_id);
  1315. if (connection == nullptr)
  1316. {
  1317. continue;
  1318. }
  1319. const RowLayout *upper = layoutForRung(connection->upperRungId);
  1320. const RowLayout *lower = layoutForRung(connection->lowerRungId);
  1321. if (upper == nullptr || lower == nullptr)
  1322. {
  1323. continue;
  1324. }
  1325. const qreal x = kLeftBus
  1326. + static_cast<qreal>(connection->columnBoundary) * kCellWidth;
  1327. selection_rectangles.emplace_back(
  1328. x - 7.0,
  1329. upper->centerY,
  1330. 14.0,
  1331. lower->centerY - upper->centerY);
  1332. }
  1333. if (selected_cell_)
  1334. {
  1335. const std::pair<std::string, int> cursor{
  1336. selected_rung_id_, selected_column_};
  1337. const LadderCell *cell = editor_service_.findCell(
  1338. logic_id_, cursor.first, cursor.second);
  1339. const bool no_object_selection = selected_cells_.empty()
  1340. && selected_output_rung_ids_.empty()
  1341. && selected_vertical_connection_ids_.empty();
  1342. if (!containsCell(selected_cells_, cursor) && cell != nullptr
  1343. && (cell->kind == LadderCellKind::Gap || no_object_selection))
  1344. {
  1345. const RowLayout *layout = layoutForRung(cursor.first);
  1346. if (layout != nullptr)
  1347. {
  1348. selection_rectangles.emplace_back(
  1349. kLeftBus + static_cast<qreal>(cursor.second) * kCellWidth,
  1350. layout->gridTop,
  1351. kCellWidth,
  1352. kRowHeight);
  1353. }
  1354. }
  1355. }
  1356. if (selected_output_
  1357. && !containsId(selected_output_rung_ids_, selected_rung_id_))
  1358. {
  1359. const RowLayout *layout = layoutForRung(selected_rung_id_);
  1360. if (layout != nullptr)
  1361. {
  1362. selection_rectangles.emplace_back(
  1363. kConditionRight,
  1364. layout->gridTop,
  1365. kOutputWidth,
  1366. kRowHeight);
  1367. }
  1368. }
  1369. if (!selection_rectangles.empty())
  1370. {
  1371. scene_->addItem(new SelectionOverlayItem(
  1372. std::move(selection_rectangles)));
  1373. }
  1374. }
  1375. void LogicEditorWidget::clearObjectSelection()
  1376. {
  1377. selected_node_ids_.clear();
  1378. selected_cells_.clear();
  1379. selected_output_rung_ids_.clear();
  1380. selected_vertical_connection_ids_.clear();
  1381. selected_row_ids_.clear();
  1382. selected_vertical_connection_id_.clear();
  1383. }
  1384. void LogicEditorWidget::synchronizeSelectedNodes()
  1385. {
  1386. selected_node_ids_.clear();
  1387. for (const auto &position : selected_cells_)
  1388. {
  1389. const LadderCell *cell = editor_service_.findCell(
  1390. logic_id_, position.first, position.second);
  1391. if (cell != nullptr && cell->node.has_value())
  1392. {
  1393. selected_node_ids_.push_back(cell->node->id);
  1394. }
  1395. }
  1396. for (const std::string &rung_id : selected_output_rung_ids_)
  1397. {
  1398. const LadderRung *rung = editor_service_.findRung(logic_id_, rung_id);
  1399. if (rung != nullptr && rung->output.has_value())
  1400. {
  1401. selected_node_ids_.push_back(rung->output->id);
  1402. }
  1403. }
  1404. }
  1405. void LogicEditorWidget::notifySelectionChanged()
  1406. {
  1407. emit nodeSelected(selected_node_ids_.empty()
  1408. ? QString{} : QString::fromStdString(selected_node_ids_.front()));
  1409. }
  1410. void LogicEditorWidget::selectObject(
  1411. const Hit &hit, bool extend_node_selection)
  1412. {
  1413. if (hit.rowHeader)
  1414. {
  1415. if (!extend_node_selection)
  1416. {
  1417. clearObjectSelection();
  1418. selected_row_ids_.clear();
  1419. }
  1420. else if (!selected_cells_.empty()
  1421. || !selected_output_rung_ids_.empty()
  1422. || !selected_vertical_connection_ids_.empty())
  1423. {
  1424. clearObjectSelection();
  1425. }
  1426. const auto selected = std::find(
  1427. selected_row_ids_.begin(), selected_row_ids_.end(), hit.rungId);
  1428. if (extend_node_selection && selected != selected_row_ids_.end())
  1429. {
  1430. selected_row_ids_.erase(selected);
  1431. }
  1432. else if (selected == selected_row_ids_.end())
  1433. {
  1434. selected_row_ids_.push_back(hit.rungId);
  1435. }
  1436. std::sort(
  1437. selected_row_ids_.begin(), selected_row_ids_.end(),
  1438. [this](const std::string &left, const std::string &right)
  1439. {
  1440. return rowAt(left) < rowAt(right);
  1441. });
  1442. selected_rung_id_ = hit.rungId;
  1443. selected_column_ = -1;
  1444. selected_cell_ = false;
  1445. selected_output_ = false;
  1446. selected_boundary_ = false;
  1447. synchronizeSelectedNodes();
  1448. rebuildScene();
  1449. notifySelectionChanged();
  1450. return;
  1451. }
  1452. selected_row_ids_.clear();
  1453. if (!extend_node_selection)
  1454. {
  1455. clearObjectSelection();
  1456. }
  1457. if (hit.rungId.empty())
  1458. {
  1459. if (!extend_node_selection)
  1460. {
  1461. selected_rung_id_.clear();
  1462. selected_column_ = -1;
  1463. selected_cell_ = false;
  1464. selected_output_ = false;
  1465. selected_boundary_ = false;
  1466. }
  1467. rebuildScene();
  1468. notifySelectionChanged();
  1469. return;
  1470. }
  1471. selected_rung_id_ = hit.rungId;
  1472. selected_column_ = hit.column;
  1473. selected_cell_ = hit.column >= 0 && !hit.boundary
  1474. && !hit.output && !hit.vertical;
  1475. selected_output_ = hit.output;
  1476. selected_boundary_ = hit.boundary || hit.vertical;
  1477. if (hit.vertical)
  1478. {
  1479. const auto selected = std::find(
  1480. selected_vertical_connection_ids_.begin(),
  1481. selected_vertical_connection_ids_.end(),
  1482. hit.objectId);
  1483. if (extend_node_selection
  1484. && selected != selected_vertical_connection_ids_.end())
  1485. {
  1486. selected_vertical_connection_ids_.erase(selected);
  1487. }
  1488. else if (selected == selected_vertical_connection_ids_.end())
  1489. {
  1490. selected_vertical_connection_ids_.push_back(hit.objectId);
  1491. }
  1492. }
  1493. else if (hit.output && !hit.objectId.empty())
  1494. {
  1495. const auto selected = std::find(
  1496. selected_output_rung_ids_.begin(),
  1497. selected_output_rung_ids_.end(),
  1498. hit.rungId);
  1499. if (extend_node_selection && selected != selected_output_rung_ids_.end())
  1500. {
  1501. selected_output_rung_ids_.erase(selected);
  1502. }
  1503. else if (selected == selected_output_rung_ids_.end())
  1504. {
  1505. selected_output_rung_ids_.push_back(hit.rungId);
  1506. }
  1507. }
  1508. else if (!hit.boundary && !hit.vertical
  1509. && hit.cellKind != LadderCellKind::Gap)
  1510. {
  1511. const std::pair<std::string, int> position{hit.rungId, hit.column};
  1512. const auto selected = std::find(
  1513. selected_cells_.begin(), selected_cells_.end(), position);
  1514. if (extend_node_selection && selected != selected_cells_.end())
  1515. {
  1516. selected_cells_.erase(selected);
  1517. }
  1518. else if (selected == selected_cells_.end())
  1519. {
  1520. selected_cells_.push_back(position);
  1521. }
  1522. }
  1523. selected_vertical_connection_id_ =
  1524. selected_vertical_connection_ids_.empty()
  1525. ? std::string{} : selected_vertical_connection_ids_.back();
  1526. synchronizeSelectedNodes();
  1527. rebuildScene();
  1528. notifySelectionChanged();
  1529. }
  1530. void LogicEditorWidget::selectObjectsInBand(
  1531. const QRect &viewport_rect, bool extend_selection)
  1532. {
  1533. if (!selected_row_ids_.empty())
  1534. {
  1535. selected_row_ids_.clear();
  1536. clearObjectSelection();
  1537. }
  1538. if (!extend_selection)
  1539. {
  1540. clearObjectSelection();
  1541. }
  1542. const QPolygonF scene_polygon = mapToScene(viewport_rect.normalized());
  1543. QPainterPath selection_path;
  1544. selection_path.addPolygon(scene_polygon);
  1545. selection_path.closeSubpath();
  1546. const QList<QGraphicsItem *> items = scene_->items(
  1547. selection_path,
  1548. Qt::IntersectsItemShape,
  1549. Qt::DescendingOrder);
  1550. for (QGraphicsItem *item : items)
  1551. {
  1552. const QString type = item->data(0).toString();
  1553. if (type == QStringLiteral("cell"))
  1554. {
  1555. const LadderCellKind kind = static_cast<LadderCellKind>(
  1556. item->data(4).toInt());
  1557. if (kind == LadderCellKind::Gap)
  1558. {
  1559. continue;
  1560. }
  1561. const std::pair<std::string, int> position{
  1562. item->data(1).toString().toStdString(),
  1563. item->data(2).toInt()};
  1564. if (!containsCell(selected_cells_, position))
  1565. {
  1566. selected_cells_.push_back(position);
  1567. }
  1568. }
  1569. else if (type == QStringLiteral("output")
  1570. && !item->data(2).toString().isEmpty())
  1571. {
  1572. const std::string rung_id = item->data(1).toString().toStdString();
  1573. if (!containsId(selected_output_rung_ids_, rung_id))
  1574. {
  1575. selected_output_rung_ids_.push_back(rung_id);
  1576. }
  1577. }
  1578. else if (type == QStringLiteral("vertical"))
  1579. {
  1580. const std::string connection_id =
  1581. item->data(1).toString().toStdString();
  1582. if (!containsId(selected_vertical_connection_ids_, connection_id))
  1583. {
  1584. selected_vertical_connection_ids_.push_back(connection_id);
  1585. }
  1586. }
  1587. }
  1588. std::sort(
  1589. selected_cells_.begin(),
  1590. selected_cells_.end(),
  1591. [this](const auto &left, const auto &right)
  1592. {
  1593. const int left_row = rowAt(left.first);
  1594. const int right_row = rowAt(right.first);
  1595. return left_row != right_row
  1596. ? left_row < right_row : left.second < right.second;
  1597. });
  1598. std::sort(
  1599. selected_output_rung_ids_.begin(),
  1600. selected_output_rung_ids_.end(),
  1601. [this](const std::string &left, const std::string &right)
  1602. {
  1603. return rowAt(left) < rowAt(right);
  1604. });
  1605. const Hit center_hit = hitAt(mapToScene(viewport_rect.center()));
  1606. if (!center_hit.rungId.empty())
  1607. {
  1608. selected_rung_id_ = center_hit.rungId;
  1609. selected_column_ = center_hit.column;
  1610. selected_cell_ = center_hit.column >= 0 && !center_hit.output
  1611. && !center_hit.vertical && !center_hit.boundary
  1612. && !center_hit.rowHeader;
  1613. selected_output_ = center_hit.output;
  1614. selected_boundary_ = center_hit.boundary || center_hit.vertical;
  1615. }
  1616. selected_vertical_connection_id_ =
  1617. selected_vertical_connection_ids_.empty()
  1618. ? std::string{} : selected_vertical_connection_ids_.back();
  1619. synchronizeSelectedNodes();
  1620. rebuildScene();
  1621. notifySelectionChanged();
  1622. }
  1623. void LogicEditorWidget::beginGesture(const Hit &hit)
  1624. {
  1625. if (!editing_enabled_ || hit.rungId.empty() || hit.output)
  1626. {
  1627. return;
  1628. }
  1629. gesture_active_ = true;
  1630. gesture_origin_ = hit;
  1631. gesture_current_ = hit;
  1632. }
  1633. void LogicEditorWidget::updateGesture(const Hit &hit)
  1634. {
  1635. if (gesture_active_ && !hit.rungId.empty())
  1636. {
  1637. gesture_current_ = hit;
  1638. }
  1639. }
  1640. void LogicEditorWidget::finishGesture(const Hit &hit)
  1641. {
  1642. if (!gesture_active_)
  1643. {
  1644. return;
  1645. }
  1646. gesture_active_ = false;
  1647. if (gesture_origin_.rungId.empty() || hit.rungId.empty())
  1648. {
  1649. return;
  1650. }
  1651. LogicEditorResult result;
  1652. const bool connected = mouse_wire_mode_ == MouseWireMode::Draw;
  1653. if ((gesture_origin_.vertical || gesture_origin_.boundary)
  1654. && (hit.vertical || hit.boundary))
  1655. {
  1656. if (!connected && gesture_origin_.vertical
  1657. && gesture_origin_.objectId == hit.objectId)
  1658. {
  1659. result = editor_service_.removeVerticalConnections(
  1660. logic_id_, {gesture_origin_.objectId});
  1661. }
  1662. else if (gesture_origin_.column != hit.column)
  1663. {
  1664. result = {false, LogicEditorError::InvalidOperation,
  1665. "竖线拖动必须保持在同一列边界", {}};
  1666. }
  1667. else
  1668. {
  1669. std::string first_rung = gesture_origin_.rungId;
  1670. std::string last_rung = hit.rungId;
  1671. if (last_rung.empty() && !gesture_origin_.lowerRungId.empty())
  1672. {
  1673. last_rung = gesture_origin_.lowerRungId;
  1674. }
  1675. result = editor_service_.setVerticalConnectionRange(
  1676. logic_id_, first_rung, last_rung,
  1677. gesture_origin_.column, connected);
  1678. }
  1679. }
  1680. else if (gesture_origin_.vertical || hit.vertical)
  1681. {
  1682. result = {false, LogicEditorError::InvalidOperation,
  1683. "请从网格边界开始竖向拖动", {}};
  1684. }
  1685. else if (gesture_origin_.column == hit.column
  1686. && gesture_origin_.rungId != hit.rungId)
  1687. {
  1688. result = editor_service_.setVerticalConnectionRange(
  1689. logic_id_, gesture_origin_.rungId, hit.rungId,
  1690. gesture_origin_.column, connected);
  1691. }
  1692. else if (gesture_origin_.rungId == hit.rungId)
  1693. {
  1694. result = editor_service_.setHorizontalWireRange(
  1695. logic_id_, gesture_origin_.rungId,
  1696. gesture_origin_.column, hit.column, connected);
  1697. }
  1698. else
  1699. {
  1700. result = {false, LogicEditorError::InvalidOperation,
  1701. "画线必须沿同一行或同一列边界进行", {}};
  1702. }
  1703. if (!result.succeeded)
  1704. {
  1705. reportFailure(result);
  1706. }
  1707. else
  1708. {
  1709. clearObjectSelection();
  1710. selected_output_ = false;
  1711. selected_boundary_ = false;
  1712. rebuildScene();
  1713. notifySelectionChanged();
  1714. emit graphChanged();
  1715. }
  1716. }
  1717. void LogicEditorWidget::showCommandEditor(const Hit &hit)
  1718. {
  1719. if (!editing_enabled_ || hit.rungId.empty()
  1720. || hit.boundary || hit.vertical)
  1721. {
  1722. return;
  1723. }
  1724. const std::vector<std::string> parallel_node_ids = selectedNodeIds();
  1725. cancelCommandInput();
  1726. command_target_.rungId = hit.rungId;
  1727. command_target_.column = hit.column;
  1728. const LogicNode *existing = hit.objectId.empty()
  1729. ? nullptr : editor_service_.findNode(logic_id_, hit.objectId);
  1730. if (existing != nullptr)
  1731. {
  1732. command_target_.kind = LogicCommandTargetKind::ExistingNode;
  1733. }
  1734. else if (hit.output)
  1735. {
  1736. command_target_.kind = LogicCommandTargetKind::Output;
  1737. }
  1738. else
  1739. {
  1740. const LadderCell *cell = editor_service_.findCell(
  1741. logic_id_, hit.rungId, hit.column);
  1742. command_target_.kind = cell != nullptr
  1743. && cell->kind == LadderCellKind::Wire
  1744. ? LogicCommandTargetKind::WireColumn
  1745. : LogicCommandTargetKind::GapColumn;
  1746. }
  1747. command_target_.expressionId = hit.objectId;
  1748. command_parallel_node_ids_ = parallel_node_ids;
  1749. QString initial_text;
  1750. if (existing != nullptr && existing->isConfigured())
  1751. {
  1752. initial_text = logicCommandText(existing->config);
  1753. }
  1754. command_editor_->setStyleSheet(QString{});
  1755. command_editor_->setToolTip(
  1756. tr("触点使用 M 地址,比较和数据指令使用 D 地址"));
  1757. command_editor_->setText(initial_text);
  1758. command_editor_->setVisible(true);
  1759. QPointF center;
  1760. if (!findCommandTargetCenter(command_target_, &center))
  1761. {
  1762. cancelCommandInput();
  1763. return;
  1764. }
  1765. positionCommandInput(center);
  1766. command_editor_->raise();
  1767. command_editor_->setFocus(Qt::MouseFocusReason);
  1768. command_editor_->selectAll();
  1769. }
  1770. void LogicEditorWidget::commitCommandInput()
  1771. {
  1772. if (command_editor_ == nullptr || !command_editor_->isVisible())
  1773. {
  1774. return;
  1775. }
  1776. LogicCommandRequest request;
  1777. request.logicId = logic_id_;
  1778. request.text = command_editor_->text().trimmed().toStdString();
  1779. request.target = command_target_;
  1780. request.parallelNodeIds = command_parallel_node_ids_;
  1781. const LogicCommandResult result = command_service_.execute(request);
  1782. if (!result.succeeded)
  1783. {
  1784. command_editor_->setStyleSheet(
  1785. QStringLiteral("QLineEdit { border: 2px solid #c5362e; }"));
  1786. command_editor_->setToolTip(QString::fromStdString(result.message));
  1787. command_editor_->setFocus(Qt::OtherFocusReason);
  1788. command_editor_->selectAll();
  1789. emit editorError(QString::fromStdString(result.message));
  1790. return;
  1791. }
  1792. command_editor_->setStyleSheet(QString{});
  1793. command_editor_->setToolTip(
  1794. tr("触点使用 M 地址,比较和数据指令使用 D 地址"));
  1795. command_parallel_node_ids_.clear();
  1796. if (!result.hasNextCursor)
  1797. {
  1798. selectNode(result.id);
  1799. emit graphChanged();
  1800. cancelCommandInput();
  1801. return;
  1802. }
  1803. moveToCursor(result.nextCursor);
  1804. emit graphChanged();
  1805. command_target_ = commandTargetForCursor(result.nextCursor);
  1806. QPointF center;
  1807. if (command_target_.rungId.empty()
  1808. || !findCommandTargetCenter(command_target_, &center))
  1809. {
  1810. cancelCommandInput();
  1811. return;
  1812. }
  1813. command_editor_->clear();
  1814. positionCommandInput(center);
  1815. command_editor_->raise();
  1816. command_editor_->setFocus(Qt::OtherFocusReason);
  1817. }
  1818. void LogicEditorWidget::cancelCommandInput()
  1819. {
  1820. if (command_editor_ != nullptr)
  1821. {
  1822. command_editor_->clear();
  1823. command_editor_->setStyleSheet(QString{});
  1824. command_editor_->setVisible(false);
  1825. }
  1826. command_target_ = {};
  1827. command_parallel_node_ids_.clear();
  1828. }
  1829. void LogicEditorWidget::positionCommandInput(const QPointF &scene_center)
  1830. {
  1831. if (command_editor_ == nullptr)
  1832. {
  1833. return;
  1834. }
  1835. const QPoint view_center = mapFromScene(scene_center);
  1836. const int width = 360;
  1837. const int height = 38;
  1838. const QRect bounds = viewport()->rect().adjusted(4, 4, -4, -4);
  1839. const int left = std::clamp(
  1840. view_center.x() - width / 2,
  1841. bounds.left(),
  1842. std::max(bounds.left(), bounds.right() - width + 1));
  1843. const int top = std::clamp(
  1844. view_center.y() - height / 2,
  1845. bounds.top(),
  1846. std::max(bounds.top(), bounds.bottom() - height + 1));
  1847. command_editor_->setGeometry(left, top, width, height);
  1848. }
  1849. bool LogicEditorWidget::findCommandTargetCenter(
  1850. const LogicCommandTarget &target,
  1851. QPointF *scene_center) const
  1852. {
  1853. if (scene_center == nullptr)
  1854. {
  1855. return false;
  1856. }
  1857. const RowLayout *layout = layoutForRung(target.rungId);
  1858. if (layout == nullptr)
  1859. {
  1860. return false;
  1861. }
  1862. if (target.kind == LogicCommandTargetKind::Output)
  1863. {
  1864. *scene_center = QPointF(
  1865. kConditionRight + kOutputWidth / 2.0,
  1866. layout->centerY);
  1867. return true;
  1868. }
  1869. int column = target.column;
  1870. if (target.kind == LogicCommandTargetKind::ExistingNode)
  1871. {
  1872. const LadderRung *rung = editor_service_.findRung(
  1873. logic_id_, target.rungId);
  1874. if (rung == nullptr)
  1875. {
  1876. return false;
  1877. }
  1878. if (rung->output.has_value()
  1879. && rung->output->id == target.expressionId)
  1880. {
  1881. *scene_center = QPointF(
  1882. kConditionRight + kOutputWidth / 2.0,
  1883. layout->centerY);
  1884. return true;
  1885. }
  1886. const auto found = std::find_if(
  1887. rung->cells.cbegin(), rung->cells.cend(),
  1888. [&target](const LadderCell &cell)
  1889. {
  1890. return cell.node.has_value()
  1891. && cell.node->id == target.expressionId;
  1892. });
  1893. if (found == rung->cells.cend())
  1894. {
  1895. return false;
  1896. }
  1897. column = static_cast<int>(std::distance(rung->cells.cbegin(), found));
  1898. }
  1899. if (column < 0 || column >= ProjectLimits::kMaximumConditionColumns)
  1900. {
  1901. return false;
  1902. }
  1903. *scene_center = QPointF(
  1904. kLeftBus + (static_cast<qreal>(column) + 0.5) * kCellWidth,
  1905. layout->centerY);
  1906. return true;
  1907. }
  1908. LogicCommandTarget LogicEditorWidget::commandTargetForCursor(
  1909. const LogicEditCursor &cursor) const
  1910. {
  1911. LogicCommandTarget target;
  1912. target.rungId = cursor.rungId;
  1913. target.column = cursor.column;
  1914. if (cursor.output)
  1915. {
  1916. target.kind = LogicCommandTargetKind::Output;
  1917. return target;
  1918. }
  1919. const LadderCell *cell = editor_service_.findCell(
  1920. logic_id_, cursor.rungId, cursor.column);
  1921. if (cell != nullptr && cell->node.has_value())
  1922. {
  1923. target.kind = LogicCommandTargetKind::ExistingNode;
  1924. target.expressionId = cell->node->id;
  1925. }
  1926. else
  1927. {
  1928. target.kind = cell != nullptr && cell->kind == LadderCellKind::Wire
  1929. ? LogicCommandTargetKind::WireColumn
  1930. : LogicCommandTargetKind::GapColumn;
  1931. }
  1932. return target;
  1933. }
  1934. LogicEditCursor LogicEditorWidget::conditionInsertionCursor() const
  1935. {
  1936. const ControlLogic *logic = editor_service_.findLogic(logic_id_);
  1937. if (logic == nullptr || logic->rungs.empty())
  1938. {
  1939. return {};
  1940. }
  1941. const std::string rung_id = currentRungId();
  1942. if (selected_output_)
  1943. {
  1944. return {rung_id, ProjectLimits::kMaximumConditionColumns, true};
  1945. }
  1946. if (selected_cell_ && selected_column_ >= 0)
  1947. {
  1948. int target_column = selected_column_;
  1949. const LadderCell *cell = editor_service_.findCell(
  1950. logic_id_, rung_id, selected_column_);
  1951. if (cell != nullptr && cell->kind == LadderCellKind::Node)
  1952. {
  1953. ++target_column;
  1954. }
  1955. return {
  1956. rung_id,
  1957. std::min(target_column, ProjectLimits::kMaximumConditionColumns),
  1958. target_column >= ProjectLimits::kMaximumConditionColumns};
  1959. }
  1960. const LadderRung *rung = editor_service_.findRung(logic_id_, rung_id);
  1961. if (rung != nullptr)
  1962. {
  1963. const auto empty = std::find_if(
  1964. rung->cells.cbegin(), rung->cells.cend(),
  1965. [](const LadderCell &cell)
  1966. {
  1967. return cell.kind == LadderCellKind::Gap;
  1968. });
  1969. if (empty != rung->cells.cend())
  1970. {
  1971. return {
  1972. rung_id,
  1973. static_cast<int>(std::distance(rung->cells.cbegin(), empty)),
  1974. false};
  1975. }
  1976. }
  1977. return {rung_id, ProjectLimits::kMaximumConditionColumns, true};
  1978. }
  1979. void LogicEditorWidget::moveToCursor(const LogicEditCursor &cursor)
  1980. {
  1981. clearObjectSelection();
  1982. selected_rung_id_ = cursor.rungId;
  1983. selected_column_ = cursor.column;
  1984. selected_cell_ = !cursor.output;
  1985. selected_output_ = cursor.output;
  1986. selected_boundary_ = false;
  1987. rebuildScene();
  1988. notifySelectionChanged();
  1989. const LogicCommandTarget target = commandTargetForCursor(cursor);
  1990. QPointF center;
  1991. if (findCommandTargetCenter(target, &center))
  1992. {
  1993. ensureVisible(
  1994. QRectF(center.x() - kCellWidth / 2.0,
  1995. center.y() - kRowHeight / 2.0,
  1996. cursor.output ? kOutputWidth : kCellWidth,
  1997. kRowHeight),
  1998. 24,
  1999. 24);
  2000. }
  2001. }
  2002. LogicEditorResult LogicEditorWidget::finishCursorEdit(
  2003. const LogicEditResult &result)
  2004. {
  2005. if (!result.edit.succeeded)
  2006. {
  2007. reportFailure(result.edit);
  2008. return result.edit;
  2009. }
  2010. moveToCursor(result.nextCursor);
  2011. emit graphChanged();
  2012. return result.edit;
  2013. }
  2014. void LogicEditorWidget::reportFailure(const LogicEditorResult &result)
  2015. {
  2016. emit editorError(QString::fromStdString(result.message));
  2017. }
  2018. void LogicEditorWidget::mousePressEvent(QMouseEvent *event)
  2019. {
  2020. const Hit hit = hitAt(mapToScene(event->pos()));
  2021. setFocus(Qt::MouseFocusReason);
  2022. if (event->button() == Qt::LeftButton
  2023. && mouse_wire_mode_ != MouseWireMode::Select && editing_enabled_)
  2024. {
  2025. beginGesture(hit);
  2026. }
  2027. else if (event->button() == Qt::LeftButton
  2028. && mouse_wire_mode_ == MouseWireMode::Select)
  2029. {
  2030. selection_pressed_ = true;
  2031. selection_dragging_ = false;
  2032. selection_origin_ = event->pos();
  2033. selection_modifiers_ = event->modifiers();
  2034. selection_band_->setGeometry(QRect(selection_origin_, QSize{}));
  2035. selection_band_->hide();
  2036. }
  2037. else
  2038. {
  2039. QGraphicsView::mousePressEvent(event);
  2040. return;
  2041. }
  2042. event->accept();
  2043. }
  2044. void LogicEditorWidget::mouseMoveEvent(QMouseEvent *event)
  2045. {
  2046. if (mouse_wire_mode_ == MouseWireMode::Select && selection_pressed_)
  2047. {
  2048. if (!selection_dragging_
  2049. && (event->pos() - selection_origin_).manhattanLength()
  2050. >= QApplication::startDragDistance())
  2051. {
  2052. selection_dragging_ = true;
  2053. selection_band_->show();
  2054. }
  2055. if (selection_dragging_)
  2056. {
  2057. selection_band_->setGeometry(
  2058. QRect(selection_origin_, event->pos()).normalized());
  2059. }
  2060. }
  2061. else
  2062. {
  2063. updateGesture(hitAt(mapToScene(event->pos())));
  2064. }
  2065. event->accept();
  2066. }
  2067. void LogicEditorWidget::mouseReleaseEvent(QMouseEvent *event)
  2068. {
  2069. const Hit hit = hitAt(mapToScene(event->pos()));
  2070. if (event->button() == Qt::LeftButton
  2071. && mouse_wire_mode_ == MouseWireMode::Select && selection_pressed_)
  2072. {
  2073. const bool extend = selection_modifiers_.testFlag(Qt::ControlModifier);
  2074. selection_pressed_ = false;
  2075. selection_band_->hide();
  2076. if (selection_dragging_)
  2077. {
  2078. selectObjectsInBand(
  2079. QRect(selection_origin_, event->pos()).normalized(), extend);
  2080. }
  2081. else
  2082. {
  2083. selectObject(hit, extend);
  2084. }
  2085. selection_dragging_ = false;
  2086. }
  2087. else if (mouse_wire_mode_ != MouseWireMode::Select)
  2088. {
  2089. finishGesture(hit);
  2090. }
  2091. else
  2092. {
  2093. QGraphicsView::mouseReleaseEvent(event);
  2094. return;
  2095. }
  2096. event->accept();
  2097. }
  2098. void LogicEditorWidget::mouseDoubleClickEvent(QMouseEvent *event)
  2099. {
  2100. showCommandEditor(hitAt(mapToScene(event->pos())));
  2101. event->accept();
  2102. }
  2103. void LogicEditorWidget::resizeEvent(QResizeEvent *event)
  2104. {
  2105. QGraphicsView::resizeEvent(event);
  2106. if (command_editor_ != nullptr && command_editor_->isVisible())
  2107. {
  2108. QPointF center;
  2109. if (findCommandTargetCenter(command_target_, &center))
  2110. {
  2111. positionCommandInput(center);
  2112. }
  2113. }
  2114. }
  2115. void LogicEditorWidget::scrollContentsBy(int dx, int dy)
  2116. {
  2117. QGraphicsView::scrollContentsBy(dx, dy);
  2118. if (command_editor_ != nullptr && command_editor_->isVisible())
  2119. {
  2120. QPointF center;
  2121. if (findCommandTargetCenter(command_target_, &center))
  2122. {
  2123. positionCommandInput(center);
  2124. }
  2125. }
  2126. }
  2127. bool LogicEditorWidget::eventFilter(QObject *watched, QEvent *event)
  2128. {
  2129. if (watched == command_editor_ && event != nullptr)
  2130. {
  2131. if (event->type() == QEvent::KeyPress)
  2132. {
  2133. const auto *key_event = static_cast<QKeyEvent *>(event);
  2134. if (key_event->key() == Qt::Key_Escape)
  2135. {
  2136. cancelCommandInput();
  2137. return true;
  2138. }
  2139. }
  2140. else if (event->type() == QEvent::FocusOut)
  2141. {
  2142. QTimer::singleShot(
  2143. 0,
  2144. this,
  2145. [this]
  2146. {
  2147. if (command_editor_ == nullptr
  2148. || !command_editor_->isVisible()
  2149. || command_editor_->hasFocus())
  2150. {
  2151. return;
  2152. }
  2153. QWidget *focus = QApplication::focusWidget();
  2154. QWidget *popup = command_completer_ == nullptr
  2155. ? nullptr : command_completer_->popup();
  2156. if (popup != nullptr && popup->isVisible()
  2157. && focus != nullptr
  2158. && (focus == popup || popup->isAncestorOf(focus)))
  2159. {
  2160. return;
  2161. }
  2162. cancelCommandInput();
  2163. });
  2164. }
  2165. }
  2166. return QGraphicsView::eventFilter(watched, event);
  2167. }
  2168. LogicEditorResult LogicEditorWidget::addRung()
  2169. {
  2170. const LogicEditorResult result = editor_service_.addRung(logic_id_);
  2171. if (result.succeeded)
  2172. {
  2173. clearObjectSelection();
  2174. selected_rung_id_ = result.id;
  2175. selected_column_ = -1;
  2176. selected_cell_ = false;
  2177. selected_output_ = false;
  2178. selected_boundary_ = false;
  2179. rebuildScene();
  2180. notifySelectionChanged();
  2181. emit graphChanged();
  2182. }
  2183. else
  2184. {
  2185. reportFailure(result);
  2186. }
  2187. return result;
  2188. }
  2189. LogicEditorResult LogicEditorWidget::insertRung(bool after)
  2190. {
  2191. const std::string reference = selected_rung_id_;
  2192. const LogicEditorResult result = reference.empty()
  2193. ? editor_service_.addRung(logic_id_)
  2194. : editor_service_.insertRung(logic_id_, reference, after);
  2195. if (result.succeeded)
  2196. {
  2197. clearObjectSelection();
  2198. selected_rung_id_ = result.id;
  2199. selected_column_ = -1;
  2200. selected_cell_ = false;
  2201. selected_output_ = false;
  2202. selected_boundary_ = false;
  2203. rebuildScene();
  2204. notifySelectionChanged();
  2205. emit graphChanged();
  2206. }
  2207. else
  2208. {
  2209. reportFailure(result);
  2210. }
  2211. return result;
  2212. }
  2213. LogicEditorResult LogicEditorWidget::deleteRung()
  2214. {
  2215. const std::string rung_id = selected_rung_id_;
  2216. if (rung_id.empty())
  2217. {
  2218. return {false, LogicEditorError::RungNotFound, "请先选择要删除的行", {}};
  2219. }
  2220. const LogicEditorResult result = editor_service_.removeRung(logic_id_, rung_id);
  2221. if (result.succeeded)
  2222. {
  2223. clearObjectSelection();
  2224. selected_rung_id_.clear();
  2225. selected_column_ = -1;
  2226. selected_cell_ = false;
  2227. selected_output_ = false;
  2228. selected_boundary_ = false;
  2229. rebuildScene();
  2230. notifySelectionChanged();
  2231. emit graphChanged();
  2232. }
  2233. else
  2234. {
  2235. reportFailure(result);
  2236. }
  2237. return result;
  2238. }
  2239. LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config)
  2240. {
  2241. return finishCursorEdit(editor_service_.applyConditionAndAdvance(
  2242. logic_id_, conditionInsertionCursor(), config, false));
  2243. }
  2244. LogicEditorResult LogicEditorWidget::addParallelBranch(const LogicNodeConfig &config)
  2245. {
  2246. if (selected_node_ids_.empty() || selectedRungId().empty())
  2247. {
  2248. const LogicEditorResult result{
  2249. false,
  2250. LogicEditorError::InvalidOperation,
  2251. "请先选择同一行中要并联的连续条件节点",
  2252. {}};
  2253. reportFailure(result);
  2254. return result;
  2255. }
  2256. const LogicEditorResult result = editor_service_.addParallelBranch(
  2257. logic_id_, selectedRungId(), selected_node_ids_, config, false);
  2258. if (result.succeeded)
  2259. {
  2260. selectNode(result.id);
  2261. emit graphChanged();
  2262. }
  2263. else
  2264. {
  2265. reportFailure(result);
  2266. }
  2267. return result;
  2268. }
  2269. LogicEditorResult LogicEditorWidget::addHorizontalWire()
  2270. {
  2271. const ControlLogic *logic = editor_service_.findLogic(logic_id_);
  2272. if (logic == nullptr)
  2273. {
  2274. const LogicEditorResult result{
  2275. false, LogicEditorError::LogicNotFound, "未找到控制逻辑", {}};
  2276. reportFailure(result);
  2277. return result;
  2278. }
  2279. if (!logic->rungs.empty()
  2280. && (selectedRungId().empty() || !selected_cell_))
  2281. {
  2282. const LogicEditorResult result{
  2283. false,
  2284. LogicEditorError::InvalidOperation,
  2285. "请先选择一个条件网格,再插入横线",
  2286. {}};
  2287. reportFailure(result);
  2288. return result;
  2289. }
  2290. LogicEditCursor cursor = logic->rungs.empty()
  2291. ? LogicEditCursor{} : conditionInsertionCursor();
  2292. if (cursor.output)
  2293. {
  2294. const LogicEditorResult result{
  2295. false,
  2296. LogicEditorError::InvalidOperation,
  2297. "条件区已经填满,请在输出槽配置输出指令",
  2298. {}};
  2299. reportFailure(result);
  2300. return result;
  2301. }
  2302. return finishCursorEdit(editor_service_.applyWireAndAdvance(
  2303. logic_id_, cursor));
  2304. }
  2305. LogicEditorResult LogicEditorWidget::addVerticalWire()
  2306. {
  2307. const ControlLogic *logic = editor_service_.findLogic(logic_id_);
  2308. if (logic == nullptr || logic->rungs.size() < 2U)
  2309. {
  2310. return {false, LogicEditorError::InvalidOperation, "至少需要两行才能连接竖线", {}};
  2311. }
  2312. const std::string upper = selectedRungId();
  2313. if (upper.empty() || selected_column_ < 0
  2314. || selected_column_ > ProjectLimits::kMaximumConditionColumns)
  2315. {
  2316. return {false, LogicEditorError::InvalidOperation,
  2317. "请先选择一个列边界或网格,再插入竖线", {}};
  2318. }
  2319. const int index = rowAt(upper);
  2320. if (index < 0 || static_cast<std::size_t>(index + 1) >= logic->rungs.size())
  2321. {
  2322. return {false, LogicEditorError::InvalidOperation, "请选择非末行作为连接起点", {}};
  2323. }
  2324. const LogicEditorResult result = editor_service_.setVerticalConnection(
  2325. logic_id_, upper, logic->rungs[static_cast<std::size_t>(index + 1)].id,
  2326. selected_column_, true);
  2327. if (result.succeeded)
  2328. {
  2329. rebuildScene();
  2330. emit graphChanged();
  2331. }
  2332. else
  2333. {
  2334. reportFailure(result);
  2335. }
  2336. return result;
  2337. }
  2338. LogicEditorResult LogicEditorWidget::deleteHorizontalWire()
  2339. {
  2340. const std::string rung_id = selectedRungId();
  2341. if (rung_id.empty() || !selected_cell_ || selected_column_ < 0
  2342. || selected_column_ >= ProjectLimits::kMaximumConditionColumns)
  2343. {
  2344. return {false, LogicEditorError::InvalidOperation,
  2345. "请先选择要删除的横线网格", {}};
  2346. }
  2347. const LadderCell *cell = editor_service_.findCell(
  2348. logic_id_, rung_id, selected_column_);
  2349. if (cell == nullptr || cell->kind != LadderCellKind::Wire)
  2350. {
  2351. return {false, LogicEditorError::InvalidOperation,
  2352. "请选择一格横线后再删除", {}};
  2353. }
  2354. const LogicEditorResult result = editor_service_.setHorizontalWireRange(
  2355. logic_id_, rung_id, selected_column_, selected_column_, false);
  2356. if (result.succeeded)
  2357. {
  2358. selected_cells_.erase(
  2359. std::remove(
  2360. selected_cells_.begin(),
  2361. selected_cells_.end(),
  2362. std::make_pair(rung_id, selected_column_)),
  2363. selected_cells_.end());
  2364. synchronizeSelectedNodes();
  2365. rebuildScene();
  2366. notifySelectionChanged();
  2367. emit graphChanged();
  2368. }
  2369. else
  2370. {
  2371. reportFailure(result);
  2372. }
  2373. return result;
  2374. }
  2375. LogicEditorResult LogicEditorWidget::deleteVerticalWire()
  2376. {
  2377. if (selected_vertical_connection_id_.empty())
  2378. {
  2379. return {false, LogicEditorError::ConnectionNotFound, "请选择要删除的竖线", {}};
  2380. }
  2381. const LogicEditorResult result = editor_service_.removeVerticalConnections(
  2382. logic_id_, {selected_vertical_connection_id_});
  2383. if (result.succeeded)
  2384. {
  2385. selected_vertical_connection_ids_.erase(
  2386. std::remove(
  2387. selected_vertical_connection_ids_.begin(),
  2388. selected_vertical_connection_ids_.end(),
  2389. selected_vertical_connection_id_),
  2390. selected_vertical_connection_ids_.end());
  2391. selected_vertical_connection_id_ =
  2392. selected_vertical_connection_ids_.empty()
  2393. ? std::string{} : selected_vertical_connection_ids_.back();
  2394. selected_column_ = -1;
  2395. selected_cell_ = false;
  2396. selected_output_ = false;
  2397. selected_boundary_ = false;
  2398. rebuildScene();
  2399. notifySelectionChanged();
  2400. emit graphChanged();
  2401. }
  2402. else
  2403. {
  2404. reportFailure(result);
  2405. }
  2406. return result;
  2407. }
  2408. LogicEditorResult LogicEditorWidget::setOutput(
  2409. const LogicNodeConfig &config, bool configured)
  2410. {
  2411. return finishCursorEdit(editor_service_.applyOutputAndAdvance(
  2412. logic_id_,
  2413. {currentRungId(), ProjectLimits::kMaximumConditionColumns, true},
  2414. config,
  2415. configured));
  2416. }
  2417. LogicClipboardPasteResult LogicEditorWidget::pasteClipboard(
  2418. const LogicClipboardFragment &fragment)
  2419. {
  2420. LogicClipboardPasteResult result = editor_service_.pasteClipboard(
  2421. logic_id_, fragment, pasteTarget());
  2422. if (result.edit.succeeded)
  2423. {
  2424. clearObjectSelection();
  2425. selected_cells_ = result.selection.cells;
  2426. selected_output_rung_ids_ = result.selection.outputRungIds;
  2427. selected_vertical_connection_ids_ =
  2428. result.selection.verticalConnectionIds;
  2429. selected_row_ids_ = result.wholeRungIds;
  2430. if (!selected_row_ids_.empty())
  2431. {
  2432. selected_rung_id_ = selected_row_ids_.back();
  2433. selected_column_ = -1;
  2434. selected_cell_ = false;
  2435. selected_output_ = false;
  2436. selected_boundary_ = false;
  2437. }
  2438. else if (!selected_cells_.empty())
  2439. {
  2440. selected_rung_id_ = selected_cells_.front().first;
  2441. selected_column_ = selected_cells_.front().second;
  2442. selected_cell_ = true;
  2443. selected_output_ = false;
  2444. selected_boundary_ = false;
  2445. }
  2446. else if (!selected_output_rung_ids_.empty())
  2447. {
  2448. selected_rung_id_ = selected_output_rung_ids_.front();
  2449. selected_column_ = ProjectLimits::kMaximumConditionColumns;
  2450. selected_cell_ = false;
  2451. selected_output_ = true;
  2452. selected_boundary_ = false;
  2453. }
  2454. else if (!selected_vertical_connection_ids_.empty())
  2455. {
  2456. const VerticalConnection *connection = editor_service_.findConnection(
  2457. logic_id_, selected_vertical_connection_ids_.front());
  2458. if (connection != nullptr)
  2459. {
  2460. selected_rung_id_ = connection->upperRungId;
  2461. selected_column_ = connection->columnBoundary;
  2462. selected_cell_ = false;
  2463. selected_output_ = false;
  2464. selected_boundary_ = true;
  2465. }
  2466. }
  2467. selected_vertical_connection_id_ =
  2468. selected_vertical_connection_ids_.empty()
  2469. ? std::string{} : selected_vertical_connection_ids_.back();
  2470. synchronizeSelectedNodes();
  2471. rebuildScene();
  2472. notifySelectionChanged();
  2473. emit graphChanged();
  2474. }
  2475. else
  2476. {
  2477. reportFailure(result.edit);
  2478. }
  2479. return result;
  2480. }
  2481. LogicEditorResult LogicEditorWidget::deleteSelected()
  2482. {
  2483. LogicSelectionDeleteRequest selection;
  2484. selection.cells = selected_cells_;
  2485. selection.outputRungIds = selected_output_rung_ids_;
  2486. selection.verticalConnectionIds = selected_vertical_connection_ids_;
  2487. if (selection.cells.empty() && selection.outputRungIds.empty()
  2488. && selection.verticalConnectionIds.empty() && selected_cell_
  2489. && !selected_rung_id_.empty() && selected_column_ >= 0
  2490. && selected_column_ < ProjectLimits::kMaximumConditionColumns)
  2491. {
  2492. const LadderCell *cell = editor_service_.findCell(
  2493. logic_id_, selected_rung_id_, selected_column_);
  2494. if (cell != nullptr && cell->kind != LadderCellKind::Gap)
  2495. {
  2496. selection.cells.push_back({
  2497. selected_rung_id_, selected_column_});
  2498. }
  2499. }
  2500. if (selection.cells.empty() && selection.outputRungIds.empty()
  2501. && selection.verticalConnectionIds.empty())
  2502. {
  2503. const LogicEditorResult result{
  2504. false,
  2505. LogicEditorError::InvalidOperation,
  2506. "请先选择逻辑指令、横线、输出块或竖线;整行请使用 Shift+Delete",
  2507. {}};
  2508. reportFailure(result);
  2509. return result;
  2510. }
  2511. const LogicEditorResult result = editor_service_.deleteSelection(
  2512. logic_id_, selection);
  2513. if (result.succeeded)
  2514. {
  2515. clearObjectSelection();
  2516. selected_rung_id_.clear();
  2517. selected_column_ = -1;
  2518. selected_cell_ = false;
  2519. selected_output_ = false;
  2520. selected_boundary_ = false;
  2521. rebuildScene();
  2522. notifySelectionChanged();
  2523. emit graphChanged();
  2524. }
  2525. else
  2526. {
  2527. reportFailure(result);
  2528. }
  2529. return result;
  2530. }