| @@ -1559,6 +1559,25 @@ LogicEditorResult LogicEditorService::setWireCells( | |||||
| } | } | ||||
| } | } | ||||
| const auto should_change = [this, &logic_id, connected]( | |||||
| const std::pair<std::string, int> &position) | |||||
| { | |||||
| const LadderCell *cell = findCell( | |||||
| logic_id, position.first, position.second); | |||||
| return cell != nullptr | |||||
| && cell->kind != LadderCellKind::Node | |||||
| && (connected ? cell->kind != LadderCellKind::Wire | |||||
| : cell->kind != LadderCellKind::Gap); | |||||
| }; | |||||
| if (!std::any_of(cells.cbegin(), cells.cend(), should_change)) | |||||
| { | |||||
| return { | |||||
| true, | |||||
| LogicEditorError::None, | |||||
| connected ? "横线已经存在" : "目标位置没有横线", | |||||
| cells.front().first}; | |||||
| } | |||||
| HistoryState before = captureState(); | HistoryState before = captureState(); | ||||
| const bool modified_before = project_service_.isModified(); | const bool modified_before = project_service_.isModified(); | ||||
| Project &project = project_service_.editProject(); | Project &project = project_service_.editProject(); | ||||
| @@ -690,6 +690,7 @@ LogicEditorWidget::LogicEditorWidget( | |||||
| void LogicEditorWidget::setLogicId(const std::string &logic_id) | void LogicEditorWidget::setLogicId(const std::string &logic_id) | ||||
| { | { | ||||
| const bool logic_changed = logic_id_ != logic_id; | const bool logic_changed = logic_id_ != logic_id; | ||||
| clearGesture(); | |||||
| logic_id_ = logic_id; | logic_id_ = logic_id; | ||||
| if (logic_changed) | if (logic_changed) | ||||
| { | { | ||||
| @@ -792,6 +793,7 @@ void LogicEditorWidget::setLogicId(const std::string &logic_id) | |||||
| void LogicEditorWidget::setEditingEnabled(bool enabled) | void LogicEditorWidget::setEditingEnabled(bool enabled) | ||||
| { | { | ||||
| clearGesture(); | |||||
| editing_enabled_ = enabled; | editing_enabled_ = enabled; | ||||
| if (!enabled) | if (!enabled) | ||||
| { | { | ||||
| @@ -806,6 +808,7 @@ void LogicEditorWidget::setEditingEnabled(bool enabled) | |||||
| void LogicEditorWidget::setMouseWireMode(MouseWireMode mode) | void LogicEditorWidget::setMouseWireMode(MouseWireMode mode) | ||||
| { | { | ||||
| clearGesture(); | |||||
| mouse_wire_mode_ = editing_enabled_ ? mode : MouseWireMode::Select; | mouse_wire_mode_ = editing_enabled_ ? mode : MouseWireMode::Select; | ||||
| selection_pressed_ = false; | selection_pressed_ = false; | ||||
| selection_dragging_ = false; | selection_dragging_ = false; | ||||
| @@ -1707,14 +1710,21 @@ void LogicEditorWidget::beginGesture(const Hit &hit) | |||||
| gesture_active_ = true; | gesture_active_ = true; | ||||
| gesture_origin_ = hit; | gesture_origin_ = hit; | ||||
| gesture_current_ = hit; | gesture_current_ = hit; | ||||
| gesture_scene_position_valid_ = false; | |||||
| viewport()->update(); | |||||
| } | } | ||||
| void LogicEditorWidget::updateGesture(const Hit &hit) | |||||
| void LogicEditorWidget::updateGesture( | |||||
| const Hit &hit, const QPointF &scene_position) | |||||
| { | { | ||||
| if (gesture_active_ && !hit.rungId.empty()) | |||||
| if (!gesture_active_) | |||||
| { | { | ||||
| gesture_current_ = hit; | |||||
| return; | |||||
| } | } | ||||
| gesture_current_ = hit; | |||||
| gesture_scene_position_ = scene_position; | |||||
| gesture_scene_position_valid_ = true; | |||||
| viewport()->update(); | |||||
| } | } | ||||
| void LogicEditorWidget::finishGesture(const Hit &hit) | void LogicEditorWidget::finishGesture(const Hit &hit) | ||||
| @@ -1723,57 +1733,62 @@ void LogicEditorWidget::finishGesture(const Hit &hit) | |||||
| { | { | ||||
| return; | return; | ||||
| } | } | ||||
| const Hit origin = gesture_origin_; | |||||
| gesture_active_ = false; | gesture_active_ = false; | ||||
| if (gesture_origin_.rungId.empty() || hit.rungId.empty()) | |||||
| gesture_origin_ = {}; | |||||
| gesture_current_ = {}; | |||||
| gesture_scene_position_valid_ = false; | |||||
| viewport()->update(); | |||||
| if (origin.rungId.empty() || hit.rungId.empty()) | |||||
| { | { | ||||
| return; | return; | ||||
| } | } | ||||
| LogicEditorResult result; | LogicEditorResult result; | ||||
| const bool connected = mouse_wire_mode_ == MouseWireMode::Draw; | const bool connected = mouse_wire_mode_ == MouseWireMode::Draw; | ||||
| if ((gesture_origin_.vertical || gesture_origin_.boundary) | |||||
| if ((origin.vertical || origin.boundary) | |||||
| && (hit.vertical || hit.boundary)) | && (hit.vertical || hit.boundary)) | ||||
| { | { | ||||
| if (!connected && gesture_origin_.vertical | |||||
| && gesture_origin_.objectId == hit.objectId) | |||||
| if (!connected && origin.vertical | |||||
| && origin.objectId == hit.objectId) | |||||
| { | { | ||||
| result = editor_service_.removeVerticalConnections( | result = editor_service_.removeVerticalConnections( | ||||
| logic_id_, {gesture_origin_.objectId}); | |||||
| logic_id_, {origin.objectId}); | |||||
| } | } | ||||
| else if (gesture_origin_.column != hit.column) | |||||
| else if (origin.column != hit.column) | |||||
| { | { | ||||
| result = {false, LogicEditorError::InvalidOperation, | result = {false, LogicEditorError::InvalidOperation, | ||||
| "竖线拖动必须保持在同一列边界", {}}; | "竖线拖动必须保持在同一列边界", {}}; | ||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| std::string first_rung = gesture_origin_.rungId; | |||||
| std::string first_rung = origin.rungId; | |||||
| std::string last_rung = hit.rungId; | std::string last_rung = hit.rungId; | ||||
| if (last_rung.empty() && !gesture_origin_.lowerRungId.empty()) | |||||
| if (last_rung.empty() && !origin.lowerRungId.empty()) | |||||
| { | { | ||||
| last_rung = gesture_origin_.lowerRungId; | |||||
| last_rung = origin.lowerRungId; | |||||
| } | } | ||||
| result = editor_service_.setVerticalConnectionRange( | result = editor_service_.setVerticalConnectionRange( | ||||
| logic_id_, first_rung, last_rung, | logic_id_, first_rung, last_rung, | ||||
| gesture_origin_.column, connected); | |||||
| origin.column, connected); | |||||
| } | } | ||||
| } | } | ||||
| else if (gesture_origin_.vertical || hit.vertical) | |||||
| else if (origin.vertical || hit.vertical) | |||||
| { | { | ||||
| result = {false, LogicEditorError::InvalidOperation, | result = {false, LogicEditorError::InvalidOperation, | ||||
| "请从网格边界开始竖向拖动", {}}; | "请从网格边界开始竖向拖动", {}}; | ||||
| } | } | ||||
| else if (gesture_origin_.column == hit.column | |||||
| && gesture_origin_.rungId != hit.rungId) | |||||
| else if (origin.column == hit.column | |||||
| && origin.rungId != hit.rungId) | |||||
| { | { | ||||
| result = editor_service_.setVerticalConnectionRange( | result = editor_service_.setVerticalConnectionRange( | ||||
| logic_id_, gesture_origin_.rungId, hit.rungId, | |||||
| gesture_origin_.column, connected); | |||||
| logic_id_, origin.rungId, hit.rungId, | |||||
| origin.column, connected); | |||||
| } | } | ||||
| else if (gesture_origin_.rungId == hit.rungId) | |||||
| else if (origin.rungId == hit.rungId) | |||||
| { | { | ||||
| result = editor_service_.setHorizontalWireRange( | result = editor_service_.setHorizontalWireRange( | ||||
| logic_id_, gesture_origin_.rungId, | |||||
| gesture_origin_.column, hit.column, connected); | |||||
| logic_id_, origin.rungId, | |||||
| origin.column, hit.column, connected); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| @@ -1795,6 +1810,134 @@ void LogicEditorWidget::finishGesture(const Hit &hit) | |||||
| } | } | ||||
| } | } | ||||
| void LogicEditorWidget::clearGesture() | |||||
| { | |||||
| if (!gesture_active_ && !gesture_scene_position_valid_) | |||||
| { | |||||
| return; | |||||
| } | |||||
| gesture_active_ = false; | |||||
| gesture_origin_ = {}; | |||||
| gesture_current_ = {}; | |||||
| gesture_scene_position_ = {}; | |||||
| gesture_scene_position_valid_ = false; | |||||
| viewport()->update(); | |||||
| } | |||||
| void LogicEditorWidget::drawGesturePreview(QPainter *painter) const | |||||
| { | |||||
| if (!gesture_active_ || painter == nullptr | |||||
| || gesture_origin_.rungId.empty()) | |||||
| { | |||||
| return; | |||||
| } | |||||
| enum class PreviewKind { Invalid, Horizontal, Vertical }; | |||||
| PreviewKind kind = PreviewKind::Invalid; | |||||
| bool valid = false; | |||||
| const Hit &origin = gesture_origin_; | |||||
| const Hit ¤t = gesture_current_; | |||||
| const bool connected = mouse_wire_mode_ == MouseWireMode::Draw; | |||||
| if (!current.rungId.empty()) | |||||
| { | |||||
| if ((origin.vertical || origin.boundary) | |||||
| && (current.vertical || current.boundary)) | |||||
| { | |||||
| kind = PreviewKind::Vertical; | |||||
| valid = origin.column == current.column | |||||
| && (origin.rungId != current.rungId | |||||
| || (!connected && origin.vertical | |||||
| && origin.objectId == current.objectId)); | |||||
| } | |||||
| else if (origin.vertical || current.vertical) | |||||
| { | |||||
| kind = PreviewKind::Invalid; | |||||
| } | |||||
| else if (origin.column == current.column | |||||
| && origin.rungId != current.rungId) | |||||
| { | |||||
| kind = PreviewKind::Vertical; | |||||
| valid = origin.column >= 0 | |||||
| && origin.column <= ProjectLimits::kMaximumConditionColumns; | |||||
| } | |||||
| else if (origin.rungId == current.rungId) | |||||
| { | |||||
| kind = PreviewKind::Horizontal; | |||||
| valid = origin.column >= 0 && current.column >= 0 | |||||
| && origin.column < ProjectLimits::kMaximumConditionColumns | |||||
| && current.column < ProjectLimits::kMaximumConditionColumns; | |||||
| } | |||||
| } | |||||
| QColor preview_color = valid | |||||
| ? connected ? QColor(QStringLiteral("#277da1")) | |||||
| : QColor(QStringLiteral("#c56a1a")) | |||||
| : kFaultColor; | |||||
| preview_color.setAlpha(220); | |||||
| QPen preview_pen(preview_color, valid ? 2.8 : 2.4, Qt::DashLine); | |||||
| preview_pen.setCosmetic(true); | |||||
| preview_pen.setCapStyle(Qt::SquareCap); | |||||
| painter->save(); | |||||
| painter->setRenderHint(QPainter::Antialiasing, true); | |||||
| painter->setPen(preview_pen); | |||||
| painter->setBrush(Qt::NoBrush); | |||||
| if (valid && kind == PreviewKind::Horizontal) | |||||
| { | |||||
| const RowLayout *layout = layoutForRung(origin.rungId); | |||||
| if (layout != nullptr) | |||||
| { | |||||
| const int first = std::min(origin.column, current.column); | |||||
| const int last = std::max(origin.column, current.column); | |||||
| for (int column = first; column <= last; ++column) | |||||
| { | |||||
| const LadderCell *cell = editor_service_.findCell( | |||||
| logic_id_, origin.rungId, column); | |||||
| if (cell == nullptr || cell->kind == LadderCellKind::Node | |||||
| || (mouse_wire_mode_ == MouseWireMode::Erase | |||||
| && cell->kind != LadderCellKind::Wire)) | |||||
| { | |||||
| continue; | |||||
| } | |||||
| const qreal left = kLeftBus | |||||
| + static_cast<qreal>(column) * kCellWidth; | |||||
| painter->drawLine( | |||||
| QPointF(left, layout->centerY), | |||||
| QPointF(left + kCellWidth, layout->centerY)); | |||||
| } | |||||
| } | |||||
| } | |||||
| else if (valid && kind == PreviewKind::Vertical) | |||||
| { | |||||
| const RowLayout *first = layoutForRung(origin.rungId); | |||||
| const RowLayout *last = layoutForRung(current.rungId); | |||||
| if (first != nullptr && last != nullptr) | |||||
| { | |||||
| const qreal x = kLeftBus | |||||
| + static_cast<qreal>(origin.column) * kCellWidth; | |||||
| painter->drawLine( | |||||
| QPointF(x, first->centerY), | |||||
| QPointF(x, last->centerY)); | |||||
| } | |||||
| } | |||||
| else | |||||
| { | |||||
| const RowLayout *layout = layoutForRung(origin.rungId); | |||||
| if (layout != nullptr && gesture_scene_position_valid_) | |||||
| { | |||||
| const qreal origin_x = kLeftBus | |||||
| + (origin.vertical || origin.boundary | |||||
| ? static_cast<qreal>(origin.column) * kCellWidth | |||||
| : (static_cast<qreal>(origin.column) + 0.5) * kCellWidth); | |||||
| painter->drawLine( | |||||
| QPointF(origin_x, layout->centerY), | |||||
| gesture_scene_position_); | |||||
| } | |||||
| } | |||||
| painter->restore(); | |||||
| } | |||||
| void LogicEditorWidget::showCommandEditor(const Hit &hit) | void LogicEditorWidget::showCommandEditor(const Hit &hit) | ||||
| { | { | ||||
| if (!editing_enabled_ || hit.rungId.empty() | if (!editing_enabled_ || hit.rungId.empty() | ||||
| @@ -2199,7 +2342,8 @@ void LogicEditorWidget::mouseMoveEvent(QMouseEvent *event) | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| updateGesture(hitAt(mapToScene(event->pos()))); | |||||
| const QPointF scene_position = mapToScene(event->pos()); | |||||
| updateGesture(hitAt(scene_position), scene_position); | |||||
| } | } | ||||
| event->accept(); | event->accept(); | ||||
| } | } | ||||
| @@ -2242,6 +2386,13 @@ void LogicEditorWidget::mouseDoubleClickEvent(QMouseEvent *event) | |||||
| event->accept(); | event->accept(); | ||||
| } | } | ||||
| void LogicEditorWidget::drawForeground( | |||||
| QPainter *painter, const QRectF &rect) | |||||
| { | |||||
| QGraphicsView::drawForeground(painter, rect); | |||||
| drawGesturePreview(painter); | |||||
| } | |||||
| void LogicEditorWidget::resizeEvent(QResizeEvent *event) | void LogicEditorWidget::resizeEvent(QResizeEvent *event) | ||||
| { | { | ||||
| QGraphicsView::resizeEvent(event); | QGraphicsView::resizeEvent(event); | ||||
| @@ -17,6 +17,7 @@ class QCompleter; | |||||
| class QEvent; | class QEvent; | ||||
| class QLineEdit; | class QLineEdit; | ||||
| class QMouseEvent; | class QMouseEvent; | ||||
| class QPainter; | |||||
| class QRubberBand; | class QRubberBand; | ||||
| class QResizeEvent; | class QResizeEvent; | ||||
| @@ -76,6 +77,7 @@ protected: | |||||
| void mouseMoveEvent(QMouseEvent *event) override; | void mouseMoveEvent(QMouseEvent *event) override; | ||||
| void mouseReleaseEvent(QMouseEvent *event) override; | void mouseReleaseEvent(QMouseEvent *event) override; | ||||
| void mouseDoubleClickEvent(QMouseEvent *event) override; | void mouseDoubleClickEvent(QMouseEvent *event) override; | ||||
| void drawForeground(QPainter *painter, const QRectF &rect) override; | |||||
| void resizeEvent(QResizeEvent *event) override; | void resizeEvent(QResizeEvent *event) override; | ||||
| void scrollContentsBy(int dx, int dy) override; | void scrollContentsBy(int dx, int dy) override; | ||||
| bool eventFilter(QObject *watched, QEvent *event) override; | bool eventFilter(QObject *watched, QEvent *event) override; | ||||
| @@ -108,8 +110,10 @@ private: | |||||
| std::string currentRungId() const; | std::string currentRungId() const; | ||||
| Hit hitAt(const QPointF &scene_position) const; | Hit hitAt(const QPointF &scene_position) const; | ||||
| void beginGesture(const Hit &hit); | void beginGesture(const Hit &hit); | ||||
| void updateGesture(const Hit &hit); | |||||
| void updateGesture(const Hit &hit, const QPointF &scene_position); | |||||
| void finishGesture(const Hit &hit); | void finishGesture(const Hit &hit); | ||||
| void clearGesture(); | |||||
| void drawGesturePreview(QPainter *painter) const; | |||||
| void showCommandEditor(const Hit &hit); | void showCommandEditor(const Hit &hit); | ||||
| void commitCommandInput(); | void commitCommandInput(); | ||||
| void cancelCommandInput(); | void cancelCommandInput(); | ||||
| @@ -162,6 +166,8 @@ private: | |||||
| bool gesture_active_ = false; | bool gesture_active_ = false; | ||||
| Hit gesture_origin_; | Hit gesture_origin_; | ||||
| Hit gesture_current_; | Hit gesture_current_; | ||||
| QPointF gesture_scene_position_; | |||||
| bool gesture_scene_position_valid_ = false; | |||||
| QLineEdit *command_editor_ = nullptr; | QLineEdit *command_editor_ = nullptr; | ||||
| QCompleter *command_completer_ = nullptr; | QCompleter *command_completer_ = nullptr; | ||||
| LogicCommandTarget command_target_; | LogicCommandTarget command_target_; | ||||
| @@ -99,6 +99,16 @@ void testContinuousGridAndIndependentHorizontalWires() | |||||
| "horizontal wires must be persisted independently per cell"); | "horizontal wires must be persisted independently per cell"); | ||||
| } | } | ||||
| fixture.editor.clearHistory(); | |||||
| fixture.projects.restoreModifiedState(false); | |||||
| const LogicEditorResult duplicate = fixture.editor.setHorizontalWireRange( | |||||
| fixture.logicId, rung_id, 2, 5, true); | |||||
| require( | |||||
| duplicate.succeeded | |||||
| && !fixture.projects.isModified() | |||||
| && !fixture.editor.canUndo(), | |||||
| "repeating an existing horizontal wire range must not dirty the project or history"); | |||||
| const LogicEditorResult node = fixture.editor.setConditionAtColumn( | const LogicEditorResult node = fixture.editor.setConditionAtColumn( | ||||
| fixture.logicId, rung_id, 3, contact(3), true); | fixture.logicId, rung_id, 3, contact(3), true); | ||||
| require(node.succeeded, "a wire cell must accept a condition node"); | require(node.succeeded, "a wire cell must accept a condition node"); | ||||
| @@ -685,6 +685,160 @@ void testLadderLayoutAndDragDeletion() | |||||
| "one undo must restore every object removed by the drag selection"); | "one undo must restore every object removed by the drag selection"); | ||||
| } | } | ||||
| void testWireGesturePreviewKeepsAtomicCommit() | |||||
| { | |||||
| TestProjectStorage storage; | |||||
| ProjectService project_service(storage); | |||||
| LogicEditorService editor(project_service); | |||||
| const std::string logic_id = editor.ensureDefaultLogic().id; | |||||
| const std::string first = editor.addRung(logic_id).id; | |||||
| const std::string second = editor.addRung(logic_id).id; | |||||
| editor.clearHistory(); | |||||
| project_service.restoreModifiedState(false); | |||||
| LogicEditorWidget widget(editor); | |||||
| widget.setLogicId(logic_id); | |||||
| widget.setMouseWireMode(LogicEditorWidget::MouseWireMode::Draw); | |||||
| widget.resize(1200, 360); | |||||
| widget.show(); | |||||
| QCoreApplication::processEvents(QEventLoop::AllEvents); | |||||
| constexpr qreal left_bus = 60.0; | |||||
| constexpr qreal cell_width = 96.0; | |||||
| constexpr qreal first_row_center = 91.0; | |||||
| constexpr qreal second_row_center = 197.0; | |||||
| const QPoint first_cell = widget.mapFromScene(QPointF( | |||||
| left_bus + cell_width / 2.0, first_row_center)); | |||||
| const QPoint fourth_cell = widget.mapFromScene(QPointF( | |||||
| left_bus + 3.0 * cell_width + cell_width / 2.0, | |||||
| first_row_center)); | |||||
| const QPoint invalid_target = widget.mapFromScene(QPointF( | |||||
| left_bus + 3.0 * cell_width + cell_width / 2.0, | |||||
| second_row_center)); | |||||
| const auto press = [&widget](const QPoint &point) | |||||
| { | |||||
| QMouseEvent event( | |||||
| QEvent::MouseButtonPress, | |||||
| QPointF(point), | |||||
| Qt::LeftButton, | |||||
| Qt::LeftButton, | |||||
| Qt::NoModifier); | |||||
| QApplication::sendEvent(widget.viewport(), &event); | |||||
| }; | |||||
| const auto move = [&widget](const QPoint &point) | |||||
| { | |||||
| QMouseEvent event( | |||||
| QEvent::MouseMove, | |||||
| QPointF(point), | |||||
| Qt::NoButton, | |||||
| Qt::LeftButton, | |||||
| Qt::NoModifier); | |||||
| QApplication::sendEvent(widget.viewport(), &event); | |||||
| QCoreApplication::processEvents(QEventLoop::AllEvents); | |||||
| }; | |||||
| const auto release = [&widget](const QPoint &point) | |||||
| { | |||||
| QMouseEvent event( | |||||
| QEvent::MouseButtonRelease, | |||||
| QPointF(point), | |||||
| Qt::LeftButton, | |||||
| Qt::NoButton, | |||||
| Qt::NoModifier); | |||||
| QApplication::sendEvent(widget.viewport(), &event); | |||||
| QCoreApplication::processEvents(QEventLoop::AllEvents); | |||||
| }; | |||||
| const auto countPreviewPixels = [&widget](bool invalid) | |||||
| { | |||||
| const QImage image = widget.viewport()->grab().toImage(); | |||||
| int count = 0; | |||||
| for (int y = 0; y < image.height(); ++y) | |||||
| { | |||||
| for (int x = 0; x < image.width(); ++x) | |||||
| { | |||||
| const QColor color = image.pixelColor(x, y); | |||||
| const bool matches = invalid | |||||
| ? color.red() > color.green() + 45 | |||||
| && color.red() > color.blue() + 25 | |||||
| : color.blue() > color.red() + 50 | |||||
| && color.blue() > color.green() + 20; | |||||
| count += matches ? 1 : 0; | |||||
| } | |||||
| } | |||||
| return count; | |||||
| }; | |||||
| QString error_message; | |||||
| QObject::connect( | |||||
| &widget, | |||||
| &LogicEditorWidget::editorError, | |||||
| [&error_message](const QString &message) { error_message = message; }); | |||||
| press(first_cell); | |||||
| move(fourth_cell); | |||||
| require(countPreviewPixels(false) > 20, | |||||
| "a legal drag must paint a visible temporary wire preview"); | |||||
| const LadderRung *first_rung = editor.findRung(logic_id, first); | |||||
| require( | |||||
| std::all_of( | |||||
| first_rung->cells.cbegin(), | |||||
| first_rung->cells.cend(), | |||||
| [](const LadderCell &cell) | |||||
| { | |||||
| return cell.kind == LadderCellKind::Gap; | |||||
| }) | |||||
| && !project_service.isModified() | |||||
| && !editor.canUndo(), | |||||
| "the temporary preview must not modify the project or history"); | |||||
| move(invalid_target); | |||||
| require(countPreviewPixels(true) > 20, | |||||
| "an invalid drag direction must switch the preview to an error color"); | |||||
| release(invalid_target); | |||||
| first_rung = editor.findRung(logic_id, first); | |||||
| require( | |||||
| !error_message.isEmpty() | |||||
| && std::all_of( | |||||
| first_rung->cells.cbegin(), | |||||
| first_rung->cells.cend(), | |||||
| [](const LadderCell &cell) | |||||
| { | |||||
| return cell.kind == LadderCellKind::Gap; | |||||
| }) | |||||
| && editor.findRung(logic_id, second) != nullptr | |||||
| && !project_service.isModified() | |||||
| && !editor.canUndo(), | |||||
| "releasing an invalid gesture must discard its complete preview"); | |||||
| error_message.clear(); | |||||
| press(first_cell); | |||||
| move(fourth_cell); | |||||
| require(!project_service.isModified() && !editor.canUndo(), | |||||
| "a legal gesture must remain temporary until mouse release"); | |||||
| release(fourth_cell); | |||||
| first_rung = editor.findRung(logic_id, first); | |||||
| require( | |||||
| error_message.isEmpty() | |||||
| && first_rung->cells[0].kind == LadderCellKind::Wire | |||||
| && first_rung->cells[1].kind == LadderCellKind::Wire | |||||
| && first_rung->cells[2].kind == LadderCellKind::Wire | |||||
| && first_rung->cells[3].kind == LadderCellKind::Wire | |||||
| && project_service.isModified() | |||||
| && editor.canUndo(), | |||||
| "releasing a legal gesture must commit its complete wire range once"); | |||||
| require(editor.undo().succeeded && !editor.canUndo(), | |||||
| "one undo must remove the complete committed gesture"); | |||||
| first_rung = editor.findRung(logic_id, first); | |||||
| require( | |||||
| std::all_of( | |||||
| first_rung->cells.cbegin(), | |||||
| first_rung->cells.cend(), | |||||
| [](const LadderCell &cell) | |||||
| { | |||||
| return cell.kind == LadderCellKind::Gap; | |||||
| }), | |||||
| "undo must restore every cell changed by the gesture"); | |||||
| } | |||||
| void testCursorAdvanceAndInlineCommandInput() | void testCursorAdvanceAndInlineCommandInput() | ||||
| { | { | ||||
| TestProjectStorage storage; | TestProjectStorage storage; | ||||
| @@ -1077,6 +1231,7 @@ int main(int argc, char *argv[]) | |||||
| testQueuedOfflineTraceIsIgnoredAfterReturningToEditing(); | testQueuedOfflineTraceIsIgnoredAfterReturningToEditing(); | ||||
| testLogicEditorGridSelectionAndDeletion(); | testLogicEditorGridSelectionAndDeletion(); | ||||
| testLadderLayoutAndDragDeletion(); | testLadderLayoutAndDragDeletion(); | ||||
| testWireGesturePreviewKeepsAtomicCommit(); | |||||
| testCursorAdvanceAndInlineCommandInput(); | testCursorAdvanceAndInlineCommandInput(); | ||||
| testVerticalWireShortcutAdvancesDownward(); | testVerticalWireShortcutAdvancesDownward(); | ||||
| testSegmentLevelTraceProjection(); | testSegmentLevelTraceProjection(); | ||||