diff --git a/ui/pageview.h b/ui/pageview.h --- a/ui/pageview.h +++ b/ui/pageview.h @@ -210,6 +210,7 @@ void createAnnotationsVideoWidgets(PageViewItem *item, const QLinkedList< Okular::Annotation * > &annotations); + void holdZoomCenter( ZoomMode newZm, QPointF zoomCenter, float newZoom = 0.0 ); // don't want to expose classes in here class PageViewPrivate * d; diff --git a/ui/pageview.cpp b/ui/pageview.cpp --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -146,7 +146,6 @@ QPoint mousePressPos; QPoint mouseSelectPos; QPoint previousMouseMovePos; - int mouseMidLastY; bool mouseSelecting; QRect mouseSelectionRect; QColor mouseSelectionColor; @@ -245,6 +244,8 @@ // Keep track of mouse over link object const Okular::ObjectRect * mouseOverLinkObject; + bool zoomActive; + QPointF scrollRest; }; PageViewPrivate::PageViewPrivate( PageView *qq ) @@ -365,6 +366,8 @@ d->aMouseMagnifier = nullptr; d->aFitWindowToPage = nullptr; d->trimBoundingBox = Okular::NormalizedRect(); // Null box + d->zoomActive = false; + d->scrollRest = QPointF( 0.0, 0.0 ); switch( Okular::Settings::zoomMode() ) { @@ -1579,19 +1582,15 @@ if (pinch->state() == Qt::GestureStarted) { vanillaZoom = d->zoomFactor; + d->zoomActive = true; } const QPinchGesture::ChangeFlags changeFlags = pinch->changeFlags(); // Zoom if (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged) { - d->zoomFactor = vanillaZoom * pinch->totalScaleFactor(); - - d->blockPixmapsRequest = true; - updateZoom( ZoomRefreshCurrent ); - d->blockPixmapsRequest = false; - viewport()->repaint(); + holdZoomCenter( ZoomRefreshCurrent, mapFromGlobal( pinch->centerPoint().toPoint()), vanillaZoom * pinch->totalScaleFactor() ); } // Count the number of 90-degree rotations we did since the start of the pinch gesture. @@ -1615,9 +1614,11 @@ } } - if (pinch->state() == Qt::GestureFinished) + if ( pinch->state() == Qt::GestureFinished || pinch->state() == Qt::GestureCanceled ) { rotations = 0; + d->zoomActive = false; + d->scrollRest = QPointF( 0.0, 0.0 ); } return true; @@ -1889,8 +1890,16 @@ return; } - // start a timer that will refresh the pixmap after 0.2s - d->delayResizeEventTimer->start( 200 ); + if ( d->zoomActive ) + { + // if we make a continuous zooming with pinch gesture or mouse, we call delayedResizeEvent() direct. + delayedResizeEvent(); + } else + { + // start a timer that will refresh the pixmap after 0.2s + d->delayResizeEventTimer->start( 200 ); + } + d->verticalScrollBarVisible = verticalScrollBar()->isVisible(); d->horizontalScrollBarVisible = horizontalScrollBar()->isVisible(); } @@ -2004,6 +2013,12 @@ d->scrollIncrement = 0; d->autoScrollTimer->stop(); } + + if ( e->key() == Qt::Key_Control ) + { + d->scrollRest = QPointF( 0.0, 0.0 ); + d->zoomActive = false; + } } void PageView::inputMethodEvent( QInputMethodEvent * e ) @@ -2051,6 +2066,9 @@ void PageView::mouseMoveEvent( QMouseEvent * e ) { + if ( e->source() == Qt::MouseEventSynthesizedByQt && d->zoomActive ) + return; + // For some reason in Qt 5.11.2 (no idea when this started) all wheel // events are followed by mouse move events (without changing position), // so we only actually reset the controlWheelAccumulatedDelta if there is a mouse movement @@ -2071,42 +2089,20 @@ // if holding mouse mid button, perform zoom if ( e->buttons() & Qt::MidButton ) { - 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 we lock the mouse cursor position with QCursor::setPos() we will get a mouseMoveEvent + // so we will ignore all mouseMoveEvent to the position d->mousePressPos + if ( e->globalPos() == d->mousePressPos ) + return; - 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 int deltaY = d->mousePressPos.y() - e->globalPos().y(); // update zoom level, perform zoom and redraw if ( deltaY ) { - d->zoomFactor *= ( 1.0 + ( (double)deltaY / 500.0 ) ); - d->blockPixmapsRequest = true; - updateZoom( ZoomRefreshCurrent ); - d->blockPixmapsRequest = false; - viewport()->repaint(); + d->zoomActive = true; + // lock mouse cursor in the position of the mousePressEvent + QCursor::setPos ( d->mousePressPos ); + holdZoomCenter( ZoomRefreshCurrent, mapFromGlobal( d->mousePressPos ), d->zoomFactor * ( 1.0 + ( (double)deltaY / 500.0 ) ) ); } return; } @@ -2253,10 +2249,12 @@ d->autoScrollTimer->stop(); } + // update press / 'start drag' mouse position + d->mousePressPos = e->globalPos(); + // if pressing mid mouse button while not doing other things, begin 'continuous zoom' mode if ( e->button() == Qt::MidButton ) { - d->mouseMidLastY = e->globalPos().y(); setCursor( Qt::SizeVerCursor ); return; } @@ -2283,9 +2281,6 @@ return; } - // update press / 'start drag' mouse position - d->mousePressPos = e->globalPos(); - // handle mode dependent mouse press actions bool leftButton = e->button() == Qt::LeftButton, rightButton = e->button() == Qt::RightButton; @@ -2491,8 +2486,8 @@ // handle mode independent mid bottom zoom if ( e->button() == Qt::MidButton ) { - // request pixmaps since it was disabled during drag - slotRequestVisiblePixmaps(); + d->zoomActive = false; + d->scrollRest = QPointF( 0.0, 0.0 ); // the cursor may now be over a link.. update it updateCursor( eventPos ); return; @@ -3302,12 +3297,14 @@ d->controlWheelAccumulatedDelta += delta; if ( d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep ) { - slotZoomOut(); + d->zoomActive = true; + holdZoomCenter( ZoomOut, e->pos() ); d->controlWheelAccumulatedDelta = 0; } else if ( d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep ) { - slotZoomIn(); + d->zoomActive = true; + holdZoomCenter( ZoomIn, e->pos() ); d->controlWheelAccumulatedDelta = 0; } } @@ -4443,6 +4440,88 @@ return nullptr; } +void PageView::holdZoomCenter(PageView::ZoomMode newZm, QPointF zoomCenter, float newZoom) +{ + const Okular::DocumentViewport & vp = d->document->viewport(); + Q_ASSERT( vp.pageNumber >= 0 ); + + //determine the page below zoom center + const QPoint contentPos = contentAreaPoint( zoomCenter.toPoint() ); + const PageViewItem* page = pickItemOnPoint( contentPos.x(), contentPos.y() ); + const int hScrollBarMaximum = horizontalScrollBar()->maximum(); + const int vScrollBarMaximum = verticalScrollBar()->maximum(); + + //if the zoom center is not over a page, use viewport page number + if ( !page ) { + page = d->items[ vp.pageNumber ]; + } + + const QRect beginGeometry = page->croppedGeometry(); + + QPoint offset { beginGeometry.left(), beginGeometry.top() }; + + const QPointF oldScroll = contentAreaPosition() - offset; + + d->blockPixmapsRequest = true; + if ( newZoom ) + d->zoomFactor = newZoom; + + updateZoom( newZm ); + d->blockPixmapsRequest = false; + + const QRect afterGeometry = page->croppedGeometry(); + const double vpZoomY = (double)afterGeometry.height() / (double)beginGeometry.height(); + const double vpZoomX = (double)afterGeometry.width() / (double)beginGeometry.width(); + + QPointF newScroll; + // The calculation for newScroll is taken from Gwenview class Abstractimageview::setZoom + newScroll.setY( vpZoomY * ( oldScroll.y() + zoomCenter.y() ) - ( zoomCenter.y() ) ); + newScroll.setX( vpZoomX * ( oldScroll.x() + zoomCenter.x() ) - ( zoomCenter.x() ) ); + + // add the remaining scroll from the previous zoom event + newScroll.setY( newScroll.y() + d->scrollRest.y() * vpZoomY ); + newScroll.setX( newScroll.x() + d->scrollRest.x() * vpZoomX ); + + // adjust newScroll to the new margins after zooming + offset = QPoint { afterGeometry.left(), afterGeometry.top() }; + newScroll += offset; + + // adjust newScroll for appear and disappear of the scrollbars + if ( Okular::Settings::showScrollBars() ) + { + if ( hScrollBarMaximum == 0 && horizontalScrollBar()->maximum() > 0 ) + newScroll.setY( newScroll.y() - ( horizontalScrollBar()->height() / 2 ) ); + + if ( hScrollBarMaximum > 0 && horizontalScrollBar()->maximum() == 0 ) + newScroll.setY( newScroll.y() + ( horizontalScrollBar()->height() / 2 ) ); + + if ( vScrollBarMaximum == 0 && verticalScrollBar()->maximum() > 0 ) + newScroll.setX( newScroll.x() - ( verticalScrollBar()->width() / 2 ) ); + + if ( vScrollBarMaximum > 0 && verticalScrollBar()->maximum() == 0 ) + newScroll.setX( newScroll.x() + ( verticalScrollBar()->width() / 2 ) ); + } + + const int newScrollX = std::round (newScroll.x()); + const int newScrollY = std::round (newScroll.y()); + scrollTo( newScrollX, newScrollY ); + + viewport()->setUpdatesEnabled( true ); + viewport()->update(); + + // test if target scroll position was reached, if not save + // the difference in d->scrollRest for later use + const QPointF diffF = newScroll - contentAreaPosition(); + if ( abs( diffF.x() ) < 0.5 && abs( diffF.y() ) < 0.5 ) + { + // scroll target reached set d->scrollRest to 0.0 + d->scrollRest = QPointF( 0.0, 0.0 ); + } else + { + d->scrollRest = diffF; + } +} + //BEGIN private SLOTS void PageView::slotRelayoutPages() // called by: notifySetup, viewportResizeEvent, slotViewMode, slotContinuousToggled, updateZoom @@ -4627,7 +4706,7 @@ viewport()->setUpdatesEnabled( false ); resizeContentArea( QSize( fullWidth, fullHeight ) ); // restore previous viewport if defined and updates enabled - if ( wasUpdatesEnabled ) + if ( wasUpdatesEnabled && !d->zoomActive ) { if ( vp.pageNumber >= 0 ) { @@ -4650,7 +4729,7 @@ } // 5) update the whole viewport if updated enabled - if ( wasUpdatesEnabled ) + if ( wasUpdatesEnabled && !d->zoomActive ) viewport()->update(); }