wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
SceneInteraction.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <utility>
7
8#include <QBrush>
9#include <QGraphicsSceneMouseEvent>
10#include <QGraphicsView>
11#include <QPainterPath>
12#include <QPen>
13
17#include "App/Scene/Commands.h"
19#include "App/Scene/Scene.h"
21#include "App/Wiring/Port.h"
22
24 : m_scene(scene)
25{
26}
27
29{
30 // The rubber-band selection rectangle must not itself be selectable or it would
31 // appear in selectedItems() and interfere with element operations
32 m_selectionRect.setFlag(QGraphicsItem::ItemIsSelectable, false);
33 m_scene->addItem(&m_selectionRect);
34
35 // Used to throttle expensive operations during drag (e.g., ensureVisible)
36 m_timer.start();
37}
38
40{
41 m_selectionRect.setBrush(QBrush(theme.m_selectionBrush));
42 m_selectionRect.setPen(QPen(theme.m_selectionPen, 1, Qt::SolidLine));
43}
44
45void SceneInteraction::startSelectionRect()
46{
47 m_selectionStartPoint = m_mousePos;
48 m_markingSelectionBox = true;
49 m_selectionRect.setRect(QRectF(m_selectionStartPoint, m_selectionStartPoint));
50 m_selectionRect.show();
51 m_selectionRect.update();
52}
53
54bool SceneInteraction::mousePress(QGraphicsSceneMouseEvent *event)
55{
56 m_mousePos = event->scenePos();
57 m_scene->connectionManager()->updateHover(m_mousePos);
58
59 auto *item = m_scene->itemAt(m_mousePos);
60
61 if (item) {
62 // --- Element selection and drag preparation ---
63 if (event->button() == Qt::LeftButton) {
64 if (event->modifiers().testFlag(Qt::ControlModifier)) {
65 // Ctrl+click toggles individual item membership in the selection
66 item->setSelected(!item->isSelected());
67 }
68
69 auto selectedElements_ = m_scene->selectedElements();
70
71 // Include the clicked element even if it isn't yet selected, so a
72 // single-click drag of an unselected element works immediately
73 if (auto *element = qgraphicsitem_cast<GraphicElement *>(item)) {
74 selectedElements_ << element;
75 }
76
77 m_draggingElement = ((item->type() == GraphicElement::Type) && !selectedElements_.isEmpty());
78 if (m_draggingElement) {
79 sentryBreadcrumb("ui", QStringLiteral("Drag started: %1 element(s)").arg(selectedElements_.size()));
80 }
81
82 // Snapshot positions now; MoveCommand compares these against release-time positions.
83 // QPointer in m_dragSnapshot lets entries auto-clear if the element is destroyed
84 // before release (e.g. Delete shortcut while mid-drag — see WIREDPANDA-H9).
85 m_dragSnapshot.clear();
86 m_dragSnapshot.reserve(selectedElements_.size());
87
88 for (auto *element : std::as_const(selectedElements_)) {
89 m_dragSnapshot.append({element, element->pos()});
90 }
91 }
92
93 // --- Wire connection handling ---
94 if (item->type() == Port::Type) {
95 /* When the mouse is pressed over an connected input port, the line
96 * is disconnected and can be connected to another port. */
97 if (m_scene->connectionManager()->hasEditedConnection()) {
98 // An in-progress wire exists; try to complete it at this port
99 m_scene->connectionManager()->tryComplete(m_mousePos);
100 return true;
101 }
102
103 auto *pressedPort = qgraphicsitem_cast<Port *>(item);
104
105 if (auto *startPort = dynamic_cast<OutputPort *>(pressedPort)) {
106 m_scene->connectionManager()->startFromOutput(startPort);
107 return true;
108 }
109
110 if (auto *endPort = dynamic_cast<InputPort *>(pressedPort)) {
111 // Empty input port: begin a new wire; occupied port: detach the existing wire
112 endPort->connections().isEmpty() ? m_scene->connectionManager()->startFromInput(endPort) : m_scene->connectionManager()->detach(endPort);
113 return true;
114 }
115 }
116 }
117
118 // Clicking on empty space while a wire is being drawn cancels it
119 m_scene->connectionManager()->cancel();
120
121 if (!item && (event->button() == Qt::LeftButton)) {
122 startSelectionRect();
123 }
124
125 if (event->button() == Qt::RightButton) {
126 m_scene->contextMenu(event->screenPos());
127 return true;
128 }
129
130 return false;
131}
132
133bool SceneInteraction::mouseMove(QGraphicsSceneMouseEvent *event)
134{
135 m_mousePos = event->scenePos();
136 m_scene->connectionManager()->updateHover(m_mousePos);
137
138 // Expand scene rect while dragging so elements can be moved beyond the current boundary,
139 // and auto-scroll the viewport to follow the cursor.
140 if (m_draggingElement) {
141 m_scene->resizeScene();
142
143 if (m_timer.elapsed() > 50) {
144 const auto viewList = m_scene->views();
145 if (!viewList.isEmpty()) {
146 viewList.first()->ensureVisible(m_mousePos.x(), m_mousePos.y(), 1, 1, 50, 50);
147 }
148 m_timer.restart();
149 }
150 }
151
152 // --- In-progress wire routing ---
153 if (m_scene->connectionManager()->hasEditedConnection()) {
154 m_scene->connectionManager()->updateEditedEnd(m_mousePos);
155 return true;
156 }
157
158 // --- Rubber-band selection rectangle ---
159 if (m_markingSelectionBox) {
160 const QRectF rect = QRectF(m_selectionStartPoint, m_mousePos).normalized();
161 m_selectionRect.setRect(rect);
162 QPainterPath selectionBox;
163 selectionBox.addRect(rect);
164 m_scene->setSelectionArea(selectionBox);
165 }
166
167 return false;
168}
169
170bool SceneInteraction::mouseRelease(QGraphicsSceneMouseEvent *event)
171{
172 if (m_draggingElement && (event->button() == Qt::LeftButton)) {
173 bool moved = false;
174
175 if (!m_dragSnapshot.empty()) {
176 // Filter out elements destroyed mid-drag (their QPointers are now null).
177 // Without this, dereferencing a freed element below crashes the app.
178 QList<GraphicElement *> liveElements;
179 QList<QPointF> liveOldPositions;
180 liveElements.reserve(m_dragSnapshot.size());
181 liveOldPositions.reserve(m_dragSnapshot.size());
182
183 for (const auto &[ptr, oldPos] : std::as_const(m_dragSnapshot)) {
184 if (ptr) {
185 liveElements.append(ptr.data());
186 liveOldPositions.append(oldPos);
187 }
188 }
189
190 // Only push a MoveCommand if at least one surviving element actually changed
191 // position; avoids polluting the undo stack with no-op moves (click without drag).
192 for (int i = 0; i < liveElements.size(); ++i) {
193 if (liveElements.at(i)->pos() != liveOldPositions.at(i)) {
194 moved = true;
195 break;
196 }
197 }
198
199 if (moved) {
200 m_scene->receiveCommand(new MoveCommand(liveElements, liveOldPositions, m_scene));
201
202 // Resize while m_draggingElement is still true, so this still takes
203 // resizeScene()'s expand-only branch like every other call during this
204 // gesture. Tightening here instead — after the flag is cleared below —
205 // would shrink the scene rect at the exact instant of mouse-up and jump
206 // the viewport to a different scroll position.
207 m_scene->resizeScene();
208 }
209 }
210
211 sentryBreadcrumb("ui", moved ? QStringLiteral("Drag ended: moved") : QStringLiteral("Drag ended: no move"));
212 m_draggingElement = false;
213 m_dragSnapshot.clear();
214 }
215
216 m_selectionRect.hide();
217 m_markingSelectionBox = false;
218
219 // Complete an in-progress wire on mouse release (when no button is held)
220 // — this handles the drag-to-connect gesture (press output → drag → release on input)
221 if (m_scene->connectionManager()->hasEditedConnection() && (event->buttons() == Qt::NoButton)) {
222 m_scene->connectionManager()->tryComplete(m_mousePos);
223 return true;
224 }
225
226 return false;
227}
228
229bool SceneInteraction::mouseDoubleClick(QGraphicsSceneMouseEvent *event)
230{
231 if (event->modifiers().testFlag(Qt::ControlModifier)) {
232 return true;
233 }
234
235 // Double-click on a fully connected wire inserts a routing node at the click point,
236 // splitting the wire into two segments; guard ensures it's not a dangling wire
237 if (auto *connection = qgraphicsitem_cast<Connection *>(m_scene->itemAt(m_mousePos)); connection && connection->startPort() && connection->endPort()) {
238 m_scene->receiveCommand(new SplitCommand(connection, m_mousePos, m_scene));
239 }
240
241 return false;
242}
All QUndoCommand subclasses and the CommandUtils helper namespace.
ConnectionManager: manages wire creation, deletion, validation and hover feedback.
Connection: a wire that connects an output port to an input port in the circuit scene.
Abstract base class for all graphical circuit elements.
Port classes: Port (base), InputPort, and OutputPort.
SceneInteraction: owns the scene's mouse-driven editing state and gestures.
Main circuit editing scene with undo/redo and user interaction.
Lightweight Sentry helpers gated behind HAVE_SENTRY.
void sentryBreadcrumb(const char *category, const QString &message)
Theme management types and singleton ThemeManager.
A port that receives a signal (the destination end of a wire).
Definition Port.h:229
Undo command that records a drag-move of a set of elements.
Definition Commands.h:160
A port that drives a signal (the source end of a wire).
Definition Port.h:261
@ Type
Definition Port.h:43
bool mouseMove(QGraphicsSceneMouseEvent *event)
bool mousePress(QGraphicsSceneMouseEvent *event)
SceneInteraction(Scene *scene)
void applyTheme(const ThemeAttributes &theme)
Applies the theme's selection-rectangle brush and pen.
bool mouseRelease(QGraphicsSceneMouseEvent *event)
bool mouseDoubleClick(QGraphicsSceneMouseEvent *event)
Main circuit editing scene.
Definition Scene.h:56
Undo command that inserts a Node junction into an existing connection.
Definition Commands.h:250
Contains all color attributes for a theme.
QColor m_selectionBrush
QColor m_selectionPen