Skip to content

Commit 53d4e00

Browse files
authored
Merge pull request #572 from starlit-void/feature/circular-magnifying-glass
Feature: circular magnifying glass
2 parents 2e0e96d + f51bc0b commit 53d4e00

51 files changed

Lines changed: 3567 additions & 2803 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Version counting is based on semantic versioning (Major.Feature.Patch)
66

77
### YACReader
88
* Change default shortcuts for modifying the magnifying glass size to avoid conflicts with the page zoom shortcuts, `[`, `]`.
9+
* Add an optional circular magnifying glass, with an optional ring drawn around it. The configured size is kept as a rectangle, so switching back and forth doesn't lose it.
10+
* Add optional edge easing for the magnifying glass. The magnified region is pushed toward the edges of the view, so content near the border can be inspected without pushing the cursor all the way into the corner.
11+
* Require a full wheel notch before the magnifying glass changes size or zoom, so a light trackpad gesture no longer resizes it.
912

1013
### YACReaderLibrary
1114
* Add a library repair function to restore missing covers and rescan files that previously failed to be added.

YACReader/configuration.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ void Configuration::load(QSettings *settings)
2828
settings->setValue(MAG_GLASS_SIZE, QSize(350, 175));
2929
if (!settings->contains(MAG_GLASS_ZOOM))
3030
settings->setValue(MAG_GLASS_ZOOM, 0.5);
31+
if (!settings->contains(MAG_GLASS_CIRCULAR))
32+
settings->setValue(MAG_GLASS_CIRCULAR, false);
33+
if (!settings->contains(MAG_GLASS_RING))
34+
settings->setValue(MAG_GLASS_RING, true);
35+
if (!settings->contains(MAG_GLASS_EDGE_EASE))
36+
settings->setValue(MAG_GLASS_EDGE_EASE, true);
3137
if (!settings->contains(FLOW_TYPE))
3238
settings->setValue(FLOW_TYPE, 0);
3339
if (!settings->contains(FULLSCREEN))

YACReader/configuration.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ class Configuration : public QObject
5555
void setMagnifyingGlassSize(const QSize &mgs) { settings->setValue(MAG_GLASS_SIZE, mgs); }
5656
float getMagnifyingGlassZoom() { return settings->value(MAG_GLASS_ZOOM, 0.5).toFloat(); }
5757
void setMagnifyingGlassZoom(float mgz) { settings->setValue(MAG_GLASS_ZOOM, mgz); }
58+
bool getMagnifyingGlassCircular() { return settings->value(MAG_GLASS_CIRCULAR, false).toBool(); }
59+
void setMagnifyingGlassCircular(bool circular) { settings->setValue(MAG_GLASS_CIRCULAR, circular); }
60+
bool getMagnifyingGlassRing() { return settings->value(MAG_GLASS_RING, true).toBool(); }
61+
void setMagnifyingGlassRing(bool ring) { settings->setValue(MAG_GLASS_RING, ring); }
62+
bool getMagnifyingGlassEdgeEase() { return settings->value(MAG_GLASS_EDGE_EASE, true).toBool(); }
63+
void setMagnifyingGlassEdgeEase(bool ease) { settings->setValue(MAG_GLASS_EDGE_EASE, ease); }
5864
QSize getGotoSlideSize() { return settings->value(GO_TO_FLOW_SIZE).toSize(); }
5965
void setGotoSlideSize(const QSize &gss) { settings->setValue(GO_TO_FLOW_SIZE, gss); }
6066
float getZoomLevel() { return settings->value(ZOOM_LEVEL).toFloat(); }

YACReader/magnifying_glass.cpp

Lines changed: 181 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,118 @@
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

1720
void 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

25119
void MagnifyingGlass::mouseMoveEvent(QMouseEvent *event)
@@ -31,7 +125,12 @@ void MagnifyingGlass::mouseMoveEvent(QMouseEvent *event)
31125
void 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
}
47146
void 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
}
83211
void MagnifyingGlass::zoomIn()
@@ -100,46 +228,46 @@ void MagnifyingGlass::zoomOut()
100228

101229
void 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

109237
void 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

117245
void 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

124252
void 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

131259
void 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

138266
void 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

145273
void MagnifyingGlass::reset()
@@ -151,8 +279,10 @@ void MagnifyingGlass::reset()
151279

152280
void 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

Comments
 (0)