// SPDX-License-Identifier: GPL-2.0-or-later /* * Page editing tool * * Authors: * Martin Owens * * Copyright (C) 2021 Authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "pages-tool.h" #include #include #include "desktop.h" #include "display/control/canvas-item-bpath.h" #include "display/control/canvas-item-curve.h" #include "display/control/canvas-item-group.h" #include "display/control/canvas-item-rect.h" #include "display/control/snap-indicator.h" #include "document-undo.h" #include "include/macros.h" #include "object/sp-page.h" #include "path/path-outline.h" #include "pure-transform.h" #include "rubberband.h" #include "selection-chemistry.h" #include "selection.h" #include "snap-preferences.h" #include "snap.h" #include "ui/icon-names.h" #include "ui/knot/knot.h" #include "ui/modifiers.h" #include "ui/widget/canvas.h" using Inkscape::Modifiers::Modifier; #define INDEX_OF(v, k) (std::distance(v.begin(), std::find(v.begin(), v.end(), k))); namespace Inkscape { namespace UI { namespace Tools { PagesTool::PagesTool(SPDesktop *desktop) : ToolBase(desktop, "/tools/pages", "select.svg") { // Stash the regular object selection so we don't modify them in base-tools root handler. desktop->getSelection()->setBackup(); desktop->getSelection()->clear(); Inkscape::Preferences *prefs = Inkscape::Preferences::get(); drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); if (resize_knots.empty()) { for (int i = 0; i < 4; i++) { auto knot = new SPKnot(desktop, _("Resize page"), Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "PageTool:Resize"); knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE); knot->setFill(0xffffff00, 0x0000ff00, 0x000000ff, 0x000000ff); knot->setSize(9); knot->setAnchor(SP_ANCHOR_CENTER); knot->updateCtrl(); knot->hide(); knot->moved_signal.connect(sigc::mem_fun(*this, &PagesTool::resizeKnotMoved)); knot->ungrabbed_signal.connect(sigc::mem_fun(*this, &PagesTool::resizeKnotFinished)); resize_knots.push_back(knot); auto m_knot = new SPKnot(desktop, _("Set page margin"), Inkscape::CANVAS_ITEM_CTRL_TYPE_MARGIN, "PageTool:Margin"); m_knot->setFill(0xffffff00, 0x0000ff00, 0x000000ff, 0x000000ff); m_knot->setStroke(0x1699d791, 0xff99d791, 0x000000ff, 0x000000ff); m_knot->setSize(11); m_knot->setAnchor(SP_ANCHOR_CENTER); m_knot->updateCtrl(); m_knot->hide(); m_knot->request_signal.connect(sigc::mem_fun(*this, &PagesTool::marginKnotMoved)); m_knot->ungrabbed_signal.connect(sigc::mem_fun(*this, &PagesTool::marginKnotFinished)); margin_knots.push_back(m_knot); if (auto window = desktop->getCanvas()->get_window()) { knot->setCursor(SP_KNOT_STATE_DRAGGING, this->get_cursor(window, "page-resizing.svg")); knot->setCursor(SP_KNOT_STATE_MOUSEOVER, this->get_cursor(window, "page-resize.svg")); m_knot->setCursor(SP_KNOT_STATE_DRAGGING, this->get_cursor(window, "page-resizing.svg")); m_knot->setCursor(SP_KNOT_STATE_MOUSEOVER, this->get_cursor(window, "page-resize.svg")); } } } if (!visual_box) { visual_box = make_canvasitem(desktop->getCanvasControls()); visual_box->set_stroke(0x0000ff7f); visual_box->hide(); } if (!drag_group) { drag_group = make_canvasitem(desktop->getCanvasTemp()); drag_group->set_name("CanvasItemGroup:PagesDragShapes"); } _doc_replaced_connection = desktop->connectDocumentReplaced([=](SPDesktop *desktop, SPDocument *doc) { connectDocument(desktop->getDocument()); }); connectDocument(desktop->getDocument()); _zoom_connection = desktop->signal_zoom_changed.connect([=](double) { // This readjusts the knot on zoom because the viewbox position // becomes detached on zoom, likely a precision problem. if (!desktop->getDocument()->getPageManager().hasPages()) { selectionChanged(desktop->getDocument(), nullptr); } }); } PagesTool::~PagesTool() { connectDocument(nullptr); ungrabCanvasEvents(); _desktop->getSelection()->restoreBackup(); visual_box.reset(); for (auto knot : resize_knots) { delete knot; } resize_knots.clear(); if (drag_group) { drag_group.reset(); drag_shapes.clear(); // Already deleted by group } _doc_replaced_connection.disconnect(); _zoom_connection.disconnect(); } void PagesTool::resizeKnotSet(Geom::Rect rect) { for (int i = 0; i < resize_knots.size(); i++) { resize_knots[i]->moveto(rect.corner(i)); resize_knots[i]->show(); } } void PagesTool::marginKnotSet(Geom::Rect margin_rect) { for (int i = 0; i < margin_knots.size(); i++) { margin_knots[i]->moveto(middleOfSide(i, margin_rect) * _desktop->doc2dt()); margin_knots[i]->show(); } } /* * Get the middle of the side of the rectangle. */ Geom::Point PagesTool::middleOfSide(int side, const Geom::Rect &rect) { return Geom::middle_point(rect.corner(side), rect.corner((side + 1) % 4)); } void PagesTool::resizeKnotMoved(SPKnot *knot, Geom::Point const &ppointer, guint state) { Geom::Rect rect; ///< Page rectangle in desktop coordinates. auto page = _desktop->getDocument()->getPageManager().getSelected(); if (page) { // Resizing a specific selected page rect = page->getDesktopRect(); } else if (auto document = _desktop->getDocument()) { // Resizing the naked viewBox rect = *(document->preferredBounds()) * document->doc2dt(); } int index; for (index = 0; index < 4; index++) { if (knot == resize_knots[index]) { break; } } Geom::Point start = rect.corner(index); Geom::Point point = getSnappedResizePoint(knot->position(), state, start, page); if (point != start) { if (index % 3 == 0) rect[Geom::X].setMin(point[Geom::X]); else rect[Geom::X].setMax(point[Geom::X]); if (index < 2) rect[Geom::Y].setMin(point[Geom::Y]); else rect[Geom::Y].setMax(point[Geom::Y]); visual_box->show(); visual_box->set_rect(rect); on_screen_rect = rect; mouse_is_pressed = true; } } /** * Resize snapping allows knot and tool point snapping consistency. */ Geom::Point PagesTool::getSnappedResizePoint(Geom::Point point, guint state, Geom::Point origin, SPObject *target) { if (!(state & GDK_SHIFT_MASK)) { SnapManager &snap_manager = _desktop->namedview->snap_manager; snap_manager.setup(_desktop, true, target); Inkscape::SnapCandidatePoint scp(point, Inkscape::SNAPSOURCE_PAGE_CORNER); scp.addOrigin(origin); Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp); point = sp.getPoint(); snap_manager.unSetup(); } return point; } void PagesTool::resizeKnotFinished(SPKnot *knot, guint state) { auto document = _desktop->getDocument(); auto page = document->getPageManager().getSelected(); if (on_screen_rect) { document->getPageManager().fitToRect(*on_screen_rect * document->dt2doc(), page); Inkscape::DocumentUndo::done(document, "Resize page", INKSCAPE_ICON("tool-pages")); on_screen_rect = {}; } visual_box->hide(); mouse_is_pressed = false; } bool PagesTool::marginKnotMoved(SPKnot *knot, Geom::Point *ppointer, guint state) { auto document = _desktop->getDocument(); auto &pm = document->getPageManager(); // Editing margins creates a page for the margin to be stored in. pm.enablePages(); if (auto page = pm.getSelected()) { Geom::Point point = *ppointer * document->dt2doc(); // Confine knot to edge auto confine = Modifiers::Modifier::get(Modifiers::Type::TRANS_CONFINE)->active(state); if (!Modifiers::Modifier::get(Modifiers::Type::MOVE_SNAPPING)->active(state)) { point = getSnappedResizePoint(point, state, knot->drag_origin, page); } // Calculate what we're acting on, clamp it depending on the side. int side = INDEX_OF(margin_knots, knot); auto axis = (side & 1) ? Geom::X : Geom::Y; auto delta = (point - page->getDocumentRect().corner(side))[axis]; auto value = std::max(0.0, (side + 1) & 2 ? -delta : delta); // Set to page and back to to knot to inform confinement. page->setMarginSide(side, value, confine); knot->setPosition(middleOfSide(side, page->getDocumentMargin()) * document->doc2dt(), state); Inkscape::DocumentUndo::maybeDone(document, "page-margin", ("Adjust page margin"), INKSCAPE_ICON("tool-pages")); } else { g_warning("Can't add margin, pages not enabled correctly!"); } return true; } void PagesTool::marginKnotFinished(SPKnot *knot, guint state) { // Margins are updated in real time. } bool PagesTool::root_handler(GdkEvent *event) { bool ret = false; auto &page_manager = _desktop->getDocument()->getPageManager(); switch (event->type) { case GDK_BUTTON_PRESS: { if (event->button.button == 1) { mouse_is_pressed = true; drag_origin_w = Geom::Point(event->button.x, event->button.y); drag_origin_dt = _desktop->w2d(drag_origin_w); ret = true; if (auto page = pageUnder(drag_origin_dt, false)) { // Select the clicked on page. Manager ignores the same-page. _desktop->getDocument()->getPageManager().selectPage(page); this->set_cursor("page-dragging.svg"); } else if (viewboxUnder(drag_origin_dt)) { dragging_viewbox = true; this->set_cursor("page-dragging.svg"); } else { drag_origin_dt = getSnappedResizePoint(drag_origin_dt, event->button.state, Geom::Point(0, 0)); } } break; } case GDK_MOTION_NOTIFY: { auto point_w = Geom::Point(event->motion.x, event->motion.y); auto point_dt = _desktop->w2d(point_w); bool snap = !(event->motion.state & GDK_SHIFT_MASK); if (event->motion.state & GDK_BUTTON1_MASK) { if (!mouse_is_pressed) { // this sometimes happens if the mouse was off the edge when the event started drag_origin_w = point_w; drag_origin_dt = point_dt; mouse_is_pressed = true; } if (dragging_item || dragging_viewbox) { // Continue to drag item. Geom::Affine tr = moveTo(point_dt, snap); // XXX Moving the existing shapes would be much better, but it has // a weird bug which stops it from working well. // drag_group->update(tr * drag_group->get_parent()->get_affine()); addDragShapes(dragging_item, tr); _desktop->getCanvas()->enable_autoscroll(); } else if (on_screen_rect) { // Continue to drag new box point_dt = getSnappedResizePoint(point_dt, event->motion.state, drag_origin_dt); on_screen_rect = Geom::Rect(drag_origin_dt, point_dt); } else if (Geom::distance(drag_origin_w, point_w) < drag_tolerance) { // do not start dragging anything new if we're within tolerance from origin. // pass } else if (auto page = pageUnder(drag_origin_dt)) { // Starting to drag page around the screen, the pageUnder must // be the drag_origin as small movements can kill the UX feel. dragging_item = page; page_manager.selectPage(page); addDragShapes(page, Geom::Affine()); grabPage(page); } else if (viewboxUnder(drag_origin_dt)) { // Special handling of viewbox dragging dragging_viewbox = true; } else { // Start making a new page. dragging_item = nullptr; on_screen_rect = Geom::Rect(drag_origin_dt, drag_origin_dt); this->set_cursor("page-draw.svg"); } } else { mouse_is_pressed = false; drag_origin_dt = point_dt; } break; } case GDK_BUTTON_RELEASE: { if (event->button.button != 1) { break; } auto point_w = Geom::Point(event->button.x, event->button.y); auto point_dt = _desktop->w2d(point_w); bool snap = !(event->button.state & GDK_SHIFT_MASK); auto document = _desktop->getDocument(); if (dragging_viewbox || dragging_item) { if (dragging_viewbox || dragging_item->isViewportPage()) { // Move the document's viewport first auto page_items = page_manager.getOverlappingItems(_desktop, dragging_item); auto rect = document->preferredBounds(); auto affine = moveTo(point_dt, snap); document->fitToRect(*rect * affine * document->dt2doc(), false); // Now move the page back to where we expect it. if (dragging_item) { dragging_item->movePage(affine, false); dragging_item->setDesktopRect(*rect); } // We have a custom move object because item detection is fubar after fitToRect if (page_manager.move_objects()) { SPPage::moveItems(affine, page_items); } } else { // Move the page object on the canvas. dragging_item->movePage(moveTo(point_dt, snap), page_manager.move_objects()); } Inkscape::DocumentUndo::done(_desktop->getDocument(), "Move page position", INKSCAPE_ICON("tool-pages")); } else if (on_screen_rect) { // conclude box here (make new page) page_manager.selectPage(page_manager.newDesktopPage(*on_screen_rect)); Inkscape::DocumentUndo::done(_desktop->getDocument(), "Create new drawn page", INKSCAPE_ICON("tool-pages")); } mouse_is_pressed = false; drag_origin_dt = point_dt; ret = true; // Clear snap indication on mouse up. _desktop->snapindicator->remove_snaptarget(); break; } case GDK_KEY_PRESS: { if (event->key.keyval == GDK_KEY_Escape) { mouse_is_pressed = false; ret = true; } if (event->key.keyval == GDK_KEY_Delete) { page_manager.deletePage(page_manager.move_objects()); Inkscape::DocumentUndo::done(_desktop->getDocument(), "Delete Page", INKSCAPE_ICON("tool-pages")); ret = true; } } default: break; } // Clean up any finished dragging, doesn't matter how it ends if (!mouse_is_pressed && (dragging_item || on_screen_rect || dragging_viewbox)) { dragging_viewbox = false; dragging_item = nullptr; on_screen_rect = {}; clearDragShapes(); visual_box->hide(); ret = true; } else if (on_screen_rect) { visual_box->show(); visual_box->set_rect(*on_screen_rect); ret = true; } if (!mouse_is_pressed) { if (pageUnder(drag_origin_dt) || viewboxUnder(drag_origin_dt)) { // This page under uses the current mouse position (unlike the above) this->set_cursor("page-mouseover.svg"); } else { this->set_cursor("page-draw.svg"); } } return ret ? true : ToolBase::root_handler(event); } void PagesTool::menu_popup(GdkEvent *event, SPObject *obj) { auto &page_manager = _desktop->getDocument()->getPageManager(); SPPage *page = page_manager.getSelected(); if (event->type != GDK_KEY_PRESS) { drag_origin_w = Geom::Point(event->button.x, event->button.y); drag_origin_dt = _desktop->w2d(drag_origin_w); page = pageUnder(drag_origin_dt); } if (page) { ToolBase::menu_popup(event, page); } } /** * Creates the right snapping setup for dragging items around. */ void PagesTool::grabPage(SPPage *target) { _bbox_points.clear(); getBBoxPoints(target->getDesktopRect(), &_bbox_points, false, SNAPSOURCE_PAGE_CORNER, SNAPTARGET_UNDEFINED, SNAPSOURCE_UNDEFINED, SNAPTARGET_UNDEFINED, SNAPSOURCE_PAGE_CENTER, SNAPTARGET_UNDEFINED); } /* * Generate the movement affine as the page is dragged around (including snapping) */ Geom::Affine PagesTool::moveTo(Geom::Point xy, bool snap) { Geom::Point dxy = xy - drag_origin_dt; if (snap) { SnapManager &snap_manager = _desktop->namedview->snap_manager; snap_manager.setup(_desktop, true, dragging_item); snap_manager.snapprefs.clearTargetMask(0); // Disable all snapping targets snap_manager.snapprefs.setTargetMask(SNAPTARGET_ALIGNMENT_CATEGORY, -1); snap_manager.snapprefs.setTargetMask(SNAPTARGET_ALIGNMENT_PAGE_EDGE_CORNER, -1); snap_manager.snapprefs.setTargetMask(SNAPTARGET_ALIGNMENT_PAGE_EDGE_CENTER, -1); snap_manager.snapprefs.setTargetMask(SNAPTARGET_PAGE_EDGE_CORNER, -1); snap_manager.snapprefs.setTargetMask(SNAPTARGET_PAGE_EDGE_CENTER, -1); snap_manager.snapprefs.setTargetMask(SNAPTARGET_GRID_INTERSECTION, -1); snap_manager.snapprefs.setTargetMask(SNAPTARGET_GUIDE, -1); snap_manager.snapprefs.setTargetMask(SNAPTARGET_GUIDE_INTERSECTION, -1); Inkscape::PureTranslate *bb = new Inkscape::PureTranslate(dxy); snap_manager.snapTransformed(_bbox_points, drag_origin_dt, (*bb)); if (bb->best_snapped_point.getSnapped()) { dxy = bb->getTranslationSnapped(); _desktop->snapindicator->set_new_snaptarget(bb->best_snapped_point); } snap_manager.snapprefs.clearTargetMask(-1); // Reset preferences snap_manager.unSetup(); } return Geom::Translate(dxy); } /** * Add all the shapes needed to see it being dragged. */ void PagesTool::addDragShapes(SPPage *page, Geom::Affine tr) { clearDragShapes(); auto doc = _desktop->getDocument(); if (page) { addDragShape(Geom::PathVector(Geom::Path(page->getDesktopRect())), tr); } else { auto doc_rect = doc->preferredBounds(); addDragShape(Geom::PathVector(Geom::Path(*doc_rect)), tr); } if (Inkscape::Preferences::get()->getBool("/tools/pages/move_objects", true)) { for (auto &item : doc->getPageManager().getOverlappingItems(_desktop, page)) { if (item && !item->isLocked()) { addDragShape(item, tr); } } } } /** * Add an SPItem to the things being dragged. */ void PagesTool::addDragShape(SPItem *item, Geom::Affine tr) { if (auto shape = item_to_outline(item)) { addDragShape(*shape * item->i2dt_affine(), tr); } } /** * Add a shape to the set of dragging shapes, these are deleted when dragging stops. */ void PagesTool::addDragShape(Geom::PathVector &&pth, Geom::Affine tr) { auto shape = new CanvasItemBpath(drag_group.get(), pth * tr, false); shape->set_stroke(0x00ff007f); shape->set_fill(0x00000000, SP_WIND_RULE_EVENODD); drag_shapes.push_back(shape); } /** * Remove all drag shapes from the canvas. */ void PagesTool::clearDragShapes() { for (auto &shape : drag_shapes) { shape->unlink(); } drag_shapes.clear(); } /** * Find a page under the cursor point. */ SPPage *PagesTool::pageUnder(Geom::Point pt, bool retain_selected) { auto &pm = _desktop->getDocument()->getPageManager(); // If the point is still on the selected, favour that one. if (auto selected = pm.getSelected()) { if (retain_selected && selected->getSensitiveRect().contains(pt)) { return selected; } } // This provides a simple way of selecting a page based on their layering // Pages which are entirely contained within another are selected before // their larger parents. SPPage* ret = nullptr; for (auto &page : pm.getPages()) { auto rect = page->getSensitiveRect(); // If the point is inside the page boundry if (rect.contains(pt)) { // If we don't have a page yet, or the new page is inside the old one. if (!ret || ret->getSensitiveRect().contains(rect)) { ret = page; } } } return ret; } /** * Returns true if the document contains no pages AND the point * is within the document viewbox. */ bool PagesTool::viewboxUnder(Geom::Point pt) { if (auto document = _desktop->getDocument()) { auto rect = document->preferredBounds(); rect->expandBy(-0.1); // see sp-page getSensitiveRect return !document->getPageManager().hasPages() && rect.contains(pt); } return true; } void PagesTool::connectDocument(SPDocument *doc) { _selector_changed_connection.disconnect(); if (doc) { auto &page_manager = doc->getPageManager(); _selector_changed_connection = page_manager.connectPageSelected([=](SPPage *page) { selectionChanged(doc, page); }); selectionChanged(doc, page_manager.getSelected()); } else { selectionChanged(doc, nullptr); } } void PagesTool::selectionChanged(SPDocument *doc, SPPage *page) { if (_page_modified_connection) { _page_modified_connection.disconnect(); for (auto knot : resize_knots) { knot->hide(); } for (auto knot : margin_knots) { knot->hide(); } } // Loop existing pages because highlight_item is unsafe. // Use desktop's document instead of doc, which may be nullptr. for (auto &possible : _desktop->getDocument()->getPageManager().getPages()) { if (highlight_item == possible) { highlight_item->setSelected(false); } } highlight_item = page; if (doc) { if (page) { _page_modified_connection = page->connectModified(sigc::mem_fun(*this, &PagesTool::pageModified)); page->setSelected(true); pageModified(page, 0); } else { // This is for viewBox editng directly. A special extra feature _page_modified_connection = doc->connectModified([=](guint){ resizeKnotSet(*(doc->preferredBounds())); marginKnotSet(*(doc->preferredBounds())); }); resizeKnotSet(*(doc->preferredBounds())); marginKnotSet(*(doc->preferredBounds())); } } } void PagesTool::pageModified(SPObject *object, guint /*flags*/) { if (auto page = cast(object)) { resizeKnotSet(page->getDesktopRect()); marginKnotSet(page->getDocumentMargin()); } } } // namespace Tools } // namespace UI } // namespace Inkscape /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :