| @@ -2,7 +2,9 @@ | |||||
| #include <QGraphicsItem> | #include <QGraphicsItem> | ||||
| #include <QGraphicsScene> | #include <QGraphicsScene> | ||||
| #include <QGraphicsTextItem> | |||||
| #include <QPainter> | #include <QPainter> | ||||
| #include <QPainterPath> | |||||
| #include <QResizeEvent> | #include <QResizeEvent> | ||||
| #include <QStyleOptionGraphicsItem> | #include <QStyleOptionGraphicsItem> | ||||
| @@ -15,9 +17,20 @@ namespace { | |||||
| constexpr qreal kMinimumSceneWidth = 1280.0; | constexpr qreal kMinimumSceneWidth = 1280.0; | ||||
| constexpr qreal kLeftRailX = 72.0; | constexpr qreal kLeftRailX = 72.0; | ||||
| constexpr qreal kStageWidth = 184.0; | constexpr qreal kStageWidth = 184.0; | ||||
| constexpr qreal kNodeWidth = 118.0; | |||||
| constexpr qreal kBranchSpacing = 58.0; | |||||
| constexpr qreal kRungGap = 30.0; | |||||
| constexpr qreal kNodeWidth = 128.0; | |||||
| constexpr qreal kNodeTerminalX = kNodeWidth / 2.0; | |||||
| constexpr qreal kBranchSpacing = 96.0; | |||||
| constexpr qreal kRungGap = 18.0; | |||||
| constexpr qreal kRungBaseHeight = 132.0; | |||||
| constexpr qreal kMainLineOffset = 78.0; | |||||
| constexpr qreal kLadderLineWidth = 1.6; | |||||
| constexpr qreal kPlaceholderWidth = 112.0; | |||||
| constexpr qreal kPlaceholderHeight = 30.0; | |||||
| const QColor kLadderColor(QStringLiteral("#26343d")); | |||||
| const QColor kSelectionColor(QStringLiteral("#dcecf4")); | |||||
| const QColor kSelectionBorderColor(QStringLiteral("#1677a8")); | |||||
| const QColor kPlaceholderColor(QStringLiteral("#82919b")); | |||||
| QString comparisonText(ComparisonOperator comparison) | QString comparisonText(ComparisonOperator comparison) | ||||
| { | { | ||||
| @@ -33,21 +46,46 @@ QString comparisonText(ComparisonOperator comparison) | |||||
| } | } | ||||
| } | } | ||||
| QString nodeAddressText(const LogicNodeConfig &config) | |||||
| QString registerAddressText(const RegisterAddress &address) | |||||
| { | |||||
| return QStringLiteral("%1%2") | |||||
| .arg(address.area() == RegisterArea::M ? QStringLiteral("M") | |||||
| : QStringLiteral("D")) | |||||
| .arg(address.index()); | |||||
| } | |||||
| QString nodeToolTip(const LogicNodeConfig &config) | |||||
| { | { | ||||
| return std::visit( | return std::visit( | ||||
| [](const auto &value) -> QString | [](const auto &value) -> QString | ||||
| { | { | ||||
| using Config = std::decay_t<decltype(value)>; | using Config = std::decay_t<decltype(value)>; | ||||
| if constexpr (std::is_same_v<Config, ContactNodeConfig> | |||||
| || std::is_same_v<Config, CoilNodeConfig>) | |||||
| if constexpr (std::is_same_v<Config, ContactNodeConfig>) | |||||
| { | |||||
| return value.mode == ContactMode::NormallyOpen | |||||
| ? LogicEditorWidget::tr("常开触点 %1").arg( | |||||
| registerAddressText(value.address)) | |||||
| : LogicEditorWidget::tr("常闭触点 %1").arg( | |||||
| registerAddressText(value.address)); | |||||
| } | |||||
| else if constexpr (std::is_same_v<Config, CoilNodeConfig>) | |||||
| { | { | ||||
| return QStringLiteral("M%1").arg(value.address.index()); | |||||
| QString mode = LogicEditorWidget::tr("普通线圈"); | |||||
| if (value.mode == CoilMode::Set) | |||||
| { | |||||
| mode = LogicEditorWidget::tr("置位线圈"); | |||||
| } | |||||
| else if (value.mode == CoilMode::Reset) | |||||
| { | |||||
| mode = LogicEditorWidget::tr("复位线圈"); | |||||
| } | |||||
| return QStringLiteral("%1 %2").arg( | |||||
| mode, registerAddressText(value.address)); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| return QStringLiteral("D%1 %2 %3") | |||||
| .arg(value.address.index()) | |||||
| return LogicEditorWidget::tr("比较条件:%1 %2 %3") | |||||
| .arg(registerAddressText(value.address)) | |||||
| .arg(comparisonText(value.comparison)) | .arg(comparisonText(value.comparison)) | ||||
| .arg(value.value); | .arg(value.value); | ||||
| } | } | ||||
| @@ -62,7 +100,30 @@ qreal rungHeight(const LadderRung &rung) | |||||
| { | { | ||||
| maximum_branches = std::max(maximum_branches, stage.branches.size()); | maximum_branches = std::max(maximum_branches, stage.branches.size()); | ||||
| } | } | ||||
| return 100.0 + static_cast<qreal>(maximum_branches - 1U) * kBranchSpacing; | |||||
| return kRungBaseHeight | |||||
| + static_cast<qreal>(maximum_branches - 1U) * kBranchSpacing; | |||||
| } | |||||
| void addPlaceholder( | |||||
| QGraphicsScene &scene, | |||||
| const QPointF ¢er, | |||||
| const QString &text, | |||||
| qreal width = kPlaceholderWidth) | |||||
| { | |||||
| const QRectF bounds( | |||||
| center.x() - width / 2.0, | |||||
| center.y() - kPlaceholderHeight / 2.0, | |||||
| width, | |||||
| kPlaceholderHeight); | |||||
| scene.addRect( | |||||
| bounds, | |||||
| QPen(kPlaceholderColor, 1.2, Qt::DashLine), | |||||
| QBrush(Qt::white)); | |||||
| QGraphicsTextItem *label = scene.addText(text); | |||||
| label->setDefaultTextColor(kPlaceholderColor); | |||||
| label->setPos( | |||||
| center.x() - label->boundingRect().width() / 2.0, | |||||
| center.y() - label->boundingRect().height() / 2.0); | |||||
| } | } | ||||
| } // namespace | } // namespace | ||||
| @@ -82,11 +143,13 @@ public: | |||||
| { | { | ||||
| setPos(center); | setPos(center); | ||||
| setFlag(ItemIsSelectable, true); | setFlag(ItemIsSelectable, true); | ||||
| setZValue(1.0); | |||||
| setToolTip(nodeToolTip(config_)); | |||||
| } | } | ||||
| QRectF boundingRect() const override | QRectF boundingRect() const override | ||||
| { | { | ||||
| return {-kNodeWidth / 2.0, -27.0, kNodeWidth, 54.0}; | |||||
| return {-kNodeWidth / 2.0, -46.0, kNodeWidth, 92.0}; | |||||
| } | } | ||||
| void paint( | void paint( | ||||
| @@ -98,27 +161,45 @@ public: | |||||
| const bool selected = (option->state & QStyle::State_Selected) != 0; | const bool selected = (option->state & QStyle::State_Selected) != 0; | ||||
| if (selected) | if (selected) | ||||
| { | { | ||||
| painter->fillRect( | |||||
| boundingRect(), QColor(QStringLiteral("#dceef7"))); | |||||
| painter->fillRect(boundingRect(), kSelectionColor); | |||||
| } | } | ||||
| painter->setPen(QPen( | |||||
| selected ? QColor(QStringLiteral("#1677a8")) | |||||
| : QColor(QStringLiteral("#1f2b33")), | |||||
| selected ? 2.0 : 1.6)); | |||||
| const QColor symbol_color = selected ? kSelectionBorderColor : kLadderColor; | |||||
| painter->setPen(QPen(symbol_color, selected ? 2.0 : kLadderLineWidth)); | |||||
| QFont text_font = painter->font(); | |||||
| text_font.setPointSizeF(9.5); | |||||
| painter->setFont(text_font); | |||||
| if (const auto *contact = std::get_if<ContactNodeConfig>(&config_)) | if (const auto *contact = std::get_if<ContactNodeConfig>(&config_)) | ||||
| { | { | ||||
| painter->fillRect(QRectF(-24, -21, 48, 42), Qt::white); | |||||
| painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-18, 0)); | |||||
| painter->drawLine(QPointF(18, 0), QPointF(kNodeTerminalX, 0)); | |||||
| painter->drawLine(QPointF(-18, -15), QPointF(-18, 15)); | painter->drawLine(QPointF(-18, -15), QPointF(-18, 15)); | ||||
| painter->drawLine(QPointF(18, -15), QPointF(18, 15)); | painter->drawLine(QPointF(18, -15), QPointF(18, 15)); | ||||
| if (contact->mode == ContactMode::NormallyClosed) | if (contact->mode == ContactMode::NormallyClosed) | ||||
| { | { | ||||
| painter->drawLine(QPointF(-23, 18), QPointF(23, -18)); | painter->drawLine(QPointF(-23, 18), QPointF(23, -18)); | ||||
| } | } | ||||
| painter->setPen(kLadderColor); | |||||
| painter->drawText( | |||||
| QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), | |||||
| Qt::AlignCenter, | |||||
| registerAddressText(contact->address)); | |||||
| } | } | ||||
| else if (const auto *coil = std::get_if<CoilNodeConfig>(&config_)) | else if (const auto *coil = std::get_if<CoilNodeConfig>(&config_)) | ||||
| { | { | ||||
| painter->drawArc(QRectF(-29, -20, 30, 40), -80 * 16, 160 * 16); | |||||
| painter->drawArc(QRectF(-1, -20, 30, 40), 100 * 16, 160 * 16); | |||||
| painter->fillRect(QRectF(-34, -23, 68, 46), Qt::white); | |||||
| painter->drawLine(QPointF(-kNodeTerminalX, 0), QPointF(-16, 0)); | |||||
| painter->drawLine(QPointF(16, 0), QPointF(kNodeTerminalX, 0)); | |||||
| QPainterPath left_coil; | |||||
| left_coil.moveTo(-2, -20); | |||||
| left_coil.cubicTo(-24, -16, -24, 16, -2, 20); | |||||
| painter->drawPath(left_coil); | |||||
| QPainterPath right_coil; | |||||
| right_coil.moveTo(2, -20); | |||||
| right_coil.cubicTo(24, -16, 24, 16, 2, 20); | |||||
| painter->drawPath(right_coil); | |||||
| if (coil->mode == CoilMode::Set || coil->mode == CoilMode::Reset) | if (coil->mode == CoilMode::Set || coil->mode == CoilMode::Reset) | ||||
| { | { | ||||
| painter->drawText( | painter->drawText( | ||||
| @@ -127,16 +208,36 @@ public: | |||||
| coil->mode == CoilMode::Set ? QStringLiteral("S") | coil->mode == CoilMode::Set ? QStringLiteral("S") | ||||
| : QStringLiteral("R")); | : QStringLiteral("R")); | ||||
| } | } | ||||
| painter->setPen(kLadderColor); | |||||
| painter->drawText( | |||||
| QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), | |||||
| Qt::AlignCenter, | |||||
| registerAddressText(coil->address)); | |||||
| } | } | ||||
| else | |||||
| else if (const auto *comparison = std::get_if<CompareNodeConfig>(&config_)) | |||||
| { | { | ||||
| painter->drawRect(QRectF(-43, -18, 86, 36)); | |||||
| constexpr qreal box_half_width = 47.0; | |||||
| const QRectF instruction_box(-box_half_width, -17, 94, 34); | |||||
| painter->fillRect(instruction_box.adjusted(-1, -1, 1, 1), Qt::white); | |||||
| painter->drawLine( | |||||
| QPointF(-kNodeTerminalX, 0), QPointF(-box_half_width, 0)); | |||||
| painter->drawLine( | |||||
| QPointF(box_half_width, 0), QPointF(kNodeTerminalX, 0)); | |||||
| painter->drawRect(instruction_box); | |||||
| painter->drawText( | |||||
| instruction_box, | |||||
| Qt::AlignCenter, | |||||
| QStringLiteral("%1 INT").arg(comparisonText(comparison->comparison))); | |||||
| painter->setPen(kLadderColor); | |||||
| painter->drawText( | |||||
| QRectF(-kNodeWidth / 2.0, -44, kNodeWidth, 18), | |||||
| Qt::AlignCenter, | |||||
| registerAddressText(comparison->address)); | |||||
| painter->drawText( | |||||
| QRectF(-kNodeWidth / 2.0, 22, kNodeWidth, 18), | |||||
| Qt::AlignCenter, | |||||
| QString::number(comparison->value)); | |||||
| } | } | ||||
| painter->setPen(QColor(QStringLiteral("#263640"))); | |||||
| painter->drawText( | |||||
| QRectF(-kNodeWidth / 2.0, -42, kNodeWidth, 18), | |||||
| Qt::AlignCenter, | |||||
| nodeAddressText(config_)); | |||||
| } | } | ||||
| const std::string &nodeId() const { return node_id_; } | const std::string &nodeId() const { return node_id_; } | ||||
| @@ -186,10 +287,15 @@ public: | |||||
| boundingRect(), QColor(QStringLiteral("#f0f7fa"))); | boundingRect(), QColor(QStringLiteral("#f0f7fa"))); | ||||
| } | } | ||||
| painter->setPen(QColor(QStringLiteral("#62717b"))); | painter->setPen(QColor(QStringLiteral("#62717b"))); | ||||
| QString title = QStringLiteral("网络 %1").arg(number_); | |||||
| if (!name_.isEmpty() && !name_.startsWith(QStringLiteral("网络 "))) | |||||
| { | |||||
| title += QStringLiteral(":") + name_; | |||||
| } | |||||
| painter->drawText( | painter->drawText( | ||||
| QRectF(28, 6, 260, 22), | QRectF(28, 6, 260, 22), | ||||
| Qt::AlignLeft | Qt::AlignVCenter, | Qt::AlignLeft | Qt::AlignVCenter, | ||||
| QStringLiteral("%1 %2").arg(number_).arg(name_)); | |||||
| title); | |||||
| painter->setPen(QPen(QColor(QStringLiteral("#d4dce1")), 1)); | painter->setPen(QPen(QColor(QStringLiteral("#d4dce1")), 1)); | ||||
| painter->drawLine( | painter->drawLine( | ||||
| QPointF(28, height_ - 1), QPointF(scene_width_ - 28, height_ - 1)); | QPointF(28, height_ - 1), QPointF(scene_width_ - 28, height_ - 1)); | ||||
| @@ -267,14 +373,14 @@ void LogicEditorWidget::reloadLogic() | |||||
| { | { | ||||
| const qreal height = rungHeight(rung); | const qreal height = rungHeight(rung); | ||||
| scene_->addItem(new RungItem(rung, rung_number, top, height, scene_width)); | scene_->addItem(new RungItem(rung, rung_number, top, height, scene_width)); | ||||
| const qreal main_y = top + 62.0; | |||||
| const qreal main_y = top + kMainLineOffset; | |||||
| scene_->addLine( | scene_->addLine( | ||||
| kLeftRailX, top + 32.0, kLeftRailX, top + height - 18.0, | |||||
| QPen(QColor(QStringLiteral("#202a31")), 3)); | |||||
| kLeftRailX, top + 38.0, kLeftRailX, top + height - 16.0, | |||||
| QPen(kLadderColor, 2.4)); | |||||
| scene_->addLine( | scene_->addLine( | ||||
| right_rail_x, top + 32.0, right_rail_x, top + height - 18.0, | |||||
| QPen(QColor(QStringLiteral("#202a31")), 3)); | |||||
| right_rail_x, top + 38.0, right_rail_x, top + height - 16.0, | |||||
| QPen(kLadderColor, 2.4)); | |||||
| qreal cursor_x = kLeftRailX; | qreal cursor_x = kLeftRailX; | ||||
| for (const LadderStage &stage : rung.stages) | for (const LadderStage &stage : rung.stages) | ||||
| @@ -284,27 +390,27 @@ void LogicEditorWidget::reloadLogic() | |||||
| const qreal right_join = next_x - 18.0; | const qreal right_join = next_x - 18.0; | ||||
| scene_->addLine( | scene_->addLine( | ||||
| cursor_x, main_y, left_join, main_y, | cursor_x, main_y, left_join, main_y, | ||||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||||
| QPen(kLadderColor, kLadderLineWidth)); | |||||
| scene_->addLine( | scene_->addLine( | ||||
| right_join, main_y, next_x, main_y, | right_join, main_y, next_x, main_y, | ||||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||||
| QPen(kLadderColor, kLadderLineWidth)); | |||||
| if (stage.branches.size() > 1U) | if (stage.branches.size() > 1U) | ||||
| { | { | ||||
| const qreal bottom_y = main_y | const qreal bottom_y = main_y | ||||
| + static_cast<qreal>(stage.branches.size() - 1U) * kBranchSpacing; | + static_cast<qreal>(stage.branches.size() - 1U) * kBranchSpacing; | ||||
| scene_->addLine( | scene_->addLine( | ||||
| left_join, main_y, left_join, bottom_y, | left_join, main_y, left_join, bottom_y, | ||||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||||
| QPen(kLadderColor, kLadderLineWidth)); | |||||
| scene_->addLine( | scene_->addLine( | ||||
| right_join, main_y, right_join, bottom_y, | right_join, main_y, right_join, bottom_y, | ||||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||||
| QPen(kLadderColor, kLadderLineWidth)); | |||||
| } | } | ||||
| for (std::size_t branch = 0; branch < stage.branches.size(); ++branch) | for (std::size_t branch = 0; branch < stage.branches.size(); ++branch) | ||||
| { | { | ||||
| const qreal branch_y = main_y + static_cast<qreal>(branch) * kBranchSpacing; | const qreal branch_y = main_y + static_cast<qreal>(branch) * kBranchSpacing; | ||||
| scene_->addLine( | scene_->addLine( | ||||
| left_join, branch_y, right_join, branch_y, | left_join, branch_y, right_join, branch_y, | ||||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||||
| QPen(kLadderColor, kLadderLineWidth)); | |||||
| scene_->addItem(new NodeItem( | scene_->addItem(new NodeItem( | ||||
| stage.branches.at(branch), | stage.branches.at(branch), | ||||
| rung.id, | rung.id, | ||||
| @@ -314,32 +420,58 @@ void LogicEditorWidget::reloadLogic() | |||||
| cursor_x = next_x; | cursor_x = next_x; | ||||
| } | } | ||||
| const qreal output_x = right_rail_x - 116.0; | |||||
| scene_->addLine( | |||||
| cursor_x, main_y, output_x, main_y, | |||||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||||
| scene_->addLine( | |||||
| output_x, main_y, right_rail_x, main_y, | |||||
| QPen(QColor(QStringLiteral("#202a31")), 2)); | |||||
| const qreal output_center_x = right_rail_x - 68.0; | |||||
| if (rung.stages.empty()) | |||||
| { | |||||
| const qreal condition_center_x = kLeftRailX + kStageWidth / 2.0; | |||||
| scene_->addLine( | |||||
| kLeftRailX, | |||||
| main_y, | |||||
| condition_center_x - kPlaceholderWidth / 2.0, | |||||
| main_y, | |||||
| QPen(kLadderColor, kLadderLineWidth)); | |||||
| scene_->addLine( | |||||
| condition_center_x + kPlaceholderWidth / 2.0, | |||||
| main_y, | |||||
| output_center_x, | |||||
| main_y, | |||||
| QPen(kLadderColor, kLadderLineWidth)); | |||||
| addPlaceholder( | |||||
| *scene_, | |||||
| QPointF(condition_center_x, main_y), | |||||
| tr("添加条件")); | |||||
| } | |||||
| else | |||||
| { | |||||
| scene_->addLine( | |||||
| cursor_x, main_y, output_center_x, main_y, | |||||
| QPen(kLadderColor, kLadderLineWidth)); | |||||
| } | |||||
| if (rung.output.has_value()) | if (rung.output.has_value()) | ||||
| { | { | ||||
| scene_->addLine( | |||||
| output_center_x, main_y, right_rail_x, main_y, | |||||
| QPen(kLadderColor, kLadderLineWidth)); | |||||
| scene_->addItem(new NodeItem( | scene_->addItem(new NodeItem( | ||||
| *rung.output, | *rung.output, | ||||
| rung.id, | rung.id, | ||||
| {}, | {}, | ||||
| QPointF(output_x + 50.0, main_y))); | |||||
| QPointF(output_center_x, main_y))); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| auto *placeholder = scene_->addText(QStringLiteral("< 输出线圈 >")); | |||||
| placeholder->setDefaultTextColor(QColor(QStringLiteral("#87949c"))); | |||||
| placeholder->setPos(output_x + 4.0, main_y - 14.0); | |||||
| } | |||||
| if (rung.stages.empty()) | |||||
| { | |||||
| auto *placeholder = scene_->addText(QStringLiteral("< 添加条件 >")); | |||||
| placeholder->setDefaultTextColor(QColor(QStringLiteral("#87949c"))); | |||||
| placeholder->setPos(kLeftRailX + 32.0, main_y - 14.0); | |||||
| constexpr qreal output_placeholder_width = 96.0; | |||||
| scene_->addLine( | |||||
| output_center_x + output_placeholder_width / 2.0, | |||||
| main_y, | |||||
| right_rail_x, | |||||
| main_y, | |||||
| QPen(kLadderColor, kLadderLineWidth)); | |||||
| addPlaceholder( | |||||
| *scene_, | |||||
| QPointF(output_center_x, main_y), | |||||
| tr("输出线圈"), | |||||
| output_placeholder_width); | |||||
| } | } | ||||
| top += height + kRungGap; | top += height + kRungGap; | ||||
| ++rung_number; | ++rung_number; | ||||
| @@ -450,7 +582,7 @@ LogicEditorResult LogicEditorWidget::addParallelCondition( | |||||
| { | { | ||||
| result = {false, | result = {false, | ||||
| LogicEditorError::InvalidOperation, | LogicEditorError::InvalidOperation, | ||||
| "select a condition before adding a parallel branch", | |||||
| "请先选择要添加并联分支的触点或比较条件", | |||||
| {}}; | {}}; | ||||
| } | } | ||||
| else | else | ||||
| @@ -501,7 +633,8 @@ LogicEditorResult LogicEditorWidget::deleteSelected() | |||||
| const std::string rung_id = selectedRungId(); | const std::string rung_id = selectedRungId(); | ||||
| if (rung_id.empty()) | if (rung_id.empty()) | ||||
| { | { | ||||
| return {false, LogicEditorError::InvalidOperation, "no ladder object is selected", {}}; | |||||
| return {false, LogicEditorError::InvalidOperation, | |||||
| "请先选择要删除的逻辑节点或网络", {}}; | |||||
| } | } | ||||
| result = editor_service_.removeRung(logic_id_, rung_id); | result = editor_service_.removeRung(logic_id_, rung_id); | ||||
| if (result.succeeded && current_rung_id_ == rung_id) | if (result.succeeded && current_rung_id_ == rung_id) | ||||