// SPDX-License-Identifier: GPL-2.0-or-later /* * Inkscape::PageManager - Multi-Page management. * * Copyright 2021 Martin Owens * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "page-manager.h" #include #include "attributes.h" #include "desktop.h" #include "display/control/canvas-page.h" #include "document.h" #include "extension/template.h" #include "object/object-set.h" #include "object/sp-item.h" #include "object/sp-namedview.h" #include "object/sp-page.h" #include "object/sp-root.h" #include "selection-chemistry.h" #include "svg/svg-color.h" #include "util/parse-int-range.h" #include "util/numeric/converters.h" namespace Inkscape { bool PageManager::move_objects() { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); return prefs->getBool("/tools/pages/move_objects", true); } PageManager::PageManager(SPDocument *document) : border_show(true) , border_on_top(true) , shadow_show(true) , checkerboard(false) { _document = document; } PageManager::~PageManager() { pages.clear(); _selected_page = nullptr; _document = nullptr; } /** * Add a page to this manager, called from namedview parent. */ void PageManager::addPage(SPPage *page) { g_assert(page->document == _document); if (std::find(pages.begin(), pages.end(), page) != pages.end()) { // Refuse to double add pages to list. return; } if (auto next = page->getNextPage()) { // Inserted in the middle, probably an undo. auto it = std::find(pages.begin(), pages.end(), next); if (it != pages.end()) { pages.insert(it, page); } else { // This means the next page is not yet added either pages.push_back(page); } } else { pages.push_back(page); } pagesChanged(); } /** * Remove a page from this manager, called from namedview parent. */ void PageManager::removePage(Inkscape::XML::Node *child) { for (auto it = pages.begin(); it != pages.end(); ++it) { SPPage *page = *it; if (page->getRepr() == child) { pages.erase(it); // Reselect because this page is gone. if (_selected_page == page) { if (auto next = page->getNextPage()) { selectPage(next); } else if (auto prev = page->getPreviousPage()) { selectPage(prev); } else { selectPage(nullptr); } } pagesChanged(); break; } } } /** * Reorder page within the internal list to keep it up to date. */ void PageManager::reorderPage(Inkscape::XML::Node *child) { auto nv = _document->getNamedView(); pages.clear(); for (auto &child : nv->children) { if (auto page = cast(&child)) { pages.push_back(page); } } pagesChanged(); } /** * Enables multi page support by turning the document viewBox into * the first page. */ void PageManager::enablePages() { if (!hasPages()) { _selected_page = newDocumentPage(*_document->preferredBounds(), true); } } /** * Add a new page of the default size, this will be either * the size of the viewBox if no pages exist, or the size * of the selected page. */ SPPage *PageManager::newPage() { enablePages(); auto rect = _selected_page->getRect(); auto new_page = newPage(rect.width(), rect.height()); new_page->copyFrom(_selected_page); return new_page; } /** * Add a new page of the given width and height. */ SPPage *PageManager::newPage(double width, double height) { auto loc = nextPageLocation(); return newPage(Geom::Rect::from_xywh(loc, Geom::Point(width, height))); } /** * Return the location of the next created page. */ Geom::Point PageManager::nextPageLocation() const { // Get a new location for the page. double top = 0.0; double left = 0.0; for (auto &page : pages) { auto rect = page->getRect(); if (rect.right() > left) { left = rect.right() + 10; } } return Geom::Point(left, top); } /** * Add a new page with the given rectangle. */ SPPage *PageManager::newPage(Geom::Rect rect, bool first_page) { // This turns on pages support, which will make two pages if none exist yet. // The first is the ViewBox page, and the second is made below as the "second" if (!hasPages() && !first_page) { enablePages(); } auto xml_doc = _document->getReprDoc(); auto repr = xml_doc->createElement("inkscape:page"); repr->setAttributeSvgDouble("x", rect.left()); repr->setAttributeSvgDouble("y", rect.top()); repr->setAttributeSvgDouble("width", rect.width()); repr->setAttributeSvgDouble("height", rect.height()); if (auto nv = _document->getNamedView()) { if (auto page = cast(nv->appendChildRepr(repr))) { Inkscape::GC::release(repr); return page; } } return nullptr; } /** * Create a new page, resizing the rectangle from desktop coordinates. */ SPPage *PageManager::newDesktopPage(Geom::Rect rect, bool first_page) { rect *= _document->dt2doc(); return newDocumentPage(rect, first_page); } /** * Create a new page, using document coordinates. */ SPPage *PageManager::newDocumentPage(Geom::Rect rect, bool first_page) { return newPage(rect * _document->getDocumentScale().inverse(), first_page); } /** * Delete the given page. * * @param page - The page to be deleted. * @param content - Also remove the svg objects that are inside the page. */ void PageManager::deletePage(SPPage *page, bool content) { if (page) { if (content) { for (auto &item : page->getExclusiveItems()) { item->deleteObject(); } for (auto &item : page->getOverlappingItems()) { // Only delete objects when they rest on one page. if (getPagesFor(item, false).size() == 1) { item->deleteObject(); } } } // Only adjust if there will be a page after viewport page is deleted bool fit_viewport = page->isViewportPage() && getPageCount() > 2; // Removal from pages is done automatically via signals. page->deleteObject(); if (fit_viewport) { _document->fitToRect(getFirstPage()->getDesktopRect(), false); } } // As above with the viewbox shadowing, we need go back to a single page // (which is zero pages) when needed. if (auto page = getFirstPage()) { if (getPageCount() == 1) { auto rect = page->getDesktopRect(); // We delete the page, only if it's bare (no margins etc) if (page->isBarePage()) deletePage(page, false); _document->fitToRect(rect, false); } } } /** * Delete the selected page. * * @param content - Also remove the svg objects that are inside the page. */ void PageManager::deletePage(bool content) { deletePage(_selected_page, content); } /** * Disables multi page supply by removing all the page objects. */ void PageManager::disablePages() { while (hasPages()) { deletePage(getLastPage(), false); } } /** * Get page index, returns -1 if the page is not found in this document. */ int PageManager::getPageIndex(const SPPage *page) const { if (page) { auto it = std::find(pages.begin(), pages.end(), page); if (it != pages.end()) { return it - pages.begin(); } g_warning("Can't get page index for %s", page->getId()); } return -1; } /** * Return the index of the page in the index */ int PageManager::getSelectedPageIndex() const { return getPageIndex(_selected_page); } /** * Returns the selected page rect, OR the viewbox rect. */ Geom::Rect PageManager::getSelectedPageRect() const { return _selected_page ? _selected_page->getDesktopRect() : *(_document->preferredBounds()); } Geom::Affine PageManager::getSelectedPageAffine() const { return _selected_page ? _selected_page->getDesktopAffine() : Geom::identity(); } /** * Called when the pages vector is updated, either page * deleted or page created (but not if the page is modified) */ void PageManager::pagesChanged() { if (pages.empty() || getSelectedPageIndex() == -1) { selectPage(nullptr); } _pages_changed_signal.emit(); if (!_selected_page) { for (auto &page : pages) { selectPage(page); break; } } } /** * Set the given page as the selected page. * * @param page - The page to set as the selected page. */ bool PageManager::selectPage(SPPage *page) { if (!page || getPageIndex(page) >= 0) { if (_selected_page != page) { _selected_page = page; _page_selected_signal.emit(_selected_page); // Modified signal for when the attributes themselves are modified. _page_modified_connection.disconnect(); if (page) { _page_modified_connection = page->connectModified([=](SPObject *, unsigned int) { _page_modified_signal.emit(_selected_page); }); } return true; } } return false; } /** * Select the first page the given sp-item object is within. * * If the item is between two pages and one of them is already selected * then don't change the selection. */ bool PageManager::selectPage(SPItem *item, bool contains) { if (_selected_page && _selected_page->itemOnPage(item, contains)) { return true; } for (auto &page : getPagesFor(item, contains)) { return selectPage(page); } return false; } /** * Get the page at the given position or return nullptr if out of range. * * @param index - The page index (from 0) of the page. */ SPPage *PageManager::getPage(int index) const { if (index < 0 || index >= pages.size()) { return nullptr; } return pages[index]; } /** * Get the pages from a set of pages in the given set. * * @param pages - A set of page positions in the format "1,2-3...etc" * @param inverse - Reverse the selection, selecting pages not in page_pos. * * @returns A vector of SPPage objects found. Not found pages are ignored. */ std::vector PageManager::getPages(const std::string &pages, bool inverse) const { return getPages(parseIntRange(pages, 1, getPageCount()), inverse); } /** * Get the pages from a set of pages in the given set. * * @param page_pos - A set of page positions indexed from 1. * @param inverse - Reverse the selection, selecting pages not in page_pos. * * @returns A vector of SPPage objects found. Not found pages are ignored. */ std::vector PageManager::getPages(std::set page_pos, bool inverse) const { std::vector ret; for (auto page : pages) { bool contains = page_pos.find(page->getPagePosition()) != page_pos.end(); if (contains != inverse) { ret.push_back(page); } } return ret; } /** * Return a list of pages this item is on. */ std::vector PageManager::getPagesFor(SPItem *item, bool contains) const { std::vector ret; for (auto &page : pages) { if (page->itemOnPage(item, contains)) { ret.push_back(page); } } return ret; } /** * Return the first page that contains the given item */ SPPage *PageManager::getPageFor(SPItem *item, bool contains) const { for (auto &page : pages) { if (page->itemOnPage(item, contains)) return page; } return nullptr; } /** * Get a page at a specific starting location. */ SPPage *PageManager::getPageAt(Geom::Point pos) const { for (auto &page : pages) { if (page->getDesktopRect().corner(0) == pos) { return page; } } return nullptr; } /** * Returns the page attached to the viewport, or nullptr if no pages * or none of the pages are the viewport page. */ SPPage *PageManager::getViewportPage() const { for (auto &page : pages) { if (page->isViewportPage()) { return page; } } return nullptr; } /** * Returns the total area of all the pages in desktop units. */ Geom::OptRect PageManager::getDesktopRect() const { Geom::OptRect total_area; for (auto &page : pages) { if (total_area) { total_area->unionWith(page->getDesktopRect()); } else { total_area = page->getDesktopRect(); } } return total_area; } /** * Center/zoom on the given page. */ void PageManager::zoomToPage(SPDesktop *desktop, SPPage *page, bool width_only) { Geom::Rect rect = page ? page->getDesktopRect() : *(_document->preferredBounds()); if (rect.minExtent() < 1.0) return; if (width_only) { desktop->set_display_width(rect, 10); } else { desktop->set_display_area(rect, 10); } } /** * Center without zooming on the given page */ void PageManager::centerToPage(SPDesktop *desktop, SPPage *page) { Geom::Rect rect = page ? page->getDesktopRect() : *(_document->preferredBounds()); desktop->set_display_center(rect); } void PageManager::resizePage(double width, double height) { resizePage(_selected_page, width, height); } void PageManager::resizePage(SPPage *page, double width, double height) { if (pages.empty() || page) { // Resizing the Viewport, means the page gets updated automatically if (pages.empty() || (page && page->isViewportPage())) { auto rect = Geom::Rect(Geom::Point(0, 0), Geom::Point(width, height)); _document->fitToRect(rect, false); } else if (page) { page->setSize(width, height); } } } /** * Change page orientation, landscape to portrait and back. */ void PageManager::changeOrientation() { auto rect = getSelectedPageRect(); resizePage(rect.height(), rect.width()); } /** * Resize the page to the given selection. If nothing is selected, * Resize to all the items on the selected page. */ void PageManager::fitToSelection(ObjectSet *selection, bool add_margins) { auto desktop = selection->desktop(); if (!selection || selection->isEmpty()) { // This means there aren't any pages, so revert to the default assumption // that the viewport is resized around ALL objects. if (!_selected_page) { fitToRect(_document->getRoot()->documentVisualBounds(), _selected_page, add_margins); } else { // This allows the pages to be resized around the items related to the page only. auto contents = ObjectSet(); contents.setList(getOverlappingItems(desktop, _selected_page)); if (contents.isEmpty()) { fitToRect(_document->getRoot()->documentVisualBounds(), _selected_page, add_margins); } else { fitToSelection(&contents, add_margins); } } } else if (auto rect = selection->documentBounds(SPItem::VISUAL_BBOX)) { fitToRect(rect, _selected_page, add_margins); } } /** * Fit the selected page to the given rectangle. */ void PageManager::fitToRect(Geom::OptRect rect, SPPage *page, bool add_margins) { if (!rect) return; bool viewport = true; if (page) { viewport = page->isViewportPage(); page->setDocumentRect(*rect, add_margins); rect = page->getDocumentRect(); } if (viewport) { _document->fitToRect(*rect); if (page && !page->isViewportPage()) { // The document's fitToRect has slightly mangled the page rect, fix it. page->setDocumentRect(Geom::Rect(Geom::Point(0, 0), rect->dimensions())); } } } /** * Return a list of objects touching this page, or viewbox (of single page document) */ std::vector PageManager::getOverlappingItems(SPDesktop *desktop, SPPage *page, bool hidden, bool in_bleed, bool in_layers) { if (page) { return page->getOverlappingItems(hidden, in_bleed, in_layers); } auto doc_rect = _document->preferredBounds(); return _document->getItemsPartiallyInBox(desktop->dkey, *doc_rect, true, true, true, false, in_layers); } /** * Manage the page subset of attributes from sp-namedview and store them. */ bool PageManager::subset(SPAttr key, const gchar *value) { switch (key) { case SPAttr::SHOWBORDER: this->border_show.readOrUnset(value); break; case SPAttr::BORDERLAYER: this->border_on_top.readOrUnset(value); break; case SPAttr::BORDERCOLOR: this->border_color = this->border_color & 0xff; if (value) { this->border_color = this->border_color | sp_svg_read_color(value, this->border_color); } break; case SPAttr::BORDEROPACITY: sp_ink_read_opacity(value, &this->border_color, 0x000000ff); break; case SPAttr::PAGECOLOR: if (value) { this->background_color = sp_svg_read_color(value, this->background_color) | 0xff; } break; case SPAttr::SHOWPAGESHADOW: // Deprecated this->shadow_show.readOrUnset(value); break; case SPAttr::INKSCAPE_DESK_CHECKERBOARD: checkerboard.readOrUnset(value); return false; // propagate further case SPAttr::PAGELABELSTYLE: label_style = value ? value : "default"; // Update user action button if (auto action = _document->getActionGroup()->lookup_action("page-label-style")) { action->change_state(label_style == "below"); } break; default: return false; } return true; } /** * Update the canvas item with the default display attributes. */ bool PageManager::setDefaultAttributes(Inkscape::CanvasPage *item) { // note: page background color doesn't have configurable transparency; it is considered to be opaque; // here alpha gets manipulated to reveal checkerboard pattern, if needed auto bgcolor = checkerboard ? background_color & ~0xff : background_color | 0xff; auto dkcolor = _document->getNamedView()->desk_color; bool ret = item->setOnTop(border_on_top); // fixed shadow size, not configurable; shadow changes size with zoom ret |= item->setShadow(border_show && shadow_show ? 2 : 0); ret |= item->setPageColor(border_show ? border_color : 0x0, bgcolor, dkcolor, margin_color, bleed_color); ret |= item->setLabelStyle(label_style); return ret; } /** * Return a page's size label, or match via width and height. */ std::string PageManager::getSizeLabel(SPPage *page) { auto box = *_document->preferredBounds(); if (page) { box = page->getDesktopRect(); auto label = page->getSizeLabel(); if (!label.empty()) return _(label.c_str()); } return getSizeLabel(box.width(), box.height()); } /** * Loop through all page sizes to find a matching one for this width and height. * * @param width - The X axis size in pixels * @param height - The Y axis size in pixels */ std::string PageManager::getSizeLabel(double width, double height) { using namespace Inkscape::Util; if (auto preset = Inkscape::Extension::Template::get_any_preset(width, height)) { return _(preset->get_name().c_str()); } static auto px = Inkscape::Util::unit_table.getUnit("px"); auto unit = _document->getDisplayUnit(); return format_number(Quantity::convert(width, px, unit), 2) + " × " + format_number(Quantity::convert(height, px, unit), 2) + " " + unit->abbr; } /** * Called when the viewbox is resized. */ void PageManager::movePages(Geom::Affine tr) { // Adjust each page against the change in position of the viewbox. for (auto &page : pages) { page->movePage(tr, false); } } }; // 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:fileencoding=utf-8:textwidth=99 :