wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
GraphicsView.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 <cmath>
7
8#include <QApplication>
9#include <QDebug>
10#include <QElapsedTimer>
11#include <QGraphicsItem>
12#include <QKeyEvent>
13#include <QScrollBar>
14
16#include "App/Scene/Scene.h"
17
18namespace {
19// Zoom step ladder (factor 1.25 per step). The range is deliberately asymmetric: elements
20// stay useful when tiny, so we allow far more zoom-out than in.
21constexpr int kMaxViewZoomLevel = 7;
22constexpr int kMinViewZoomLevel = -9;
23} // namespace
24
26 : QGraphicsView(parent)
27{
28 setAccessibleName(tr("Circuit canvas"));
29 setWhatsThis(tr("The circuit canvas. Drag elements here from the palette to build a "
30 "circuit, drag between ports to wire them together, and drag a "
31 "selection to move it."));
32
33 setAcceptDrops(true);
34 // MouseTracking is needed so the scene receives mouseMoveEvents even when
35 // no button is pressed (required for wire-routing cursor feedback)
36 setMouseTracking(true);
37
38 // AnchorUnderMouse keeps the point under the cursor fixed during zoom/resize,
39 // giving the intuitive "zoom into where you're looking" behavior
40 setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
41 setResizeAnchor(QGraphicsView::AnchorUnderMouse);
42
43 // MinimalViewport mode repaints each dirty item's rect individually rather than
44 // merging them into one large bounding rect, reducing pixel fill when changed
45 // items are spread across the scene.
46 setViewportUpdateMode(QGraphicsView::MinimalViewportUpdate);
47
48 setCacheMode(QGraphicsView::CacheBackground);
49}
50
51void GraphicsView::paintEvent(QPaintEvent *event)
52{
53 // Time the pass so Scene can adapt wire antialiasing to the measured paint workload
54 // (see Scene's wire-antialiasing accessors).
55 QElapsedTimer timer;
56 timer.start();
57
58 QGraphicsView::paintEvent(event);
59
60 if (auto *scene_ = qobject_cast<Scene *>(scene())) {
61 scene_->recordWirePaintPass(timer.nsecsElapsed());
62 }
63}
64
66{
67 return m_zoomLevel < kMaxViewZoomLevel;
68}
69
71{
72 return m_zoomLevel > kMinViewZoomLevel;
73}
74
75void GraphicsView::mousePressEvent(QMouseEvent *event)
76{
77 if (event->button() == Qt::MiddleButton) {
78 // Middle-button drag pans the canvas; capture the start position so
79 // mouseMoveEvent can compute the delta to scroll by
80 m_pan = true;
81 m_panStartX = event->pos().x();
82 m_panStartY = event->pos().y();
83 viewport()->setCursor(Qt::ClosedHandCursor);
84 event->accept();
85 return;
86 }
87
88 QGraphicsView::mousePressEvent(event);
89}
90
91void GraphicsView::mouseMoveEvent(QMouseEvent *event)
92{
93 if (m_pan || m_space) {
94 // Subtract delta from scroll values: moving right decreases scrollbar value
95 // because scene coordinates grow leftward as the view scrolls right
96 horizontalScrollBar()->setValue(horizontalScrollBar()->value() - (event->pos().x() - m_panStartX));
97 verticalScrollBar()->setValue(verticalScrollBar()->value() - (event->pos().y() - m_panStartY));
98 // Update the anchor each frame so the next delta is relative to this position
99 m_panStartX = event->pos().x();
100 m_panStartY = event->pos().y();
101 event->accept();
102 return;
103 }
104
105 // Keep panStart updated even when not panning so that panning can begin
106 // correctly mid-drag without a position jump
107 m_panStartX = event->pos().x();
108 m_panStartY = event->pos().y();
109
110 QGraphicsView::mouseMoveEvent(event);
111}
112
113void GraphicsView::mouseReleaseEvent(QMouseEvent *event)
114{
115 if (event->button() == Qt::MiddleButton) {
116 m_pan = false;
117 viewport()->unsetCursor();
118 event->accept();
119 return;
120 }
121
122 QGraphicsView::mouseReleaseEvent(event);
123}
124
125void GraphicsView::keyPressEvent(QKeyEvent *event)
126{
127 // Ignore auto-repeat so a held key counts as one logical press (matches Scene's trigger
128 // handling and avoids spacebar-pan churn on X11, where auto-repeat arrives as release/press
129 // pairs).
130 if (event->isAutoRepeat()) {
131 QGraphicsView::keyPressEvent(event);
132 return;
133 }
134
135 if (event->key() == Qt::Key_Space) {
136 // Spacebar held = pan mode (same as middle-mouse drag), a common convention
137 // in design tools; the flag is checked in mouseMoveEvent
138 m_space = true;
139 viewport()->setCursor(Qt::ClosedHandCursor);
140 event->accept();
141 }
142
143 QGraphicsView::keyPressEvent(event);
144}
145
146void GraphicsView::keyReleaseEvent(QKeyEvent *event)
147{
148 // Ignore auto-repeat releases (X11 emits them mid-hold) so pan mode isn't dropped while the
149 // spacebar is still physically held.
150 if (event->isAutoRepeat()) {
151 QGraphicsView::keyReleaseEvent(event);
152 return;
153 }
154
155 if (event->key() == Qt::Key_Space) {
156 m_space = false;
157 viewport()->unsetCursor();
158 event->accept();
159 }
160
161 QGraphicsView::keyReleaseEvent(event);
162}
163
164void GraphicsView::wheelEvent(QWheelEvent *event)
165{
166 const int zoomDirection = event->angleDelta().y();
167 bool zoomed = false;
168
169 if (zoomDirection > 0 && canZoomIn()) {
170 zoomIn();
171 zoomed = true;
172 } else if (zoomDirection < 0 && canZoomOut()) {
173 zoomOut();
174 zoomed = true;
175 }
176
177 // AnchorUnderMouse handles the scale transform, but scroll bars may shift after
178 // the scale is applied; centerOn() corrects any residual drift so the scene point
179 // that was under the cursor before the wheel event remains under it afterwards.
180 if (zoomed) {
181 centerOn(mapToScene(event->position().toPoint()));
182 }
183
184 event->accept();
185}
186
187void GraphicsView::setFastMode(const bool fastMode)
188{
189 // Fast mode disables expensive rendering hints to maintain frame rate when
190 // the circuit is large or the machine is slow; used during active simulation
191 setRenderHint(QPainter::Antialiasing, !fastMode);
192 setRenderHint(QPainter::TextAntialiasing, !fastMode);
193 setRenderHint(QPainter::SmoothPixmapTransform, !fastMode);
194}
195
197{
198 // 1.25 and 0.8 are reciprocals (1/1.25 == 0.8) so zoom-in then zoom-out
199 // returns to exactly the original scale without floating-point drift
200 scale(1.25, 1.25);
201 ++m_zoomLevel;
202 sentryBreadcrumb("ui", QStringLiteral("Zoom in: level %1").arg(m_zoomLevel));
203 emit zoomChanged();
204}
205
207{
208 scale(0.8, 0.8);
209 m_zoomLevel--;
210 sentryBreadcrumb("ui", QStringLiteral("Zoom out: level %1").arg(m_zoomLevel));
211 emit zoomChanged();
212}
213
215{
216 resetTransform();
217 m_zoomLevel = 0;
218 sentryBreadcrumb("ui", QStringLiteral("Zoom reset"));
219 emit zoomChanged();
220}
221
223{
224 if (!scene()) {
225 return;
226 }
227
228 // Fit the current selection if there is one (zoom-to-selection); otherwise the whole circuit.
229 QRectF target;
230 const auto selected = scene()->selectedItems();
231 if (!selected.isEmpty()) {
232 for (auto *item : selected) {
233 target |= item->sceneBoundingRect();
234 }
235 } else if (auto *scene_ = qobject_cast<Scene *>(scene())) {
236 target = scene_->cachedItemsBoundingRect();
237 } else {
238 target = scene()->itemsBoundingRect();
239 }
240
241 if (!target.isValid() || target.isEmpty()) {
242 return;
243 }
244
245 // A little breathing room around the content.
246 target.adjust(-16.0, -16.0, 16.0, 16.0);
247
248 // fitInView gives the ideal continuous scale; snap it DOWN to the nearest discrete zoom step
249 // so the whole target still fits (floor never overshoots the viewport) and the level ladder
250 // stays consistent with zoomIn()/zoomOut()/canZoom*(). The step is clamped to the same range
251 // those methods enforce.
252 fitInView(target, Qt::KeepAspectRatio);
253 const double fitScale = transform().m11();
254 const int level = qBound(kMinViewZoomLevel, static_cast<int>(std::floor(std::log(fitScale) / std::log(1.25))), kMaxViewZoomLevel);
255
256 resetTransform();
257 const double scaleFactor = std::pow(1.25, level);
258 scale(scaleFactor, scaleFactor);
259 m_zoomLevel = level;
260 centerOn(target.center());
261
262 sentryBreadcrumb("ui", QStringLiteral("Zoom to fit: level %1").arg(m_zoomLevel));
263 emit zoomChanged();
264}
Extended QGraphicsView with zoom, pan, and fast-rendering modes.
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)
void zoomOut()
Decreases the view magnification by one zoom step.
void paintEvent(QPaintEvent *event) override
void mousePressEvent(QMouseEvent *event) override
void keyPressEvent(QKeyEvent *event) override
int m_zoomLevel
Current zoom step (0 = 1:1, positive = zoomed in).
void mouseReleaseEvent(QMouseEvent *event) override
bool canZoomOut() const
Returns true if zooming out further is possible.
GraphicsView(QWidget *parent=nullptr)
Constructs the view with optional parent.
void zoomIn()
Increases the view magnification by one zoom step.
void zoomChanged()
Emitted whenever the zoom level changes.
void keyReleaseEvent(QKeyEvent *event) override
void resetZoom()
Resets the view scale to 1:1.
bool canZoomIn() const
Returns true if zooming in further is possible.
void wheelEvent(QWheelEvent *event) override
void mouseMoveEvent(QMouseEvent *event) override
void setFastMode(const bool fastMode)
Enables or disables fast rendering mode (disables antialiasing).