22
33#include " viewer.h"
44
5- MagnifyingGlass::MagnifyingGlass (int w, int h, float zoomLevel, QWidget *parent)
6- : QLabel(parent), zoomLevel(zoomLevel)
5+ #include < QPainter>
6+ #include < QPainterPath>
7+
8+ MagnifyingGlass::MagnifyingGlass (int w, int h, float zoomLevel, bool circular, bool ring, QWidget *parent)
9+ : QLabel(parent), zoomLevel(zoomLevel), circular(circular), ring(ring)
710{
811 setup (QSize (w, h));
912}
1013
11- MagnifyingGlass::MagnifyingGlass (const QSize &size, float zoomLevel, QWidget *parent)
12- : QLabel(parent), zoomLevel(zoomLevel)
14+ MagnifyingGlass::MagnifyingGlass (const QSize &size, float zoomLevel, bool circular, bool ring, QWidget *parent)
15+ : QLabel(parent), zoomLevel(zoomLevel), circular(circular), ring(ring)
1316{
1417 setup (size);
1518}
1619
1720void MagnifyingGlass::setup (const QSize &size)
1821{
19- resize (size);
22+ logicalSize = size;
23+ resize (displaySize ());
2024 setScaledContents (true );
2125 setMouseTracking (true );
2226 setCursor (QCursor (QBitmap (1 , 1 ), QBitmap (1 , 1 )));
27+ applyShape ();
28+ }
29+
30+ QSize MagnifyingGlass::displaySize () const
31+ {
32+ if (circular) {
33+ const int side = qMax (logicalSize.width (), logicalSize.height ());
34+ return QSize (side, side);
35+ }
36+ return logicalSize;
37+ }
38+
39+ void MagnifyingGlass::applyShape ()
40+ {
41+ if (circular)
42+ setMask (QRegion (rect (), QRegion::Ellipse));
43+ else
44+ clearMask ();
45+ }
46+
47+ void MagnifyingGlass::setCircular (bool circular)
48+ {
49+ if (this ->circular == circular)
50+ return ;
51+ this ->circular = circular;
52+ // Only the display geometry and mask change; logicalSize (and thus the saved
53+ // MAG_GLASS_SIZE) must not be touched, so do not emit sizeChanged here.
54+ resize (displaySize ());
55+ applyShape ();
56+ updateImage ();
57+ }
58+
59+ void MagnifyingGlass::setRing (bool ring)
60+ {
61+ if (this ->ring == ring)
62+ return ;
63+ this ->ring = ring;
64+ if (circular)
65+ update (); // ring only affects the circular rendering; repaint, no geometry change
66+ }
67+
68+ void MagnifyingGlass::paintEvent (QPaintEvent *event)
69+ {
70+ if (!circular) {
71+ QLabel::paintEvent (event);
72+ return ;
73+ }
74+
75+ const QPixmap pm = pixmap ();
76+ QPainter painter (this );
77+ painter.setRenderHint (QPainter::Antialiasing, true );
78+ painter.setRenderHint (QPainter::SmoothPixmapTransform, true );
79+
80+ const QRectF fullRect (rect ());
81+
82+ if (!ring) {
83+ QPainterPath clip;
84+ clip.addEllipse (fullRect);
85+ painter.setClipPath (clip);
86+ if (!pm.isNull ())
87+ painter.drawPixmap (rect (), pm); // mirrors setScaledContents: scale to fill
88+ return ;
89+ }
90+
91+ // Circular + ring. The widget mask (setMask) is a hard-edged ellipse, so anything
92+ // drawn out to the widget boundary keeps that aliased silhouette. Instead, inset the
93+ // whole loupe a couple of pixels inside the mask and let the bezel's own antialiased
94+ // outer edge be the silhouette: the thin margin between bezel and mask stays unpainted
95+ // (transparent) so the page shows through and the antialiased edge blends into it.
96+ const qreal bezelWidth = qMax (2.0 , width () / 80.0 );
97+ const qreal outerInset = 1.5 ; // transparent margin left for the antialiased blend
98+ const QRectF outerRect = fullRect.adjusted (outerInset, outerInset, -outerInset, -outerInset);
99+ const QRectF innerRect = outerRect.adjusted (bezelWidth, bezelWidth, -bezelWidth, -bezelWidth);
100+
101+ // Content clipped to just past the bezel's inner edge, so the content's own (hard)
102+ // clip edge is hidden underneath the opaque part of the bezel.
103+ QPainterPath contentClip;
104+ contentClip.addEllipse (innerRect.adjusted (-0.5 , -0.5 , 0.5 , 0.5 ));
105+ painter.setClipPath (contentClip);
106+ if (!pm.isNull ())
107+ painter.drawPixmap (rect (), pm);
108+ painter.setClipping (false );
109+
110+ // Bezel as a filled annulus so both edges are antialiased: the inner edge blends onto
111+ // the content, the outer edge blends onto the page.
112+ QPainterPath bezel;
113+ bezel.setFillRule (Qt::OddEvenFill);
114+ bezel.addEllipse (outerRect);
115+ bezel.addEllipse (innerRect);
116+ painter.fillPath (bezel, QColor (30 , 30 , 30 ));
23117}
24118
25119void MagnifyingGlass::mouseMoveEvent (QMouseEvent *event)
@@ -31,7 +125,12 @@ void MagnifyingGlass::mouseMoveEvent(QMouseEvent *event)
31125void MagnifyingGlass::updateImage (int x, int y)
32126{
33127 auto *const viewer = qobject_cast<const Viewer *>(parentWidget ());
34- QImage img = viewer->grabMagnifiedRegion (QPoint (x, y), size (), zoomLevel);
128+ // The loupe widget follows the cursor (and may overhang the window edge, as before). Its
129+ // *content* is sampled at the eased center, so the zoomed image swims a little toward the
130+ // edges within the loupe — bounded by the loupe's own half-size so the cursor's point
131+ // never leaves the view.
132+ const QPoint sampleCenter = viewer->easeViewerPos (QPoint (x, y), size (), circular);
133+ QImage img = viewer->grabMagnifiedRegion (sampleCenter, size (), zoomLevel);
35134 setPixmap (QPixmap::fromImage (img));
36135 move (static_cast <int >(x - float (width ()) / 2 ), static_cast <int >(y - float (height ()) / 2 ));
37136}
@@ -46,38 +145,67 @@ void MagnifyingGlass::updateImage()
46145}
47146void MagnifyingGlass::wheelEvent (QWheelEvent *event)
48147{
49- switch (event->modifiers ()) {
50- // size
51- case Qt::NoModifier:
52- if (event->angleDelta ().y () < 0 )
53- sizeUp ();
54- else
55- sizeDown ();
56- break ;
57- // size height
58- case Qt::ControlModifier:
59- if (event->angleDelta ().y () < 0 )
60- heightUp ();
61- else
62- heightDown ();
63- break ;
64- // size width
65- case Qt::AltModifier: // alt modifier can actually modify the behavior of the event delta, so let's check both x & y
66- if (event->angleDelta ().y () < 0 || event->angleDelta ().x () < 0 )
67- widthUp ();
68- else
69- widthDown ();
70- break ;
71- // zoom level
72- case Qt::ShiftModifier:
73- if (event->angleDelta ().y () < 0 )
74- zoomIn ();
75- else
76- zoomOut ();
77- break ;
78- default :
79- break ; // Never propagate a wheel event to the parent widget, even if we ignore it.
148+ // One notch of a real mouse wheel is 120 angle-delta units in a single event, so this
149+ // threshold makes a mouse still step once per notch while a trackpad's tiny events must
150+ // sum to 120 before stepping — the "intent" that stops a faint brush from resizing.
151+ static constexpr int scrollStepThreshold = 120 ;
152+ // Drop a partial accumulation that has gone stale, so an old half-finished gesture can't
153+ // leak into an unrelated later one.
154+ static constexpr qint64 scrollResetMs = 400 ;
155+
156+ const Qt::KeyboardModifiers modifiers = event->modifiers ();
157+
158+ // The active gesture reads a single signed axis. Alt (width) can swap the delta onto the
159+ // x axis, so for it take whichever axis carries the larger movement.
160+ int delta = 0 ;
161+ if (modifiers == Qt::AltModifier) {
162+ const int dy = event->angleDelta ().y ();
163+ const int dx = event->angleDelta ().x ();
164+ delta = (qAbs (dx) > qAbs (dy)) ? dx : dy;
165+ } else {
166+ delta = event->angleDelta ().y ();
167+ }
168+
169+ // Only the four handled gestures accumulate; anything else is swallowed (never propagated
170+ // to the parent) without touching the accumulator.
171+ const bool handled = modifiers == Qt::NoModifier || modifiers == Qt::ControlModifier || modifiers == Qt::AltModifier || modifiers == Qt::ShiftModifier;
172+ if (!handled || delta == 0 ) {
173+ event->setAccepted (true );
174+ return ;
175+ }
176+
177+ // Reset the running total when the gesture changes (different modifier) or when too much
178+ // time has passed since the last wheel event of this gesture.
179+ if (modifiers != lastScrollModifiers || !scrollTimer.isValid () || scrollTimer.elapsed () > scrollResetMs)
180+ scrollAccumulator = 0 ;
181+ lastScrollModifiers = modifiers;
182+ scrollTimer.restart ();
183+
184+ scrollAccumulator += delta;
185+
186+ // A fast, high-magnitude event may cross the threshold several times over; step once per
187+ // crossing and keep the remainder so accumulation stays smooth.
188+ while (qAbs (scrollAccumulator) >= scrollStepThreshold) {
189+ const bool up = scrollAccumulator < 0 ; // convention: negative delta grows the loupe
190+ switch (modifiers) {
191+ case Qt::NoModifier:
192+ up ? sizeUp () : sizeDown ();
193+ break ;
194+ case Qt::ControlModifier:
195+ up ? heightUp () : heightDown ();
196+ break ;
197+ case Qt::AltModifier:
198+ up ? widthUp () : widthDown ();
199+ break ;
200+ case Qt::ShiftModifier:
201+ up ? zoomIn () : zoomOut ();
202+ break ;
203+ default :
204+ break ;
205+ }
206+ scrollAccumulator -= up ? -scrollStepThreshold : scrollStepThreshold;
80207 }
208+
81209 event->setAccepted (true );
82210}
83211void MagnifyingGlass::zoomIn ()
@@ -100,46 +228,46 @@ void MagnifyingGlass::zoomOut()
100228
101229void MagnifyingGlass::sizeUp ()
102230{
103- auto w = width ();
104- auto h = height ();
231+ auto w = logicalSize. width ();
232+ auto h = logicalSize. height ();
105233 if (growWidth (w) | growHeight (h)) // bitwise OR prevents short-circuiting
106234 resizeAndUpdate (w, h);
107235}
108236
109237void MagnifyingGlass::sizeDown ()
110238{
111- auto w = width ();
112- auto h = height ();
239+ auto w = logicalSize. width ();
240+ auto h = logicalSize. height ();
113241 if (shrinkWidth (w) | shrinkHeight (h)) // bitwise OR prevents short-circuiting
114242 resizeAndUpdate (w, h);
115243}
116244
117245void MagnifyingGlass::heightUp ()
118246{
119- auto h = height ();
247+ auto h = logicalSize. height ();
120248 if (growHeight (h))
121- resizeAndUpdate (width (), h);
249+ resizeAndUpdate (logicalSize. width (), h);
122250}
123251
124252void MagnifyingGlass::heightDown ()
125253{
126- auto h = height ();
254+ auto h = logicalSize. height ();
127255 if (shrinkHeight (h))
128- resizeAndUpdate (width (), h);
256+ resizeAndUpdate (logicalSize. width (), h);
129257}
130258
131259void MagnifyingGlass::widthUp ()
132260{
133- auto w = width ();
261+ auto w = logicalSize. width ();
134262 if (growWidth (w))
135- resizeAndUpdate (w, height ());
263+ resizeAndUpdate (w, logicalSize. height ());
136264}
137265
138266void MagnifyingGlass::widthDown ()
139267{
140- auto w = width ();
268+ auto w = logicalSize. width ();
141269 if (shrinkWidth (w))
142- resizeAndUpdate (w, height ());
270+ resizeAndUpdate (w, logicalSize. height ());
143271}
144272
145273void MagnifyingGlass::reset ()
@@ -151,8 +279,10 @@ void MagnifyingGlass::reset()
151279
152280void MagnifyingGlass::resizeAndUpdate (int w, int h)
153281{
154- resize (w, h);
155- emit sizeChanged (size ());
282+ logicalSize = QSize (w, h);
283+ resize (displaySize ());
284+ applyShape ();
285+ emit sizeChanged (logicalSize); // persist the rectangle, never the circular square
156286 updateImage ();
157287}
158288
0 commit comments