wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ConnectionManager.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 <QGraphicsView>
9
12#include "App/Scene/Commands.h"
15#include "App/Scene/Scene.h"
19#include "App/Wiring/Port.h"
20
21namespace {
24PortHoverLabel::Side sideFor(Port *port)
25{
26 auto *element = port->graphicElement();
27 if (!element) {
29 }
30
31 const QPointF delta = port->scenePos() - element->sceneBoundingRect().center();
32 if (qAbs(delta.x()) >= qAbs(delta.y())) {
34 }
36}
37
40int encodePortIndex(GraphicElement *element, Port *port)
41{
42 for (int i = 0; i < (element->inputSize() + element->outputSize()); ++i) {
43 if (i < element->inputSize()) {
44 if (port == element->inputPort(i)) {
45 return i;
46 }
47 } else if (port == element->outputPort(i - element->inputSize())) {
48 return i;
49 }
50 }
51 return -1;
52}
53
57Port *decodePort(Scene *scene, const int elmId, const int portNumber)
58{
59 auto *element = dynamic_cast<GraphicElement *>(scene->itemById(elmId));
60 if (!element) {
61 return nullptr;
62 }
63 if (portNumber < element->inputSize()) {
64 return element->inputPort(portNumber);
65 }
66 if ((portNumber - element->inputSize()) < element->outputSize()) {
67 return element->outputPort(portNumber - element->inputSize());
68 }
69 return nullptr;
70}
71} // namespace
72
74 : m_scene(scene)
75{
76}
77
78// --- Wire creation workflow ---
79
81{
82 sentryBreadcrumb("ui", QStringLiteral("Wire started"));
83 auto *connection = new Connection();
84 connection->setStartPort(startPort);
85 connection->setEndPos(m_scene->mousePos());
86
87 m_scene->addItem(connection);
88 setEditedConnection(connection);
89 connection->updatePath();
90}
91
93{
94 auto *connection = new Connection();
95 connection->setEndPort(endPort);
96 connection->setStartPos(m_scene->mousePos());
97
98 m_scene->addItem(connection);
99 setEditedConnection(connection);
100 connection->updatePath();
101}
102
103void ConnectionManager::tryComplete(const QPointF &scenePos)
104{
105 auto *connection = editedConnection();
106 // Same lookup as updateHover(), so the wire lands on the port whose hover
107 // highlight the user is currently seeing.
108 auto *port = m_scene->portAt(scenePos);
109
110 if (!port || !connection) {
111 return;
112 }
113
114 /* The mouse is released over a Port. */
115 OutputPort *startPort = nullptr;
116 InputPort *endPort = nullptr;
117
118 // A wire being dragged from an output needs the drop target to be an input, and vice versa.
119 // The dynamic_cast returns nullptr if the port type doesn't match, which is caught below.
120 if (connection->startPort() != nullptr) {
121 startPort = connection->startPort();
122 endPort = dynamic_cast<InputPort *>(port);
123 } else if (connection->endPort() != nullptr) {
124 startPort = dynamic_cast<OutputPort *>(port);
125 endPort = connection->endPort();
126 }
127
128 if (!startPort || !endPort) {
129 return;
130 }
131
132 /* Verifying if the connection is valid. */
133 const QString rejection = connectionRejectionReason(startPort, endPort);
134 if (rejection.isEmpty()) {
135 connection->setStartPort(startPort);
136 connection->setEndPort(endPort);
137 sentryBreadcrumb("ui", QStringLiteral("Wire connected"));
138 m_scene->receiveCommand(new AddItemsCommand({connection}, m_scene));
139 setEditedConnection(nullptr);
140 } else {
141 // Explain why the wire vanished instead of dropping it silently.
142 m_scene->showStatusMessage(rejection);
143 deleteEditedConnection();
144 }
145}
146
148{
149 sentryBreadcrumb("ui", QStringLiteral("Wire cancelled"));
150 if (editedConnection()) {
151 deleteEditedConnection();
152 }
153}
154
156{
157 sentryBreadcrumb("ui", QStringLiteral("Wire detached"));
158 const auto connections = endPort->connections();
159 if (connections.isEmpty()) {
160 return;
161 }
162 // Take the last connection — an input port normally has at most one, but
163 // .last() is the safe choice if the model ever allows multiple
164 auto *connection = connections.last();
165
166 if (auto *startPort = connection->startPort()) {
167 // Delete the existing wire, then immediately start a new in-progress wire
168 // anchored to the same output port, so the user can re-route it
169 m_scene->receiveCommand(new DeleteItemsCommand({connection}, m_scene));
170 startFromOutput(startPort);
171 }
172}
173
174// --- In-progress wire state ---
175
177{
178 return editedConnection() != nullptr;
179}
180
182{
183 return dynamic_cast<Connection *>(m_scene->itemById(m_editedConnectionId));
184}
185
186void ConnectionManager::updateEditedEnd(const QPointF &scenePos)
187{
188 auto *connection = editedConnection();
189 if (!connection) {
190 return;
191 }
192
193 if (connection->startPort()) {
194 // Wire anchored at start: free end follows the mouse
195 connection->setEndPos(scenePos);
196 connection->updatePath();
197 return;
198 }
199
200 if (connection->endPort()) {
201 // Wire anchored at end (dragged from input): free start follows the mouse
202 connection->setStartPos(scenePos);
203 connection->updatePath();
204 return;
205 }
206
207 // Connection lost both ports (e.g., element deleted mid-drag) — clean up
208 deleteEditedConnection();
209}
210
211// --- Hover feedback ---
212
213void ConnectionManager::updateHover(const QPointF &scenePos)
214{
215 // Runs on every mouse move: portAt() is the cheap bounding-box lookup -- itemAt()'s
216 // shape-exact queries would re-stroke and clip every wire near the cursor just to
217 // discard them here.
218 auto *port = m_scene->portAt(scenePos);
219 auto *hoverPort_ = hoverPort();
220
221 // Only tear down and rebuild the hover state when the port under the cursor
222 // actually changes. Rebuilding every mouse-move would recreate the peer label
223 // chips each frame and make them flicker while the cursor sits on one port.
224 if (hoverPort_ != port) {
225 if (hoverPort_) {
226 releaseHoverPort();
227 }
228 if (port) {
229 setHoverPort(port);
230 }
231 }
232
233 if (port) {
234 auto *editedConn = editedConnection();
235 if (editedConn && editedConn->startPort() && (editedConn->startPort()->isOutput() == port->isOutput())) {
236 auto *view = m_scene->view();
237 view->viewport()->setCursor(Qt::ForbiddenCursor);
238 }
239 }
240}
241
243{
244 releaseHoverPort();
245}
246
247// --- Validation ---
248
250{
251 return connectionRejectionReason(startPort, endPort).isEmpty();
252}
253
255{
256 if (!startPort || !endPort) {
257 return tr("This connection is not allowed.");
258 }
259 if (startPort->graphicElement() == endPort->graphicElement()) {
260 return tr("Can't connect an element to itself.");
261 }
262 if (startPort->isConnected(endPort)) {
263 return tr("These ports are already connected.");
264 }
265 // Rx nodes receive their signal over the air; a physical wire on the input
266 // port would be silently overridden by the simulation (tunnel convention).
267 if (auto *elm = endPort->graphicElement(); elm && elm->wirelessMode() == WirelessMode::Rx) {
268 return tr("This element receives wirelessly — no input wire needed.");
269 }
270 // Tx nodes are dead-end transmitters; their output port drives the wireless
271 // channel only — no physical wire should bypass the channel (tunnel convention).
272 if (auto *elm = startPort->graphicElement(); elm && elm->wirelessMode() == WirelessMode::Tx) {
273 return tr("This element transmits wirelessly — no output wire needed.");
274 }
275 return {};
276}
277
278// --- Private helpers ---
279
280void ConnectionManager::setEditedConnection(Connection *connection)
281{
282 if (connection) {
283 connection->setFocus();
284 m_editedConnectionId = connection->id();
285 } else {
286 m_editedConnectionId = 0;
287 }
288}
289
290void ConnectionManager::deleteEditedConnection()
291{
292 if (auto *connection = editedConnection()) {
293 SimulationBlocker blocker(m_scene->simulation());
294 m_scene->removeItem(connection);
295 delete connection;
296 m_scene->setCircuitUpdateRequired();
297 }
298
299 setEditedConnection(nullptr);
300}
301
302void ConnectionManager::setHoverPort(Port *port)
303{
304 if (!port) {
305 m_hoverPortElmId = 0;
306 m_hoverPortNumber = 0;
307 return;
308 }
309
310 port->hoverEnter();
311 auto *hoverElm = port->graphicElement();
312
313 // Store element ID + port index rather than raw pointers so the hover state
314 // remains valid across undo/redo operations that may recreate the element
315 if (hoverElm && m_scene->contains(hoverElm->id())) {
316 m_hoverPortElmId = hoverElm->id();
317 m_hoverPortNumber = encodePortIndex(hoverElm, port);
318 }
319
320 // Highlight the connected ports immediately so the user can trace where the hovered
321 // port's wires land. Track exactly which peers (same element-ID + port-index scheme as
322 // above — Port isn't a QObject, so a raw pointer here could dangle if the peer's
323 // element is deleted mid-hover) so releaseHoverPort() can release the same set later
324 // even if connectivity changes mid-hover. Labels are shown separately by
325 // showHoverLabels(), timed to match the native tooltip via Scene::helpEvent.
326 m_highlightedPeers.clear();
327 for (auto *peer : connectedPeers(port)) {
328 peer->hoverEnter();
329 if (auto *peerElm = peer->graphicElement(); peerElm && m_scene->contains(peerElm->id())) {
330 m_highlightedPeers.append(qMakePair(peerElm->id(), encodePortIndex(peerElm, peer)));
331 }
332 }
333}
334
336{
337 clearHoverLabels();
338
339 if (!port) {
340 return;
341 }
342
343 // Reveal the label of the hovered port itself and every connected peer in situ, so the
344 // user can compare a wire's two endpoint labels without tracing it across the canvas.
345 // Ports without a name (most basic gates) are skipped, matching the native tooltip this
346 // replaces (which silently shows nothing for empty text). Each chip is anchored at its
347 // port and biased away from the element body, accounting for rotation/mirroring.
348 const auto addLabel = [this](Port *target) {
349 if (!target || target->name().isEmpty()) {
350 return;
351 }
352 auto *label = new PortHoverLabel(target->name(), sideFor(target));
353 label->setPos(target->scenePos());
354 m_scene->addItem(label);
355 m_peerLabels.append(label);
356 };
357
358 addLabel(port);
359 for (auto *peer : connectedPeers(port)) {
360 addLabel(peer);
361 }
362}
363
364void ConnectionManager::releaseHoverPort()
365{
366 clearHoverLabels();
367
368 const auto peerRefs = std::exchange(m_highlightedPeers, {});
369 for (const auto &ref : peerRefs) {
370 if (auto *peer = decodePort(m_scene, ref.first, ref.second)) {
371 peer->hoverLeave();
372 }
373 }
374
375 if (auto *hoverPort_ = hoverPort()) {
376 hoverPort_->hoverLeave();
377 setHoverPort(nullptr);
378 auto *view = m_scene->view();
379 view->viewport()->unsetCursor();
380 }
381}
382
383QList<Port *> ConnectionManager::connectedPeers(Port *port)
384{
385 QList<Port *> peers;
386 if (!port) {
387 return peers;
388 }
389
390 const auto &connections = port->connections();
391 peers.reserve(connections.size());
392 for (auto *conn : connections) {
393 Port *peer = (conn->startPort() == port) ? static_cast<Port *>(conn->endPort())
394 : static_cast<Port *>(conn->startPort());
395 if (peer) {
396 peers.append(peer);
397 }
398 }
399
400 return peers;
401}
402
403void ConnectionManager::clearHoverLabels()
404{
405 for (const auto &label : std::as_const(m_peerLabels)) {
406 if (!label.isNull()) {
407 m_scene->removeItem(label);
408 delete label.data();
409 }
410 }
411
412 m_peerLabels.clear();
413}
414
415Port *ConnectionManager::hoverPort()
416{
417 Port *hoverPort = decodePort(m_scene, m_hoverPortElmId, m_hoverPortNumber);
418
419 if (!hoverPort) {
420 setHoverPort(nullptr);
421 }
422
423 return hoverPort;
424}
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.
Extended QGraphicsView with zoom, pan, and fast-rendering modes.
PortHoverLabel: a transient tooltip-style chip showing a port's label on the canvas.
Port classes: Port (base), InputPort, and OutputPort.
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)
RAII guard that temporarily stops the simulation while in scope.
Synchronous cycle-based simulation engine with event-driven clock support.
Undo command that adds a list of graphic elements to the scene.
Definition Commands.h:67
void clearHover()
Clears the current hover state (unhighlights the port, resets cursor).
void detach(InputPort *endPort)
Detaches the existing wire on endPort and starts a new one from the same output.
bool hasEditedConnection() const
Returns true if a wire is currently being drawn.
void tryComplete(const QPointF &scenePos)
Attempts to complete the in-progress wire at the port under scenePos.
void startFromOutput(OutputPort *startPort)
Begins a new in-progress wire anchored at startPort (output port).
Connection * editedConnection() const
Returns the in-progress wire, or nullptr.
static bool isConnectionAllowed(OutputPort *startPort, InputPort *endPort)
Returns true if a wire from startPort to endPort is permitted.
void showHoverLabels(Port *port)
Shows in-situ label chips for port itself and every port connected to it.
void updateHover(const QPointF &scenePos)
Updates the hover state for the port under scenePos.
ConnectionManager(Scene *scene)
void updateEditedEnd(const QPointF &scenePos)
Updates the free end of the in-progress wire to follow scenePos.
void cancel()
Cancels and deletes the in-progress wire, if any.
static QString connectionRejectionReason(OutputPort *startPort, InputPort *endPort)
void startFromInput(InputPort *endPort)
Begins a new in-progress wire anchored at endPort (input port).
A bezier-curve wire connecting an output port to an input port in the scene.
Definition Connection.h:38
Undo command that removes a list of items from the scene.
Definition Commands.h:98
Abstract base class for all graphical circuit elements in wiRedPanda.
virtual WirelessMode wirelessMode() const
Returns the wireless routing mode for this element.
int inputSize() const
Returns the current number of input ports.
InputPort * inputPort(const int index=0) const
Returns the input port at index (default 0).
int outputSize() const
Returns the current number of output ports.
OutputPort * outputPort(const int index=0) const
Returns the output port at index (default 0).
A port that receives a signal (the destination end of a wire).
Definition Port.h:229
int id() const
Returns the unique integer identifier of this item, or -1 if unassigned.
Definition ItemWithId.h:40
A port that drives a signal (the source end of a wire).
Definition Port.h:261
A small floating label drawn next to a port while a connected port is hovered.
Side
Which side of the port the chip is biased towards, away from the element body.
Abstract base class for circuit element ports (connection endpoints).
Definition Port.h:39
virtual bool isOutput() const =0
Returns true if this is an output port. Pure virtual.
void hoverEnter()
Applies hover-enter visual feedback.
Definition Port.cpp:218
GraphicElement * graphicElement()
Returns the graphic element that owns this port.
Definition Port.h:66
bool isConnected(Port *otherPort)
Returns true if this port is connected to otherPort via any wire.
Definition Port.cpp:94
const QList< Connection * > & connections() const
Returns the list of wires attached to this port.
Definition Port.cpp:57
Main circuit editing scene.
Definition Scene.h:56
ItemWithId * itemById(int id) const
Returns the item registered under id, or nullptr if not found.
Definition Scene.cpp:171