diff --git a/app/src/services/logic_editor_service.cpp b/app/src/services/logic_editor_service.cpp index cb02b50..2d0d8ea 100644 --- a/app/src/services/logic_editor_service.cpp +++ b/app/src/services/logic_editor_service.cpp @@ -1645,6 +1645,84 @@ LogicEditorResult LogicEditorService::setVerticalConnection( connected); } +LogicVerticalEditResult LogicEditorService::applyVerticalConnectionAndAdvance( + const std::string &logic_id, + const std::string &upper_rung_id, + int column_boundary) +{ + const ControlLogic *logic = findLogic(logic_id); + if (logic == nullptr) + { + return { + failure(LogicEditorError::LogicNotFound, "未找到控制逻辑"), + {}, + -1, + false}; + } + if (column_boundary < 0 + || column_boundary > ProjectLimits::kMaximumConditionColumns) + { + return { + failure( + LogicEditorError::InvalidOperation, + "请先选择一个列边界或网格,再插入竖线"), + {}, + -1, + false}; + } + const std::size_t upper = rungIndex(*logic, upper_rung_id); + if (upper == logic->rungs.size()) + { + return { + failure(LogicEditorError::RungNotFound, "未找到梯形图行"), + {}, + -1, + false}; + } + if (upper + 1U >= logic->rungs.size()) + { + return { + failure( + LogicEditorError::InvalidOperation, + "当前已经是末行,无法继续建立竖线"), + {}, + -1, + false}; + } + + const std::string lower_rung_id = logic->rungs[upper + 1U].id; + const bool already_connected = std::any_of( + logic->verticalConnections.cbegin(), + logic->verticalConnections.cend(), + [&upper_rung_id, &lower_rung_id, column_boundary]( + const VerticalConnection &connection) + { + return connectionMatches( + connection, + upper_rung_id, + lower_rung_id, + column_boundary); + }); + LogicEditorResult edit = setVerticalConnection( + logic_id, + upper_rung_id, + lower_rung_id, + column_boundary, + true); + if (!edit.succeeded) + { + return {std::move(edit), {}, -1, false}; + } + edit.message = already_connected + ? "竖线已经存在,已移至下一行" + : "竖线连接已建立,已移至下一行"; + return { + std::move(edit), + lower_rung_id, + column_boundary, + !already_connected}; +} + LogicEditorResult LogicEditorService::setVerticalConnectionRange( const std::string &logic_id, const std::string &first_rung_id, @@ -1672,6 +1750,35 @@ LogicEditorResult LogicEditorService::setVerticalConnectionRange( std::swap(first, last); } + bool changed = false; + for (std::size_t row = first; row < last; ++row) + { + const std::string &upper_id = logic->rungs[row].id; + const std::string &lower_id = logic->rungs[row + 1U].id; + const bool exists = std::any_of( + logic->verticalConnections.cbegin(), + logic->verticalConnections.cend(), + [&upper_id, &lower_id, column_boundary]( + const VerticalConnection &connection) + { + return connectionMatches( + connection, upper_id, lower_id, column_boundary); + }); + if (exists != connected) + { + changed = true; + break; + } + } + if (!changed) + { + return { + true, + LogicEditorError::None, + connected ? "竖线连接已经存在" : "目标位置没有竖线", + first_rung_id}; + } + HistoryState before = captureState(); const bool modified_before = project_service_.isModified(); Project &project = project_service_.editProject(); diff --git a/app/src/services/logic_editor_service.h b/app/src/services/logic_editor_service.h index a439ed1..a81fd00 100644 --- a/app/src/services/logic_editor_service.h +++ b/app/src/services/logic_editor_service.h @@ -127,6 +127,14 @@ struct LogicEditResult LogicEditCursor nextCursor; }; +struct LogicVerticalEditResult +{ + LogicEditorResult edit; + std::string nextRungId; + int columnBoundary = -1; + bool changed = false; +}; + struct LogicSyntaxLocation { std::string logicId; @@ -254,6 +262,10 @@ public: const std::string &lower_rung_id, int column_boundary, bool connected); + LogicVerticalEditResult applyVerticalConnectionAndAdvance( + const std::string &logic_id, + const std::string &upper_rung_id, + int column_boundary); LogicEditorResult setVerticalConnectionRange( const std::string &logic_id, const std::string &first_rung_id, diff --git a/app/src/ui/logic_editor_widget.cpp b/app/src/ui/logic_editor_widget.cpp index 9fbf9c9..abe61ae 100644 --- a/app/src/ui/logic_editor_widget.cpp +++ b/app/src/ui/logic_editor_widget.cpp @@ -2092,6 +2092,49 @@ void LogicEditorWidget::moveToCursor(const LogicEditCursor &cursor) } } +void LogicEditorWidget::moveToVerticalTarget( + const LogicVerticalEditResult &result) +{ + const bool keep_cell = selected_cell_; + const bool keep_output = selected_output_; + const bool keep_boundary = selected_boundary_; + clearObjectSelection(); + selected_rung_id_ = result.nextRungId; + selected_column_ = result.columnBoundary; + selected_cell_ = keep_cell; + selected_output_ = keep_output; + selected_boundary_ = keep_boundary; + rebuildScene(); + notifySelectionChanged(); + + const RowLayout *layout = layoutForRung(result.nextRungId); + if (layout == nullptr) + { + return; + } + qreal center_x = kLeftBus + + (static_cast(result.columnBoundary) + 0.5) * kCellWidth; + qreal target_width = kCellWidth; + if (keep_boundary) + { + center_x = kLeftBus + + static_cast(result.columnBoundary) * kCellWidth; + } + else if (keep_output) + { + center_x = kConditionRight + kOutputWidth / 2.0; + target_width = kOutputWidth; + } + ensureVisible( + QRectF( + center_x - target_width / 2.0, + layout->centerY - kRowHeight / 2.0, + target_width, + kRowHeight), + 24, + 24); +} + LogicEditorResult LogicEditorWidget::finishCursorEdit( const LogicEditResult &result) { @@ -2412,36 +2455,30 @@ LogicEditorResult LogicEditorWidget::addHorizontalWire() LogicEditorResult LogicEditorWidget::addVerticalWire() { - const ControlLogic *logic = editor_service_.findLogic(logic_id_); - if (logic == nullptr || logic->rungs.size() < 2U) - { - return {false, LogicEditorError::InvalidOperation, "至少需要两行才能连接竖线", {}}; - } const std::string upper = selectedRungId(); if (upper.empty() || selected_column_ < 0 - || selected_column_ > ProjectLimits::kMaximumConditionColumns) + || selected_column_ > ProjectLimits::kMaximumConditionColumns + || (!selected_cell_ && !selected_output_ && !selected_boundary_)) { return {false, LogicEditorError::InvalidOperation, "请先选择一个列边界或网格,再插入竖线", {}}; } - const int index = rowAt(upper); - if (index < 0 || static_cast(index + 1) >= logic->rungs.size()) - { - return {false, LogicEditorError::InvalidOperation, "请选择非末行作为连接起点", {}}; - } - const LogicEditorResult result = editor_service_.setVerticalConnection( - logic_id_, upper, logic->rungs[static_cast(index + 1)].id, - selected_column_, true); - if (result.succeeded) + const LogicVerticalEditResult result = + editor_service_.applyVerticalConnectionAndAdvance( + logic_id_, upper, selected_column_); + if (result.edit.succeeded) { - rebuildScene(); - emit graphChanged(); + moveToVerticalTarget(result); + if (result.changed) + { + emit graphChanged(); + } } else { - reportFailure(result); + reportFailure(result.edit); } - return result; + return result.edit; } LogicEditorResult LogicEditorWidget::deleteHorizontalWire() diff --git a/app/src/ui/logic_editor_widget.h b/app/src/ui/logic_editor_widget.h index 0524e0e..3bfb4cc 100644 --- a/app/src/ui/logic_editor_widget.h +++ b/app/src/ui/logic_editor_widget.h @@ -121,6 +121,7 @@ private: const LogicEditCursor &cursor) const; LogicEditCursor conditionInsertionCursor() const; void moveToCursor(const LogicEditCursor &cursor); + void moveToVerticalTarget(const LogicVerticalEditResult &result); LogicEditorResult finishCursorEdit(const LogicEditResult &result); void rebuildScene(); void selectObject(const Hit &hit, bool extend_node_selection = false); diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 94ac4b6..d6b9d96 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -1710,7 +1710,10 @@ void MainWindow::addLogicVerticalWire() selected_logic_node_id_.clear(); showLogicNodeProperties({}); refreshProjectUi(); - statusBar()->showMessage(tr("竖线连接已建立"), 3000); + statusBar()->showMessage( + result.message.empty() + ? tr("竖线连接已建立") : fromUtf8(result.message), + 3000); } void MainWindow::deleteLogicHorizontalWire() diff --git a/app/src/ui/main_window.ui b/app/src/ui/main_window.ui index 9a7f751..a469d9d 100644 --- a/app/src/ui/main_window.ui +++ b/app/src/ui/main_window.ui @@ -1206,7 +1206,7 @@ 竖线 - 在当前行与下一行的选中列边界建立一段竖线连接 + 在当前行与下一行的选中列边界建立竖线,成功后自动下移到下一行 F12 diff --git a/app/tests/logic_editor_service_tests.cpp b/app/tests/logic_editor_service_tests.cpp index d3a1529..1bcd973 100644 --- a/app/tests/logic_editor_service_tests.cpp +++ b/app/tests/logic_editor_service_tests.cpp @@ -136,6 +136,22 @@ void testIndependentVerticalConnectionsAndNetworkSplit() && connectionAt(*logic, second, third, 4) != nullptr, "a long vertical line must be represented by independent segments"); + fixture.editor.clearHistory(); + fixture.projects.restoreModifiedState(false); + const LogicVerticalEditResult duplicate = + fixture.editor.applyVerticalConnectionAndAdvance( + fixture.logicId, first, 4); + require( + duplicate.edit.succeeded + && !duplicate.changed + && duplicate.nextRungId == second + && duplicate.columnBoundary == 4 + && fixture.editor.findLogic(fixture.logicId) + ->verticalConnections.size() == 2U + && !fixture.projects.isModified() + && !fixture.editor.canUndo(), + "an existing vertical edge must advance without dirty state or empty history"); + const std::string first_segment = connectionAt(*logic, first, second, 4)->id; require( diff --git a/app/tests/runtime_panel_controller_tests.cpp b/app/tests/runtime_panel_controller_tests.cpp index 054e286..42650cf 100644 --- a/app/tests/runtime_panel_controller_tests.cpp +++ b/app/tests/runtime_panel_controller_tests.cpp @@ -31,12 +31,14 @@ #include #include #include +#include #include #include #include #include #include +#include namespace { @@ -781,6 +783,95 @@ void testCursorAdvanceAndInlineCommandInput() "an inline output must append a row and keep continuous input active"); } +void testVerticalWireShortcutAdvancesDownward() +{ + TestProjectStorage storage; + ProjectService project_service(storage); + LogicEditorService editor(project_service); + const std::string logic_id = editor.ensureDefaultLogic().id; + std::vector rung_ids; + for (int row = 0; row < 4; ++row) + { + rung_ids.push_back(editor.addRung(logic_id).id); + } + editor.clearHistory(); + + LogicEditorWidget widget(editor); + widget.setLogicId(logic_id); + widget.resize(1320, 240); + widget.show(); + QCoreApplication::processEvents(QEventLoop::AllEvents); + + constexpr int boundary = 4; + constexpr qreal left_bus = 60.0; + constexpr qreal cell_width = 96.0; + constexpr qreal first_grid_top = 46.0; + constexpr qreal row_height = 78.0; + const QPoint point = widget.mapFromScene(QPointF( + left_bus + (static_cast(boundary) + 0.5) * cell_width, + first_grid_top + row_height / 2.0)); + QMouseEvent press( + QEvent::MouseButtonPress, + QPointF(point), + Qt::LeftButton, + Qt::LeftButton, + Qt::NoModifier); + QApplication::sendEvent(widget.viewport(), &press); + QMouseEvent release( + QEvent::MouseButtonRelease, + QPointF(point), + Qt::LeftButton, + Qt::NoButton, + Qt::NoModifier); + QApplication::sendEvent(widget.viewport(), &release); + require(widget.selectedRungId() == rung_ids.front(), + "the vertical shortcut fixture must select the first row"); + + for (std::size_t row = 0U; row + 1U < rung_ids.size(); ++row) + { + const LogicEditorResult result = widget.addVerticalWire(); + require( + result.succeeded + && widget.selectedRungId() == rung_ids[row + 1U], + "each vertical shortcut must advance to the next row"); + const ControlLogic *logic = editor.findLogic(logic_id); + const bool connected = std::any_of( + logic->verticalConnections.cbegin(), + logic->verticalConnections.cend(), + [&rung_ids, row, boundary](const VerticalConnection &connection) + { + return connection.upperRungId == rung_ids[row] + && connection.lowerRungId == rung_ids[row + 1U] + && connection.columnBoundary == boundary; + }); + require(connected, + "repeated vertical shortcuts must stay on one column boundary"); + } + require( + editor.findLogic(logic_id)->verticalConnections.size() == 3U + && widget.verticalScrollBar()->value() > 0, + "the vertical shortcut must create three segments and keep the target visible"); + + const LogicEditorResult at_last_row = widget.addVerticalWire(); + require( + !at_last_row.succeeded + && at_last_row.message.find("末行") != std::string::npos + && editor.findLogic(logic_id)->verticalConnections.size() == 3U + && widget.selectedRungId() == rung_ids.back(), + "the vertical shortcut must stop cleanly at the last existing row"); + + for (int remaining = 2; remaining >= 0; --remaining) + { + require( + editor.undo().succeeded + && editor.findLogic(logic_id)->verticalConnections.size() + == static_cast(remaining), + "each undo must remove exactly one shortcut-created vertical segment"); + } + require(!editor.canUndo(), + "three vertical shortcuts must create exactly three history entries"); +} + void testSegmentLevelTraceProjection() { TestProjectStorage storage; @@ -987,6 +1078,7 @@ int main(int argc, char *argv[]) testLogicEditorGridSelectionAndDeletion(); testLadderLayoutAndDragDeletion(); testCursorAdvanceAndInlineCommandInput(); + testVerticalWireShortcutAdvancesDownward(); testSegmentLevelTraceProjection(); testLogicClipboardUsesExplicitObjectAndRowSelection(); }