// SPDX-License-Identifier: GPL-2.0-or-later /** * @file * Align and Distribute dialog - implementation. */ /* Authors: * Bryce W. Harrington * Aubanel MONNIER * Frank Felfe * Lauris Kaplinski * Tim Dwyer * Jon A. Cruz * Abhishek Sharma * * Copyright (C) 1999-2004, 2005 Authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include #include <2geom/transforms.h> #include #include "align-and-distribute.h" #include "desktop.h" #include "document-undo.h" #include "document.h" #include "graphlayout.h" #include "inkscape.h" #include "preferences.h" #include "removeoverlap.h" #include "text-editing.h" #include "unclump.h" #include "verbs.h" #include "object/sp-flowtext.h" #include "object/sp-item-transform.h" #include "object/sp-root.h" #include "object/sp-text.h" #include "ui/icon-loader.h" #include "ui/icon-names.h" #include "ui/tool/control-point-selection.h" #include "ui/tool/multi-path-manipulator.h" #include "ui/tools-switch.h" #include "ui/tools/node-tool.h" #include "ui/widget/spinbutton.h" namespace Inkscape { namespace UI { namespace Dialog { /////////helper classes////////////////////////////////// Action::Action(Glib::ustring id, const Glib::ustring &tiptext, guint row, guint column, Gtk::Grid &parent, AlignAndDistribute &dialog): _dialog(dialog), _id(std::move(id)), _parent(parent) { Gtk::Image* pIcon = Gtk::manage(new Gtk::Image()); pIcon = sp_get_icon_image(_id, Gtk::ICON_SIZE_LARGE_TOOLBAR); Gtk::Button * pButton = Gtk::manage(new Gtk::Button()); pButton->set_relief(Gtk::RELIEF_NONE); pIcon->show(); pButton->add(*pIcon); pButton->show(); pButton->signal_clicked() .connect(sigc::mem_fun(*this, &Action::on_button_click)); pButton->set_tooltip_text(tiptext); parent.attach(*pButton, column, row, 1, 1); } void ActionAlign::do_node_action(Inkscape::UI::Tools::NodeTool *nt, int verb) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int prev_pref = prefs->getInt("/dialogs/align/align-nodes-to"); switch(verb){ case SP_VERB_ALIGN_HORIZONTAL_LEFT: prefs->setInt("/dialogs/align/align-nodes-to", MIN_NODE ); nt->_multipath->alignNodes(Geom::Y); break; case SP_VERB_ALIGN_HORIZONTAL_CENTER: nt->_multipath->alignNodes(Geom::Y); break; case SP_VERB_ALIGN_HORIZONTAL_RIGHT: prefs->setInt("/dialogs/align/align-nodes-to", MAX_NODE ); nt->_multipath->alignNodes(Geom::Y); break; case SP_VERB_ALIGN_VERTICAL_TOP: prefs->setInt("/dialogs/align/align-nodes-to", MAX_NODE ); nt->_multipath->alignNodes(Geom::X); break; case SP_VERB_ALIGN_VERTICAL_CENTER: nt->_multipath->alignNodes(Geom::X); break; case SP_VERB_ALIGN_VERTICAL_BOTTOM: prefs->setInt("/dialogs/align/align-nodes-to", MIN_NODE ); nt->_multipath->alignNodes(Geom::X); break; case SP_VERB_ALIGN_BOTH_CENTER: nt->_multipath->alignNodes(Geom::X); nt->_multipath->alignNodes(Geom::Y); break; default:return; } prefs->setInt("/dialogs/align/align-nodes-to", prev_pref ); } void ActionAlign::do_action(SPDesktop *desktop, int index) { Inkscape::Selection *selection = desktop->getSelection(); if (!selection) return; Inkscape::Preferences *prefs = Inkscape::Preferences::get(); bool sel_as_group = prefs->getBool("/dialogs/align/sel-as-groups"); std::vector selected(selection->items().begin(), selection->items().end()); if (selected.empty()) return; Coeffs a = _allCoeffs[index]; // copy SPItem *focus = nullptr; Geom::OptRect b = Geom::OptRect(); Selection::CompareSize horiz = (a.mx0 != 0.0) || (a.mx1 != 0.0) ? Selection::VERTICAL : Selection::HORIZONTAL; switch (AlignTarget(prefs->getInt("/dialogs/align/align-to", 6))) { case LAST: focus = SP_ITEM(selected.back()); break; case FIRST: focus = SP_ITEM(selected.front()); break; case BIGGEST: focus = selection->largestItem(horiz); break; case SMALLEST: focus = selection->smallestItem(horiz); break; case PAGE: b = desktop->getDocument()->preferredBounds(); break; case DRAWING: b = desktop->getDocument()->getRoot()->desktopPreferredBounds(); break; case SELECTION: b = selection->preferredBounds(); break; default: g_assert_not_reached (); break; }; if(focus) b = focus->desktopPreferredBounds(); g_return_if_fail(b); if (desktop->is_yaxisdown()) { std::swap(a.my0, a.my1); std::swap(a.sy0, a.sy1); } // Generate the move point from the selected bounding box Geom::Point mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X], a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]); if (sel_as_group) { if (focus) { // use bounding box of all selected elements except the "focused" element Inkscape::ObjectSet copy; copy.add(selection->objects().begin(), selection->objects().end()); copy.remove(focus); b = copy.preferredBounds(); } else { // use bounding box of all selected elements b = selection->preferredBounds(); } } //Move each item in the selected list separately bool changed = false; for (auto item : selected) { desktop->getDocument()->ensureUpToDate(); if (!sel_as_group) b = (item)->desktopPreferredBounds(); if (b && (!focus || (item) != focus)) { Geom::Point const sp(a.sx0 * b->min()[Geom::X] + a.sx1 * b->max()[Geom::X], a.sy0 * b->min()[Geom::Y] + a.sy1 * b->max()[Geom::Y]); Geom::Point const mp_rel( mp - sp ); if (LInfty(mp_rel) > 1e-9) { item->move_rel(Geom::Translate(mp_rel)); changed = true; } } } if (changed) { DocumentUndo::done( desktop->getDocument() , SP_VERB_DIALOG_ALIGN_DISTRIBUTE, _("Align")); } } ActionAlign::Coeffs const ActionAlign::_allCoeffs[19] = { {1., 0., 0., 0., 0., 1., 0., 0., SP_VERB_ALIGN_HORIZONTAL_RIGHT_TO_ANCHOR}, {1., 0., 0., 0., 1., 0., 0., 0., SP_VERB_ALIGN_HORIZONTAL_LEFT}, {.5, .5, 0., 0., .5, .5, 0., 0., SP_VERB_ALIGN_HORIZONTAL_CENTER}, {0., 1., 0., 0., 0., 1., 0., 0., SP_VERB_ALIGN_HORIZONTAL_RIGHT}, {0., 1., 0., 0., 1., 0., 0., 0., SP_VERB_ALIGN_HORIZONTAL_LEFT_TO_ANCHOR}, {0., 0., 0., 1., 0., 0., 1., 0., SP_VERB_ALIGN_VERTICAL_BOTTOM_TO_ANCHOR}, {0., 0., 0., 1., 0., 0., 0., 1., SP_VERB_ALIGN_VERTICAL_TOP}, {0., 0., .5, .5, 0., 0., .5, .5, SP_VERB_ALIGN_VERTICAL_CENTER}, {0., 0., 1., 0., 0., 0., 1., 0., SP_VERB_ALIGN_VERTICAL_BOTTOM}, {0., 0., 1., 0., 0., 0., 0., 1., SP_VERB_ALIGN_VERTICAL_TOP_TO_ANCHOR}, {1., 0., 0., 1., 1., 0., 0., 1., SP_VERB_ALIGN_BOTH_TOP_LEFT}, {0., 1., 0., 1., 0., 1., 0., 1., SP_VERB_ALIGN_BOTH_TOP_RIGHT}, {0., 1., 1., 0., 0., 1., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT}, {1., 0., 1., 0., 1., 0., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_LEFT}, {0., 1., 1., 0., 1., 0., 0., 1., SP_VERB_ALIGN_BOTH_TOP_LEFT_TO_ANCHOR}, {1., 0., 1., 0., 0., 1., 0., 1., SP_VERB_ALIGN_BOTH_TOP_RIGHT_TO_ANCHOR}, {1., 0., 0., 1., 0., 1., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT_TO_ANCHOR}, {0., 1., 0., 1., 1., 0., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_LEFT_TO_ANCHOR}, {.5, .5, .5, .5, .5, .5, .5, .5, SP_VERB_ALIGN_BOTH_CENTER} }; void ActionAlign::do_verb_action(SPDesktop *desktop, int verb) { Inkscape::UI::Tools::ToolBase *event_context = desktop->getEventContext(); if (INK_IS_NODE_TOOL(event_context)) { Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(event_context); if(!nt->_selected_nodes->empty()){ do_node_action(nt, verb); return; } } do_action(desktop, verb_to_coeff(verb)); } int ActionAlign::verb_to_coeff(int verb) { for(guint i = 0; i < G_N_ELEMENTS(_allCoeffs); i++) { if (_allCoeffs[i].verb_id == verb) { return i; } } return -1; } BBoxSort::BBoxSort(SPItem *pItem, Geom::Rect const &bounds, Geom::Dim2 orientation, double kBegin, double kEnd) : item(pItem), bbox (bounds) { anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation]; } BBoxSort::BBoxSort(const BBoxSort &rhs) //NOTE : this copy ctor is called O(sort) when sorting the vector //this is bad. The vector should be a vector of pointers. //But I'll wait the bohem GC before doing that = default; bool operator< (const BBoxSort &a, const BBoxSort &b) { return (a.anchor < b.anchor); } class ActionDistribute : public Action { public : ActionDistribute(const Glib::ustring &id, const Glib::ustring &tiptext, guint row, guint column, AlignAndDistribute &dialog, bool onInterSpace, Geom::Dim2 orientation, double kBegin, double kEnd ): Action(id, tiptext, row, column, dialog.distribute_table(), dialog), _dialog(dialog), _onInterSpace(onInterSpace), _orientation(orientation), _kBegin(kBegin), _kEnd( kEnd) {} private : void on_button_click() override { //Retrieve selected objects SPDesktop *desktop = _dialog.getDesktop(); if (!desktop) return; Inkscape::Selection *selection = desktop->getSelection(); if (!selection) return; std::vector selected(selection->items().begin(), selection->items().end()); if (selected.empty()) return; //Check 2 or more selected objects std::vector::iterator second(selected.begin()); ++second; if (second == selected.end()) return; double kBegin = _kBegin; double kEnd = _kEnd; if (_orientation == Geom::Y && desktop->is_yaxisdown()) { kBegin = 1. - kBegin; kEnd = 1. - kEnd; } Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int prefs_bbox = prefs->getBool("/tools/bounding_box"); std::vector< BBoxSort > sorted; for (auto item : selected){ Geom::OptRect bbox = !prefs_bbox ? (item)->desktopVisualBounds() : (item)->desktopGeometricBounds(); if (bbox) { sorted.emplace_back(item, *bbox, _orientation, kBegin, kEnd); } } //sort bbox by anchors std::stable_sort(sorted.begin(), sorted.end()); // see comment in ActionAlign above int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); unsigned int len = sorted.size(); bool changed = false; if (_onInterSpace) { //overall bboxes span float dist = (sorted.back().bbox.max()[_orientation] - sorted.front().bbox.min()[_orientation]); //space eaten by bboxes float span = 0; for (unsigned int i = 0; i < len; i++) { span += sorted[i].bbox[_orientation].extent(); } //new distance between each bbox float step = (dist - span) / (len - 1); float pos = sorted.front().bbox.min()[_orientation]; for ( std::vector ::iterator it (sorted.begin()); it < sorted.end(); ++it ) { if (!Geom::are_near(pos, it->bbox.min()[_orientation], 1e-6)) { Geom::Point t(0.0, 0.0); t[_orientation] = pos - it->bbox.min()[_orientation]; it->item->move_rel(Geom::Translate(t)); changed = true; } pos += it->bbox[_orientation].extent(); pos += step; } } else { //overall anchor span float dist = sorted.back().anchor - sorted.front().anchor; //distance between anchors float step = dist / (len - 1); for ( unsigned int i = 0; i < len ; i ++ ) { BBoxSort & it(sorted[i]); //new anchor position float pos = sorted.front().anchor + i * step; //Don't move if we are really close if (!Geom::are_near(pos, it.anchor, 1e-6)) { //Compute translation Geom::Point t(0.0, 0.0); t[_orientation] = pos - it.anchor; //translate it.item->move_rel(Geom::Translate(t)); changed = true; } } } // restore compensation setting prefs->setInt("/options/clonecompensation/value", saved_compensation); if (changed) { DocumentUndo::done( desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, _("Distribute")); } } guint _index; AlignAndDistribute &_dialog; bool _onInterSpace; Geom::Dim2 _orientation; double _kBegin; double _kEnd; }; class ActionNode : public Action { public : ActionNode(const Glib::ustring &id, const Glib::ustring &tiptext, guint column, AlignAndDistribute &dialog, Geom::Dim2 orientation, bool distribute): Action(id, tiptext, 0, column, dialog.nodes_table(), dialog), _orientation(orientation), _distribute(distribute) {} private : Geom::Dim2 _orientation; bool _distribute; void on_button_click() override { if (!_dialog.getDesktop()) { return; } Inkscape::UI::Tools::ToolBase *event_context = _dialog.getDesktop()->getEventContext(); if (!INK_IS_NODE_TOOL(event_context)) { return; } Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(event_context); if (_distribute) { nt->_multipath->distributeNodes(_orientation); } else { nt->_multipath->alignNodes(_orientation); } } }; class ActionRemoveOverlaps : public Action { private: Gtk::Label removeOverlapXGapLabel; Gtk::Label removeOverlapYGapLabel; Inkscape::UI::Widget::SpinButton removeOverlapXGap; Inkscape::UI::Widget::SpinButton removeOverlapYGap; public: ActionRemoveOverlaps(Glib::ustring const &id, Glib::ustring const &tiptext, guint row, guint column, AlignAndDistribute &dialog) : Action(id, tiptext, row, column + 4, dialog.removeOverlap_table(), dialog) { dialog.removeOverlap_table().set_column_spacing(3); removeOverlapXGap.set_digits(1); removeOverlapXGap.set_size_request(60, -1); removeOverlapXGap.set_increments(1.0, 0); removeOverlapXGap.set_range(-1000.0, 1000.0); removeOverlapXGap.set_value(0); removeOverlapXGap.set_tooltip_text(_("Minimum horizontal gap (in px units) between bounding boxes")); //TRANSLATORS: "H:" stands for horizontal gap removeOverlapXGapLabel.set_text_with_mnemonic(C_("Gap", "_H:")); removeOverlapXGapLabel.set_mnemonic_widget(removeOverlapXGap); removeOverlapYGap.set_digits(1); removeOverlapYGap.set_size_request(60, -1); removeOverlapYGap.set_increments(1.0, 0); removeOverlapYGap.set_range(-1000.0, 1000.0); removeOverlapYGap.set_value(0); removeOverlapYGap.set_tooltip_text(_("Minimum vertical gap (in px units) between bounding boxes")); /* TRANSLATORS: Vertical gap */ removeOverlapYGapLabel.set_text_with_mnemonic(C_("Gap", "_V:")); removeOverlapYGapLabel.set_mnemonic_widget(removeOverlapYGap); dialog.removeOverlap_table().attach(removeOverlapXGapLabel, column, row, 1, 1); dialog.removeOverlap_table().attach(removeOverlapXGap, column+1, row, 1, 1); dialog.removeOverlap_table().attach(removeOverlapYGapLabel, column+2, row, 1, 1); dialog.removeOverlap_table().attach(removeOverlapYGap, column+3, row, 1, 1); } private : void on_button_click() override { if (!_dialog.getDesktop()) return; // see comment in ActionAlign above Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); // xGap and yGap are the minimum space required between bounding rectangles. double const xGap = removeOverlapXGap.get_value(); double const yGap = removeOverlapYGap.get_value(); auto tmp = _dialog.getDesktop()->getSelection()->items(); std::vector vec(tmp.begin(), tmp.end()); removeoverlap(vec, xGap, yGap); // restore compensation setting prefs->setInt("/options/clonecompensation/value", saved_compensation); DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, _("Remove overlaps")); } }; class ActionGraphLayout : public Action { public: ActionGraphLayout(Glib::ustring const &id, Glib::ustring const &tiptext, guint row, guint column, AlignAndDistribute &dialog) : Action(id, tiptext, row, column, dialog.rearrange_table(), dialog) {} private : void on_button_click() override { if (!_dialog.getDesktop()) return; // see comment in ActionAlign above Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); auto tmp = _dialog.getDesktop()->getSelection()->items(); std::vector vec(tmp.begin(), tmp.end()); graphlayout(vec); // restore compensation setting prefs->setInt("/options/clonecompensation/value", saved_compensation); DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, _("Arrange connector network")); } }; class ActionExchangePositions : public Action { public: enum SortOrder { None, ZOrder, Clockwise }; ActionExchangePositions(Glib::ustring const &id, Glib::ustring const &tiptext, guint row, guint column, AlignAndDistribute &dialog, SortOrder order = None) : Action(id, tiptext, row, column, dialog.rearrange_table(), dialog), sortOrder(order) {}; private : const SortOrder sortOrder; static boost::optional center; static bool sort_compare(const SPItem * a,const SPItem * b) { if (a == nullptr) return false; if (b == nullptr) return true; if (center) { Geom::Point point_a = a->getCenter() - (*center); Geom::Point point_b = b->getCenter() - (*center); // First criteria: Sort according to the angle to the center point double angle_a = atan2(double(point_a[Geom::Y]), double(point_a[Geom::X])); double angle_b = atan2(double(point_b[Geom::Y]), double(point_b[Geom::X])); double dt_yaxisdir = SP_ACTIVE_DESKTOP ? SP_ACTIVE_DESKTOP->yaxisdir() : 1; angle_a *= -dt_yaxisdir; angle_b *= -dt_yaxisdir; if (angle_a != angle_b) return (angle_a < angle_b); // Second criteria: Sort according to the distance the center point Geom::Coord length_a = point_a.length(); Geom::Coord length_b = point_b.length(); if (length_a != length_b) return (length_a > length_b); } // Last criteria: Sort according to the z-coordinate return sp_item_repr_compare_position(a,b)<0; } void on_button_click() override { SPDesktop *desktop = _dialog.getDesktop(); if (!desktop) return; Inkscape::Selection *selection = desktop->getSelection(); if (!selection) return; std::vector selected(selection->items().begin(), selection->items().end()); //Check 2 or more selected objects if (selected.size() < 2) return; // see comment in ActionAlign above Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); // sort the list if (sortOrder != None) { if (sortOrder == Clockwise) { center = selection->center(); } else { // sorting by ZOrder is outomatically done by not setting the center center.reset(); } sort(selected.begin(),selected.end(),sort_compare); } Geom::Point p1 = selected.back()->getCenter(); for (SPItem *item : selected) { Geom::Point p2 = item->getCenter(); Geom::Point delta = p1 - p2; item->move_rel(Geom::Translate(delta[Geom::X],delta[Geom::Y] )); p1 = p2; } // restore compensation setting prefs->setInt("/options/clonecompensation/value", saved_compensation); DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, _("Exchange Positions")); } }; // instantiate the private static member boost::optional ActionExchangePositions::center; class ActionUnclump : public Action { public : ActionUnclump(const Glib::ustring &id, const Glib::ustring &tiptext, guint row, guint column, AlignAndDistribute &dialog): Action(id, tiptext, row, column, dialog.rearrange_table(), dialog) {} private : void on_button_click() override { if (!_dialog.getDesktop()) return; // see comment in ActionAlign above Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); auto tmp = _dialog.getDesktop()->getSelection()->items(); std::vector x(tmp.begin(), tmp.end()); unclump (x); // restore compensation setting prefs->setInt("/options/clonecompensation/value", saved_compensation); DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, _("Unclump")); } }; class ActionRandomize : public Action { public : ActionRandomize(const Glib::ustring &id, const Glib::ustring &tiptext, guint row, guint column, AlignAndDistribute &dialog): Action(id, tiptext, row, column, dialog.rearrange_table(), dialog) {} private : void on_button_click() override { SPDesktop *desktop = _dialog.getDesktop(); if (!desktop) return; Inkscape::Selection *selection = desktop->getSelection(); if (!selection) return; std::vector selected(selection->items().begin(), selection->items().end()); if (selected.empty()) return; //Check 2 or more selected objects if (selected.size() < 2) return; Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int prefs_bbox = prefs->getBool("/tools/bounding_box"); Geom::OptRect sel_bbox = !prefs_bbox ? selection->visualBounds() : selection->geometricBounds(); if (!sel_bbox) { return; } // This bbox is cached between calls to randomize, so that there's no growth nor shrink // nor drift on sequential randomizations. Discard cache on global (or better active // desktop's) selection_change signal. if (!_dialog.randomize_bbox) { _dialog.randomize_bbox = *sel_bbox; } // see comment in ActionAlign above int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); for (auto item : selected) { desktop->getDocument()->ensureUpToDate(); Geom::OptRect item_box = !prefs_bbox ? (item)->desktopVisualBounds() : (item)->desktopGeometricBounds(); if (item_box) { // find new center, staying within bbox double x = _dialog.randomize_bbox->min()[Geom::X] + (*item_box)[Geom::X].extent() /2 + g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::X].extent() - (*item_box)[Geom::X].extent()); double y = _dialog.randomize_bbox->min()[Geom::Y] + (*item_box)[Geom::Y].extent()/2 + g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::Y].extent() - (*item_box)[Geom::Y].extent()); // displacement is the new center minus old: Geom::Point t = Geom::Point (x, y) - 0.5*(item_box->max() + item_box->min()); item->move_rel(Geom::Translate(t)); } } // restore compensation setting prefs->setInt("/options/clonecompensation/value", saved_compensation); DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, _("Randomize positions")); } }; struct Baselines { SPItem *_item; Geom::Point _base; Geom::Dim2 _orientation; Baselines(SPItem *item, Geom::Point base, Geom::Dim2 orientation) : _item (item), _base (base), _orientation (orientation) {} }; static bool operator< (const Baselines &a, const Baselines &b) { return (a._base[a._orientation] < b._base[b._orientation]); } class ActionBaseline : public Action { public : ActionBaseline(const Glib::ustring &id, const Glib::ustring &tiptext, guint row, guint column, AlignAndDistribute &dialog, Gtk::Grid &table, Geom::Dim2 orientation, bool distribute): Action(id, tiptext, row, column, table, dialog), _orientation(orientation), _distribute(distribute) {} private : Geom::Dim2 _orientation; bool _distribute; void on_button_click() override { SPDesktop *desktop = _dialog.getDesktop(); if (!desktop) return; Inkscape::Selection *selection = desktop->getSelection(); if (!selection) return; std::vector selected(selection->items().begin(), selection->items().end()); //Check 2 or more selected objects if (selected.size() < 2) return; Geom::Point b_min = Geom::Point (HUGE_VAL, HUGE_VAL); Geom::Point b_max = Geom::Point (-HUGE_VAL, -HUGE_VAL); std::vector sorted; for (auto item : selected) { if (SP_IS_TEXT (item) || SP_IS_FLOWTEXT (item)) { Inkscape::Text::Layout const *layout = te_get_layout(item); boost::optional pt = layout->baselineAnchorPoint(); if (pt) { Geom::Point base = *pt * (item)->i2dt_affine(); if (base[Geom::X] < b_min[Geom::X]) b_min[Geom::X] = base[Geom::X]; if (base[Geom::Y] < b_min[Geom::Y]) b_min[Geom::Y] = base[Geom::Y]; if (base[Geom::X] > b_max[Geom::X]) b_max[Geom::X] = base[Geom::X]; if (base[Geom::Y] > b_max[Geom::Y]) b_max[Geom::Y] = base[Geom::Y]; Baselines b (item, base, _orientation); sorted.push_back(b); } } } if (sorted.size() <= 1) return; //sort baselines std::stable_sort(sorted.begin(), sorted.end()); bool changed = false; if (_distribute) { double step = (b_max[_orientation] - b_min[_orientation])/(sorted.size() - 1); for (unsigned int i = 0; i < sorted.size(); i++) { SPItem *item = sorted[i]._item; Geom::Point base = sorted[i]._base; Geom::Point t(0.0, 0.0); t[_orientation] = b_min[_orientation] + step * i - base[_orientation]; item->move_rel(Geom::Translate(t)); changed = true; } if (changed) { DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, _("Distribute text baselines")); } } else { //align Geom::Point ref_point; SPItem *focus = nullptr; Geom::OptRect b = Geom::OptRect(); Inkscape::Preferences *prefs = Inkscape::Preferences::get(); switch (AlignTarget(prefs->getInt("/dialogs/align/align-to", 6))) { case LAST: focus = SP_ITEM(selected.back()); break; case FIRST: focus = SP_ITEM(selected.front()); break; case BIGGEST: focus = selection->largestItem(Selection::AREA); break; case SMALLEST: focus = selection->smallestItem(Selection::AREA); break; case PAGE: b = desktop->getDocument()->preferredBounds(); break; case DRAWING: b = desktop->getDocument()->getRoot()->desktopPreferredBounds(); break; case SELECTION: b = selection->preferredBounds(); break; default: g_assert_not_reached (); break; }; if(focus) { if (SP_IS_TEXT (focus) || SP_IS_FLOWTEXT (focus)) { ref_point = *(te_get_layout(focus)->baselineAnchorPoint())*(focus->i2dt_affine()); } else { ref_point = focus->desktopPreferredBounds()->min(); } } else { ref_point = b->min(); } for (auto item : selected) { if (SP_IS_TEXT (item) || SP_IS_FLOWTEXT (item)) { Inkscape::Text::Layout const *layout = te_get_layout(item); boost::optional pt = layout->baselineAnchorPoint(); if (pt) { Geom::Point base = *pt * (item)->i2dt_affine(); Geom::Point t(0.0, 0.0); t[_orientation] = ref_point[_orientation] - base[_orientation]; item->move_rel(Geom::Translate(t)); changed = true; } } } if (changed) { DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, _("Align text baselines")); } } } }; static void on_tool_changed(AlignAndDistribute *daad) { SPDesktop *desktop = SP_ACTIVE_DESKTOP; if (desktop && desktop->getEventContext()) daad->setMode(tools_active(desktop) == TOOLS_NODES); else daad->setMode(false); } static void on_selection_changed(AlignAndDistribute *daad) { daad->randomize_bbox = Geom::OptRect(); } ///////////////////////////////////////////////////////// AlignAndDistribute::AlignAndDistribute() : UI::Widget::Panel("/dialogs/align", SP_VERB_DIALOG_ALIGN_DISTRIBUTE), randomize_bbox(), _alignFrame(_("Align")), _distributeFrame(_("Distribute")), _rearrangeFrame(_("Rearrange")), _removeOverlapFrame(_("Remove overlaps")), _nodesFrame(_("Nodes")), _alignTable(), _distributeTable(), _rearrangeTable(), _removeOverlapTable(), _nodesTable(), _anchorLabel(_("Relative to: ")), _anchorLabelNode(_("Relative to: ")) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); //Instantiate the align buttons addAlignButton(INKSCAPE_ICON("align-horizontal-right-to-anchor"), _("Align right edges of objects to the left edge of the anchor"), 0, 0); addAlignButton(INKSCAPE_ICON("align-horizontal-left"), _("Align left edges"), 0, 1); addAlignButton(INKSCAPE_ICON("align-horizontal-center"), _("Center on vertical axis"), 0, 2); addAlignButton(INKSCAPE_ICON("align-horizontal-right"), _("Align right sides"), 0, 3); addAlignButton(INKSCAPE_ICON("align-horizontal-left-to-anchor"), _("Align left edges of objects to the right edge of the anchor"), 0, 4); addAlignButton(INKSCAPE_ICON("align-vertical-bottom-to-anchor"), _("Align bottom edges of objects to the top edge of the anchor"), 1, 0); addAlignButton(INKSCAPE_ICON("align-vertical-top"), _("Align top edges"), 1, 1); addAlignButton(INKSCAPE_ICON("align-vertical-center"), _("Center on horizontal axis"), 1, 2); addAlignButton(INKSCAPE_ICON("align-vertical-bottom"), _("Align bottom edges"), 1, 3); addAlignButton(INKSCAPE_ICON("align-vertical-top-to-anchor"), _("Align top edges of objects to the bottom edge of the anchor"), 1, 4); //Baseline aligns addBaselineButton(INKSCAPE_ICON("align-horizontal-baseline"), _("Align baseline anchors of texts horizontally"), 0, 5, this->align_table(), Geom::X, false); addBaselineButton(INKSCAPE_ICON("align-vertical-baseline"), _("Align baselines of texts"), 1, 5, this->align_table(), Geom::Y, false); //The distribute buttons addDistributeButton(INKSCAPE_ICON("distribute-horizontal-gaps"), _("Make horizontal gaps between objects equal"), 0, 4, true, Geom::X, .5, .5); addDistributeButton(INKSCAPE_ICON("distribute-horizontal-left"), _("Distribute left edges equidistantly"), 0, 1, false, Geom::X, 1., 0.); addDistributeButton(INKSCAPE_ICON("distribute-horizontal-center"), _("Distribute centers equidistantly horizontally"), 0, 2, false, Geom::X, .5, .5); addDistributeButton(INKSCAPE_ICON("distribute-horizontal-right"), _("Distribute right edges equidistantly"), 0, 3, false, Geom::X, 0., 1.); addDistributeButton(INKSCAPE_ICON("distribute-vertical-gaps"), _("Make vertical gaps between objects equal"), 1, 4, true, Geom::Y, .5, .5); addDistributeButton(INKSCAPE_ICON("distribute-vertical-top"), _("Distribute top edges equidistantly"), 1, 1, false, Geom::Y, 0, 1); addDistributeButton(INKSCAPE_ICON("distribute-vertical-center"), _("Distribute centers equidistantly vertically"), 1, 2, false, Geom::Y, .5, .5); addDistributeButton(INKSCAPE_ICON("distribute-vertical-bottom"), _("Distribute bottom edges equidistantly"), 1, 3, false, Geom::Y, 1., 0.); //Baseline distribs addBaselineButton(INKSCAPE_ICON("distribute-horizontal-baseline"), _("Distribute baseline anchors of texts horizontally"), 0, 5, this->distribute_table(), Geom::X, true); addBaselineButton(INKSCAPE_ICON("distribute-vertical-baseline"), _("Distribute baselines of texts vertically"), 1, 5, this->distribute_table(), Geom::Y, true); // Rearrange //Graph Layout addGraphLayoutButton(INKSCAPE_ICON("distribute-graph"), _("Nicely arrange selected connector network"), 0, 0); addExchangePositionsButton(INKSCAPE_ICON("exchange-positions"), _("Exchange positions of selected objects - selection order"), 0, 1); addExchangePositionsByZOrderButton(INKSCAPE_ICON("exchange-positions-zorder"), _("Exchange positions of selected objects - stacking order"), 0, 2); addExchangePositionsClockwiseButton(INKSCAPE_ICON("exchange-positions-clockwise"), _("Exchange positions of selected objects - clockwise rotate"), 0, 3); //Randomize & Unclump addRandomizeButton(INKSCAPE_ICON("distribute-randomize"), _("Randomize centers in both dimensions"), 0, 4); addUnclumpButton(INKSCAPE_ICON("distribute-unclump"), _("Unclump objects: try to equalize edge-to-edge distances"), 0, 5); //Remove overlaps addRemoveOverlapsButton(INKSCAPE_ICON("distribute-remove-overlaps"), _("Move objects as little as possible so that their bounding boxes do not overlap"), 0, 0); //Node Mode buttons // NOTE: "align nodes vertically" means "move nodes vertically until they align on a common // _horizontal_ line". This is analogous to what the "align-vertical-center" icon means. // There is no doubt some ambiguity. For this reason the descriptions are different. addNodeButton(INKSCAPE_ICON("align-vertical-node"), _("Align selected nodes to a common horizontal line"), 0, Geom::X, false); addNodeButton(INKSCAPE_ICON("align-horizontal-node"), _("Align selected nodes to a common vertical line"), 1, Geom::Y, false); addNodeButton(INKSCAPE_ICON("distribute-horizontal-node"), _("Distribute selected nodes horizontally"), 2, Geom::X, true); addNodeButton(INKSCAPE_ICON("distribute-vertical-node"), _("Distribute selected nodes vertically"), 3, Geom::Y, true); //Rest of the widgetry _combo.append(_("Last selected")); _combo.append(_("First selected")); _combo.append(_("Biggest object")); _combo.append(_("Smallest object")); _combo.append(_("Page")); _combo.append(_("Drawing")); _combo.append(_("Selection Area")); _combo.set_active(prefs->getInt("/dialogs/align/align-to", 6)); _combo.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_ref_change)); _comboNode.append(_("Last selected")); _comboNode.append(_("First selected")); _comboNode.append(_("Middle of selection")); _comboNode.append(_("Min value")); _comboNode.append(_("Max value")); _comboNode.set_active(prefs->getInt("/dialogs/align/align-nodes-to", 2)); _comboNode.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_node_ref_change)); Gtk::Image* selgrp_icon = Gtk::manage(new Gtk::Image()); selgrp_icon = sp_get_icon_image("align-sel-as-group", Gtk::ICON_SIZE_LARGE_TOOLBAR); _selgrp.add(*selgrp_icon); _selgrp.set_active(prefs->getBool("/dialogs/align/sel-as-groups")); _selgrp.set_relief(Gtk::RELIEF_NONE); _selgrp.set_tooltip_text(_("Treat selection as group")); _selgrp.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_selgrp_toggled)); _anchorBox.pack_end(_selgrp, false, false); _anchorBox.pack_end(_combo, false, false); _anchorBox.pack_end(_anchorLabel, false, false); _anchorBoxNode.pack_end(_comboNode, false, false); _anchorBoxNode.pack_end(_anchorLabelNode, false, false); Gtk::Image* oncanvas_icon = Gtk::manage(new Gtk::Image()); oncanvas_icon = sp_get_icon_image("align-on-canvas", Gtk::ICON_SIZE_LARGE_TOOLBAR); _oncanvas.add(*oncanvas_icon); _oncanvas.set_relief(Gtk::RELIEF_NONE); _oncanvas.set_tooltip_text(_("Enable on-canvas alignment handles.")); _anchorBox.pack_start(_oncanvas, false, false); _oncanvas.set_active(prefs->getBool("/dialogs/align/oncanvas")); _oncanvas.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_oncanvas_toggled)); // Right align the buttons _alignTableBox.pack_end(_alignTable, false, false); _distributeTableBox.pack_end(_distributeTable, false, false); _rearrangeTableBox.pack_end(_rearrangeTable, false, false); _removeOverlapTableBox.pack_end(_removeOverlapTable, false, false); _nodesTableBox.pack_end(_nodesTable, false, false); _alignBox.pack_start(_anchorBox); _alignBox.pack_start(_selgrpBox); _alignBox.pack_start(_alignTableBox); _alignBoxNode.pack_start(_anchorBoxNode, false, false); _alignBoxNode.pack_start(_nodesTableBox); _alignFrame.add(_alignBox); _distributeFrame.add(_distributeTableBox); _rearrangeFrame.add(_rearrangeTableBox); _removeOverlapFrame.add(_removeOverlapTableBox); _nodesFrame.add(_alignBoxNode); Gtk::Box *contents = _getContents(); contents->set_spacing(4); // Notebook for individual transformations contents->pack_start(_alignFrame, true, true); contents->pack_start(_distributeFrame, true, true); contents->pack_start(_rearrangeFrame, true, true); contents->pack_start(_removeOverlapFrame, true, true); contents->pack_start(_nodesFrame, true, false); //Connect to the global tool change signal _toolChangeConn = INKSCAPE.signal_eventcontext_set.connect(sigc::hide<0>(sigc::bind(sigc::ptr_fun(&on_tool_changed), this))); // Connect to the global selection change, to invalidate cached randomize_bbox _selChangeConn = INKSCAPE.signal_selection_changed.connect(sigc::hide<0>(sigc::bind(sigc::ptr_fun(&on_selection_changed), this))); randomize_bbox = Geom::OptRect(); _desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &AlignAndDistribute::setDesktop) ); _deskTrack.connect(GTK_WIDGET(gobj())); show_all_children(); on_tool_changed (this); // set current mode } AlignAndDistribute::~AlignAndDistribute() { for (auto & it : _actionList) { delete it; } _toolChangeConn.disconnect(); _selChangeConn.disconnect(); _desktopChangeConn.disconnect(); _deskTrack.disconnect(); } void AlignAndDistribute::setTargetDesktop(SPDesktop *desktop) { if (_desktop != desktop) { _desktop = desktop; on_tool_changed (this); } } void AlignAndDistribute::on_ref_change(){ Inkscape::Preferences *prefs = Inkscape::Preferences::get(); prefs->setInt("/dialogs/align/align-to", _combo.get_active_row_number()); //Make blink the master } void AlignAndDistribute::on_node_ref_change(){ Inkscape::Preferences *prefs = Inkscape::Preferences::get(); prefs->setInt("/dialogs/align/align-nodes-to", _comboNode.get_active_row_number()); //Make blink the master } void AlignAndDistribute::on_selgrp_toggled(){ Inkscape::Preferences *prefs = Inkscape::Preferences::get(); prefs->setInt("/dialogs/align/sel-as-groups", _selgrp.get_active()); //Make blink the master } void AlignAndDistribute::on_oncanvas_toggled(){ Inkscape::Preferences *prefs = Inkscape::Preferences::get(); prefs->setInt("/dialogs/align/oncanvas", _oncanvas.get_active()); //Make blink the master } void AlignAndDistribute::setMode(bool nodeEdit) { //Act on widgets used in node mode void ( Gtk::Widget::*mNode) () = nodeEdit ? &Gtk::Widget::show_all : &Gtk::Widget::hide; //Act on widgets used in selection mode void ( Gtk::Widget::*mSel) () = nodeEdit ? &Gtk::Widget::hide : &Gtk::Widget::show_all; ((_alignFrame).*(mSel))(); ((_distributeFrame).*(mSel))(); ((_rearrangeFrame).*(mSel))(); ((_removeOverlapFrame).*(mSel))(); ((_nodesFrame).*(mNode))(); _getContents()->queue_resize(); } void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext, guint row, guint col) { _actionList.push_back( new ActionAlign( id, tiptext, row, col, *this , col + row * 5)); } void AlignAndDistribute::addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext, guint row, guint col, bool onInterSpace, Geom::Dim2 orientation, float kBegin, float kEnd) { _actionList.push_back( new ActionDistribute( id, tiptext, row, col, *this , onInterSpace, orientation, kBegin, kEnd ) ); } void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext, guint col, Geom::Dim2 orientation, bool distribute) { _actionList.push_back( new ActionNode( id, tiptext, col, *this, orientation, distribute)); } void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext, guint row, guint col) { _actionList.push_back( new ActionRemoveOverlaps( id, tiptext, row, col, *this) ); } void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext, guint row, guint col) { _actionList.push_back( new ActionGraphLayout( id, tiptext, row, col, *this) ); } void AlignAndDistribute::addExchangePositionsButton(const Glib::ustring &id, const Glib::ustring tiptext, guint row, guint col) { _actionList.push_back( new ActionExchangePositions( id, tiptext, row, col, *this) ); } void AlignAndDistribute::addExchangePositionsByZOrderButton(const Glib::ustring &id, const Glib::ustring tiptext, guint row, guint col) { _actionList.push_back( new ActionExchangePositions( id, tiptext, row, col, *this, ActionExchangePositions::ZOrder) ); } void AlignAndDistribute::addExchangePositionsClockwiseButton(const Glib::ustring &id, const Glib::ustring tiptext, guint row, guint col) { _actionList.push_back( new ActionExchangePositions( id, tiptext, row, col, *this, ActionExchangePositions::Clockwise) ); } void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext, guint row, guint col) { _actionList.push_back( new ActionUnclump( id, tiptext, row, col, *this) ); } void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext, guint row, guint col) { _actionList.push_back( new ActionRandomize( id, tiptext, row, col, *this) ); } void AlignAndDistribute::addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext, guint row, guint col, Gtk::Grid &table, Geom::Dim2 orientation, bool distribute) { _actionList.push_back( new ActionBaseline( id, tiptext, row, col, *this, table, orientation, distribute)); } } // namespace Dialog } // 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:fileencoding=utf-8:textwidth=99 :