Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Version counting is based on semantic versioning (Major.Feature.Patch)
* Fix showing the go to flow bar asking for permission to control the computer on macOS. Moving the cursor into the bar now works without granting any accessibility permission, where before it was silently doing nothing.
* Add a setting to control what the Escape key does. It can keep quitting the reader, as before, or instead cancel the topmost active mode: magnifying glass, dictionary, go to flow and then fullscreen.
* Fix crash caused by changing reading direction while quickly turning pages.
* Use pinch and ctrl+wheel mouse to change the zoom level.

### YACReaderLibrary
* Add a library repair function to restore missing covers and rescan files that previously failed to be added.
Expand Down
194 changes: 194 additions & 0 deletions YACReader/viewer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <QKeyEvent>
#include <QMessageBox>
#include <QPainter>
#include <QPinchGesture>
#include <QPropertyAnimation>
#include <QScrollBar>

Expand Down Expand Up @@ -72,8 +73,34 @@ Viewer::Viewer(QWidget *parent)
shouldOpenPrevious(false),
magnifyingGlassShown(false),
restoreMagnifyingGlass(false),
pinchStartZoom(100),
zoomAnchorNormX(0.5),
zoomAnchorNormY(0.5),
zoomHud(nullptr),
zoomHudHideTimer(nullptr),
zoomPreviewFinishTimer(nullptr),
mouseHandler(std::make_unique<YACReader::MouseHandler>(this))
{
grabGesture(Qt::PinchGesture);

zoomHud = new QLabel(this);
zoomHud->setAlignment(Qt::AlignCenter);
zoomHud->setAttribute(Qt::WA_TransparentForMouseEvents);
zoomHud->setTextFormat(Qt::RichText);
zoomHud->setStyleSheet(
"background-color: rgba(0, 0, 0, 153); border-radius: 3px;");
zoomHud->setFixedSize(100, 60);
zoomHud->hide();

zoomHudHideTimer = new QTimer(this);
zoomHudHideTimer->setSingleShot(true);
connect(zoomHudHideTimer, &QTimer::timeout, zoomHud, &QWidget::hide);

zoomPreviewFinishTimer = new QTimer(this);
zoomPreviewFinishTimer->setSingleShot(true);
zoomPreviewFinishTimer->setInterval(250);
connect(zoomPreviewFinishTimer, &QTimer::timeout, this, &Viewer::renderFinalZoomImage);

translator = new YACReaderTranslator(this);
translator->hide();
translatorAnimation = new QPropertyAnimation(translator, "pos");
Expand Down Expand Up @@ -473,6 +500,8 @@ void Viewer::updatePage()

void Viewer::updateContentSize()
{
cancelZoomPreview();

// there is an image to resize
if (currentPage != nullptr && !currentPage->isNull()) {
QSize pagefit = currentPage->size();
Expand Down Expand Up @@ -802,13 +831,53 @@ void Viewer::wheelEvent(QWheelEvent *event)
return;
}

// Check the modifier before choosing the regular mouse/trackpad scroll path so
// high-resolution devices with pixelDelta (notably on macOS) zoom as well. Qt maps
// ControlModifier to Command on macOS unless the application opts out of that mapping.
if (event->modifiers() == Qt::ControlModifier && event->angleDelta().y() != 0) {
wheelEventZoom(event);
return;
}

wheelZoomAccumulator = 0;

if (!event->pixelDelta().isNull()) {
wheelEventTrackpad(event);
} else {
wheelEventMouse(event);
}
}

void Viewer::wheelEventZoom(QWheelEvent *event)
{
static constexpr int wheelStep = 120;
static constexpr int zoomStep = 10;
static constexpr qint64 accumulatorResetMs = 400;
static constexpr int hudTimeoutMs = 500;

horizontalScroller->stop();
verticalScroller->stop();
wheelStop = false;

if (!wheelZoomTimer.isValid() || wheelZoomTimer.elapsed() > accumulatorResetMs) {
wheelZoomAccumulator = 0;
}
wheelZoomTimer.restart();

wheelZoomAccumulator += event->angleDelta().y();
const int steps = wheelZoomAccumulator / wheelStep;
wheelZoomAccumulator -= steps * wheelStep;

if (steps != 0) {
captureZoomAnchor();
if (applyZoomAtAnchor(zoom + steps * zoomStep)) {
zoomHudHideTimer->start(hudTimeoutMs);
}
}

event->accept();
}

void Viewer::wheelEventMouse(QWheelEvent *event)
{
auto delta = event->angleDelta();
Expand Down Expand Up @@ -1822,11 +1891,136 @@ bool Viewer::eventFilter(QObject *obj, QEvent *event)
return QScrollArea::eventFilter(obj, event);
}

bool Viewer::event(QEvent *event)
{
if (event->type() == QEvent::Gesture) {
return gestureEvent(static_cast<QGestureEvent *>(event));
}
return QScrollArea::event(event);
}

void Viewer::captureZoomAnchor()
{
zoomAnchorViewport = viewport()->mapFromGlobal(QCursor::pos());
if (content->width() > 0 && content->height() > 0) {
const QPoint cursorInContent = content->mapFrom(viewport(), zoomAnchorViewport);
zoomAnchorNormX = std::clamp(double(cursorInContent.x()) / content->width(), 0.0, 1.0);
zoomAnchorNormY = std::clamp(double(cursorInContent.y()) / content->height(), 0.0, 1.0);
} else {
zoomAnchorNormX = 0.5;
zoomAnchorNormY = 0.5;
}
}

bool Viewer::applyZoomAtAnchor(int newZoom)
{
newZoom = std::clamp(newZoom, 30, 500);
if (newZoom == zoom) {
return false;
}

if (continuousScroll) {
updateZoomRatio(newZoom);
} else {
const int previousZoom = zoom;
zoom = newZoom;

if (!zoomPreviewActive) {
// Reuse the current high-quality pixmap while the label follows the requested
// geometry. The normal renderer replaces it after the interaction pauses.
scaledContentsBeforeZoomPreview = content->hasScaledContents();
zoomPreviewBaseSize = content->size();
zoomPreviewBaseZoom = previousZoom;
content->setScaledContents(true);
zoomPreviewActive = true;
}

const double scale = static_cast<double>(newZoom) / zoomPreviewBaseZoom;
content->resize(std::max(1, qRound(zoomPreviewBaseSize.width() * scale)),
std::max(1, qRound(zoomPreviewBaseSize.height() * scale)));
restoreZoomAnchor();
zoomPreviewFinishTimer->start();
}

zoomHud->setText(QStringLiteral("<span style=\"color:white; font-size:12px;\">%1%</span>").arg(zoom));
positionZoomHud();
zoomHud->show();

emit zoomUpdated(zoom);
return true;
}

void Viewer::restoreZoomAnchor()
{
const int alignX = std::max(0, (viewport()->width() - content->width()) / 2);
const int alignY = std::max(0, (viewport()->height() - content->height()) / 2);
const int targetH = std::lround(zoomAnchorNormX * content->width()) + alignX - zoomAnchorViewport.x();
const int targetV = std::lround(zoomAnchorNormY * content->height()) + alignY - zoomAnchorViewport.y();
horizontalScrollBar()->setValue(targetH);
verticalScrollBar()->setValue(targetV);
}

void Viewer::cancelZoomPreview()
{
if (!zoomPreviewActive) {
return;
}

zoomPreviewFinishTimer->stop();
content->setScaledContents(scaledContentsBeforeZoomPreview);
zoomPreviewActive = false;
}

void Viewer::renderFinalZoomImage()
{
if (!zoomPreviewActive) {
return;
}

cancelZoomPreview();
updateContentSize();
restoreZoomAnchor();
}

void Viewer::positionZoomHud()
{
const int margin = 16;
zoomHud->move(width() - zoomHud->width() - margin,
height() - zoomHud->height() - margin);
zoomHud->raise();
}

bool Viewer::gestureEvent(QGestureEvent *event)
{
if (QGesture *g = event->gesture(Qt::PinchGesture)) {
auto *pinch = static_cast<QPinchGesture *>(g);
if (!render->hasLoadedComic()) {
event->accept(pinch);
return true;
}
if (pinch->state() == Qt::GestureStarted) {
zoomHudHideTimer->stop();
pinchStartZoom = zoom;
captureZoomAnchor();
}
int newZoom = std::clamp<int>(std::lround(pinchStartZoom * pinch->totalScaleFactor()), 30, 500);
applyZoomAtAnchor(newZoom);
if (pinch->state() == Qt::GestureFinished || pinch->state() == Qt::GestureCanceled) {
renderFinalZoomImage();
zoomHud->hide();
}
event->accept(pinch);
return true;
}
return QScrollArea::event(event);
}

void Viewer::setActiveWidget(QWidget *w)
{
if (widget() == w) {
return;
}
cancelZoomPreview();
verticalScrollBar()->blockSignals(true);
takeWidget();
const bool isContinuous = (w == continuousWidget);
Expand Down
26 changes: 26 additions & 0 deletions YACReader/viewer.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include <QAction>
#include <QByteArray>
#include <QCloseEvent>
#include <QElapsedTimer>
#include <QGestureEvent>
#include <QLabel>
#include <QList>
#include <QMainWindow>
Expand All @@ -18,6 +20,7 @@
#include <QResizeEvent>
#include <QScrollArea>
#include <QSettings>
#include <QSize>
#include <QTimer>
#include <QWheelEvent>

Expand Down Expand Up @@ -191,7 +194,30 @@ public slots:
void wheelEvent(QWheelEvent *event) override;
void wheelEventMouse(QWheelEvent *event);
void wheelEventTrackpad(QWheelEvent *event);
void wheelEventZoom(QWheelEvent *event);
void mouseMoveEvent(QMouseEvent *event) override;
bool event(QEvent *event) override;
bool gestureEvent(QGestureEvent *event);
void captureZoomAnchor();
bool applyZoomAtAnchor(int newZoom);
void restoreZoomAnchor();
void cancelZoomPreview();
void renderFinalZoomImage();
void positionZoomHud();

int pinchStartZoom;
QPoint zoomAnchorViewport;
double zoomAnchorNormX;
double zoomAnchorNormY;
QLabel *zoomHud;
QTimer *zoomHudHideTimer;
QTimer *zoomPreviewFinishTimer;
bool zoomPreviewActive = false;
bool scaledContentsBeforeZoomPreview = false;
QSize zoomPreviewBaseSize;
int zoomPreviewBaseZoom = 100;
int wheelZoomAccumulator = 0;
QElapsedTimer wheelZoomTimer;

int verticalScrollStep() const;
int horizontalScrollStep() const;
Expand Down
Loading