Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions YACReader/configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ void Configuration::load(QSettings *settings)
settings->setValue(MAG_GLASS_SIZE, QSize(350, 175));
if (!settings->contains(MAG_GLASS_ZOOM))
settings->setValue(MAG_GLASS_ZOOM, 0.5);
if (!settings->contains(MAG_GLASS_CIRCULAR))
settings->setValue(MAG_GLASS_CIRCULAR, false);
if (!settings->contains(MAG_GLASS_RING))
settings->setValue(MAG_GLASS_RING, true);
if (!settings->contains(MAG_GLASS_EDGE_EASE))
settings->setValue(MAG_GLASS_EDGE_EASE, true);
if (!settings->contains(FLOW_TYPE))
settings->setValue(FLOW_TYPE, 0);
if (!settings->contains(FULLSCREEN))
Expand Down
6 changes: 6 additions & 0 deletions YACReader/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ class Configuration : public QObject
void setMagnifyingGlassSize(const QSize &mgs) { settings->setValue(MAG_GLASS_SIZE, mgs); }
float getMagnifyingGlassZoom() { return settings->value(MAG_GLASS_ZOOM, 0.5).toFloat(); }
void setMagnifyingGlassZoom(float mgz) { settings->setValue(MAG_GLASS_ZOOM, mgz); }
bool getMagnifyingGlassCircular() { return settings->value(MAG_GLASS_CIRCULAR, false).toBool(); }
void setMagnifyingGlassCircular(bool circular) { settings->setValue(MAG_GLASS_CIRCULAR, circular); }
bool getMagnifyingGlassRing() { return settings->value(MAG_GLASS_RING, true).toBool(); }
void setMagnifyingGlassRing(bool ring) { settings->setValue(MAG_GLASS_RING, ring); }
bool getMagnifyingGlassEdgeEase() { return settings->value(MAG_GLASS_EDGE_EASE, true).toBool(); }
void setMagnifyingGlassEdgeEase(bool ease) { settings->setValue(MAG_GLASS_EDGE_EASE, ease); }
QSize getGotoSlideSize() { return settings->value(GO_TO_FLOW_SIZE).toSize(); }
void setGotoSlideSize(const QSize &gss) { settings->setValue(GO_TO_FLOW_SIZE, gss); }
float getZoomLevel() { return settings->value(ZOOM_LEVEL).toFloat(); }
Expand Down
232 changes: 181 additions & 51 deletions YACReader/magnifying_glass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,118 @@

#include "viewer.h"

MagnifyingGlass::MagnifyingGlass(int w, int h, float zoomLevel, QWidget *parent)
: QLabel(parent), zoomLevel(zoomLevel)
#include <QPainter>
#include <QPainterPath>

MagnifyingGlass::MagnifyingGlass(int w, int h, float zoomLevel, bool circular, bool ring, QWidget *parent)
: QLabel(parent), zoomLevel(zoomLevel), circular(circular), ring(ring)
{
setup(QSize(w, h));
}

MagnifyingGlass::MagnifyingGlass(const QSize &size, float zoomLevel, QWidget *parent)
: QLabel(parent), zoomLevel(zoomLevel)
MagnifyingGlass::MagnifyingGlass(const QSize &size, float zoomLevel, bool circular, bool ring, QWidget *parent)
: QLabel(parent), zoomLevel(zoomLevel), circular(circular), ring(ring)
{
setup(size);
}

void MagnifyingGlass::setup(const QSize &size)
{
resize(size);
logicalSize = size;
resize(displaySize());
setScaledContents(true);
setMouseTracking(true);
setCursor(QCursor(QBitmap(1, 1), QBitmap(1, 1)));
applyShape();
}

QSize MagnifyingGlass::displaySize() const
{
if (circular) {
const int side = qMax(logicalSize.width(), logicalSize.height());
return QSize(side, side);
}
return logicalSize;
}

void MagnifyingGlass::applyShape()
{
if (circular)
setMask(QRegion(rect(), QRegion::Ellipse));
else
clearMask();
}

void MagnifyingGlass::setCircular(bool circular)
{
if (this->circular == circular)
return;
this->circular = circular;
// Only the display geometry and mask change; logicalSize (and thus the saved
// MAG_GLASS_SIZE) must not be touched, so do not emit sizeChanged here.
resize(displaySize());
applyShape();
updateImage();
}

void MagnifyingGlass::setRing(bool ring)
{
if (this->ring == ring)
return;
this->ring = ring;
if (circular)
update(); // ring only affects the circular rendering; repaint, no geometry change
}

void MagnifyingGlass::paintEvent(QPaintEvent *event)
{
if (!circular) {
QLabel::paintEvent(event);
return;
}

const QPixmap pm = pixmap();
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);

const QRectF fullRect(rect());

if (!ring) {
QPainterPath clip;
clip.addEllipse(fullRect);
painter.setClipPath(clip);
if (!pm.isNull())
painter.drawPixmap(rect(), pm); // mirrors setScaledContents: scale to fill
return;
}

// Circular + ring. The widget mask (setMask) is a hard-edged ellipse, so anything
// drawn out to the widget boundary keeps that aliased silhouette. Instead, inset the
// whole loupe a couple of pixels inside the mask and let the bezel's own antialiased
// outer edge be the silhouette: the thin margin between bezel and mask stays unpainted
// (transparent) so the page shows through and the antialiased edge blends into it.
const qreal bezelWidth = qMax(2.0, width() / 80.0);
const qreal outerInset = 1.5; // transparent margin left for the antialiased blend
const QRectF outerRect = fullRect.adjusted(outerInset, outerInset, -outerInset, -outerInset);
const QRectF innerRect = outerRect.adjusted(bezelWidth, bezelWidth, -bezelWidth, -bezelWidth);

// Content clipped to just past the bezel's inner edge, so the content's own (hard)
// clip edge is hidden underneath the opaque part of the bezel.
QPainterPath contentClip;
contentClip.addEllipse(innerRect.adjusted(-0.5, -0.5, 0.5, 0.5));
painter.setClipPath(contentClip);
if (!pm.isNull())
painter.drawPixmap(rect(), pm);
painter.setClipping(false);

// Bezel as a filled annulus so both edges are antialiased: the inner edge blends onto
// the content, the outer edge blends onto the page.
QPainterPath bezel;
bezel.setFillRule(Qt::OddEvenFill);
bezel.addEllipse(outerRect);
bezel.addEllipse(innerRect);
painter.fillPath(bezel, QColor(30, 30, 30));
}

void MagnifyingGlass::mouseMoveEvent(QMouseEvent *event)
Expand All @@ -31,7 +125,12 @@ void MagnifyingGlass::mouseMoveEvent(QMouseEvent *event)
void MagnifyingGlass::updateImage(int x, int y)
{
auto *const viewer = qobject_cast<const Viewer *>(parentWidget());
QImage img = viewer->grabMagnifiedRegion(QPoint(x, y), size(), zoomLevel);
// The loupe widget follows the cursor (and may overhang the window edge, as before). Its
// *content* is sampled at the eased centre, so the zoomed image swims a little toward the
// edges within the loupe — bounded by the loupe's own half-size so the cursor's point
// never leaves the view.
const QPoint sampleCenter = viewer->easeViewerPos(QPoint(x, y), size(), circular);
QImage img = viewer->grabMagnifiedRegion(sampleCenter, size(), zoomLevel);
setPixmap(QPixmap::fromImage(img));
move(static_cast<int>(x - float(width()) / 2), static_cast<int>(y - float(height()) / 2));
}
Expand All @@ -46,38 +145,67 @@ void MagnifyingGlass::updateImage()
}
void MagnifyingGlass::wheelEvent(QWheelEvent *event)
{
switch (event->modifiers()) {
// size
case Qt::NoModifier:
if (event->angleDelta().y() < 0)
sizeUp();
else
sizeDown();
break;
// size height
case Qt::ControlModifier:
if (event->angleDelta().y() < 0)
heightUp();
else
heightDown();
break;
// size width
case Qt::AltModifier: // alt modifier can actually modify the behavior of the event delta, so let's check both x & y
if (event->angleDelta().y() < 0 || event->angleDelta().x() < 0)
widthUp();
else
widthDown();
break;
// zoom level
case Qt::ShiftModifier:
if (event->angleDelta().y() < 0)
zoomIn();
else
zoomOut();
break;
default:
break; // Never propagate a wheel event to the parent widget, even if we ignore it.
// One notch of a real mouse wheel is 120 angle-delta units in a single event, so this
// threshold makes a mouse still step once per notch while a trackpad's tiny events must
// sum to 120 before stepping — the "intent" that stops a faint brush from resizing.
static constexpr int scrollStepThreshold = 120;
// Drop a partial accumulation that has gone stale, so an old half-finished gesture can't
// leak into an unrelated later one.
static constexpr qint64 scrollResetMs = 400;

const Qt::KeyboardModifiers modifiers = event->modifiers();

// The active gesture reads a single signed axis. Alt (width) can swap the delta onto the
// x axis, so for it take whichever axis carries the larger movement.
int delta = 0;
if (modifiers == Qt::AltModifier) {
const int dy = event->angleDelta().y();
const int dx = event->angleDelta().x();
delta = (qAbs(dx) > qAbs(dy)) ? dx : dy;
} else {
delta = event->angleDelta().y();
}

// Only the four handled gestures accumulate; anything else is swallowed (never propagated
// to the parent) without touching the accumulator.
const bool handled = modifiers == Qt::NoModifier || modifiers == Qt::ControlModifier || modifiers == Qt::AltModifier || modifiers == Qt::ShiftModifier;
if (!handled || delta == 0) {
event->setAccepted(true);
return;
}

// Reset the running total when the gesture changes (different modifier) or when too much
// time has passed since the last wheel event of this gesture.
if (modifiers != lastScrollModifiers || !scrollTimer.isValid() || scrollTimer.elapsed() > scrollResetMs)
scrollAccumulator = 0;
lastScrollModifiers = modifiers;
scrollTimer.restart();

scrollAccumulator += delta;

// A fast, high-magnitude event may cross the threshold several times over; step once per
// crossing and keep the remainder so accumulation stays smooth.
while (qAbs(scrollAccumulator) >= scrollStepThreshold) {
const bool up = scrollAccumulator < 0; // convention: negative delta grows the loupe
switch (modifiers) {
case Qt::NoModifier:
up ? sizeUp() : sizeDown();
break;
case Qt::ControlModifier:
up ? heightUp() : heightDown();
break;
case Qt::AltModifier:
up ? widthUp() : widthDown();
break;
case Qt::ShiftModifier:
up ? zoomIn() : zoomOut();
break;
default:
break;
}
scrollAccumulator -= up ? -scrollStepThreshold : scrollStepThreshold;
}

event->setAccepted(true);
}
void MagnifyingGlass::zoomIn()
Expand All @@ -100,46 +228,46 @@ void MagnifyingGlass::zoomOut()

void MagnifyingGlass::sizeUp()
{
auto w = width();
auto h = height();
auto w = logicalSize.width();
auto h = logicalSize.height();
if (growWidth(w) | growHeight(h)) // bitwise OR prevents short-circuiting
resizeAndUpdate(w, h);
}

void MagnifyingGlass::sizeDown()
{
auto w = width();
auto h = height();
auto w = logicalSize.width();
auto h = logicalSize.height();
if (shrinkWidth(w) | shrinkHeight(h)) // bitwise OR prevents short-circuiting
resizeAndUpdate(w, h);
}

void MagnifyingGlass::heightUp()
{
auto h = height();
auto h = logicalSize.height();
if (growHeight(h))
resizeAndUpdate(width(), h);
resizeAndUpdate(logicalSize.width(), h);
}

void MagnifyingGlass::heightDown()
{
auto h = height();
auto h = logicalSize.height();
if (shrinkHeight(h))
resizeAndUpdate(width(), h);
resizeAndUpdate(logicalSize.width(), h);
}

void MagnifyingGlass::widthUp()
{
auto w = width();
auto w = logicalSize.width();
if (growWidth(w))
resizeAndUpdate(w, height());
resizeAndUpdate(w, logicalSize.height());
}

void MagnifyingGlass::widthDown()
{
auto w = width();
auto w = logicalSize.width();
if (shrinkWidth(w))
resizeAndUpdate(w, height());
resizeAndUpdate(w, logicalSize.height());
}

void MagnifyingGlass::reset()
Expand All @@ -151,8 +279,10 @@ void MagnifyingGlass::reset()

void MagnifyingGlass::resizeAndUpdate(int w, int h)
{
resize(w, h);
emit sizeChanged(size());
logicalSize = QSize(w, h);
resize(displaySize());
applyShape();
emit sizeChanged(logicalSize); // persist the rectangle, never the circular square
updateImage();
}

Expand Down
Loading