Commit 67325270 authored by David Hurka's avatar David Hurka 🐬
Browse files

Fix viewport jumps when drag-scrolling beyond screen edges

This creates CursorWrapHelper, which wraps the cursor
from e. g. top screen edge to bottom screen edge,
and calculates the drag offset from the actual cursor movement.
parent 4e071d69
Pipeline #55494 passed with stage
in 10 minutes and 27 seconds
......@@ -379,6 +379,7 @@ if(BUILD_DESKTOP)
part/annotationwidgets.cpp
part/bookmarklist.cpp
part/certificateviewer.cpp
part/cursorwraphelper.cpp
part/debug_ui.cpp
part/drawingtoolactions.cpp
part/fileprinterpreview.cpp
......
/***************************************************************************
* Copyright (C) 2020 by David Hurka <david.hurka@mailbox.org> *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
***************************************************************************/
#include "cursorwraphelper.h"
#include <QCursor>
#include <QGuiApplication>
#include <QRect>
#include <QScreen>
QPointer<QScreen> CursorWrapHelper::s_lastScreen;
QPoint CursorWrapHelper::s_lastCursorPosition;
QPoint CursorWrapHelper::s_lastWrapOperation;
QPoint CursorWrapHelper::wrapCursor(QPoint eventPosition, Qt::Edges edges)
{
QScreen *screen = getScreen();
if (!screen) {
return QPoint();
}
// Step 1: Generate wrap operations.
// Assuming screen->geometry() is larger than 10x10.
const QRect screenGeometry = screen->geometry();
const QPoint screenCursorPos = QCursor::pos(screen);
if (edges & Qt::LeftEdge && screenCursorPos.x() < screenGeometry.left() + 4) {
QCursor::setPos(screen, screenCursorPos + QPoint(screenGeometry.width() - 10, 0));
s_lastWrapOperation.setX(screenGeometry.width() - 10);
} else if (edges & Qt::RightEdge && screenCursorPos.x() > screenGeometry.right() - 4) {
QCursor::setPos(screen, screenCursorPos + QPoint(-screenGeometry.width() + 10, 0));
s_lastWrapOperation.setX(-screenGeometry.width() + 10);
}
if (edges & Qt::TopEdge && screenCursorPos.y() < screenGeometry.top() + 4) {
QCursor::setPos(screen, screenCursorPos + QPoint(0, screenGeometry.height() - 10));
s_lastWrapOperation.setY(screenGeometry.height() - 10);
} else if (edges & Qt::BottomEdge && screenCursorPos.y() > screenGeometry.bottom() - 4) {
QCursor::setPos(screen, screenCursorPos + QPoint(0, -screenGeometry.height() + 10));
s_lastWrapOperation.setY(-screenGeometry.height() + 10);
}
// Step 2: Catch wrap movements.
// We observe the cursor movement since the last call of wrapCursor().
// If the cursor moves in the same magnitude as the last wrap operation,
// we return the value of this wrap operation with appropriate sign.
const QPoint cursorMovement = eventPosition - s_lastCursorPosition;
s_lastCursorPosition = eventPosition;
QPoint ret_wrapDistance;
qreal horizontalMagnitude = qAbs(qreal(s_lastWrapOperation.x()) / qreal(cursorMovement.x()));
int horizontalSign = cursorMovement.x() > 0 ? 1 : -1;
if (0.5 < horizontalMagnitude && horizontalMagnitude < 2.0) {
ret_wrapDistance.setX(qAbs(s_lastWrapOperation.x()) * horizontalSign);
}
qreal verticalMagnitude = qAbs(qreal(s_lastWrapOperation.y()) / qreal(cursorMovement.y()));
int verticalSign = cursorMovement.y() > 0 ? 1 : -1;
if (0.5 < verticalMagnitude && verticalMagnitude < 2.0) {
ret_wrapDistance.setY(qAbs(s_lastWrapOperation.y()) * verticalSign);
}
return ret_wrapDistance;
}
void CursorWrapHelper::startDrag()
{
s_lastWrapOperation.setX(0);
s_lastWrapOperation.setY(0);
}
QScreen *CursorWrapHelper::getScreen()
{
const QPoint cursorPos = QCursor::pos();
if (s_lastScreen && s_lastScreen->geometry().contains(cursorPos)) {
return s_lastScreen;
}
const QList<QScreen *> screens = QGuiApplication::screens();
for (QScreen *screen : screens) {
if (screen->geometry().contains(cursorPos)) {
s_lastScreen = screen;
return screen;
}
}
// Corner case: cursor already pushed against an edge.
for (QScreen *screen : screens) {
if (screen->geometry().adjusted(-5, -5, 5, 5).contains(cursorPos)) {
s_lastScreen = screen;
return screen;
}
}
Q_ASSERT_X(false, "CursorWrapHelper::getScreen()", "Found no screen containing QCursor::pos()");
return nullptr;
}
/***************************************************************************
* Copyright (C) 2020 by David Hurka <david.hurka@mailbox.org> *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
***************************************************************************/
#ifndef CURSORWRAPHELPER_H
#define CURSORWRAPHELPER_H
#include <QPoint>
#include <QPointer>
class QScreen;
/**
* Wrap the cursor around screen edges.
*
* Problem: When setting the cursor position,
* the actual wrap operation may happen later or not at all.
* Your application needs to observe the actual wrap operation,
* and calculate movement offsets based on this operation.
*
* This class provides this functionality in a single static function.
*
* Example:
* \code
* MyWidget::mousePressEvent(QMouseEvent *me)
* {
* CursorWrapHelper::startDrag();
* m_lastCursorPos = me->pos();
* }
*
* MyWidget::mouseMoveEvent(QMouseEvent *me)
* {
* cursorMovement = me->pos() - m_lastCursorPos;
* cursorMovement -= CursorWrapHelper::wrapCursor(me->pos(), Qt::TopEdge | Qt::BottomEdge);
*
* ...
* processMovement(cursorMovement);
* ...
* }
* \endcode
*/
class CursorWrapHelper
{
public:
/**
* Wrap the QCursor around specified screen edges.
*
* Wrapping is performed using QCursor::pos().
* You have to provide a cursor position, because QCursor::pos() is realtime,
* which means it can not be used to calculate the resulting offset for you.
* If you implement mousePressEvent() and mouseMoveEvent(),
* you can simply pass event->pos().
* @p eventPosition may have a constant offset.
*
* @param eventPosition The cursor position you are currently working with.
* @param edges At which edges to wrap. (E. g. top -> bottom: use Qt::TopEdge)
* @returns The actual distance the cursor was moved.
*/
static QPoint wrapCursor(QPoint eventPosition, Qt::Edges edges);
/**
* Call this to avoid computing a wrap distance when a drag starts.
*
* This should be called every time you get e. g. a mousePressEvent().
*/
static void startDrag();
protected:
/** Returns the screen under the cursor */
static QScreen *getScreen();
/** Remember screen to speed up screen search */
static QPointer<QScreen> s_lastScreen;
/**
* Actual wrapping of the cursor may happen later.
* By comparing the magnitude of cursor movements to the last wrap operation,
* we can catch the moment when wrapping actually happens,
* and return the wrapping offset at that time.
*
* Vertical wrapping and horizontal wrapping may happen with little delay,
* so they are handled strictly separately.
*/
static QPoint s_lastCursorPosition;
static QPoint s_lastWrapOperation;
/**
* If the user releases the mouse while it is being wrapped,
* we don’t want the wrap to be subtracted from the next drag operation.
* This timestamp allows to check whether the user possibly started a new drag.
*/
static QPoint s_lastTimeStamp;
};
#endif // CURSORWRAPHELPER_H
......@@ -27,7 +27,9 @@
#include <QClipboard>
#include <QCursor>
#include <QDesktopServices>
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
#include <QDesktopWidget>
#endif
#include <QElapsedTimer>
#include <QEvent>
#include <QGestureEvent>
......@@ -70,6 +72,7 @@
#include "annotationpopup.h"
#include "annotwindow.h"
#include "core/annotations.h"
#include "cursorwraphelper.h"
#include "debug_ui.h"
#include "formwidgets.h"
#include "guiutils.h"
......@@ -2128,29 +2131,18 @@ void PageView::mouseMoveEvent(QMouseEvent *e)
return;
// if holding mouse mid button, perform zoom
if (e->buttons() & Qt::MiddleButton) {
int mouseY = e->globalPos().y();
int deltaY = d->mouseMidLastY - mouseY;
// wrap mouse from top to bottom
const QRect mouseContainer = QApplication::desktop()->screenGeometry(this);
const int absDeltaY = abs(deltaY);
if (absDeltaY > mouseContainer.height() / 2) {
deltaY = mouseContainer.height() - absDeltaY;
}
if (e->buttons() & Qt::MidButton) {
int deltaY = d->mouseMidLastY - e->globalPos().y();
d->mouseMidLastY = e->globalPos().y();
const float upperZoomLimit = d->document->supportsTiles() ? 15.99 : 3.99;
if (mouseY <= mouseContainer.top() + 4 && d->zoomFactor < upperZoomLimit) {
mouseY = mouseContainer.bottom() - 5;
QCursor::setPos(e->globalPos().x(), mouseY);
}
// wrap mouse from bottom to top
else if (mouseY >= mouseContainer.bottom() - 4 && d->zoomFactor > 0.101) {
mouseY = mouseContainer.top() + 5;
QCursor::setPos(e->globalPos().x(), mouseY);
}
// remember last position
d->mouseMidLastY = mouseY;
const float upperZoomLimit = d->document->supportsTiles() ? 99.99 : 3.99;
// Wrap mouse cursor
Qt::Edges wrapEdges;
wrapEdges.setFlag(Qt::TopEdge, d->zoomFactor < upperZoomLimit);
wrapEdges.setFlag(Qt::BottomEdge, d->zoomFactor > 0.101);
deltaY += CursorWrapHelper::wrapCursor(e->globalPos(), wrapEdges).y();
// update zoom level, perform zoom and redraw
if (deltaY) {
......@@ -2194,23 +2186,12 @@ void PageView::mouseMoveEvent(QMouseEvent *e)
setCursor(Qt::ClosedHandCursor);
QPoint mousePos = e->globalPos();
const QRect mouseContainer = QApplication::desktop()->screenGeometry(this);
// Wrap mouse cursor
Qt::Edges wrapEdges;
wrapEdges.setFlag(Qt::TopEdge, verticalScrollBar()->value() > verticalScrollBar()->minimum());
wrapEdges.setFlag(Qt::BottomEdge, verticalScrollBar()->value() < verticalScrollBar()->maximum());
// wrap mouse from top to bottom
if (mousePos.y() <= mouseContainer.top() + 4 && verticalScrollBar()->value() < verticalScrollBar()->maximum() - 10) {
mousePos.setY(mouseContainer.bottom() - 5);
QCursor::setPos(mousePos);
d->mouseGrabOffset -= QPoint(0, mouseContainer.height());
}
// wrap mouse from bottom to top
else if (mousePos.y() >= mouseContainer.bottom() - 4 && verticalScrollBar()->value() > 10) {
mousePos.setY(mouseContainer.top() + 5);
d->mouseGrabOffset += QPoint(0, mouseContainer.height());
QCursor::setPos(mousePos);
}
d->mouseGrabOffset -= CursorWrapHelper::wrapCursor(e->pos(), wrapEdges);
d->scroller->handleInput(QScroller::InputMove, e->pos() + d->mouseGrabOffset, e->timestamp());
}
......@@ -2285,6 +2266,7 @@ void PageView::mousePressEvent(QMouseEvent *e)
if (e->button() == Qt::MiddleButton) {
d->mouseMidLastY = e->globalPos().y();
setCursor(Qt::SizeVerCursor);
CursorWrapHelper::startDrag();
return;
}
......@@ -2310,6 +2292,7 @@ void PageView::mousePressEvent(QMouseEvent *e)
// update press / 'start drag' mouse position
d->mousePressPos = e->globalPos();
CursorWrapHelper::startDrag();
// handle mode dependent mouse press actions
bool leftButton = e->button() == Qt::LeftButton, rightButton = e->button() == Qt::RightButton;
......
......@@ -12,7 +12,6 @@
// qt/kde includes
#include <QAction>
#include <QApplication>
#include <QDesktopWidget>
#include <QIcon>
#include <QPainter>
#include <QResizeEvent>
......@@ -34,6 +33,7 @@
#include "core/document.h"
#include "core/generator.h"
#include "core/page.h"
#include "cursorwraphelper.h"
#include "pagepainter.h"
#include "priorities.h"
#include "settings.h"
......@@ -745,6 +745,8 @@ void ThumbnailListPrivate::mousePressEvent(QMouseEvent *e)
m_mouseGrabPos.setY(0);
m_mouseGrabItem = nullptr;
}
CursorWrapHelper::startDrag();
}
void ThumbnailListPrivate::mouseReleaseEvent(QMouseEvent *e)
......@@ -862,19 +864,9 @@ void ThumbnailListPrivate::mouseMoveEvent(QMouseEvent *e)
m_pageCurrentlyGrabbed = newPageOn;
m_mouseGrabItem = getPageByNumber(m_pageCurrentlyGrabbed);
}
// wrap mouse from top to bottom
const QRect mouseContainer = QApplication::desktop()->screenGeometry(this);
QPoint currentMousePos = QCursor::pos();
if (currentMousePos.y() <= mouseContainer.top() + 4) {
currentMousePos.setY(mouseContainer.bottom() - 5);
QCursor::setPos(currentMousePos);
m_mouseGrabPos.setX(0);
m_mouseGrabPos.setY(0);
}
// wrap mouse from bottom to top
else if (currentMousePos.y() >= mouseContainer.bottom() - 4) {
currentMousePos.setY(mouseContainer.top() + 5);
QCursor::setPos(currentMousePos);
// Wrap mouse cursor
if (!CursorWrapHelper::wrapCursor(mousePos, Qt::TopEdge | Qt::BottomEdge).isNull()) {
m_mouseGrabPos.setX(0);
m_mouseGrabPos.setY(0);
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment