wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
Scene.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
4#include "App/Scene/Scene.h"
5
6#include <algorithm>
7#include <cmath>
8
9#include <QClipboard>
10#include <QDrag>
11#include <QGraphicsSceneDragDropEvent>
12#include <QGraphicsSceneHelpEvent>
13#include <QKeyEvent>
14#include <QMenu>
15#include <QMimeData>
16#include <QPolygon>
17#include <QScrollBar>
18#include <QUrl>
19
20#include "App/Core/Common.h"
21#include "App/Core/Constants.h"
22#include "App/Core/ItemWithId.h"
23#include "App/Core/MimeTypes.h"
24#include "App/Core/Priorities.h"
32#include "App/Element/IC.h"
35#include "App/Scene/Commands.h"
38
39namespace {
40// Adaptive wire antialiasing constants (see the Scene.h accessor block for the design).
41// All are time-based or dimensionless, so their meaning is machine-independent.
42
44constexpr qint64 kWireAaBudgetNs = 50'000'000;
45
48constexpr int kWireAaDebouncePasses = 2;
49
51constexpr qint64 kWireAaPassIdleMs = 300;
52
56constexpr qint64 kWireAaFlipIdleMs = 3000;
57
62constexpr qint64 kWireAaWorstCaseRatio = 10;
63
66constexpr int kWireAaHeadroomRestorePasses = 10;
67} // namespace
68
69Scene::Scene(QObject *parent)
70 : QGraphicsScene(parent)
71 , m_simulation(this, this)
72{
73 // The scene filters its own events to intercept mouse events before items see them
74 // (e.g., to detect Ctrl+drag for cloning before Qt's default drag-selection activates)
75 installEventFilter(this);
76
77 // Attach the interaction layer: adds its rubber-band selection rectangle to the
78 // scene and starts its drag throttle timer.
79 m_interaction.attachToScene();
80
81 m_undoAction = new QAction(tr("&Undo"), this);
82 m_undoAction->setEnabled(false);
83 m_undoAction->setIcon(QIcon(":/Interface/Toolbar/undo.svg"));
84 m_undoAction->setShortcut(QKeySequence::Undo);
85 connect(&m_undoStack, &QUndoStack::canUndoChanged, m_undoAction, &QAction::setEnabled);
86 connect(&m_undoStack, &QUndoStack::undoTextChanged, this, &Scene::updateUndoText);
87 connect(m_undoAction, &QAction::triggered, &m_undoStack, &QUndoStack::undo);
88
89 m_redoAction = new QAction(tr("&Redo"), this);
90 m_redoAction->setEnabled(false);
91 m_redoAction->setIcon(QIcon(":/Interface/Toolbar/redo.svg"));
92 m_redoAction->setShortcut(QKeySequence::Redo);
93 connect(&m_undoStack, &QUndoStack::canRedoChanged, m_redoAction, &QAction::setEnabled);
94 connect(&m_undoStack, &QUndoStack::redoTextChanged, this, &Scene::updateRedoText);
95 connect(m_redoAction, &QAction::triggered, &m_undoStack, &QUndoStack::redo);
96
98 // Emit autosave signal only after each undo-stack index change (not on every internal state update)
99 connect(&m_undoStack, &QUndoStack::indexChanged, this, &Scene::checkUpdateRequest);
100
101 // Primary restore path for adaptive wire antialiasing: the timeout re-checks both
102 // activity timestamps and restores only once their windows have truly elapsed.
103 m_wireAaIdleTimer.setSingleShot(true);
104 connect(&m_wireAaIdleTimer, &QTimer::timeout, this, [this] { checkWireIdleRestore(); });
105 m_wireFlipTimer.start();
106 m_wirePassTimer.start();
107}
108
109void Scene::checkUpdateRequest()
110{
111 // Coalesces multiple rapid undo-stack changes into a single autosave signal:
112 // the flag is set by setCircuitUpdateRequired() and cleared here after emitting.
113 if (m_autosaveRequired) {
114 emit circuitHasChanged();
115 m_autosaveRequired = false;
116 }
117}
118
119void Scene::addItem(QGraphicsItem *item)
120{
121 if (!item) {
122 return;
123 }
124 QGraphicsScene::addItem(item);
125 if (auto *iwid = dynamic_cast<ItemWithId *>(item)) {
126 if (iwid->id() < 0) {
127 // Unassigned item: give it the next scene-local ID
128 iwid->setId(nextId());
129 } else {
130 // Pre-assigned item (restored by updateItemId for undo/redo): preserve it
131 setLastId(iwid->id());
132 }
133 registerItem(iwid);
134 }
135
136 // Register element-type-specific hooks
137 if (item->type() == GraphicElement::Type) {
138 auto *elm = qgraphicsitem_cast<GraphicElement *>(item);
139 if (!elm) {
140 return;
141 }
142 connect(elm, &GraphicElement::inlineEditRequested, this, [this](GraphicElement *element) {
143 m_inlineLabelEditor.start(element);
144 });
145 if (auto *ic = qobject_cast<IC *>(elm)) {
146 if (!ic->file().isEmpty()) {
147 m_icRegistry.watchFile(ic->file());
148 }
151 connect(ic, &IC::previewMoved, this, &Scene::icPreviewMoved);
154 } else if (auto *tt = qobject_cast<TruthTable *>(elm)) {
156 }
157 }
158}
159
160void Scene::removeItem(QGraphicsItem *item)
161{
162 if (!item) {
163 return;
164 }
165 if (auto *iwid = dynamic_cast<ItemWithId *>(item)) {
166 unregisterItem(iwid);
167 }
168 QGraphicsScene::removeItem(item);
169}
170
171ItemWithId *Scene::itemById(const int id) const
172{
173 return m_itemRegistry.itemById(id);
174}
175
176bool Scene::contains(const int id) const
177{
178 return m_itemRegistry.contains(id);
179}
180
181int Scene::lastId() const
182{
183 return m_itemRegistry.lastId();
184}
185
186void Scene::setLastId(const int newLastId)
187{
188 m_itemRegistry.setLastId(newLastId);
189}
190
192{
193 return m_itemRegistry.nextId();
194}
195
196void Scene::updateItemId(ItemWithId *item, const int newId)
197{
198 m_itemRegistry.updateItemId(item, newId);
199}
200
201void Scene::forgetItemId(const int id)
202{
203 m_itemRegistry.forgetItemId(id);
204}
205
206void Scene::registerItem(ItemWithId *item)
207{
208 m_itemRegistry.registerItem(item);
209}
210
211void Scene::unregisterItem(ItemWithId *item)
212{
213 m_itemRegistry.unregisterItem(item);
214}
215
216SerializationContext Scene::deserializationContext(QHash<quint64, Port *> &portMap, const QVersionNumber &version, SerializationPurpose purpose)
217{
218 SerializationContext context = {.portMap = portMap, .version = version, .purpose = purpose, .contextDir = contextDir()};
219 context.blobRegistry = &m_icRegistry.blobMapRef();
220 return context;
221}
222
223void Scene::drawBackground(QPainter *painter, const QRectF &rect)
224{
225 // Flush any pending visibility reapply before items are painted: drawBackground()
226 // always runs before Qt's item-painting pass for this same frame (Qt calls
227 // drawBackground(), then paints visible items, then drawForeground()), so applying
228 // setVisible() here still takes effect for the frame about to be drawn. See
229 // setPropertyUpdateRequired() for why this is deferred instead of done eagerly there.
230 if (m_visibilityDirty) {
231 m_visibilityDirty = false;
232 m_visibilityManager.reapply();
233 }
234
235 // m11() is the X-axis scale factor of the view transform; below 0.3 the grid dots
236 // would be sub-pixel and invisible anyway, so skip drawing for performance
237 if (view() and view()->transform().m11() < 0.3) {
238 return;
239 }
240
241 QGraphicsScene::drawBackground(painter, rect);
242
243 const int gridSize = Constants::gridSize;
244 const int left = static_cast<int>(std::floor(rect.left() / gridSize)) * gridSize;
245 const int top = static_cast<int>(std::floor(rect.top() / gridSize)) * gridSize;
246 const int right = static_cast<int>(std::ceil(rect.right() / gridSize)) * gridSize;
247 const int bottom = static_cast<int>(std::ceil(rect.bottom() / gridSize)) * gridSize;
248
249 // Bound the grid regardless of view zoom: \a rect isn't necessarily clipped to the
250 // interactive view's viewport — a headless scene->render() call (CircuitExporter's
251 // PDF/image export) passes the *entire* scene bounding rect as the exposed area, which
252 // for a scene containing one element at an extreme-but-finite position (the only check
253 // on load rejects non-finite, not large, coordinates) can span millions of grid cells.
254 // Left unchecked, the loop below builds a point list with billions of entries — hanging
255 // and exhausting memory well before drawPoints() ever runs. Compute in 64-bit: the naive
256 // int product overflows for exactly this scenario, wrapping to a small/negative value
257 // that would pass a small reserve() through to an unbounded loop anyway.
258 const qint64 columns = (static_cast<qint64>(right) - left) / gridSize + 1;
259 const qint64 rows = (static_cast<qint64>(bottom) - top) / gridSize + 1;
260 constexpr qint64 kMaxGridPoints = 1'000'000;
261 if (columns * rows > kMaxGridPoints) {
262 return;
263 }
264
265 painter->setPen(m_dots);
266
267 QPolygon points;
268 // kMaxGridPoints (checked above) comfortably fits in int on every supported platform;
269 // cast explicitly rather than relying on the implicit qint64->qsizetype narrowing, since
270 // qsizetype is 32-bit on i386 (would warn under -Wconversion) but the same width as qint64
271 // on 64-bit platforms (a same-width static_cast there would instead warn -Wuseless-cast).
272 points.reserve(static_cast<int>(columns * rows));
273
274 for (int x = left; x <= right; x += gridSize) {
275 for (int y = top; y <= bottom; y += gridSize) {
276 points.append(QPoint(x, y));
277 }
278 }
279
280 painter->drawPoints(points);
281}
282
283void Scene::setDots(const QPen &dots)
284{
285 m_dots = dots;
286}
287
289{
290 return &m_simulation;
291}
292
294{
295 m_autosaveRequired = true;
296}
297
299{
301
302 // Invalidate the cached topology; the next Simulation::update() (or start())
303 // lazily rebuilds it. Structural edits often arrive in tight batches (MCP-driven
304 // circuit construction, multi-element paste/delete) — eagerly rebuilding here on
305 // every single one, as this used to do, turned an O(N) circuit build into O(N^2):
306 // each element/connection add paid a full topology rebuild over everything already
307 // in the scene. restart() alone is sufficient: it already unconditionally clears
308 // m_initialized, so it stays correct even on the "scene dropped to just the border
309 // rect" bail-out case that previously needed the eager initialize() call to detect.
310 m_simulation.restart();
311}
312
314{
315 // Re-applying visibility ensures newly-added ports/wires respect the current
316 // show/hide state; without this, ports on fresh elements would always appear visible.
317 // Deferred to the next drawBackground() (see its comment) rather than done eagerly
318 // here: this runs on every structural edit, and reapply() rescans the whole scene,
319 // which is fine for one edit but pathological for a tight batch of them (MCP-driven
320 // circuit construction, multi-element paste/delete) — O(N) per edit, O(N^2) overall.
321 m_visibilityDirty = true;
322
323 update();
324
325 m_autosaveRequired = true;
326 m_itemsBoundingRectDirty = true;
327}
328
329const QVector<GraphicElement *> Scene::visibleElements() const
330{
331 const auto visibleRect = m_view->mapToScene(m_view->viewport()->geometry()).boundingRect();
332
333 return elements(visibleRect);
334}
335
336const QVector<GraphicElement *> Scene::elements() const
337{
339}
340
341const QVector<GraphicElement *> Scene::unsortedElements() const
342{
343 const auto items_ = items();
344 QVector<GraphicElement *> elements_;
345 elements_.reserve(items_.size());
346
347 for (auto *item : items_) {
348 if (item->type() == GraphicElement::Type) {
349 elements_.append(qgraphicsitem_cast<GraphicElement *>(item));
350 }
351 }
352
353 return elements_;
354}
355
356const QVector<GraphicElement *> Scene::elements(const QRectF &rect) const
357{
358 const auto items_ = items(rect);
359 QVector<GraphicElement *> elements_;
360 elements_.reserve(items_.size());
361
362 for (auto *item : items_) {
363 if (item->type() == GraphicElement::Type) {
364 elements_.append(qgraphicsitem_cast<GraphicElement *>(item));
365 }
366 }
367
368 return sortByTopology(elements_);
369}
370
371QVector<GraphicElement *> Scene::sortByTopology(QVector<GraphicElement *> elements)
372{
373 QHash<GraphicElement *, QVector<GraphicElement *>> successors;
374 for (auto *elm : elements) {
375 for (auto *port : elm->outputs()) {
376 for (auto *conn : port->connections()) {
377 if (auto *endPort = conn->endPort()) {
378 if (auto *successor = endPort->graphicElement()) {
379 auto &vec = successors[elm];
380 if (!vec.contains(successor)) {
381 vec.append(successor);
382 }
383 }
384 }
385 }
386 }
387 }
388
389 QHash<GraphicElement *, int> priorities;
390 calculatePriorities(elements, successors, priorities);
391
392 std::stable_sort(elements.begin(), elements.end(), [&priorities](const auto &e1, const auto &e2) {
393 return priorities.value(e1) > priorities.value(e2);
394 });
395
396 return elements;
397}
398
399QHash<QString, InputPort *> Scene::wirelessTxInputPorts(const QVector<GraphicElement *> &elements)
400{
401 QHash<QString, InputPort *> txMap;
402 for (auto *elm : elements) {
403 if (elm->wirelessMode() == WirelessMode::Tx && !elm->label().isEmpty() && elm->inputPort(0)) {
404 if (!txMap.contains(elm->label())) {
405 txMap.insert(elm->label(), elm->inputPort(0));
406 }
407 }
408 }
409 return txMap;
410}
411
412const QVector<Connection *> Scene::connections() const
413{
414 const auto items_ = items();
415 QVector<Connection *> conns;
416 conns.reserve(items_.size());
417
418 for (auto *item : items_) {
419 if (item->type() == Connection::Type) {
420 conns.append(qgraphicsitem_cast<Connection *>(item));
421 }
422 }
423
424 return conns;
425}
426
427const QList<GraphicElement *> Scene::selectedElements() const
428{
429 const auto items_ = selectedItems();
430 QList<GraphicElement *> elements_;
431
432 for (auto *item : items_) {
433 if (item->type() == GraphicElement::Type) {
434 elements_.append(qgraphicsitem_cast<GraphicElement *>(item));
435 }
436 }
437
438 return elements_;
439}
440
441QGraphicsItem *Scene::itemAt(const QPointF pos) const
442{
443 auto items_ = items(pos);
444 // Also check a small surrounding rectangle so port hit-testing works when
445 // the cursor is near but not exactly on the port's bounding box
446 items_.append(itemsAt(pos));
447
448 // Ports get priority so wire dragging begins/ends on the port, not the element body
449 for (auto *item : std::as_const(items_)) {
450 if (item->type() == Port::Type) {
451 return item;
452 }
453 }
454
455 // Elements take priority over connections since they render above wires
456 for (auto *item : std::as_const(items_)) {
457 if (item->type() == GraphicElement::Type) {
458 return item;
459 }
460 }
461
462 // Return any remaining custom item (connections, etc.)
463 for (auto *item : std::as_const(items_)) {
464 if (item->type() > QGraphicsItem::UserType) {
465 return item;
466 }
467 }
468
469 return nullptr;
470}
471
472QList<QGraphicsItem *> Scene::itemsAt(const QPointF pos) const
473{
474 // 9×9 pixel hit area (4px margin around the exact point) compensates for the
475 // small visual size of ports and makes them easier to click precisely
476 QRectF rect(pos - QPointF(4, 4), QSize(9, 9));
477 return items(rect.normalized());
478}
479
480Port *Scene::portAt(const QPointF pos) const
481{
482 // Port-only fast path for per-mouse-move consumers (hover feedback, wire completion,
483 // port tooltips). itemAt()'s generic queries use Qt::IntersectsItemShape, which
484 // exact-shape-tests every element and wire whose bounding box overlaps -- for wires
485 // that means re-stroking and clipping Bézier curves, and their fat bounding boxes
486 // collect dozens of candidates near a wire bundle. A bounding-box query skips all of
487 // that, and ports themselves are cheap to test exactly (shape() is a fixed square).
488 //
489 // Same 9x9 hit area as itemsAt(). An exactly-hit port wins over a merely-nearby one,
490 // like itemAt()'s two-phase lookup; among nearby-only ports acceptance is by bounding
491 // box rather than shape-vs-rect -- a difference of at most the ~1 unit of bounding-box
492 // padding, well inside the deliberate 4 px slop.
493 const QRectF rect(pos - QPointF(4, 4), QSize(9, 9));
494 const auto candidates = items(rect.normalized(), Qt::IntersectsItemBoundingRect);
495
496 Port *nearby = nullptr;
497
498 for (auto *item : candidates) { // default order: topmost first
499 if (item->type() != Port::Type) {
500 continue;
501 }
502
503 // static_cast, not qgraphicsitem_cast: the type() check above already proves the
504 // downcast, and the maybe-null result of qgraphicsitem_cast trips
505 // -Wnull-dereference at -O3 on the immediate dereference below.
506 auto *port = static_cast<Port *>(item);
507
508 if (port->shape().contains(port->mapFromScene(pos))) {
509 return port;
510 }
511
512 if (!nearby) {
513 nearby = port;
514 }
515 }
516
517 return nearby;
518}
519
520void Scene::receiveCommand(QUndoCommand *cmd)
521{
522 sentryBreadcrumb("command", QStringLiteral("Command: %1").arg(cmd->text()));
523 m_undoStack.push(cmd);
524 update();
525}
526
528{
529 // A live drag needs the item bounds to track the dragged element's currently-changing
530 // position (see resizeScene()'s isDraggingElement() branch) -- a cached value would
531 // stop the scene rect from expanding to follow it mid-drag.
532 if (m_interaction.isDraggingElement()) {
533 return itemsBoundingRect();
534 }
535
536 if (m_itemsBoundingRectDirty) {
537 m_cachedItemsBoundingRect = itemsBoundingRect();
538 m_itemsBoundingRectDirty = false;
539 }
540
541 return m_cachedItemsBoundingRect;
542}
543
545{
546 return m_wireAntialiasing;
547}
548
549void Scene::recordWirePaintPass(const qint64 elapsedNs)
550{
551 m_wirePassTimer.restart();
552
553 if (m_wireAntialiasing) {
554 if (elapsedNs > kWireAaBudgetNs) {
555 if (++m_wireAaSlowPasses >= kWireAaDebouncePasses) {
556 m_wireAntialiasing = false;
557 m_wireAaSlowPasses = 0;
558 m_wireAaHeadroomPasses = 0;
559 m_wireAaIdleTimer.start(static_cast<int>(kWireAaPassIdleMs));
560 }
561 } else {
562 m_wireAaSlowPasses = 0;
563 }
564 return;
565 }
566
567 // Degraded: sustained deep headroom is the only in-storm way back to full quality;
568 // the idle path is handled by checkWireIdleRestore() re-arming itself.
569 if (elapsedNs < kWireAaBudgetNs / kWireAaWorstCaseRatio) {
570 if (++m_wireAaHeadroomPasses >= kWireAaHeadroomRestorePasses) {
572 }
573 } else {
574 m_wireAaHeadroomPasses = 0;
575 }
576}
577
579{
580 m_wireFlipTimer.restart();
581}
582
583void Scene::checkWireIdleRestore()
584{
585 const qint64 passRemainingMs = kWireAaPassIdleMs - m_wirePassTimer.elapsed();
586 const qint64 flipRemainingMs = kWireAaFlipIdleMs - m_wireFlipTimer.elapsed();
587 const qint64 remainingMs = qMax(passRemainingMs, flipRemainingMs);
588
589 if (remainingMs <= 0) {
591 return;
592 }
593
594 // Still active: re-arm for the remainder of the longer window (deadline pattern --
595 // avoids restarting a timer on every one of the thousands of flips per second).
596 m_wireAaIdleTimer.start(static_cast<int>(remainingMs));
597}
598
600{
601 m_wireAaIdleTimer.stop();
602 m_wireAaSlowPasses = 0;
603 m_wireAaHeadroomPasses = 0;
604
605 if (m_wireAntialiasing) {
606 return;
607 }
608
609 m_wireAntialiasing = true;
610 // Refinement repaint: without it the wires would stay visually jagged until the next
611 // naturally-occurring repaint, which on an idle view may never come.
612 update();
613}
614
615namespace {
623constexpr qreal kSceneRectQuantum = 256.0;
624
625QRectF quantizeOutward(const QRectF &rect)
626{
627 const qreal left = std::floor(rect.left() / kSceneRectQuantum) * kSceneRectQuantum;
628 const qreal top = std::floor(rect.top() / kSceneRectQuantum) * kSceneRectQuantum;
629 const qreal right = std::ceil(rect.right() / kSceneRectQuantum) * kSceneRectQuantum;
630 const qreal bottom = std::ceil(rect.bottom() / kSceneRectQuantum) * kSceneRectQuantum;
631 return QRectF(QPointF(left, top), QPointF(right, bottom));
632}
633} // namespace
634
636{
637 const auto bounds = cachedItemsBoundingRect();
638
639 if (m_interaction.isDraggingElement()) {
640 // While dragging, only expand the scene rect (union with current rect).
641 // Never shrink during drag — shrinking shifts the viewport origin and
642 // causes jarring visual jumps as the scene rect chases the items.
643 setSceneRect(quantizeOutward(sceneRect().united(bounds)));
644 } else {
645 // Tighten to item bounds, but ensure the scene rect stays larger than the
646 // viewport. When the scene rect is smaller than (or barely larger than) the
647 // viewport, Qt re-centers it, causing a visual jump. Adding margins ensures
648 // enough scrollbar range to preserve the exact scroll position.
649 auto tightRect = bounds;
650 const auto viewList = views();
651 if (!viewList.isEmpty()) {
652 auto *view = viewList.first();
653 constexpr qreal margin = 100.0;
654 const auto visibleScene = view->mapToScene(view->viewport()->rect()).boundingRect()
655 .adjusted(-margin, -margin, margin, margin);
656 tightRect = tightRect.united(visibleScene);
657
658 // Preserve exact scrollbar positions across the scene rect change.
659 // centerOn() converts scene floats to integer pixels, causing 1px
660 // drift — saving/restoring scrollbar values avoids the rounding.
661 const int hVal = view->horizontalScrollBar()->value();
662 const int vVal = view->verticalScrollBar()->value();
663 setSceneRect(quantizeOutward(tightRect));
664 view->horizontalScrollBar()->setValue(hVal);
665 view->verticalScrollBar()->setValue(vVal);
666 } else {
667 setSceneRect(quantizeOutward(tightRect));
668 }
669 }
670}
671
672QUndoStack *Scene::undoStack()
673{
674 return &m_undoStack;
675}
676
678{
679 return ConnectionManager::isConnectionAllowed(startPort, endPort);
680}
681
682void Scene::showStatusMessage(const QString &message)
683{
684 emit showStatusMessageRequested(message);
685}
686
687void Scene::prevMainPropShortcut() { m_propertyShortcutHandler.prevMainProperty(); }
688
689void Scene::nextMainPropShortcut() { m_propertyShortcutHandler.nextMainProperty(); }
690
691void Scene::prevSecndPropShortcut() { m_propertyShortcutHandler.prevSecondaryProperty(); }
692
693void Scene::nextSecndPropShortcut() { m_propertyShortcutHandler.nextSecondaryProperty(); }
694
695void Scene::nextElm() { m_propertyShortcutHandler.nextElement(); }
696
697void Scene::prevElm() { m_propertyShortcutHandler.prevElement(); }
698
700{
701 qCDebug(zero) << "Updating theme.";
703 setBackgroundBrush(theme.m_sceneBgBrush);
704 setDots(QPen(theme.m_sceneBgDots));
705 m_interaction.applyTheme(theme);
706
707 for (auto *element : elements()) {
708 element->updateTheme();
709 }
710
711 for (auto *conn : connections()) {
712 conn->updateTheme();
713 }
714
715 qCDebug(zero) << "Finished updating theme.";
716}
717
718QList<QGraphicsItem *> Scene::items(Qt::SortOrder order) const
719{
720 return QGraphicsScene::items(order);
721}
722
723QList<QGraphicsItem *> Scene::items(QPointF pos, Qt::ItemSelectionMode mode, Qt::SortOrder order, const QTransform &deviceTransform) const
724{
725 return QGraphicsScene::items(pos, mode, order, deviceTransform);
726}
727
728QList<QGraphicsItem *> Scene::items(const QRectF &rect, Qt::ItemSelectionMode mode, Qt::SortOrder order, const QTransform &deviceTransform) const
729{
730 return QGraphicsScene::items(rect, mode, order, deviceTransform);
731}
732
733void Scene::showGates(const bool checked)
734{
735 m_visibilityManager.showGates(checked);
736}
737
738void Scene::showWires(const bool checked)
739{
740 m_visibilityManager.showWires(checked);
741}
742
744{
745 return m_view;
746}
747
749{
750 m_view = view;
751}
752
753QAction *Scene::undoAction() const
754{
755 return m_undoAction;
756}
757
758QAction *Scene::redoAction() const
759{
760 return m_redoAction;
761}
762
764{
765 updateUndoText(m_undoStack.undoText());
766 updateRedoText(m_undoStack.redoText());
767}
768
769void Scene::updateUndoText(const QString &text)
770{
771 const QString prefix = tr("&Undo");
772 m_undoAction->setText(text.isEmpty() ? prefix : prefix + QLatin1Char(' ') + text);
773}
774
775void Scene::updateRedoText(const QString &text)
776{
777 const QString prefix = tr("&Redo");
778 m_redoAction->setText(text.isEmpty() ? prefix : prefix + QLatin1Char(' ') + text);
779}
780
781void Scene::contextMenu(const QPoint screenPos)
782{
783 if (auto *item = itemAt(m_interaction.lastMousePos())) {
784 // Right-clicking a selected item opens the property context menu for that selection
785 if (selectedItems().contains(item)) {
786 emit contextMenuPos(screenPos, item);
787 } else if (item->type() == GraphicElement::Type) {
788 // Right-clicking an unselected element clears the old selection and selects
789 // only this item, then opens its context menu
790 clearSelection();
791 item->setSelected(true);
792 emit contextMenuPos(screenPos, item);
793 }
794 } else {
795 // Right-click on empty canvas: Paste plus Select all.
796 QMenu menu;
797 auto *pasteAction = menu.addAction(QIcon(QPixmap(":/Interface/Toolbar/paste.svg")), tr("Paste"));
798 const auto *mimeData = QApplication::clipboard()->mimeData();
799
800 if (ClipboardManager::canPaste(mimeData)) {
801 connect(pasteAction, &QAction::triggered, this, &Scene::pasteAction);
802 } else {
803 pasteAction->setEnabled(false);
804 }
805
806 auto *selectAllAction = menu.addAction(tr("Select all"));
807 selectAllAction->setEnabled(!elements().isEmpty());
808 connect(selectAllAction, &QAction::triggered, this, &Scene::selectAll);
809
810 menu.exec(screenPos);
811 }
812}
813
815{
816 sentryBreadcrumb("clipboard", QStringLiteral("Copy"));
817 m_clipboardManager.copy();
818}
819
821{
822 sentryBreadcrumb("clipboard", QStringLiteral("Cut"));
823 m_clipboardManager.cut();
824}
825
827{
828 sentryBreadcrumb("clipboard", QStringLiteral("Paste"));
829 m_clipboardManager.paste();
830}
831
833{
834 sentryBreadcrumb("clipboard", QStringLiteral("Duplicate"));
835 m_clipboardManager.duplicate();
836}
837
839{
840 sentryBreadcrumb("ui", QStringLiteral("Delete"));
841 const auto selectedItems_ = selectedItems();
842 // Clear selection before the command so that the scene's selectedItems()
843 // list is empty during the command's redo() — avoids double-processing
844 clearSelection();
845
846 if (!selectedItems_.isEmpty()) {
847 receiveCommand(new DeleteItemsCommand(selectedItems_, this));
848 // No explicit Simulation::restart() needed: DeleteItemsCommand::redo
849 // calls setCircuitUpdateRequired() which already drives a full
850 // initialize()/restart() cycle once the deletion completes.
851 }
852}
853
855{
856 const auto items_ = items();
857
858 for (auto *item : items_) {
859 item->setSelected(true);
860 }
861}
862
864{
865 sentryBreadcrumb("ui", QStringLiteral("Rotate right"));
866 rotate(90);
867}
868
870{
871 sentryBreadcrumb("ui", QStringLiteral("Rotate left"));
872 rotate(-90);
873}
874
875void Scene::rotate(const int angle)
876{
877 const auto elements_ = selectedElements();
878
879 if (!elements_.isEmpty()) {
880 receiveCommand(new RotateCommand(elements_, angle, this));
881 }
882}
883
884void Scene::mute(const bool mute)
885{
886 for (auto *element : unsortedElements()) {
887 if (auto *audioElement = qobject_cast<AudioOutputElement *>(element)) {
888 audioElement->mute(mute);
889 }
890 }
891}
892
894{
895 sentryBreadcrumb("ui", QStringLiteral("Flip horizontal"));
896 const auto elements_ = selectedElements();
897
898 if (!elements_.isEmpty()) {
899 receiveCommand(new FlipCommand(elements_, 0, this));
900 }
901}
902
904{
905 sentryBreadcrumb("ui", QStringLiteral("Flip vertical"));
906 const auto elements_ = selectedElements();
907
908 if (!elements_.isEmpty()) {
909 receiveCommand(new FlipCommand(elements_, 1, this));
910 }
911}
912
913namespace {
914
918void moveElementsTo(Scene *scene, const QList<GraphicElement *> &elements, const QList<QPointF> &newPositions)
919{
920 QList<QPointF> oldPositions;
921 oldPositions.reserve(elements.size());
922 for (auto *elm : elements) {
923 oldPositions.append(elm->pos());
924 }
925
926 for (int i = 0; i < elements.size(); ++i) {
927 elements.at(i)->setPos(newPositions.at(i));
928 }
929
930 scene->receiveCommand(new MoveCommand(elements, oldPositions, scene));
931}
932
933} // namespace
934
936{
937 const auto elements_ = selectedElements();
938 if (elements_.size() < 2) {
939 return;
940 }
941
942 qreal target = elements_.constFirst()->sceneBoundingRect().left();
943 for (auto *elm : elements_) {
944 target = std::min(target, elm->sceneBoundingRect().left());
945 }
946
947 QList<QPointF> newPositions;
948 newPositions.reserve(elements_.size());
949 for (auto *elm : elements_) {
950 const qreal delta = target - elm->sceneBoundingRect().left();
951 newPositions.append(elm->pos() + QPointF(delta, 0));
952 }
953
954 moveElementsTo(this, elements_, newPositions);
955}
956
958{
959 const auto elements_ = selectedElements();
960 if (elements_.size() < 2) {
961 return;
962 }
963
964 qreal target = elements_.constFirst()->sceneBoundingRect().right();
965 for (auto *elm : elements_) {
966 target = std::max(target, elm->sceneBoundingRect().right());
967 }
968
969 QList<QPointF> newPositions;
970 newPositions.reserve(elements_.size());
971 for (auto *elm : elements_) {
972 const qreal delta = target - elm->sceneBoundingRect().right();
973 newPositions.append(elm->pos() + QPointF(delta, 0));
974 }
975
976 moveElementsTo(this, elements_, newPositions);
977}
978
980{
981 const auto elements_ = selectedElements();
982 if (elements_.size() < 2) {
983 return;
984 }
985
986 qreal target = elements_.constFirst()->sceneBoundingRect().top();
987 for (auto *elm : elements_) {
988 target = std::min(target, elm->sceneBoundingRect().top());
989 }
990
991 QList<QPointF> newPositions;
992 newPositions.reserve(elements_.size());
993 for (auto *elm : elements_) {
994 const qreal delta = target - elm->sceneBoundingRect().top();
995 newPositions.append(elm->pos() + QPointF(0, delta));
996 }
997
998 moveElementsTo(this, elements_, newPositions);
999}
1000
1002{
1003 const auto elements_ = selectedElements();
1004 if (elements_.size() < 2) {
1005 return;
1006 }
1007
1008 qreal target = elements_.constFirst()->sceneBoundingRect().bottom();
1009 for (auto *elm : elements_) {
1010 target = std::max(target, elm->sceneBoundingRect().bottom());
1011 }
1012
1013 QList<QPointF> newPositions;
1014 newPositions.reserve(elements_.size());
1015 for (auto *elm : elements_) {
1016 const qreal delta = target - elm->sceneBoundingRect().bottom();
1017 newPositions.append(elm->pos() + QPointF(0, delta));
1018 }
1019
1020 moveElementsTo(this, elements_, newPositions);
1021}
1022
1024{
1025 const auto elements_ = selectedElements();
1026 if (elements_.size() < 2) {
1027 return;
1028 }
1029
1030 qreal sum = 0;
1031 for (auto *elm : elements_) {
1032 sum += elm->sceneBoundingRect().center().x();
1033 }
1034 const qreal target = sum / static_cast<qreal>(elements_.size());
1035
1036 QList<QPointF> newPositions;
1037 newPositions.reserve(elements_.size());
1038 for (auto *elm : elements_) {
1039 const qreal delta = target - elm->sceneBoundingRect().center().x();
1040 newPositions.append(elm->pos() + QPointF(delta, 0));
1041 }
1042
1043 moveElementsTo(this, elements_, newPositions);
1044}
1045
1047{
1048 const auto elements_ = selectedElements();
1049 if (elements_.size() < 2) {
1050 return;
1051 }
1052
1053 qreal sum = 0;
1054 for (auto *elm : elements_) {
1055 sum += elm->sceneBoundingRect().center().y();
1056 }
1057 const qreal target = sum / static_cast<qreal>(elements_.size());
1058
1059 QList<QPointF> newPositions;
1060 newPositions.reserve(elements_.size());
1061 for (auto *elm : elements_) {
1062 const qreal delta = target - elm->sceneBoundingRect().center().y();
1063 newPositions.append(elm->pos() + QPointF(0, delta));
1064 }
1065
1066 moveElementsTo(this, elements_, newPositions);
1067}
1068
1070{
1071 auto elements_ = selectedElements();
1072 if (elements_.size() < 3) {
1073 return;
1074 }
1075
1076 std::sort(elements_.begin(), elements_.end(), [](GraphicElement *a, GraphicElement *b) {
1077 return a->sceneBoundingRect().left() < b->sceneBoundingRect().left();
1078 });
1079
1080 const qreal spanStart = elements_.constFirst()->sceneBoundingRect().left();
1081 const qreal spanEnd = elements_.constLast()->sceneBoundingRect().right();
1082
1083 qreal totalWidth = 0;
1084 for (auto *elm : elements_) {
1085 totalWidth += elm->sceneBoundingRect().width();
1086 }
1087 const qreal gap = (spanEnd - spanStart - totalWidth) / static_cast<qreal>(elements_.size() - 1);
1088
1089 QList<QPointF> newPositions;
1090 newPositions.reserve(elements_.size());
1091 newPositions.append(elements_.constFirst()->pos()); // leftmost stays fixed as an anchor
1092
1093 qreal cursor = elements_.constFirst()->sceneBoundingRect().right() + gap;
1094 for (int i = 1; i < elements_.size() - 1; ++i) {
1095 auto *elm = elements_.at(i);
1096 const qreal delta = cursor - elm->sceneBoundingRect().left();
1097 newPositions.append(elm->pos() + QPointF(delta, 0));
1098 cursor += elm->sceneBoundingRect().width() + gap;
1099 }
1100
1101 newPositions.append(elements_.constLast()->pos()); // rightmost stays fixed as an anchor
1102
1103 moveElementsTo(this, elements_, newPositions);
1104}
1105
1107{
1108 auto elements_ = selectedElements();
1109 if (elements_.size() < 3) {
1110 return;
1111 }
1112
1113 std::sort(elements_.begin(), elements_.end(), [](GraphicElement *a, GraphicElement *b) {
1114 return a->sceneBoundingRect().top() < b->sceneBoundingRect().top();
1115 });
1116
1117 const qreal spanStart = elements_.constFirst()->sceneBoundingRect().top();
1118 const qreal spanEnd = elements_.constLast()->sceneBoundingRect().bottom();
1119
1120 qreal totalHeight = 0;
1121 for (auto *elm : elements_) {
1122 totalHeight += elm->sceneBoundingRect().height();
1123 }
1124 const qreal gap = (spanEnd - spanStart - totalHeight) / static_cast<qreal>(elements_.size() - 1);
1125
1126 QList<QPointF> newPositions;
1127 newPositions.reserve(elements_.size());
1128 newPositions.append(elements_.constFirst()->pos()); // topmost stays fixed as an anchor
1129
1130 qreal cursor = elements_.constFirst()->sceneBoundingRect().bottom() + gap;
1131 for (int i = 1; i < elements_.size() - 1; ++i) {
1132 auto *elm = elements_.at(i);
1133 const qreal delta = cursor - elm->sceneBoundingRect().top();
1134 newPositions.append(elm->pos() + QPointF(0, delta));
1135 cursor += elm->sceneBoundingRect().height() + gap;
1136 }
1137
1138 newPositions.append(elements_.constLast()->pos()); // bottommost stays fixed as an anchor
1139
1140 moveElementsTo(this, elements_, newPositions);
1141}
1142
1143QString Scene::droppedPandaFile(const QMimeData *mimeData)
1144{
1145 if (!mimeData->hasUrls()) {
1146 return {};
1147 }
1148 for (const QUrl &url : mimeData->urls()) {
1149 if (!url.isLocalFile()) {
1150 continue;
1151 }
1152 const QString path = url.toLocalFile();
1153 if (path.endsWith(QLatin1String(".panda"), Qt::CaseInsensitive)) {
1154 return path;
1155 }
1156 }
1157 return {};
1158}
1159
1160void Scene::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
1161{
1162 if (SceneDropHandler::isSupportedDropFormat(event->mimeData())
1163 || !droppedPandaFile(event->mimeData()).isEmpty()) {
1164 event->acceptProposedAction();
1165 return;
1166 }
1167
1168 QGraphicsScene::dragEnterEvent(event);
1169}
1170
1171void Scene::dragMoveEvent(QGraphicsSceneDragDropEvent *event)
1172{
1173 if (SceneDropHandler::isSupportedDropFormat(event->mimeData())
1174 || !droppedPandaFile(event->mimeData()).isEmpty()) {
1175 event->acceptProposedAction();
1176 return;
1177 }
1178
1179 QGraphicsScene::dragMoveEvent(event);
1180}
1181
1182void Scene::dropEvent(QGraphicsSceneDragDropEvent *event)
1183{
1184 sentryBreadcrumb("ui", QStringLiteral("Drop event"));
1185
1186 // A .panda file dragged from the file manager opens as a project (like File > Open).
1187 const QString pandaFile = droppedPandaFile(event->mimeData());
1188 if (!pandaFile.isEmpty()) {
1189 event->acceptProposedAction();
1190 emit fileDropRequested(pandaFile);
1191 return;
1192 }
1193
1194 if (event->mimeData()->hasFormat(MimeType::DragDropLegacy)
1195 || event->mimeData()->hasFormat(MimeType::DragDrop)) {
1196 m_dropHandler.handleNewElementDrop(event);
1197 }
1198
1199 if (event->mimeData()->hasFormat(MimeType::CloneDragLegacy)
1200 || event->mimeData()->hasFormat(MimeType::CloneDrag)) {
1201 m_dropHandler.handleCloneDrag(event);
1202 }
1203
1204 QGraphicsScene::dropEvent(event);
1205}
1206
1207bool Scene::nudgeSelection(QKeyEvent *event)
1208{
1209 // Only plain / Shift+arrow — leave Ctrl/Alt combinations to other handlers.
1210 if (event->modifiers().testFlag(Qt::ControlModifier) || event->modifiers().testFlag(Qt::AltModifier)) {
1211 return false;
1212 }
1213
1214 int dx = 0;
1215 int dy = 0;
1216 switch (event->key()) {
1217 case Qt::Key_Left: dx = -1; break;
1218 case Qt::Key_Right: dx = 1; break;
1219 case Qt::Key_Up: dy = -1; break;
1220 case Qt::Key_Down: dy = 1; break;
1221 default: return false;
1222 }
1223
1224 const QList<GraphicElement *> selected = selectedElements();
1225 if (selected.isEmpty()) {
1226 return false; // nothing selected — let the arrow key scroll the view
1227 }
1228
1229 // One grid cell by default; Shift jumps four cells for coarse positioning. Both are
1230 // multiples of the grid, so grid-aligned elements stay aligned.
1231 const int step = event->modifiers().testFlag(Qt::ShiftModifier) ? Constants::gridSize * 4 : Constants::gridSize;
1232 const QPointF delta(dx * step, dy * step);
1233
1234 QList<QPointF> oldPositions;
1235 oldPositions.reserve(selected.size());
1236 for (auto *elm : selected) {
1237 oldPositions.append(elm->pos());
1238 }
1239 for (auto *elm : selected) {
1240 elm->setPos(elm->pos() + delta);
1241 }
1242
1243 receiveCommand(new MoveCommand(selected, oldPositions, this));
1244 resizeScene();
1245 event->accept();
1246 return true;
1247}
1248
1249void Scene::keyPressEvent(QKeyEvent *event)
1250{
1251 // Ignore auto-repeat: holding a trigger key must fire once, not oscillate an InputSwitch
1252 // (whose keyboard trigger toggles on every press) dozens of times a second.
1253 if (event->isAutoRepeat()) {
1254 QGraphicsScene::keyPressEvent(event);
1255 return;
1256 }
1257
1258 // Arrow keys nudge the current selection by a grid step (Shift = a larger step) as one
1259 // undoable move; consumes the event only when it actually moves a selection.
1260 if (nudgeSelection(event)) {
1261 return;
1262 }
1263
1264 // Skip keyboard triggers while Ctrl is held to avoid firing during Ctrl+Z/C/V shortcuts
1265 if (!(event->modifiers().testFlag(Qt::ControlModifier))) {
1266 for (auto *element : unsortedElements()) {
1267 if (element->hasTrigger() && !element->trigger().isEmpty() && element->trigger().matches(event->key())) {
1268 if (auto *input = qobject_cast<GraphicElementInput *>(element); input && !input->isLocked()) {
1269 input->setOn();
1270 }
1271 }
1272 }
1273 }
1274
1275 QGraphicsScene::keyPressEvent(event);
1276}
1277
1278void Scene::keyReleaseEvent(QKeyEvent *event)
1279{
1280 // Ignore auto-repeat: on X11 a held key emits release/press pairs, which would otherwise
1281 // release a momentary InputButton mid-hold.
1282 if (event->isAutoRepeat()) {
1283 QGraphicsScene::keyReleaseEvent(event);
1284 return;
1285 }
1286
1287 if (!(event->modifiers().testFlag(Qt::ControlModifier))) {
1288 for (auto *element : unsortedElements()) {
1289 if (element->hasTrigger() && !element->trigger().isEmpty() && element->trigger().matches(event->key())) {
1290 // Only InputButton (momentary) is released on key-up; InputSwitch stays latched
1291 if (auto *input = qobject_cast<GraphicElementInput *>(element); input && !input->isLocked() && (element->elementType() == ElementType::InputButton)) {
1292 input->setOff();
1293 }
1294 }
1295 }
1296 }
1297
1298 QGraphicsScene::keyReleaseEvent(event);
1299}
1300
1301bool Scene::eventFilter(QObject *watched, QEvent *event)
1302{
1303 if (auto *mouseEvent = dynamic_cast<QGraphicsSceneMouseEvent *>(event)) {
1304 // Qt's default QGraphicsScene selection behaviour treats Shift+click as
1305 // "extend selection to range" (like a list widget), which is meaningless for
1306 // a free-form canvas. Remapping Shift→Ctrl activates the Ctrl toggle-select
1307 // path instead, which is what users expect from a design tool.
1308 if (mouseEvent->modifiers().testFlag(Qt::ShiftModifier)) {
1309 mouseEvent->setModifiers(Qt::ControlModifier);
1310 return QGraphicsScene::eventFilter(watched, event);
1311 }
1312
1313 // Intercept Ctrl+Left-click on an element to begin a clone-drag instead of
1314 // the default rubber-band selection; return true to swallow the event
1315 if ((mouseEvent->button() == Qt::LeftButton) && mouseEvent->modifiers().testFlag(Qt::ControlModifier)) {
1316 if (auto *item = itemAt(mouseEvent->scenePos()); item && ((item->type() == GraphicElement::Type) || (item->type() == Connection::Type))) {
1317 item->setSelected(true);
1318 m_clipboardManager.cloneDrag(mouseEvent->scenePos());
1319 return true;
1320 }
1321 }
1322 }
1323
1324 return QGraphicsScene::eventFilter(watched, event);
1325}
1326
1327void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event)
1328{
1329 if (!m_interaction.mousePress(event)) {
1330 QGraphicsScene::mousePressEvent(event);
1331 }
1332}
1333
1334void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
1335{
1336 // Guard against re-entrant calls. When ensureVisible() scrolls the viewport,
1337 // Qt synchronously calls scrollContentsBy() → replayLastMouseEvent() →
1338 // mouseMoveEvent() before ensureVisible() returns. At a viewport corner,
1339 // scrolling to satisfy the H-margin violates the V-margin, so the replay
1340 // triggers another ensureVisible() that does the opposite, oscillating
1341 // until a stack overflow. Dropping the re-entrant call breaks the loop.
1342 // The guard lives here, not in SceneInteraction, because it must wrap the
1343 // base-class call below where the re-entrancy actually originates.
1344 if (m_handlingMouseMove) {
1345 return;
1346 }
1347 m_handlingMouseMove = true;
1348 const auto resetFlag = qScopeGuard([this] { m_handlingMouseMove = false; });
1349
1350 if (!m_interaction.mouseMove(event)) {
1351 QGraphicsScene::mouseMoveEvent(event);
1352 }
1353}
1354
1355void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
1356{
1357 if (!m_interaction.mouseRelease(event)) {
1358 QGraphicsScene::mouseReleaseEvent(event);
1359 }
1360}
1361
1362void Scene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
1363{
1364 if (!m_interaction.mouseDoubleClick(event)) {
1365 QGraphicsScene::mouseDoubleClickEvent(event);
1366 }
1367}
1368
1369void Scene::helpEvent(QGraphicsSceneHelpEvent *event)
1370{
1371 // A help event fires exactly when Qt is about to show a tooltip (after the wake-up
1372 // delay). If the cursor is over a port, show its own label plus its connected peers'
1373 // labels in situ instead of the native tooltip, so they all appear together at the
1374 // same wake-up delay a tooltip would use. Other items keep their native tooltip.
1375 if (auto *port = portAt(event->scenePos())) {
1376 m_connectionManager.showHoverLabels(port);
1377 return;
1378 }
1379
1380 QGraphicsScene::helpEvent(event);
1381}
1382
1383void Scene::addItem(QMimeData *mimeData, std::optional<QPointF> scenePos)
1384{
1385 m_dropHandler.addFromMimeData(mimeData, scenePos);
1386}
Shared base class for audio output elements (AudioBox and Buzzer).
All QUndoCommand subclasses and the CommandUtils helper namespace.
Common logging utilities, the Pandaception error type, and helper macros.
#define qCDebug(category)
Definition Common.h:29
Connection: a wire that connects an output port to an input port in the circuit scene.
Shared numeric constants used across layers.
Singleton factory for all circuit element types.
Abstract base class for user-controllable input elements.
Abstract base class for all graphical circuit elements.
Extended QGraphicsView with zoom, pan, and fast-rendering modes.
Integrated Circuit (IC) graphic element that encapsulates a sub-circuit file.
Defines ItemWithId, the base class for all uniquely identified circuit items.
MIME type string constants for drag-and-drop operations.
Graph algorithms for topological priority assignment and cycle detection.
void calculatePriorities(const QVector< T * > &elements, const QHash< T *, QVector< T * > > &successors, QHash< T *, int > &outPriorities)
Priority calculation for directed graphs.
Definition Priorities.h:224
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)
Deserialization/serialization context structs passed through load()/save() call chains.
SerializationPurpose
What a serialization operation is for – shared by both load() and save(), so the same distinction is ...
Circuit and waveform file serialization/deserialization utilities.
Theme management types and singleton ThemeManager.
Graphic element for a user-programmable truth table.
static bool canPaste(const QMimeData *mimeData)
Returns whether mimeData carries circuit data paste() accepts.
static bool isConnectionAllowed(OutputPort *startPort, InputPort *endPort)
Returns true if a wire from startPort to endPort is permitted.
Undo command that removes a list of items from the scene.
Definition Commands.h:98
Undo command that flips a selection of elements along a horizontal or vertical axis.
Definition Commands.h:385
Abstract base class for all graphical circuit elements in wiRedPanda.
void inlineEditRequested(GraphicElement *element)
Extended QGraphicsView with enhanced navigation capabilities.
void previewHideRequested()
void previewRequested(IC *ic, const QPoint &screenPos)
void previewMoved(IC *ic, const QPoint &screenPos)
void requestOpenSubCircuit(int elementId, const QString &blobName, const QString &filePath)
Emitted on double-click to request opening the sub-circuit in a new tab.
void previewCancelRequested(IC *ic)
A port that receives a signal (the destination end of a wire).
Definition Port.h:229
Base class providing a unique integer identifier for circuit items.
Definition ItemWithId.h:31
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
Abstract base class for circuit element ports (connection endpoints).
Definition Port.h:39
@ Type
Definition Port.h:43
Undo command that rotates a list of elements by a fixed angle.
Definition Commands.h:129
static bool isSupportedDropFormat(const QMimeData *mimeData)
Returns true if mimeData carries any recognised wiRedPanda drop format.
void registerItem(ItemWithId *item)
Registers item under its current id.
Main circuit editing scene.
Definition Scene.h:56
void flipHorizontally()
Flips selected elements horizontally.
Definition Scene.cpp:893
const QVector< GraphicElement * > elements() const
Returns all graphic elements in the scene.
Definition Scene.cpp:336
void alignBottom()
Aligns selected elements' bottom edges to the bottommost selected edge. No-op below 2 elements.
Definition Scene.cpp:1001
void icPreviewRequested(IC *ic, const QPoint &screenPos)
void restoreWireAntialiasing()
Definition Scene.cpp:599
void setLastId(int newLastId)
Advances the scene's ID counter to at least newLastId.
Definition Scene.cpp:186
void dropEvent(QGraphicsSceneDragDropEvent *event) override
Definition Scene.cpp:1182
void recordWirePaintPass(qint64 elapsedNs)
Definition Scene.cpp:549
void pasteAction()
Pastes items from the internal clipboard into the scene.
Definition Scene.cpp:826
void fileDropRequested(const QString &filePath)
Emitted when a .panda file is dropped onto the canvas from the file manager.
QRectF cachedItemsBoundingRect() const
Definition Scene.cpp:527
void resizeScene()
Tightens the scene rect to item bounds while preserving the viewport position.
Definition Scene.cpp:635
bool wireAntialiasingEnabled() const
true while wires should stroke with antialiasing; Connection::paint() consults this.
Definition Scene.cpp:544
void keyPressEvent(QKeyEvent *event) override
Definition Scene.cpp:1249
void nextSecndPropShortcut()
Advances the secondary property of selected elements to the next value.
Definition Scene.cpp:693
void showStatusMessageRequested(const QString &message)
GraphicsView * view() const
Returns the GraphicsView currently displaying this scene.
Definition Scene.cpp:743
void contextMenuPos(QPoint screenPos, QGraphicsItem *itemAtMouse)
Emitted when a context menu should appear.
void mousePressEvent(QGraphicsSceneMouseEvent *event) override
Definition Scene.cpp:1327
bool eventFilter(QObject *watched, QEvent *event) override
Definition Scene.cpp:1301
void alignRight()
Aligns selected elements' right edges to the rightmost selected edge. No-op below 2 elements.
Definition Scene.cpp:957
void setCircuitUpdateRequired()
Marks the simulation mapping as stale so it is rebuilt on the next tick.
Definition Scene.cpp:298
Port * portAt(QPointF pos) const
Definition Scene.cpp:480
void prevElm()
Cycles selection backward to the previous element.
Definition Scene.cpp:697
void forgetItemId(int id)
Definition Scene.cpp:201
void removeItem(QGraphicsItem *item)
Removes item from the scene and unregisters it from the per-scene ID registry.
Definition Scene.cpp:160
void copyAction()
Copies the selected items to the internal clipboard.
Definition Scene.cpp:814
QAction * redoAction() const
Returns the redo QAction bound to the undo stack.
Definition Scene.cpp:758
void icOpenRequested(int elementId, const QString &blobName, const QString &filePath)
Emitted when an IC element is double-clicked to request opening its sub-circuit.
void showStatusMessage(const QString &message)
Definition Scene.cpp:682
void icPreviewMoved(IC *ic, const QPoint &screenPos)
static QHash< QString, InputPort * > wirelessTxInputPorts(const QVector< GraphicElement * > &elements)
Returns a map from wireless channel label to the Tx node's input port.
Definition Scene.cpp:399
void rotateLeft()
Rotates selected elements 90 degrees counter-clockwise.
Definition Scene.cpp:869
void receiveCommand(QUndoCommand *cmd)
Pushes cmd onto the undo stack (immediately executes its redo()).
Definition Scene.cpp:520
int lastId() const
Returns the current highest ID in use by this scene.
Definition Scene.cpp:181
void contextMenu(const QPoint screenPos)
Opens the element/selection context menu at screenPos (driven by SceneInteraction on right-click).
Definition Scene.cpp:781
const QList< GraphicElement * > selectedElements() const
Returns the list of currently selected graphic elements.
Definition Scene.cpp:427
static bool isConnectionAllowed(OutputPort *startPort, InputPort *endPort)
Returns true if a wire from startPort to endPort is permitted.
Definition Scene.cpp:677
void distributeVertically()
Definition Scene.cpp:1106
void noteWireActivity()
Definition Scene.cpp:578
void duplicateAction()
Duplicates the selection in place (leaves the system clipboard untouched).
Definition Scene.cpp:832
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override
Definition Scene.cpp:1355
QString contextDir() const override
Returns the directory of the .panda file associated with this scene.
Definition Scene.h:352
void icPreviewCancelRequested(IC *ic)
void prevMainPropShortcut()
Retreats the main property of selected elements to the previous value.
Definition Scene.cpp:687
void prevSecndPropShortcut()
Retreats the secondary property of selected elements to the previous value.
Definition Scene.cpp:691
void dragEnterEvent(QGraphicsSceneDragDropEvent *event) override
Definition Scene.cpp:1160
void setAutosaveRequired()
Schedules an autosave of the current circuit state.
Definition Scene.cpp:293
bool contains(int id) const
Returns true if an item with id is registered in this scene.
Definition Scene.cpp:176
void selectAll()
Selects all items in the scene.
Definition Scene.cpp:854
QGraphicsItem * itemAt(QPointF pos) const
Returns the topmost item at pos, prioritising ports over elements.
Definition Scene.cpp:441
ItemWithId * itemById(int id) const
Returns the item registered under id, or nullptr if not found.
Definition Scene.cpp:171
void rotateRight()
Rotates selected elements 90 degrees clockwise.
Definition Scene.cpp:863
void alignVerticalCenter()
Definition Scene.cpp:1046
static QVector< GraphicElement * > sortByTopology(QVector< GraphicElement * > elements)
Returns elements sorted in topological dependency order (inputs first).
Definition Scene.cpp:371
void setPropertyUpdateRequired()
Definition Scene.cpp:313
void retranslateUi()
Updates undo/redo action text to reflect the current UI language.
Definition Scene.cpp:763
void flipVertically()
Flips selected elements vertically.
Definition Scene.cpp:903
void addItem(QGraphicsItem *item)
Adds item to the scene and registers it in the per-scene ID registry.
Definition Scene.cpp:119
void mute(const bool mute=true)
Mutes or unmutes selected elements according to mute.
Definition Scene.cpp:884
void keyReleaseEvent(QKeyEvent *event) override
Definition Scene.cpp:1278
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override
Definition Scene.cpp:1334
void showWires(bool checked)
Shows or hides connection wires. Delegates to VisibilityManager.
Definition Scene.cpp:738
void nextMainPropShortcut()
Advances the main property of selected elements to the next value.
Definition Scene.cpp:689
void nextElm()
Cycles selection forward to the next element.
Definition Scene.cpp:695
void icPreviewHideRequested()
void deleteAction()
Deletes the currently selected items.
Definition Scene.cpp:838
void helpEvent(QGraphicsSceneHelpEvent *event) override
Definition Scene.cpp:1369
int nextId()
Returns the next available scene-local ID (does not assign it).
Definition Scene.cpp:191
Scene(QObject *parent=nullptr)
Constructs a Scene and initialises the undo stack and simulation.
Definition Scene.cpp:69
void alignTop()
Aligns selected elements' top edges to the topmost selected edge. No-op below 2 elements.
Definition Scene.cpp:979
QList< QGraphicsItem * > items(Qt::SortOrder order=Qt::AscendingOrder) const
Definition Scene.cpp:718
void alignLeft()
Aligns selected elements' left edges to the leftmost selected edge. No-op below 2 elements.
Definition Scene.cpp:935
void updateItemId(ItemWithId *item, int newId)
Reassigns the ID of item to newId without adding it to the scene.
Definition Scene.cpp:196
void circuitHasChanged()
Emitted whenever the circuit changes (element added/removed/moved).
static QString droppedPandaFile(const QMimeData *mimeData)
Definition Scene.cpp:1143
void distributeHorizontally()
Definition Scene.cpp:1069
void openTruthTableRequested()
Emitted when a TruthTable element is double-clicked to request opening its editor.
void dragMoveEvent(QGraphicsSceneDragDropEvent *event) override
Definition Scene.cpp:1171
void showGates(bool checked)
Shows or hides gate elements. Delegates to VisibilityManager.
Definition Scene.cpp:733
void alignHorizontalCenter()
Definition Scene.cpp:1023
const QVector< GraphicElement * > visibleElements() const
Returns all visible (non-hidden) graphic elements in the scene.
Definition Scene.cpp:329
void updateTheme()
Propagates the current theme to all elements and connections.
Definition Scene.cpp:699
SerializationContext deserializationContext(QHash< quint64, Port * > &portMap, const QVersionNumber &version, SerializationPurpose purpose)
Definition Scene.cpp:216
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override
Definition Scene.cpp:1362
void cutAction()
Cuts the selected items to the internal clipboard.
Definition Scene.cpp:820
const QVector< GraphicElement * > unsortedElements() const
Definition Scene.cpp:341
void setView(GraphicsView *view)
Sets the GraphicsView that displays this scene to view.
Definition Scene.cpp:748
QAction * undoAction() const
Returns the undo QAction bound to the undo stack.
Definition Scene.cpp:753
Simulation * simulation()
Returns the simulation engine associated with this scene.
Definition Scene.cpp:288
QUndoStack * undoStack()
Returns the scene's undo stack.
Definition Scene.cpp:672
Manages the digital circuit simulation loop.
Definition Simulation.h:38
Contains all color attributes for a theme.
QColor m_sceneBgBrush
static ThemeManager & instance()
Returns the singleton ThemeManager instance.
void themeChanged()
Emitted whenever the active theme changes.
static const ThemeAttributes & attributes()
Returns the current ThemeAttributes color set.
void requestOpenTruthTableEditor()
Emitted on double-click to request opening the truth table editor.
void reapply()
Reapplies current visibility state (used after structural changes).
constexpr int gridSize
Scene grid unit in pixels (elements snap to gridSize/2).
Definition Constants.h:12
constexpr const char * CloneDragLegacy
Legacy clone-drag MIME type retained for backward compatibility.
Definition MimeTypes.h:20
constexpr const char * CloneDrag
Current MIME type for clone-drag (Ctrl+drag within the scene).
Definition MimeTypes.h:18
constexpr const char * DragDropLegacy
Legacy drag-and-drop MIME type retained for backward compatibility.
Definition MimeTypes.h:15
constexpr const char * DragDrop
Current drag-and-drop MIME type for element palette → scene drops.
Definition MimeTypes.h:13
Bundles all per-deserialization state so that load() overrides receive it through one explicit parame...
QMap< QString, QByteArray > * blobRegistry
Blob registry for resolving embedded IC blobNames during deserialization.