diff --git a/src/control/tools/EditSelection.cpp b/src/control/tools/EditSelection.cpp index f8d1a4a9..59b6ea59 100644 --- a/src/control/tools/EditSelection.cpp +++ b/src/control/tools/EditSelection.cpp @@ -9,6 +9,7 @@ #include "gui/PageView.h" #include "gui/XournalView.h" #include "gui/XournalppCursor.h" +#include "gui/widgets/XournalWidget.h" #include "model/Document.h" #include "model/Element.h" #include "model/Layer.h" @@ -37,6 +38,9 @@ constexpr size_t MINPIXSIZE = 5; constexpr int DELETE_PADDING = 20; constexpr int ROTATE_PADDING = 8; +/// Number of times to trigger edge pan timer per second +constexpr unsigned int PAN_TIMER_RATE = 30; + EditSelection::EditSelection(UndoRedoHandler* undo, const PageRef& page, XojPageView* view): snappingHandler(view->getXournal()->getControl()->getSettings()), x(0), @@ -162,6 +166,11 @@ EditSelection::~EditSelection() { this->view = nullptr; this->undo = nullptr; + + if (this->edgePanHandler) { + g_source_destroy(this->edgePanHandler); + g_source_unref(this->edgePanHandler); + } } /** @@ -408,7 +417,13 @@ void EditSelection::mouseUp() { this->view, this->undo, this->mouseDownType); this->mouseDownType = CURSOR_SELECTION_NONE; + + const bool wasEdgePanning = this->isEdgePanning(); + this->setEdgePan(false); updateMatrix(); + if (wasEdgePanning) { + this->ensureWithinVisibleArea(); + } } /** @@ -469,7 +484,12 @@ void EditSelection::mouseMove(double mouseX, double mouseY, bool alt) { p = snappingHandler.snapToGrid(p, alt); // move - moveSelection(p.x - cx, p.y - cy); + if (!this->edgePanInhibitNext) { + moveSelection(p.x - cx, p.y - cy); + this->setEdgePan(true); + } else { + this->edgePanInhibitNext = false; + } } else if (this->mouseDownType == CURSOR_SELECTION_ROTATE && supportRotation) { // catch rotation here double rdx = mouseX / zoom - this->snappedBounds.x - this->snappedBounds.width / 2; double rdy = mouseY / zoom - this->snappedBounds.y - this->snappedBounds.height / 2; @@ -692,13 +712,103 @@ void EditSelection::moveSelection(double dx, double dy) { this->snappedBounds.x += dx; this->snappedBounds.y += dy; - ensureWithinVisibleArea(); - updateMatrix(); this->view->getXournal()->repaintSelection(); } +void EditSelection::setEdgePan(bool pan) { + if (pan && !this->edgePanHandler) { + this->edgePanHandler = g_timeout_source_new(1000 / PAN_TIMER_RATE); + g_source_set_callback(this->edgePanHandler, reinterpret_cast(EditSelection::handleEdgePan), this, + nullptr); + g_source_attach(this->edgePanHandler, nullptr); + } else if (!pan && this->edgePanHandler) { + g_source_unref(this->edgePanHandler); + this->edgePanHandler = nullptr; + this->edgePanInhibitNext = false; + } +} + +bool EditSelection::isEdgePanning() const { return this->edgePanHandler; } + +bool EditSelection::handleEdgePan(EditSelection* self) { + if (self->view->getXournal()->getControl()->getZoomControl()->isZoomPresentationMode()) { + self->setEdgePan(false); + return false; + } + + + Layout* layout = gtk_xournal_get_layout(self->view->getXournal()->getWidget()); + const double zoom = self->view->getXournal()->getZoom(); + + // Helper function to compute scroll amount for a single dimension, based on visible region and selection bbox + const auto computeScrollAmt = [&](double visMin, double visLen, double bboxMin, double bboxLen, + double layoutSize) -> double { + const bool belowMin = bboxMin < visMin; + const bool aboveMax = bboxMin + bboxLen > visMin + visLen; + const double visMax = visMin + visLen; + const double bboxMax = bboxMin + bboxLen; + + // Scroll amount multiplier + double mult = 0.0; + + // Calculate bonus scroll amount due to proportion of selection out of view. + const double maxMult = 5.0; + int panDir = 0; + if (aboveMax) { + panDir = 1; + mult = maxMult * std::min(bboxLen, bboxMax - visMax) / bboxLen; + } else if (belowMin) { + panDir = -1; + mult = maxMult * std::min(bboxLen, visMin - bboxMin) / bboxLen; + } + + // Base amount to translate selection (in document coordinates) per timer tick + const double panSpeed = 20.0; + const double translateAmt = visLen * panSpeed / (100.0 * PAN_TIMER_RATE); + + // Amount to scroll the visible area by (in layout coordinates), accounting for multiplier + double layoutScroll = zoom * panDir * (translateAmt * mult); + + // If scrolling past layout boundaries, clamp scroll amount to boundary + if (visMin + layoutScroll < 0) { + layoutScroll = -visMin; + } else if (visMax + layoutScroll > layoutSize) { + layoutScroll = std::max(0.0, layoutSize - visMax); + } + + return layoutScroll; + }; + + // Compute scroll (for layout) and translation (for selection) for x and y + const int layoutWidth = layout->getMinimalWidth(); + const int layoutHeight = layout->getMinimalHeight(); + const auto visRect = layout->getVisibleRect(); + const auto bbox = self->getBoundingBoxInView(); + const auto layoutScrollX = computeScrollAmt(visRect.x, visRect.width, bbox.x, bbox.width, layoutWidth); + const auto layoutScrollY = computeScrollAmt(visRect.y, visRect.height, bbox.y, bbox.height, layoutHeight); + const auto translateX = layoutScrollX / zoom; + const auto translateY = layoutScrollY / zoom; + + // Perform the scrolling + bool edgePanned = false; + if (self->isMoving() && (layoutScrollX != 0.0 || layoutScrollY != 0.0)) { + layout->scrollRelative(layoutScrollX, layoutScrollY); + self->moveSelection(translateX, translateY); + edgePanned = true; + + // To prevent the selection from jumping and to reduce jitter, block the selection movement triggered by user + // input + self->edgePanInhibitNext = true; + } else { + // No panning, so disable the timer. + self->setEdgePan(false); + } + + return edgePanned; +} + auto EditSelection::getBoundingBoxInView() const -> Rectangle { int viewx = this->view->getX(); int viewy = this->view->getY(); diff --git a/src/control/tools/EditSelection.h b/src/control/tools/EditSelection.h index 768e4c29..8525e35c 100644 --- a/src/control/tools/EditSelection.h +++ b/src/control/tools/EditSelection.h @@ -287,6 +287,18 @@ private: */ void scaleShift(double fx, double fy, bool changeLeft, bool changeTop); + /** + * Set edge panning signal. + */ + void setEdgePan(bool edgePan); + + /** + * Whether the edge pan signal is set. + */ + bool isEdgePanning() const; + + static bool handleEdgePan(EditSelection* self); + private: // DATA /** * Support rotation @@ -373,4 +385,17 @@ private: // HANDLER * The handler for snapping points */ SnapToGridInputHandler snappingHandler; + + /** + * Edge pan timer + */ + GSource* edgePanHandler = nullptr; + + /** + * Inhibit the next move event after edge panning finishes. This prevents + * the selection from teleporting if the page has changed during panning. + * Additionally, this reduces the amount of "jitter" resulting from moving + * the selection in mouseDown while edge panning. + */ + bool edgePanInhibitNext = false; }; diff --git a/src/gui/inputdevices/KeyboardInputHandler.cpp b/src/gui/inputdevices/KeyboardInputHandler.cpp index 0e1cf30c..9266f206 100644 --- a/src/gui/inputdevices/KeyboardInputHandler.cpp +++ b/src/gui/inputdevices/KeyboardInputHandler.cpp @@ -40,6 +40,7 @@ auto KeyboardInputHandler::handleImpl(InputEvent const& event) -> bool { } if (xdir != 0 || ydir != 0) { selection->moveSelection(d * xdir, d * ydir); + selection->ensureWithinVisibleArea(); return true; } }