Change selection panning behavior to use a timer

This replaces the selection movement panning logic with timer-based scrolling
logic. The previous behavior would attempt to "ensure that the selection is
visible on screen", leading to extremely fast panning. The new logic now sets a
timer when the selection is out of view and pans at a speed proportional to the
amount of the selection that is not visible.
upstream-master
Bryan Tan 5 years ago committed by Bryan Tan
parent 099b555c46
commit 51c98df119
  1. 116
      src/control/tools/EditSelection.cpp
  2. 25
      src/control/tools/EditSelection.h
  3. 1
      src/gui/inputdevices/KeyboardInputHandler.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<GSourceFunc>(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<double> {
int viewx = this->view->getX();
int viewy = this->view->getY();

@ -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;
};

@ -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;
}
}

Loading…
Cancel
Save