| @@ -454,7 +454,7 @@ bool LadderRung::validateStructure(std::string *error) const | |||||
| if (containsLineBreak(comment) | if (containsLineBreak(comment) | ||||
| || comment.size() > ProjectLimits::kMaximumRungCommentBytes) | || comment.size() > ProjectLimits::kMaximumRungCommentBytes) | ||||
| { | { | ||||
| setError(error, "梯形图行注释必须是最多 128 个 UTF-8 字节的单行文本"); | |||||
| setError(error, "网络注释必须是最多 128 个 UTF-8 字节的单行文本"); | |||||
| return false; | return false; | ||||
| } | } | ||||
| if (cells.empty()) | if (cells.empty()) | ||||
| @@ -531,6 +531,33 @@ bool LadderRung::validateForRunning(std::string *error) const | |||||
| return true; | return true; | ||||
| } | } | ||||
| std::size_t ControlLogic::networkHeadIndex(std::size_t rung_index) const | |||||
| { | |||||
| if (rung_index >= rungs.size()) | |||||
| { | |||||
| return rungs.size(); | |||||
| } | |||||
| while (rung_index > 0U) | |||||
| { | |||||
| const std::string &upper_id = rungs[rung_index - 1U].id; | |||||
| const std::string &lower_id = rungs[rung_index].id; | |||||
| const bool connected_to_previous = std::any_of( | |||||
| verticalConnections.cbegin(), | |||||
| verticalConnections.cend(), | |||||
| [&upper_id, &lower_id](const VerticalConnection &connection) | |||||
| { | |||||
| return connection.upperRungId == upper_id | |||||
| && connection.lowerRungId == lower_id; | |||||
| }); | |||||
| if (!connected_to_previous) | |||||
| { | |||||
| break; | |||||
| } | |||||
| --rung_index; | |||||
| } | |||||
| return rung_index; | |||||
| } | |||||
| bool ControlLogic::validate( | bool ControlLogic::validate( | ||||
| const ProjectLimitSettings &limits, std::string *error) const | const ProjectLimitSettings &limits, std::string *error) const | ||||
| { | { | ||||
| @@ -641,6 +668,17 @@ bool ControlLogic::validateStructure( | |||||
| return false; | return false; | ||||
| } | } | ||||
| } | } | ||||
| for (std::size_t row = 1U; row < rungs.size(); ++row) | |||||
| { | |||||
| if (!rungs[row].comment.empty() && networkHeadIndex(row) != row) | |||||
| { | |||||
| setError( | |||||
| error, | |||||
| "第 " + std::to_string(row + 1U) | |||||
| + " 行不是网络首行,不能保存网络注释"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| return true; | return true; | ||||
| } | } | ||||
| @@ -175,6 +175,7 @@ struct ControlLogic | |||||
| bool enabled = true; | bool enabled = true; | ||||
| std::vector<VerticalConnection> verticalConnections; | std::vector<VerticalConnection> verticalConnections; | ||||
| std::size_t networkHeadIndex(std::size_t rung_index) const; | |||||
| bool validate( | bool validate( | ||||
| const ProjectLimitSettings &limits, | const ProjectLimitSettings &limits, | ||||
| std::string *error = nullptr) const; | std::string *error = nullptr) const; | ||||
| @@ -590,6 +590,45 @@ bool LogicEditorService::rungsEqual( | |||||
| || nodesEqual(*left.output, *right.output); | || nodesEqual(*left.output, *right.output); | ||||
| } | } | ||||
| bool LogicEditorService::mergeNetworkComments( | |||||
| ControlLogic *logic, std::string *error) | |||||
| { | |||||
| if (logic == nullptr) | |||||
| { | |||||
| if (error != nullptr) | |||||
| { | |||||
| *error = "未找到控制逻辑"; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| for (std::size_t row = 1U; row < logic->rungs.size(); ++row) | |||||
| { | |||||
| const std::size_t head = logic->networkHeadIndex(row); | |||||
| if (head == row || logic->rungs[row].comment.empty()) | |||||
| { | |||||
| continue; | |||||
| } | |||||
| std::string &head_comment = logic->rungs[head].comment; | |||||
| std::string &branch_comment = logic->rungs[row].comment; | |||||
| if (!head_comment.empty() && head_comment != branch_comment) | |||||
| { | |||||
| if (error != nullptr) | |||||
| { | |||||
| *error = "无法合并网络:第 " + std::to_string(head + 1U) | |||||
| + " 行和第 " + std::to_string(row + 1U) | |||||
| + " 行存在不同注释,请先统一或清空其中一条"; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| if (head_comment.empty()) | |||||
| { | |||||
| head_comment = branch_comment; | |||||
| } | |||||
| branch_comment.clear(); | |||||
| } | |||||
| return true; | |||||
| } | |||||
| bool LogicEditorService::cellsEqual( | bool LogicEditorService::cellsEqual( | ||||
| const LadderCell &left, const LadderCell &right) | const LadderCell &left, const LadderCell &right) | ||||
| { | { | ||||
| @@ -691,6 +730,19 @@ const LadderRung *LogicEditorService::findRung( | |||||
| return found == logic->rungs.cend() ? nullptr : &*found; | return found == logic->rungs.cend() ? nullptr : &*found; | ||||
| } | } | ||||
| const LadderRung *LogicEditorService::findNetworkHeadRung( | |||||
| const std::string &logic_id, const std::string &rung_id) const | |||||
| { | |||||
| const ControlLogic *logic = findLogic(logic_id); | |||||
| if (logic == nullptr) | |||||
| { | |||||
| return nullptr; | |||||
| } | |||||
| const std::size_t row = rungIndex(*logic, rung_id); | |||||
| const std::size_t head = logic->networkHeadIndex(row); | |||||
| return head == logic->rungs.size() ? nullptr : &logic->rungs[head]; | |||||
| } | |||||
| const LadderCell *LogicEditorService::findCell( | const LadderCell *LogicEditorService::findCell( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| @@ -1217,13 +1269,13 @@ LogicEditorResult LogicEditorService::removeRungs( | |||||
| return {true, LogicEditorError::None, {}, rung_ids.front()}; | return {true, LogicEditorError::None, {}, rung_ids.front()}; | ||||
| } | } | ||||
| LogicEditorResult LogicEditorService::updateRungComment( | |||||
| LogicEditorResult LogicEditorService::updateNetworkComment( | |||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &comment) | const std::string &comment) | ||||
| { | { | ||||
| const LadderRung *rung = findRung(logic_id, rung_id); | |||||
| if (rung == nullptr) | |||||
| const LadderRung *head = findNetworkHeadRung(logic_id, rung_id); | |||||
| if (head == nullptr) | |||||
| { | { | ||||
| return failure(LogicEditorError::RungNotFound, "未找到梯形图行"); | return failure(LogicEditorError::RungNotFound, "未找到梯形图行"); | ||||
| } | } | ||||
| @@ -1232,17 +1284,18 @@ LogicEditorResult LogicEditorService::updateRungComment( | |||||
| { | { | ||||
| return failure( | return failure( | ||||
| LogicEditorError::InvalidOperation, | LogicEditorError::InvalidOperation, | ||||
| "行注释必须是最多 128 个 UTF-8 字节的单行文本"); | |||||
| "网络注释必须是最多 128 个 UTF-8 字节的单行文本"); | |||||
| } | } | ||||
| if (rung->comment == comment) | |||||
| if (head->comment == comment) | |||||
| { | { | ||||
| return {true, LogicEditorError::None, {}, rung_id}; | |||||
| return {true, LogicEditorError::None, {}, head->id}; | |||||
| } | } | ||||
| const std::string head_rung_id = head->id; | |||||
| HistoryState before = captureState(); | HistoryState before = captureState(); | ||||
| Project &project = project_service_.editProject(); | Project &project = project_service_.editProject(); | ||||
| editableRung(editableLogic(&project, logic_id), rung_id)->comment = comment; | |||||
| editableRung(editableLogic(&project, logic_id), head_rung_id)->comment = comment; | |||||
| recordHistory(std::move(before)); | recordHistory(std::move(before)); | ||||
| return {true, LogicEditorError::None, {}, rung_id}; | |||||
| return {true, LogicEditorError::None, {}, head_rung_id}; | |||||
| } | } | ||||
| LogicEditResult LogicEditorService::applyConditionAndAdvance( | LogicEditResult LogicEditorService::applyConditionAndAdvance( | ||||
| @@ -1584,6 +1637,11 @@ LogicEditorResult LogicEditorService::setVerticalConnectionRange( | |||||
| } | } | ||||
| } | } | ||||
| std::string error; | std::string error; | ||||
| if (connected && !mergeNetworkComments(editable, &error)) | |||||
| { | |||||
| rollbackEdit(std::move(before), modified_before); | |||||
| return failure(LogicEditorError::InvalidOperation, error); | |||||
| } | |||||
| if (!editable->validateStructure(project_service_.projectLimits(), &error)) | if (!editable->validateStructure(project_service_.projectLimits(), &error)) | ||||
| { | { | ||||
| rollbackEdit(std::move(before), modified_before); | rollbackEdit(std::move(before), modified_before); | ||||
| @@ -2472,6 +2530,11 @@ LogicClipboardPasteResult LogicEditorService::pasteClipboard( | |||||
| } | } | ||||
| } | } | ||||
| std::string error; | std::string error; | ||||
| if (!mergeNetworkComments(editable, &error)) | |||||
| { | |||||
| rollbackEdit(std::move(before), modified_before); | |||||
| return {failure(LogicEditorError::InvalidOperation, error), {}, {}}; | |||||
| } | |||||
| if (!editable->validateStructure(project_service_.projectLimits(), &error)) | if (!editable->validateStructure(project_service_.projectLimits(), &error)) | ||||
| { | { | ||||
| rollbackEdit(std::move(before), modified_before); | rollbackEdit(std::move(before), modified_before); | ||||
| @@ -2687,6 +2750,12 @@ LogicClipboardPasteResult LogicEditorService::pasteClipboard( | |||||
| } | } | ||||
| } | } | ||||
| std::string error; | std::string error; | ||||
| if (!fragment.verticalConnections.empty() | |||||
| && !mergeNetworkComments(editable, &error)) | |||||
| { | |||||
| rollbackEdit(std::move(before), modified_before); | |||||
| return {failure(LogicEditorError::InvalidOperation, error), {}, {}}; | |||||
| } | |||||
| if (!editable->validateStructure(project_service_.projectLimits(), &error)) | if (!editable->validateStructure(project_service_.projectLimits(), &error)) | ||||
| { | { | ||||
| rollbackEdit(std::move(before), modified_before); | rollbackEdit(std::move(before), modified_before); | ||||
| @@ -165,6 +165,8 @@ public: | |||||
| const ControlLogic *findLogic(const std::string &logic_id) const; | const ControlLogic *findLogic(const std::string &logic_id) const; | ||||
| const LadderRung *findRung( | const LadderRung *findRung( | ||||
| const std::string &logic_id, const std::string &rung_id) const; | const std::string &logic_id, const std::string &rung_id) const; | ||||
| const LadderRung *findNetworkHeadRung( | |||||
| const std::string &logic_id, const std::string &rung_id) const; | |||||
| const LadderCell *findCell( | const LadderCell *findCell( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| @@ -209,7 +211,7 @@ public: | |||||
| LogicEditorResult removeRungs( | LogicEditorResult removeRungs( | ||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::vector<std::string> &rung_ids); | const std::vector<std::string> &rung_ids); | ||||
| LogicEditorResult updateRungComment( | |||||
| LogicEditorResult updateNetworkComment( | |||||
| const std::string &logic_id, | const std::string &logic_id, | ||||
| const std::string &rung_id, | const std::string &rung_id, | ||||
| const std::string &comment); | const std::string &comment); | ||||
| @@ -323,6 +325,8 @@ private: | |||||
| const ControlLogic &left, const ControlLogic &right); | const ControlLogic &left, const ControlLogic &right); | ||||
| static bool rungsEqual( | static bool rungsEqual( | ||||
| const LadderRung &left, const LadderRung &right); | const LadderRung &left, const LadderRung &right); | ||||
| static bool mergeNetworkComments( | |||||
| ControlLogic *logic, std::string *error); | |||||
| static bool cellsEqual( | static bool cellsEqual( | ||||
| const LadderCell &left, const LadderCell &right); | const LadderCell &left, const LadderCell &right); | ||||
| static bool nodesEqual(const LogicNode &left, const LogicNode &right); | static bool nodesEqual(const LogicNode &left, const LogicNode &right); | ||||
| @@ -1055,21 +1055,7 @@ void LogicEditorWidget::rebuildRowLayout(const ControlLogic &logic) | |||||
| qreal next_top = kTop; | qreal next_top = kTop; | ||||
| for (std::size_t row = 0U; row < logic.rungs.size(); ++row) | for (std::size_t row = 0U; row < logic.rungs.size(); ++row) | ||||
| { | { | ||||
| bool connected_to_previous = false; | |||||
| if (row > 0U) | |||||
| { | |||||
| const std::string &upper_id = logic.rungs[row - 1U].id; | |||||
| const std::string &lower_id = logic.rungs[row].id; | |||||
| connected_to_previous = std::any_of( | |||||
| logic.verticalConnections.cbegin(), | |||||
| logic.verticalConnections.cend(), | |||||
| [&upper_id, &lower_id](const VerticalConnection &connection) | |||||
| { | |||||
| return connection.upperRungId == upper_id | |||||
| && connection.lowerRungId == lower_id; | |||||
| }); | |||||
| } | |||||
| const bool network_head = row == 0U || !connected_to_previous; | |||||
| const bool network_head = logic.networkHeadIndex(row) == row; | |||||
| if (network_head) | if (network_head) | ||||
| { | { | ||||
| next_top += kCommentBandHeight; | next_top += kCommentBandHeight; | ||||
| @@ -878,8 +878,8 @@ void MainWindow::configureActions() | |||||
| }); | }); | ||||
| connect(ui_->deleteLogicAction, &QAction::triggered, | connect(ui_->deleteLogicAction, &QAction::triggered, | ||||
| this, &MainWindow::deleteSelectedLogicObject); | this, &MainWindow::deleteSelectedLogicObject); | ||||
| connect(ui_->editRungCommentAction, &QAction::triggered, | |||||
| this, &MainWindow::editSelectedRungComment); | |||||
| connect(ui_->editNetworkCommentAction, &QAction::triggered, | |||||
| this, &MainWindow::editSelectedNetworkComment); | |||||
| addToolbarMenu( | addToolbarMenu( | ||||
| ui_->logicToolBar, | ui_->logicToolBar, | ||||
| tr("更多触点"), | tr("更多触点"), | ||||
| @@ -994,7 +994,7 @@ void MainWindow::configureAppearance() | |||||
| ui_->addAddAction->setIcon(makeUiIcon(UiIcon::Add)); | ui_->addAddAction->setIcon(makeUiIcon(UiIcon::Add)); | ||||
| ui_->addSubAction->setIcon(makeUiIcon(UiIcon::Subtract)); | ui_->addSubAction->setIcon(makeUiIcon(UiIcon::Subtract)); | ||||
| ui_->addCompareAction->setIcon(makeUiIcon(UiIcon::Compare)); | ui_->addCompareAction->setIcon(makeUiIcon(UiIcon::Compare)); | ||||
| ui_->editRungCommentAction->setIcon(makeUiIcon(UiIcon::Comment)); | |||||
| ui_->editNetworkCommentAction->setIcon(makeUiIcon(UiIcon::Comment)); | |||||
| ui_->syntaxCheckAction->setIcon(makeUiIcon(UiIcon::SyntaxCheck)); | ui_->syntaxCheckAction->setIcon(makeUiIcon(UiIcon::SyntaxCheck)); | ||||
| ui_->doubleCoilCheckAction->setIcon(makeUiIcon(UiIcon::Coil)); | ui_->doubleCoilCheckAction->setIcon(makeUiIcon(UiIcon::Coil)); | ||||
| ui_->deleteLogicAction->setIcon(makeUiIcon(UiIcon::Delete)); | ui_->deleteLogicAction->setIcon(makeUiIcon(UiIcon::Delete)); | ||||
| @@ -1835,7 +1835,7 @@ void MainWindow::deleteLogicRung() | |||||
| statusBar()->showMessage(tr("当前行已删除,竖线连接已重新整理"), 3000); | statusBar()->showMessage(tr("当前行已删除,竖线连接已重新整理"), 3000); | ||||
| } | } | ||||
| void MainWindow::editSelectedRungComment() | |||||
| void MainWindow::editSelectedNetworkComment() | |||||
| { | { | ||||
| const std::string rung_id = logic_editor_widget_->selectedRungId(); | const std::string rung_id = logic_editor_widget_->selectedRungId(); | ||||
| if (rung_id.empty()) | if (rung_id.empty()) | ||||
| @@ -1843,9 +1843,9 @@ void MainWindow::editSelectedRungComment() | |||||
| showProjectResult(tr("网络注释"), tr("请先选择一个梯形图网络"), false); | showProjectResult(tr("网络注释"), tr("请先选择一个梯形图网络"), false); | ||||
| return; | return; | ||||
| } | } | ||||
| const LadderRung *rung = logic_editor_service_.findRung( | |||||
| const LadderRung *head = logic_editor_service_.findNetworkHeadRung( | |||||
| current_logic_id_, rung_id); | current_logic_id_, rung_id); | ||||
| if (rung == nullptr) | |||||
| if (head == nullptr) | |||||
| { | { | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -1855,13 +1855,13 @@ void MainWindow::editSelectedRungComment() | |||||
| tr("网络注释"), | tr("网络注释"), | ||||
| tr("说明"), | tr("说明"), | ||||
| QLineEdit::Normal, | QLineEdit::Normal, | ||||
| fromUtf8(rung->comment), | |||||
| fromUtf8(head->comment), | |||||
| &accepted); | &accepted); | ||||
| if (!accepted) | if (!accepted) | ||||
| { | { | ||||
| return; | return; | ||||
| } | } | ||||
| const LogicEditorResult result = logic_editor_service_.updateRungComment( | |||||
| const LogicEditorResult result = logic_editor_service_.updateNetworkComment( | |||||
| current_logic_id_, rung_id, toUtf8(comment)); | current_logic_id_, rung_id, toUtf8(comment)); | ||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| { | { | ||||
| @@ -2453,7 +2453,7 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| ui_->addAddAction->setEnabled(policy.allowsProjectEditing); | ui_->addAddAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addSubAction->setEnabled(policy.allowsProjectEditing); | ui_->addSubAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addCompareAction->setEnabled(policy.allowsProjectEditing); | ui_->addCompareAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->editRungCommentAction->setEnabled(policy.allowsProjectEditing); | |||||
| ui_->editNetworkCommentAction->setEnabled(policy.allowsProjectEditing); | |||||
| ui_->deleteLogicAction->setEnabled(policy.allowsProjectEditing); | ui_->deleteLogicAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->newProjectAction->setEnabled(policy.allowsProjectEditing); | ui_->newProjectAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing); | ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing); | ||||
| @@ -197,7 +197,7 @@ private: | |||||
| /** 删除当前选中的梯形图行 */ | /** 删除当前选中的梯形图行 */ | ||||
| void deleteLogicRung(); | void deleteLogicRung(); | ||||
| /** 编辑当前网络的注释 */ | /** 编辑当前网络的注释 */ | ||||
| void editSelectedRungComment(); | |||||
| void editSelectedNetworkComment(); | |||||
| /** 规整并检查当前梯形图 */ | /** 规整并检查当前梯形图 */ | ||||
| void runLogicSyntaxCheck(); | void runLogicSyntaxCheck(); | ||||
| /** 单独检查当前梯形图中的重复线圈输出 */ | /** 单独检查当前梯形图中的重复线圈输出 */ | ||||
| @@ -364,7 +364,7 @@ | |||||
| <addaction name="addNormallyOpenAction"/> | <addaction name="addNormallyOpenAction"/> | ||||
| <addaction name="addNormallyClosedAction"/> | <addaction name="addNormallyClosedAction"/> | ||||
| <addaction name="addNormalCoilAction"/> | <addaction name="addNormalCoilAction"/> | ||||
| <addaction name="editRungCommentAction"/> | |||||
| <addaction name="editNetworkCommentAction"/> | |||||
| <addaction name="separator"/> | <addaction name="separator"/> | ||||
| <addaction name="syntaxCheckAction"/> | <addaction name="syntaxCheckAction"/> | ||||
| </widget> | </widget> | ||||
| @@ -1318,7 +1318,7 @@ | |||||
| <string>在选中的空网格或横线处添加 D 值与常量比较条件</string> | <string>在选中的空网格或横线处添加 D 值与常量比较条件</string> | ||||
| </property> | </property> | ||||
| </action> | </action> | ||||
| <action name="editRungCommentAction"> | |||||
| <action name="editNetworkCommentAction"> | |||||
| <property name="text"> | <property name="text"> | ||||
| <string>网络注释</string> | <string>网络注释</string> | ||||
| </property> | </property> | ||||
| @@ -696,6 +696,28 @@ void testEdgeAndCommentBoundaries() | |||||
| comment_rung.comment = "第一行\r第二行"; | comment_rung.comment = "第一行\r第二行"; | ||||
| require(!comment_rung.validateStructure(), "a rung comment containing CR must be rejected"); | require(!comment_rung.validateStructure(), "a rung comment containing CR must be rejected"); | ||||
| ControlLogic commented_logic; | |||||
| commented_logic.id = "commented-logic"; | |||||
| commented_logic.name = "Commented logic"; | |||||
| LadderRung head; | |||||
| head.id = "head"; | |||||
| head.name = "Head"; | |||||
| head.comment = "Network comment"; | |||||
| LadderRung branch; | |||||
| branch.id = "branch"; | |||||
| branch.name = "Branch"; | |||||
| branch.comment = "Hidden branch comment"; | |||||
| commented_logic.rungs = {head, branch}; | |||||
| commented_logic.verticalConnections = { | |||||
| {"comment-edge", "head", "branch", 0}}; | |||||
| require( | |||||
| commented_logic.networkHeadIndex(1U) == 0U | |||||
| && !commented_logic.validateStructure(), | |||||
| "a connected branch row must not persist a hidden network comment"); | |||||
| commented_logic.rungs[1].comment.clear(); | |||||
| require(commented_logic.validateStructure(), | |||||
| "a connected network must be valid when only its head stores the comment"); | |||||
| Project project = makeValidProject(); | Project project = makeValidProject(); | ||||
| project.registerComments = { | project.registerComments = { | ||||
| {RegisterAddress{RegisterArea::M, 0}, "启动按钮"}, | {RegisterAddress{RegisterArea::M, 0}, "启动按钮"}, | ||||
| @@ -192,6 +192,85 @@ void testIndependentVerticalConnectionsAndNetworkSplit() | |||||
| "the model must reject vertical edges between non-adjacent rows"); | "the model must reject vertical edges between non-adjacent rows"); | ||||
| } | } | ||||
| void testNetworkCommentsFollowNetworkHeadsAndMergeAtomically() | |||||
| { | |||||
| Fixture fixture; | |||||
| const std::string first = fixture.addRung(); | |||||
| const std::string second = fixture.addRung(); | |||||
| const std::string third = fixture.addRung(); | |||||
| require( | |||||
| fixture.editor.updateNetworkComment( | |||||
| fixture.logicId, second, "下方网络注释").succeeded, | |||||
| "an independent lower network must accept its own comment"); | |||||
| fixture.editor.clearHistory(); | |||||
| const LogicEditorResult merged = fixture.editor.setVerticalConnection( | |||||
| fixture.logicId, first, second, 4, true); | |||||
| const ControlLogic *logic = fixture.editor.findLogic(fixture.logicId); | |||||
| require( | |||||
| merged.succeeded && logic->rungs[0].comment == "下方网络注释" | |||||
| && logic->rungs[1].comment.empty() | |||||
| && fixture.editor.findNetworkHeadRung(fixture.logicId, second)->id | |||||
| == first, | |||||
| "merging a commented lower network into an empty upper network must move the comment to the new head"); | |||||
| require( | |||||
| fixture.editor.undo().succeeded | |||||
| && fixture.editor.findRung(fixture.logicId, first)->comment.empty() | |||||
| && fixture.editor.findRung(fixture.logicId, second)->comment | |||||
| == "下方网络注释" | |||||
| && fixture.editor.redo().succeeded, | |||||
| "network merge comment movement must belong to the same undo transaction"); | |||||
| fixture.editor.clearHistory(); | |||||
| const LogicEditorResult edited_from_branch = | |||||
| fixture.editor.updateNetworkComment( | |||||
| fixture.logicId, second, "从支路编辑后的注释"); | |||||
| require( | |||||
| edited_from_branch.succeeded && edited_from_branch.id == first | |||||
| && fixture.editor.findRung(fixture.logicId, first)->comment | |||||
| == "从支路编辑后的注释" | |||||
| && fixture.editor.findRung(fixture.logicId, second)->comment.empty(), | |||||
| "editing from a branch row must update only the network head comment"); | |||||
| require( | |||||
| fixture.editor.updateNetworkComment( | |||||
| fixture.logicId, third, "另一个网络注释").succeeded, | |||||
| "the third independent network must accept a different comment"); | |||||
| fixture.editor.clearHistory(); | |||||
| fixture.projects.restoreModifiedState(false); | |||||
| const LogicEditorResult conflict = fixture.editor.setVerticalConnection( | |||||
| fixture.logicId, second, third, 4, true); | |||||
| logic = fixture.editor.findLogic(fixture.logicId); | |||||
| require( | |||||
| !conflict.succeeded | |||||
| && conflict.message.find("存在不同注释") != std::string::npos | |||||
| && connectionAt(*logic, second, third, 4) == nullptr | |||||
| && logic->rungs[0].comment == "从支路编辑后的注释" | |||||
| && logic->rungs[2].comment == "另一个网络注释" | |||||
| && !fixture.projects.isModified() && !fixture.editor.canUndo(), | |||||
| "merging differently commented networks must fail without partial topology or history"); | |||||
| require( | |||||
| fixture.editor.updateNetworkComment( | |||||
| fixture.logicId, third, "从支路编辑后的注释").succeeded, | |||||
| "the lower network comment must be editable before a retry"); | |||||
| fixture.editor.clearHistory(); | |||||
| require( | |||||
| fixture.editor.setVerticalConnection( | |||||
| fixture.logicId, second, third, 4, true).succeeded | |||||
| && fixture.editor.findRung(fixture.logicId, third)->comment.empty(), | |||||
| "merging equal network comments must keep one comment at the head"); | |||||
| const std::string connection_id = connectionAt( | |||||
| *fixture.editor.findLogic(fixture.logicId), second, third, 4)->id; | |||||
| require( | |||||
| fixture.editor.removeVerticalConnections( | |||||
| fixture.logicId, {connection_id}).succeeded | |||||
| && fixture.editor.findRung(fixture.logicId, first)->comment | |||||
| == "从支路编辑后的注释" | |||||
| && fixture.editor.findRung(fixture.logicId, third)->comment.empty(), | |||||
| "splitting a network must not duplicate its head comment"); | |||||
| } | |||||
| void testInsertRowSplitsVerticalEdges() | void testInsertRowSplitsVerticalEdges() | ||||
| { | { | ||||
| Fixture fixture; | Fixture fixture; | ||||
| @@ -1149,7 +1228,7 @@ void testWholeRowClipboardInsertionAndLimit() | |||||
| source_node.succeeded | source_node.succeeded | ||||
| && fixture.editor.setHorizontalWireRange( | && fixture.editor.setHorizontalWireRange( | ||||
| fixture.logicId, second, 0, 1, true).succeeded | fixture.logicId, second, 0, 1, true).succeeded | ||||
| && fixture.editor.updateRungComment( | |||||
| && fixture.editor.updateNetworkComment( | |||||
| fixture.logicId, first, "整行复制注释").succeeded | fixture.logicId, first, "整行复制注释").succeeded | ||||
| && fixture.editor.setVerticalConnection( | && fixture.editor.setVerticalConnection( | ||||
| fixture.logicId, first, second, 2, true).succeeded, | fixture.logicId, first, second, 2, true).succeeded, | ||||
| @@ -1413,6 +1492,7 @@ int main() | |||||
| { | { | ||||
| testContinuousGridAndIndependentHorizontalWires(); | testContinuousGridAndIndependentHorizontalWires(); | ||||
| testIndependentVerticalConnectionsAndNetworkSplit(); | testIndependentVerticalConnectionsAndNetworkSplit(); | ||||
| testNetworkCommentsFollowNetworkHeadsAndMergeAtomically(); | |||||
| testInsertRowSplitsVerticalEdges(); | testInsertRowSplitsVerticalEdges(); | ||||
| testDeleteRowMergesOnlyContinuousEdges(); | testDeleteRowMergesOnlyContinuousEdges(); | ||||
| testParallelBranchCreatesConnectedVisualRow(); | testParallelBranchCreatesConnectedVisualRow(); | ||||
| @@ -128,6 +128,7 @@ Project makeExampleProject() | |||||
| logic.verticalConnections = { | logic.verticalConnections = { | ||||
| {"vertical-left", "rung-1", "rung-2", 0}, | {"vertical-left", "rung-1", "rung-2", 0}, | ||||
| {"vertical-right", "rung-1", "rung-2", 1}}; | {"vertical-right", "rung-1", "rung-2", 1}}; | ||||
| logic.rungs[1].comment.clear(); | |||||
| project.controlLogics.push_back(std::move(logic)); | project.controlLogics.push_back(std::move(logic)); | ||||
| project.registerComments.push_back({ | project.registerComments.push_back({ | ||||
| RegisterAddress{RegisterArea::M, 0}, "Start signal"}); | RegisterAddress{RegisterArea::M, 0}, "Start signal"}); | ||||
| @@ -396,6 +397,21 @@ void testInvalidGridAndConnectionsAreRejected() | |||||
| == ProjectStorageError::InvalidProject, | == ProjectStorageError::InvalidProject, | ||||
| "duplicate edges at one row boundary must be rejected"); | "duplicate edges at one row boundary must be rejected"); | ||||
| QJsonObject hidden_comment = original; | |||||
| logic = firstLogic(&hidden_comment); | |||||
| rungs = logic.value(QStringLiteral("rungs")).toArray(); | |||||
| rung = rungs.at(1).toObject(); | |||||
| rung.insert(QStringLiteral("comment"), QStringLiteral("hidden branch comment")); | |||||
| rungs.replace(1, rung); | |||||
| logic.insert(QStringLiteral("rungs"), rungs); | |||||
| replaceFirstLogic(&hidden_comment, logic); | |||||
| const QString hidden_comment_path = directory.filePath( | |||||
| "hidden-branch-comment.json"); | |||||
| writeBytes(hidden_comment_path, QJsonDocument(hidden_comment).toJson()); | |||||
| require(storage.load(hidden_comment_path.toStdString()).error | |||||
| == ProjectStorageError::InvalidProject, | |||||
| "a 2.0 project must reject comments stored on connected branch rows"); | |||||
| QJsonObject non_adjacent = original; | QJsonObject non_adjacent = original; | ||||
| logic = firstLogic(&non_adjacent); | logic = firstLogic(&non_adjacent); | ||||
| edges = logic.value(QStringLiteral("verticalConnections")).toArray(); | edges = logic.value(QStringLiteral("verticalConnections")).toArray(); | ||||
| @@ -564,11 +564,14 @@ void testLadderLayoutAndDragDeletion() | |||||
| logic_id, lower, 0, 1, true).succeeded | logic_id, lower, 0, 1, true).succeeded | ||||
| && editor.setVerticalConnection( | && editor.setVerticalConnection( | ||||
| logic_id, upper, lower, 2, true).succeeded | logic_id, upper, lower, 2, true).succeeded | ||||
| && editor.updateRungComment( | |||||
| logic_id, upper, "主网络注释").succeeded | |||||
| && editor.updateRungComment( | |||||
| logic_id, lower, "支路注释不应重复").succeeded, | |||||
| && editor.updateNetworkComment( | |||||
| logic_id, lower, "主网络注释").succeeded, | |||||
| "layout fixture must create a connected two-row network"); | "layout fixture must create a connected two-row network"); | ||||
| require( | |||||
| editor.findNetworkHeadRung(logic_id, lower)->id == upper | |||||
| && editor.findRung(logic_id, upper)->comment == "主网络注释" | |||||
| && editor.findRung(logic_id, lower)->comment.empty(), | |||||
| "editing a branch row must store the comment only on the network head"); | |||||
| editor.clearHistory(); | editor.clearHistory(); | ||||
| LogicEditorWidget widget(editor); | LogicEditorWidget widget(editor); | ||||
| @@ -578,7 +581,6 @@ void testLadderLayoutAndDragDeletion() | |||||
| QCoreApplication::processEvents(QEventLoop::AllEvents); | QCoreApplication::processEvents(QEventLoop::AllEvents); | ||||
| bool found_head_comment = false; | bool found_head_comment = false; | ||||
| bool found_branch_comment = false; | |||||
| for (QGraphicsItem *item : widget.scene()->items()) | for (QGraphicsItem *item : widget.scene()->items()) | ||||
| { | { | ||||
| auto *text = dynamic_cast<QGraphicsSimpleTextItem *>(item); | auto *text = dynamic_cast<QGraphicsSimpleTextItem *>(item); | ||||
| @@ -588,11 +590,9 @@ void testLadderLayoutAndDragDeletion() | |||||
| } | } | ||||
| found_head_comment = found_head_comment | found_head_comment = found_head_comment | ||||
| || text->text() == QStringLiteral("主网络注释"); | || text->text() == QStringLiteral("主网络注释"); | ||||
| found_branch_comment = found_branch_comment | |||||
| || text->text() == QStringLiteral("支路注释不应重复"); | |||||
| } | } | ||||
| require(found_head_comment && !found_branch_comment, | |||||
| "only the network-head comment must be rendered"); | |||||
| require(found_head_comment, | |||||
| "the comment edited from a branch must render above the network head"); | |||||
| constexpr qreal left_bus = 60.0; | constexpr qreal left_bus = 60.0; | ||||
| constexpr qreal cell_width = 96.0; | constexpr qreal cell_width = 96.0; | ||||