You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1076 lines
38 KiB

#include "EditSelection.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include "control/Control.h"
#include "gui/Layout.h"
#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"
#include "model/Stroke.h"
#include "model/Text.h"
#include "serializing/ObjectInputStream.h"
#include "serializing/ObjectOutputStream.h"
#include "undo/ArrangeUndoAction.h"
#include "undo/ColorUndoAction.h"
#include "undo/FontUndoAction.h"
#include "undo/InsertUndoAction.h"
#include "undo/LineStyleUndoAction.h"
#include "undo/SizeUndoAction.h"
#include "undo/UndoRedoHandler.h"
#include "EditSelectionContents.h"
#include "Selection.h"
#include "i18n.h"
using std::vector;
/// Smallest can scale down to, in pixels.
constexpr size_t MINPIXSIZE = 5;
/// Padding for ui buttons
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),
y(0),
rotation(0),
width(0),
height(0),
snappedBounds(Rectangle<double>{}) {
contstruct(undo, view, page);
}
EditSelection::EditSelection(UndoRedoHandler* undo, Selection* selection, XojPageView* view):
snappingHandler(view->getXournal()->getControl()->getSettings()) {
calcSizeFromElements(selection->selectedElements);
contstruct(undo, view, view->getPage());
// Construct the insert order
std::vector<std::pair<Element*, Layer::ElementIndex>> order;
for (Element* e: selection->selectedElements) {
auto i = std::make_pair(e, this->sourceLayer->indexOf(e));
order.insert(std::upper_bound(order.begin(), order.end(), i, EditSelectionContents::insertOrderCmp), i);
}
for (const auto& [e, i]: order) {
this->addElement(e, i);
this->sourceLayer->removeElement(e, false);
}
view->rerenderPage();
}
EditSelection::EditSelection(UndoRedoHandler* undo, Element* e, XojPageView* view, const PageRef& page):
snappingHandler(view->getXournal()->getControl()->getSettings()) {
calcSizeFromElements(std::vector<Element*>{e});
contstruct(undo, view, page);
addElement(e, this->sourceLayer->indexOf(e));
this->sourceLayer->removeElement(e, false);
view->rerenderElement(e);
}
EditSelection::EditSelection(UndoRedoHandler* undo, const vector<Element*>& elements, XojPageView* view,
const PageRef& page):
snappingHandler(view->getXournal()->getControl()->getSettings()) {
calcSizeFromElements(elements);
contstruct(undo, view, page);
for (Element* e: elements) {
addElement(e, this->sourceLayer->indexOf(e));
this->sourceLayer->removeElement(e, false);
}
view->rerenderPage();
}
void EditSelection::calcSizeFromElements(vector<Element*> elements) {
if (elements.empty()) {
x = 0;
y = 0;
width = 0;
height = 0;
snappedBounds = Rectangle<double>{};
return;
}
Element* first = elements.front();
Range range(first->getX(), first->getY());
Rectangle<double> rect = first->getSnappedBounds();
for (Element* e: elements) {
range.addPoint(e->getX(), e->getY());
range.addPoint(e->getX() + e->getElementWidth(), e->getY() + e->getElementHeight());
rect.unite(e->getSnappedBounds()); // size has already been calculated, so skip it
}
// make the visible bounding box large enough so that anchors do not colapse even for horizontal/vertical strokes
// stroke
x = range.getX() - 1.5 * this->btnWidth;
y = range.getY() - 1.5 * this->btnWidth;
width = range.getWidth() + 3 * this->btnWidth;
height = range.getHeight() + 3 * this->btnWidth;
snappedBounds = rect;
}
void EditSelection::contstruct(UndoRedoHandler* undo, XojPageView* view, const PageRef& sourcePage) {
this->view = view;
this->undo = undo;
this->sourcePage = sourcePage;
this->sourceLayer = this->sourcePage->getSelectedLayer();
this->aspectRatio = false;
this->mirror = true;
this->relMousePosX = 0;
this->relMousePosY = 0;
this->relMousePosRotX = 0;
this->relMousePosRotY = 0;
this->mouseDownType = CURSOR_SELECTION_NONE;
int dpi = this->view->getXournal()->getControl()->getSettings()->getDisplayDpi();
this->btnWidth = std::max(10, dpi / 8);
this->contents = new EditSelectionContents(this->getRect(), this->snappedBounds, this->sourcePage,
this->sourceLayer, this->view);
cairo_matrix_init_identity(&this->cmatrix);
this->view->getXournal()->getCursor()->setRotationAngle(0);
this->view->getXournal()->getCursor()->setMirror(false);
}
EditSelection::~EditSelection() {
finalizeSelection();
this->sourcePage = nullptr;
this->sourceLayer = nullptr;
delete this->contents;
this->contents = nullptr;
this->view = nullptr;
this->undo = nullptr;
if (this->edgePanHandler) {
g_source_destroy(this->edgePanHandler);
g_source_unref(this->edgePanHandler);
}
}
/**
* Finishes all pending changes, move the elements, scale the elements and add
* them to new layer if any or to the old if no new layer
*/
void EditSelection::finalizeSelection() {
XojPageView* v = getPageViewUnderCursor();
if (v == nullptr) { // Not on any page - move back to original page and position
double ox = this->snappedBounds.x - this->x;
double oy = this->snappedBounds.y - this->y;
this->x = this->contents->getOriginalX();
this->y = this->contents->getOriginalY();
this->snappedBounds.x = this->x + ox;
this->snappedBounds.y = this->y + oy;
v = this->contents->getSourceView();
}
this->view = v;
PageRef page = this->view->getPage();
Layer* layer = page->getSelectedLayer();
this->contents->finalizeSelection(this->getRect(), this->snappedBounds, this->aspectRatio, layer, page, this->view,
this->undo);
// Calculate new clip region delta due to rotation:
double addW =
std::abs(this->width * cos(this->rotation)) + std::abs(this->height * sin(this->rotation)) - this->width;
double addH =
std::abs(this->width * sin(this->rotation)) + std::abs(this->height * cos(this->rotation)) - this->height;
this->view->rerenderRect(this->x - addW / 2.0, this->y - addH / 2.0, this->width + addW, this->height + addH);
// This is needed if the selection not was 100% on a page
this->view->getXournal()->repaintSelection(true);
}
/**
* get the X coordinate relative to the provided view (getView())
* in document coordinates
*/
auto EditSelection::getXOnView() const -> double { return this->x; }
/**
* get the Y coordinate relative to the provided view (getView())
* in document coordinates
*/
auto EditSelection::getYOnView() const -> double { return this->y; }
auto EditSelection::getOriginalXOnView() -> double { return this->contents->getOriginalX(); }
auto EditSelection::getOriginalYOnView() -> double { return this->contents->getOriginalY(); }
/**
* get the width in document coordinates (multiple with zoom)
*/
auto EditSelection::getWidth() const -> double { return this->width; }
/**
* get the height in document coordinates (multiple with zoom)
*/
auto EditSelection::getHeight() const -> double { return this->height; }
/**
* get the bounding rectangle in document coordinates (multiple with zoom)
*/
auto EditSelection::getRect() const -> Rectangle<double> {
return Rectangle<double>{this->x, this->y, this->width, this->height};
}
/**
* Get the source page (where the selection was done)
*/
auto EditSelection::getSourcePage() -> PageRef { return this->sourcePage; }
/**
* Get the source layer (form where the Elements come)
*/
auto EditSelection::getSourceLayer() -> Layer* { return this->sourceLayer; }
/**
* Get the X coordinate in View coordinates (absolute)
*/
auto EditSelection::getXOnViewAbsolute() -> int {
double zoom = view->getXournal()->getZoom();
return this->view->getX() + static_cast<int>(this->getXOnView() * zoom);
}
/**
* Get the Y coordinate in View coordinates (absolute)
*/
auto EditSelection::getYOnViewAbsolute() -> int {
double zoom = view->getXournal()->getZoom();
return this->view->getY() + static_cast<int>(this->getYOnView() * zoom);
}
/**
* Sets the tool size for pen or eraser, returs an undo action
* (or nullptr if nothing is done)
*/
auto EditSelection::setSize(ToolSize size, const double* thicknessPen, const double* thicknessHighlighter,
const double* thicknessEraser) -> UndoAction* {
return this->contents->setSize(size, thicknessPen, thicknessHighlighter, thicknessEraser);
}
/**
* Fills the stroke, return an undo action
* (Or nullptr if nothing done, e.g. because there is only an image)
*/
auto EditSelection::setFill(int alphaPen, int alphaHighligther) -> UndoAction* {
return this->contents->setFill(alphaPen, alphaHighligther);
}
/**
* Set the line style of all elements, return an undo action
* (Or nullptr if nothing done)
*/
auto EditSelection::setLineStyle(LineStyle style) -> UndoActionPtr { return this->contents->setLineStyle(style); }
/**
* Set the color of all elements, return an undo action
* (Or nullptr if nothing done, e.g. because there is only an image)
*/
auto EditSelection::setColor(Color color) -> UndoAction* { return this->contents->setColor(color); }
/**
* Sets the font of all containing text elements, return an undo action
* (or nullptr if there are no Text elements)
*/
auto EditSelection::setFont(XojFont& font) -> UndoAction* { return this->contents->setFont(font); }
/**
* Fills de undo item if the selection is deleted
* the selection is cleared after
*/
void EditSelection::fillUndoItem(DeleteUndoAction* undo) { this->contents->fillUndoItem(undo); }
/**
* Add an element to this selection
*
*/
void EditSelection::addElement(Element* e, Layer::ElementIndex order) {
this->contents->addElement(e, order);
if (e->rescaleOnlyAspectRatio()) {
this->aspectRatio = true;
}
if (!e->rescaleWithMirror()) {
this->mirror = false;
}
if (e->getType() != ELEMENT_STROKE) {
// Currently only stroke supports rotation
supportRotation = false;
}
}
/**
* Returns all containing elements of this selection
*/
auto EditSelection::getElements() -> vector<Element*>* { return this->contents->getElements(); }
/**
* Returns the insert order of this selection
*/
auto EditSelection::getInsertOrder() const -> std::deque<std::pair<Element*, Layer::ElementIndex>> const& {
return this->contents->getInsertOrder();
}
auto EditSelection::rearrangeInsertOrder(const OrderChange change) -> UndoActionPtr {
using InsertOrder = std::deque<std::pair<Element*, Layer::ElementIndex>>;
const InsertOrder oldOrd = this->getInsertOrder();
InsertOrder newOrd;
std::string desc = _("Arrange");
switch (change) {
case OrderChange::BringToFront:
// Set to largest positive signed integer
for (const auto& [e, _]: oldOrd) {
newOrd.emplace_back(e, std::numeric_limits<Layer::ElementIndex>::max());
}
desc = _("Bring to front");
break;
case OrderChange::BringForward:
// Set indices of elements to range from [max(indices) + 1, max(indices) + 1 + num elements)
newOrd = oldOrd;
std::stable_sort(newOrd.begin(), newOrd.end(), EditSelectionContents::insertOrderCmp);
if (!newOrd.empty()) {
Layer::ElementIndex i = newOrd.back().second + 1;
for (auto& it: newOrd) { it.second = i++; }
}
desc = _("Bring forward");
break;
case OrderChange::SendBackward:
// Set indices of elements to range from [min(indices) - 1, min(indices) + num elements - 1)
newOrd = oldOrd;
std::stable_sort(newOrd.begin(), newOrd.end(), EditSelectionContents::insertOrderCmp);
if (!newOrd.empty()) {
Layer::ElementIndex i = newOrd.front().second;
i = i > 0 ? i - 1 : 0;
for (auto& it: newOrd) { it.second = i++; }
}
desc = _("Send backward");
break;
case OrderChange::SendToBack:
Layer::ElementIndex i = 0;
for (const auto& [e, _]: oldOrd) {
newOrd.emplace_back(e, i);
i++;
}
desc = _("Send to back");
break;
}
this->contents->replaceInsertOrder(newOrd);
PageRef page = this->view->getPage();
return std::make_unique<ArrangeUndoAction>(page, page->getSelectedLayer(), desc, std::move(oldOrd),
std::move(newOrd));
}
/**
* Finish the current movement
* (should be called in the mouse-button-released event handler)
*/
void EditSelection::mouseUp() {
if (this->mouseDownType == CURSOR_SELECTION_DELETE) {
this->view->getXournal()->deleteSelection();
return;
}
PageRef page = this->view->getPage();
Layer* layer = page->getSelectedLayer();
this->rotation = snappingHandler.snapAngle(this->rotation, false);
this->sourcePage = page;
this->sourceLayer = layer;
this->contents->updateContent(this->getRect(), this->snappedBounds, this->rotation, this->aspectRatio, layer, page,
this->view, this->undo, this->mouseDownType);
this->mouseDownType = CURSOR_SELECTION_NONE;
const bool wasEdgePanning = this->isEdgePanning();
this->setEdgePan(false);
updateMatrix();
if (wasEdgePanning) {
this->ensureWithinVisibleArea();
}
}
/**
* Handles mouse input for moving and resizing, coordinates are relative to "view"
*/
void EditSelection::mouseDown(CursorSelectionType type, double x, double y) {
double zoom = this->view->getXournal()->getZoom();
this->mouseDownType = type;
// coordinates relative to top left corner of snapped bounds in coordinate system which is not modified
this->relMousePosX = x / zoom - this->snappedBounds.x;
this->relMousePosY = y / zoom - this->snappedBounds.y;
// coordinates relative to top left corner of snapped bounds in coordinate system which is rotated to make bounding
// box edges horizontal/vertical
cairo_matrix_transform_point(&this->cmatrix, &x, &y);
this->relMousePosRotX = x / zoom - this->snappedBounds.x;
this->relMousePosRotY = y / zoom - this->snappedBounds.y;
}
/**
* Handles mouse input for moving and resizing, coordinates are relative to "view"
*/
void EditSelection::mouseMove(double mouseX, double mouseY, bool alt) {
double zoom = this->view->getXournal()->getZoom();
if (this->mouseDownType == CURSOR_SELECTION_MOVE) {
// compute translation (without snapping)
double dx = mouseX / zoom - this->snappedBounds.x - this->relMousePosX;
double dy = mouseY / zoom - this->snappedBounds.y - this->relMousePosY;
// find corner of reduced bounding box in rotated coordinate system closest to grabbing position
double cx = this->snappedBounds.x;
double cy = this->snappedBounds.y;
if ((this->relMousePosRotX > this->snappedBounds.width / 2) ==
(this->snappedBounds.width > 0)) { // closer to the right side
cx += this->snappedBounds.width;
}
if ((this->relMousePosRotY > this->snappedBounds.height / 2) ==
(this->snappedBounds.height > 0)) { // closer to the lower side
cy += this->snappedBounds.height;
}
// compute corner of reduced bounding box in unmodified coordinate system closest to grabbing position
cairo_matrix_t inv = this->cmatrix;
cairo_matrix_invert(&inv);
cx *= zoom;
cy *= zoom;
cairo_matrix_transform_point(&inv, &cx, &cy);
cx /= zoom;
cy /= zoom;
// compute position where unsnapped corner would move
Point p = Point(cx + dx, cy + dy);
// snap this corner
p = snappingHandler.snapToGrid(p, alt);
// move
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;
double angle = atan2(rdy, rdx);
this->rotation = angle;
this->view->getXournal()->getCursor()->setRotationAngle(180 / M_PI * angle);
} else {
// Translate mouse position into rotated coordinate system:
double rx = mouseX;
double ry = mouseY;
cairo_matrix_transform_point(&this->cmatrix, &rx, &ry);
rx /= zoom;
ry /= zoom;
double minSize = MINPIXSIZE / zoom;
// store pull direction value
int xSide = 0;
int ySide = 0;
if (this->mouseDownType == CURSOR_SELECTION_TOP_LEFT) {
xSide = -1;
ySide = -1;
} else if (this->mouseDownType == CURSOR_SELECTION_TOP_RIGHT) {
xSide = 1;
ySide = -1;
} else if (this->mouseDownType == CURSOR_SELECTION_BOTTOM_LEFT) {
xSide = -1;
ySide = 1;
} else if (this->mouseDownType == CURSOR_SELECTION_BOTTOM_RIGHT) {
xSide = 1;
ySide = 1;
} else if (this->mouseDownType == CURSOR_SELECTION_TOP) {
ySide = -1;
} else if (this->mouseDownType == CURSOR_SELECTION_BOTTOM) {
ySide = 1;
} else if (this->mouseDownType == CURSOR_SELECTION_LEFT) {
xSide = -1;
} else if (this->mouseDownType == CURSOR_SELECTION_RIGHT) {
xSide = 1;
}
// sanity check
if (xSide || ySide) {
// get normalized direction vector for input interpretation (dependent on aspect ratio)
double diag = hypot(xSide * this->width, ySide * this->height);
double nx = xSide * this->width / diag;
double ny = ySide * this->height / diag;
int xMul = (xSide + 1) / 2;
int yMul = (ySide + 1) / 2;
double xOffset =
(rx - this->x) - this->width * xMul; // x-offset from corner/side that is used for resizing
double yOffset =
(ry - this->y) - this->height * yMul; // y-offset from corner/side that is used for resizing
// calculate scale factor using dot product
double f = (xOffset * nx + yOffset * ny + diag) / diag;
f = std::copysign(std::max(std::abs(f), minSize / std::min(std::abs(this->width), std::abs(this->height))),
f);
if (mirror || f > 0) {
scaleShift(xSide ? f : 1, ySide ? f : 1, xSide == -1, ySide == -1);
// in each case first scale without snapping consideration then snap
// take care that wSnap and hSnap are not too small
double snappedX =
snappingHandler.snapHorizontally(this->snappedBounds.x + this->snappedBounds.width * xMul, alt);
double snappedY =
snappingHandler.snapVertically(this->snappedBounds.y + this->snappedBounds.height * yMul, alt);
double dx = snappedX - this->snappedBounds.x - this->snappedBounds.width * xMul;
double dy = snappedY - this->snappedBounds.y - this->snappedBounds.height * yMul;
double fx = (std::abs(this->snappedBounds.width) > minSize) ?
(this->snappedBounds.width + dx * xSide) / this->snappedBounds.width :
1;
double fy = (std::abs(this->snappedBounds.height) > minSize) ?
(this->snappedBounds.height + dy * ySide) / this->snappedBounds.height :
1;
f = (((std::abs(dx) < std::abs(dy)) && (fx != 1)) || fy == 1) ? fx : fy;
f = (std::abs(this->width) * std::abs(f) < minSize || std::abs(this->height) * std::abs(f) < minSize) ?
1 :
f;
scaleShift(xSide ? f : 1, ySide ? f : 1, xSide == -1, ySide == -1);
this->view->getXournal()->getCursor()->setMirror(this->width * this->height < 0);
}
}
}
this->view->getXournal()->repaintSelection();
XojPageView* v = getPageViewUnderCursor();
if (v && v != this->view) {
XournalView* xournal = this->view->getXournal();
const auto pageNr = xournal->getControl()->getDocument()->indexOf(v->getPage());
xournal->pageSelected(pageNr);
translateToView(v);
}
}
// scales with scale factors fx and fy fixing the corner of the reduced bounding box defined by changeLeft and
// changeTop
void EditSelection::scaleShift(double fx, double fy, bool changeLeft, bool changeTop) {
double dx = (changeLeft) ? this->snappedBounds.width * (1 - fx) : 0;
double dy = (changeTop) ? this->snappedBounds.height * (1 - fy) : 0;
this->width *= fx;
this->height *= fy;
this->snappedBounds.width *= fx;
this->snappedBounds.height *= fy;
this->x += dx + (this->x - this->snappedBounds.x) * (fx - 1);
this->y += dy + (this->y - this->snappedBounds.y) * (fy - 1);
this->snappedBounds.x += dx;
this->snappedBounds.y += dy;
// compute new rotation center
double cx = this->snappedBounds.x + this->snappedBounds.width / 2;
double cy = this->snappedBounds.y + this->snappedBounds.height / 2;
// transform it back with old rotation center
double zoom = this->view->getXournal()->getZoom();
double cxRot = cx * zoom;
double cyRot = cy * zoom;
cairo_matrix_t inv = this->cmatrix;
cairo_matrix_invert(&inv);
cairo_matrix_transform_point(&inv, &cxRot, &cyRot);
cxRot /= zoom;
cyRot /= zoom;
// move to compensate for changed rotation centers
moveSelection(cxRot - cx, cyRot - cy);
}
auto EditSelection::getPageViewUnderCursor() -> XojPageView* {
double zoom = view->getXournal()->getZoom();
// get grabbing hand position
double hx = this->view->getX() + (this->snappedBounds.x + this->relMousePosX) * zoom;
double hy = this->view->getY() + (this->snappedBounds.y + this->relMousePosY) * zoom;
Layout* layout = gtk_xournal_get_layout(this->view->getXournal()->getWidget());
XojPageView* v = layout->getPageViewAt(static_cast<int>(hx), static_cast<int>(hy));
return v;
}
/**
* Translate all coordinates which are relative to the current view to the new view,
* and set the attribute view to the new view
*/
void EditSelection::translateToView(XojPageView* v) {
double zoom = view->getXournal()->getZoom();
double ox = this->snappedBounds.x - this->x;
double oy = this->snappedBounds.y - this->y;
int aX1 = getXOnViewAbsolute();
int aY1 = getYOnViewAbsolute();
this->x = (aX1 - v->getX()) / zoom;
this->y = (aY1 - v->getY()) / zoom;
this->snappedBounds.x = this->x + ox;
this->snappedBounds.y = this->y + oy;
this->view = v;
// int aX2 = getXOnViewAbsolute();
// int aY2 = getYOnViewAbsolute();
//
// if (aX1 != aX2)
// {
// g_message("aX1 != aX2!! %i / %i", aX1, aX2);
// }
// if (aY1 != aY2)
// {
// g_message("aY1 != aY2!! %i / %i", aY1, aY2);
// }
}
void EditSelection::copySelection() {
// clone elements in the insert order
std::deque<std::pair<Element*, Layer::ElementIndex>> clonedInsertOrder;
for (auto [e, index]: getInsertOrder()) { clonedInsertOrder.emplace_back(e->clone(), index); }
// apply transformations and add to layer
finalizeSelection();
// restore insert order
contents->replaceInsertOrder(clonedInsertOrder);
// add undo action
PageRef page = this->view->getPage();
Layer* layer = page->getSelectedLayer();
undo->addUndoAction(std::unique_ptr<UndoAction>(new InsertsUndoAction(page, layer, *getElements())));
}
/**
* If the selection should moved (or rescaled)
*/
auto EditSelection::isMoving() -> bool { return this->mouseDownType != CURSOR_SELECTION_NONE; }
/**
* Move the selection
*/
void EditSelection::updateMatrix() {
double zoom = this->view->getXournal()->getZoom();
// store rotation matrix for pointer use; the center of the rotation is the center of the bounding box
double rx = (this->snappedBounds.x + this->snappedBounds.width / 2) * zoom;
double ry = (this->snappedBounds.y + this->snappedBounds.height / 2) * zoom;
cairo_matrix_init_identity(&this->cmatrix);
cairo_matrix_translate(&this->cmatrix, rx, ry);
cairo_matrix_rotate(&this->cmatrix, -this->rotation);
cairo_matrix_translate(&this->cmatrix, -rx, -ry);
}
void EditSelection::moveSelection(double dx, double dy) {
this->x += dx;
this->y += dy;
this->snappedBounds.x += dx;
this->snappedBounds.y += dy;
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();
double zoom = this->view->getXournal()->getZoom();
double sin = std::sin(this->rotation);
double cos = std::cos(this->rotation);
double w = std::abs(this->width * cos) + std::abs(this->height * sin);
double h = std::abs(this->width * sin) + std::abs(this->height * cos);
double cx = this->x + this->width / 2.0;
double cy = this->y + this->height / 2.0;
double minx = cx - w / 2.0;
double miny = cy - h / 2.0;
return {viewx + minx * zoom, viewy + miny * zoom, w * zoom, h * zoom};
}
void EditSelection::ensureWithinVisibleArea() {
const Rectangle<double> viewRect = this->getBoundingBoxInView();
// need to modify this to take into account the position
// of the object, plus typecast because XojPageView takes ints
this->view->getXournal()->ensureRectIsVisible(static_cast<int>(viewRect.x), static_cast<int>(viewRect.y),
static_cast<int>(viewRect.width), static_cast<int>(viewRect.height));
}
/**
* Get the cursor type for the current position (if 0 then the default cursor should be used)
*/
auto EditSelection::getSelectionTypeForPos(double x, double y, double zoom) -> CursorSelectionType {
double x1 = getXOnView() * zoom;
double x2 = x1 + this->width * zoom;
double y1 = getYOnView() * zoom;
double y2 = y1 + this->height * zoom;
double xmin = std::min(x1, x2);
double xmax = std::max(x1, x2);
double ymin = std::min(y1, y2);
double ymax = std::max(y1, y2);
cairo_matrix_transform_point(&this->cmatrix, &x, &y);
const int EDGE_PADDING = (this->btnWidth / 2) + 2;
const int BORDER_PADDING = (this->btnWidth / 2);
if (x1 - EDGE_PADDING <= x && x <= x1 + EDGE_PADDING && y1 - EDGE_PADDING <= y && y <= y1 + EDGE_PADDING) {
return CURSOR_SELECTION_TOP_LEFT;
}
if (x2 - EDGE_PADDING <= x && x <= x2 + EDGE_PADDING && y1 - EDGE_PADDING <= y && y <= y1 + EDGE_PADDING) {
return CURSOR_SELECTION_TOP_RIGHT;
}
if (x1 - EDGE_PADDING <= x && x <= x1 + EDGE_PADDING && y2 - EDGE_PADDING <= y && y <= y2 + EDGE_PADDING) {
return CURSOR_SELECTION_BOTTOM_LEFT;
}
if (x2 - EDGE_PADDING <= x && x <= x2 + EDGE_PADDING && y2 - EDGE_PADDING <= y && y <= y2 + EDGE_PADDING) {
return CURSOR_SELECTION_BOTTOM_RIGHT;
}
if (xmin - (DELETE_PADDING + this->btnWidth) - BORDER_PADDING <= x &&
x <= xmin - (DELETE_PADDING + this->btnWidth) + BORDER_PADDING && y1 - BORDER_PADDING <= y &&
y <= y1 + BORDER_PADDING) {
return CURSOR_SELECTION_DELETE;
}
if (supportRotation && xmax - BORDER_PADDING + ROTATE_PADDING + this->btnWidth <= x &&
x <= xmax + BORDER_PADDING + ROTATE_PADDING + this->btnWidth && (y2 + y1) / 2 - 4 - BORDER_PADDING <= y &&
(y2 + y1) / 2 + 4 + BORDER_PADDING >= y) {
return CURSOR_SELECTION_ROTATE;
}
if (!this->aspectRatio) {
if (xmin <= x && x <= xmax) {
if (y1 - BORDER_PADDING <= y && y <= y1 + BORDER_PADDING) {
return CURSOR_SELECTION_TOP;
}
if (y2 - BORDER_PADDING <= y && y <= y2 + BORDER_PADDING) {
return CURSOR_SELECTION_BOTTOM;
}
}
if (ymin <= y && y <= ymax) {
if (x1 - BORDER_PADDING <= x && x <= x1 + BORDER_PADDING) {
return CURSOR_SELECTION_LEFT;
}
if (x2 - BORDER_PADDING <= x && x <= x2 + BORDER_PADDING) {
return CURSOR_SELECTION_RIGHT;
}
}
}
if (xmin <= x && x <= xmax && ymin <= y && y <= ymax) {
return CURSOR_SELECTION_MOVE;
}
return CURSOR_SELECTION_NONE;
}
/**
* Paints the selection to cr, with the given zoom factor. The coordinates of cr
* should be relative to the provided view by getView() (use translateEvent())
*/
void EditSelection::paint(cairo_t* cr, double zoom) {
double x = this->x;
double y = this->y;
if (std::abs(this->rotation) > __DBL_EPSILON__) {
this->rotation = snappingHandler.snapAngle(this->rotation, false);
double rx = (snappedBounds.x + snappedBounds.width / 2) * zoom;
double ry = (snappedBounds.y + snappedBounds.height / 2) * zoom;
cairo_translate(cr, rx, ry);
cairo_rotate(cr, this->rotation);
// Draw the rotation point for debugging
// cairo_set_source_rgb(cr, 0, 1, 0);
// cairo_rectangle(cr, 0, 0, 10, 10);
// cairo_stroke(cr);
cairo_translate(cr, -rx, -ry);
}
this->contents->paint(cr, x, y, this->rotation, this->width, this->height, zoom);
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
GdkRGBA selectionColor = view->getSelectionColor();
// set the line always the same size on display
cairo_set_line_width(cr, 1);
const double dashes[] = {10.0, 10.0};
cairo_set_dash(cr, dashes, sizeof(dashes) / sizeof(dashes[0]), 0);
gdk_cairo_set_source_rgba(cr, &selectionColor);
cairo_rectangle(cr, std::min(x, x + width) * zoom, std::min(y, y + height) * zoom, std::abs(width) * zoom,
std::abs(height) * zoom);
// for debugging
// cairo_rectangle(cr, snappedBounds.x * zoom, snappedBounds.y * zoom, snappedBounds.width * zoom,
// snappedBounds.height * zoom);
cairo_stroke_preserve(cr);
auto applied = GdkRGBA{selectionColor.red, selectionColor.green, selectionColor.blue, 0.3};
gdk_cairo_set_source_rgba(cr, &applied);
cairo_fill(cr);
cairo_set_dash(cr, nullptr, 0, 0);
if (!this->aspectRatio) {
// top
drawAnchorRect(cr, x + width / 2, y, zoom);
// bottom
drawAnchorRect(cr, x + width / 2, y + height, zoom);
// left
drawAnchorRect(cr, x, y + height / 2, zoom);
// right
drawAnchorRect(cr, x + width, y + height / 2, zoom);
if (supportRotation) {
// rotation handle
drawAnchorRotation(cr, std::min(x, x + width) + std::abs(width) + (ROTATE_PADDING + this->btnWidth) / zoom,
y + height / 2, zoom);
}
}
// top left
drawAnchorRect(cr, x, y, zoom);
// top right
drawAnchorRect(cr, x + width, y, zoom);
// bottom left
drawAnchorRect(cr, x, y + height, zoom);
// bottom right
drawAnchorRect(cr, x + width, y + height, zoom);
drawDeleteRect(cr, std::min(x, x + width) - (DELETE_PADDING + this->btnWidth) / zoom, y, zoom);
}
void EditSelection::drawAnchorRotation(cairo_t* cr, double x, double y, double zoom) {
GdkRGBA selectionColor = view->getSelectionColor();
gdk_cairo_set_source_rgba(cr, &selectionColor);
cairo_rectangle(cr, x * zoom - (this->btnWidth / 2), y * zoom - (this->btnWidth / 2), this->btnWidth,
this->btnWidth);
cairo_stroke_preserve(cr);
cairo_set_source_rgb(cr, 1, 0, 0);
cairo_fill(cr);
}
/**
* draws an idicator where you can scale the selection
*/
void EditSelection::drawAnchorRect(cairo_t* cr, double x, double y, double zoom) {
GdkRGBA selectionColor = view->getSelectionColor();
gdk_cairo_set_source_rgba(cr, &selectionColor);
cairo_rectangle(cr, x * zoom - (this->btnWidth / 2), y * zoom - (this->btnWidth / 2), this->btnWidth,
this->btnWidth);
cairo_stroke_preserve(cr);
cairo_set_source_rgb(cr, 1, 1, 1);
cairo_fill(cr);
}
/**
* draws an idicator where you can delete the selection
*/
void EditSelection::drawDeleteRect(cairo_t* cr, double x, double y, double zoom) const {
cairo_set_source_rgb(cr, 0, 0, 0);
cairo_rectangle(cr, x * zoom - (this->btnWidth / 2), y * zoom - (this->btnWidth / 2), this->btnWidth,
this->btnWidth);
cairo_stroke(cr);
cairo_set_source_rgb(cr, 1, 0, 0);
cairo_move_to(cr, x * zoom - (this->btnWidth / 2), y * zoom - (this->btnWidth / 2));
cairo_rel_move_to(cr, this->btnWidth, 0);
cairo_rel_line_to(cr, -this->btnWidth, this->btnWidth);
cairo_rel_move_to(cr, this->btnWidth, 0);
cairo_rel_line_to(cr, -this->btnWidth, -this->btnWidth);
cairo_stroke(cr);
}
auto EditSelection::getView() -> XojPageView* { return this->view; }
void EditSelection::serialize(ObjectOutputStream& out) {
out.writeObject("EditSelection");
out.writeDouble(this->x);
out.writeDouble(this->y);
out.writeDouble(this->width);
out.writeDouble(this->height);
out.writeDouble(this->snappedBounds.x);
out.writeDouble(this->snappedBounds.y);
out.writeDouble(this->snappedBounds.width);
out.writeDouble(this->snappedBounds.height);
this->contents->serialize(out);
out.endObject();
out.writeInt(static_cast<int>(this->getElements()->size()));
for (Element* e: *this->getElements()) { e->serialize(out); }
}
void EditSelection::readSerialized(ObjectInputStream& in) {
in.readObject("EditSelection");
this->x = in.readDouble();
this->y = in.readDouble();
this->width = in.readDouble();
this->height = in.readDouble();
double xSnap = in.readDouble();
double ySnap = in.readDouble();
double wSnap = in.readDouble();
double hSnap = in.readDouble();
this->snappedBounds = Rectangle<double>{xSnap, ySnap, wSnap, hSnap};
this->contents->readSerialized(in);
in.endObject();
}