summaryrefslogtreecommitdiffstats
path: root/src/actions/actions-object-align.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/actions/actions-object-align.cpp852
1 files changed, 852 insertions, 0 deletions
diff --git a/src/actions/actions-object-align.cpp b/src/actions/actions-object-align.cpp
new file mode 100644
index 0000000..bedaaeb
--- /dev/null
+++ b/src/actions/actions-object-align.cpp
@@ -0,0 +1,852 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Gio::Actions for aligning and distributing objects without GUI.
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Some code and ideas from src/ui/dialogs/align-and-distribute.cpp
+ * Authors: Bryce Harrington
+ * Martin Owens
+ * John Smith
+ * Patrick Storz
+ * Jabier Arraiza
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#include "actions-object-align.h"
+
+#include <iostream>
+#include <limits>
+
+#include <giomm.h> // Not <gtkmm.h>! To eventually allow a headless version!
+#include <glibmm/i18n.h>
+
+#include "document-undo.h"
+#include "enums.h" // Clones
+#include "filter-chemistry.h" // LPE bool
+#include "inkscape-application.h"
+#include "inkscape.h" // Inkscape::Application - preferences
+#include "text-editing.h"
+
+#include "object/sp-text.h"
+#include "object/sp-flowtext.h"
+
+#include "object/algorithms/graphlayout.h" // Graph layout objects.
+#include "object/algorithms/removeoverlap.h" // Remove overlaps between objects.
+#include "object/algorithms/unclump.h" // Rearrange objects.
+#include "object/algorithms/bboxsort.h" // Sort based on bounding box.
+
+#include "live_effects/effect-enum.h"
+#include "live_effects/effect.h"
+
+#include "object/sp-root.h" // "Desktop Bounds"
+
+#include "ui/icon-names.h" // Icon macro used in undo.
+
+enum class ObjectAlignTarget {
+ LAST,
+ FIRST,
+ BIGGEST,
+ SMALLEST,
+ PAGE,
+ DRAWING,
+ SELECTION
+};
+
+void
+object_align_on_canvas(InkscapeApplication *app)
+{
+ // Get Action
+ auto *gapp = app->gio_app();
+ auto action = gapp->lookup_action("object-align-on-canvas");
+ if (!action) {
+ std::cerr << "object_align_on_canvas: action missing!" << std::endl;
+ return;
+ }
+
+ auto saction = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic(action);
+ if (!saction) {
+ std::cerr << "object_align_on_canvas: action not SimpleAction!" << std::endl;
+ return;
+ }
+
+ // Toggle state
+ bool state = false;
+ saction->get_state(state);
+ state = !state;
+ saction->change_state(state);
+
+ // Toggle action
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/align/oncanvas", state);
+}
+
+void
+object_align(const Glib::VariantBase& value, InkscapeApplication *app)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::Variant<Glib::ustring> s = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring> >(value);
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple(" ", s.get());
+
+ // Find out if we are using an anchor.
+ bool anchor = std::find(tokens.begin(), tokens.end(), "anchor") != tokens.end();
+
+ // Default values:
+ auto target = ObjectAlignTarget::SELECTION;
+
+ bool group = false;
+ double mx0 = 0;
+ double mx1 = 0;
+ double my0 = 0;
+ double my1 = 0;
+ double sx0 = 0;
+ double sx1 = 0;
+ double sy0 = 0;
+ double sy1 = 0;
+
+ // Preference request allows alignment action to remember for key-presses
+ if (std::find(tokens.begin(), tokens.end(), "pref") != tokens.end()) {
+ group = prefs->getBool("/dialogs/align/sel-as-groups", false);
+ tokens.push_back(prefs->getString("/dialogs/align/objects-align-to", "selection"));
+ }
+
+ // clang-format off
+ for (auto const &token : tokens) {
+
+ // Target
+ if (token == "last" ) target = ObjectAlignTarget::LAST;
+ else if (token == "first" ) target = ObjectAlignTarget::FIRST;
+ else if (token == "biggest" ) target = ObjectAlignTarget::BIGGEST;
+ else if (token == "smallest" ) target = ObjectAlignTarget::SMALLEST;
+ else if (token == "page" ) target = ObjectAlignTarget::PAGE;
+ else if (token == "drawing" ) target = ObjectAlignTarget::DRAWING;
+ else if (token == "selection") target = ObjectAlignTarget::SELECTION;
+
+ // Group
+ else if (token == "group") group = true;
+
+ // Position
+ if (!anchor) {
+ if (token == "left" ) { mx0 = 1.0; mx1 = 0.0; sx0 = 1.0; sx1 = 0.0; }
+ else if (token == "hcenter" ) { mx0 = 0.5; mx1 = 0.5; sx0 = 0.5; sx1 = 0.5; }
+ else if (token == "right" ) { mx0 = 0.0; mx1 = 1.0; sx0 = 0.0; sx1 = 1.0; }
+
+ else if (token == "top" ) { my0 = 1.0; my1 = 0.0; sy0 = 1.0; sy1 = 0.0; }
+ else if (token == "vcenter" ) { my0 = 0.5; my1 = 0.5; sy0 = 0.5; sy1 = 0.5; }
+ else if (token == "bottom" ) { my0 = 0.0; my1 = 1.0; sy0 = 0.0; sy1 = 1.0; }
+ } else {
+ if (token == "left" ) { mx0 = 0.0; mx1 = 1.0; sx0 = 1.0; sx1 = 0.0; }
+ else if (token == "hcenter" ) std::cerr << "'anchor' cannot be used with 'hcenter'" << std::endl;
+ else if (token == "right" ) { mx0 = 1.0; mx1 = 0.0; sx0 = 0.0; sx1 = 1.0; }
+
+ else if (token == "top" ) { my0 = 0.0; my1 = 1.0; sy0 = 1.0; sy1 = 0.0; }
+ else if (token == "vcenter" ) std::cerr << "'anchor' cannot be used with 'vcenter'" << std::endl;
+ else if (token == "bottom" ) { my0 = 1.0; my1 = 0.0; sy0 = 0.0; sy1 = 1.0; }
+ }
+ }
+ // clang-format on
+
+ auto selection = app->get_active_selection();
+
+ // We should not have to do this!
+ auto document = app->get_active_document();
+ selection->setDocument(document);
+
+ // We force unselect operand in bool LPE. TODO: See if we can use "selected" from below.
+ auto list = selection->items();
+ std::size_t total = std::distance(list.begin(), list.end());
+ std::vector<SPItem *> selected;
+ std::vector<Inkscape::LivePathEffect::Effect *> bools;
+ for (auto itemlist = list.begin(); itemlist != list.end(); ++itemlist) {
+ SPItem *item = dynamic_cast<SPItem *>(*itemlist);
+ if (total == 2) {
+ SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item);
+ if (lpeitem) {
+ for (auto lpe : lpeitem->getPathEffectsOfType(Inkscape::LivePathEffect::EffectType::BOOL_OP)) {
+ if (!g_strcmp0(lpe->getRepr()->attribute("is_visible"), "true")) {
+ lpe->getRepr()->setAttribute("is_visible", "false");
+ bools.emplace_back(lpe);
+ item->document->ensureUpToDate();
+ }
+ }
+ }
+ }
+ if (!(item && has_hidder_filter(item) && total > 2)) {
+ selected.emplace_back(item);
+ }
+ }
+
+ if (selected.empty()) return;
+
+ // Find alignment rectangle. This can come from:
+ // - The bounding box of an object
+ // - The bounding box of a group of objects
+ // - The bounding box of the page, drawing, or selection.
+ SPItem *focus = nullptr;
+ Geom::OptRect b = Geom::OptRect();
+ Inkscape::Selection::CompareSize direction = (mx0 != 0.0 || mx1 != 0.0) ? Inkscape::Selection::VERTICAL : Inkscape::Selection::HORIZONTAL;
+
+ switch (target) {
+ case ObjectAlignTarget::LAST:
+ focus = selected.back();
+ break;
+ case ObjectAlignTarget::FIRST:
+ focus = selected.front();
+ break;
+ case ObjectAlignTarget::BIGGEST:
+ focus = selection->largestItem(direction);
+ break;
+ case ObjectAlignTarget::SMALLEST:
+ focus = selection->smallestItem(direction);
+ break;
+ case ObjectAlignTarget::PAGE:
+ b = document->pageBounds();
+ break;
+ case ObjectAlignTarget::DRAWING:
+ b = document->getRoot()->desktopPreferredBounds();
+ break;
+ case ObjectAlignTarget::SELECTION:
+ b = selection->preferredBounds();
+ break;
+ default:
+ g_assert_not_reached ();
+ break;
+ };
+
+ if (focus) {
+ b = focus->desktopPreferredBounds();
+ }
+
+ g_return_if_fail(b);
+
+ if (auto desktop = selection->desktop(); desktop && !desktop->is_yaxisdown()) {
+ std::swap(my0, my1);
+ std::swap(sy0, sy1);
+ }
+
+ // Generate the move point from the selected bounding box
+ Geom::Point mp = Geom::Point(mx0 * b->min()[Geom::X] + mx1 * b->max()[Geom::X],
+ my0 * b->min()[Geom::Y] + my1 * b->max()[Geom::Y]);
+
+ if (group) {
+ if (focus) {
+ // Use bounding box of all selected elements except the "focused" element.
+ Inkscape::ObjectSet copy(document);
+ 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) {
+ document->ensureUpToDate();
+
+ if (!group) {
+ b = (item)->desktopPreferredBounds();
+ }
+
+ if (b && (!focus || (item) != focus)) {
+ Geom::Point const sp(sx0 * b->min()[Geom::X] + sx1 * b->max()[Geom::X],
+ sy0 * b->min()[Geom::Y] + 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) {
+ Inkscape::DocumentUndo::done(document, _("Align"), INKSCAPE_ICON("dialog-align-and-distribute"));
+ }
+}
+
+void
+object_distribute(const Glib::VariantBase& value, InkscapeApplication *app)
+{
+ Glib::Variant<Glib::ustring> s = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring> >(value);
+ auto token = s.get();
+
+ auto selection = app->get_active_selection();
+
+ // We should not have to do this!
+ auto document = app->get_active_document();
+ selection->setDocument(document);
+
+ std::vector<SPItem*> selected(selection->items().begin(), selection->items().end());
+ if (selected.size() < 2) {
+ return;
+ }
+
+ // clang-format off
+ double a = 0.0;
+ double b = 0.0;
+ bool gap = false;
+ auto orientation = Geom::X;
+ if (token == "hgap" ) { gap = true; orientation = Geom::X; a = 0.5, b = 0.5; }
+ else if (token == "left" ) { gap = false; orientation = Geom::X; a = 1.0, b = 0.0; }
+ else if (token == "hcenter" ) { gap = false; orientation = Geom::X; a = 0.5, b = 0.5; }
+ else if (token == "right" ) { gap = false; orientation = Geom::X; a = 0.0, b = 1.0; }
+ else if (token == "vgap" ) { gap = true; orientation = Geom::Y; a = 0.5, b = 0.5; }
+ else if (token == "top" ) { gap = false; orientation = Geom::Y; a = 1.0, b = 0.0; }
+ else if (token == "vcenter" ) { gap = false; orientation = Geom::Y; a = 0.5, b = 0.5; }
+ else if (token == "bottom" ) { gap = false; orientation = Geom::Y; a = 0.0, b = 1.0; }
+ // clang-format on
+
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int prefs_bbox = prefs->getBool("/tools/bounding_box");
+
+ // Make a list of objects, sorted by anchors.
+ 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, a, b);
+ }
+ }
+ std::stable_sort(sorted.begin(), sorted.end());
+
+ // See comment in ActionAlign above (MISSING).
+ int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
+ prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
+
+ bool changed = false;
+ if (gap) {
+ // Evenly spaced.
+
+ // Overall bboxes span.
+ double dist = (sorted.back().bbox.max()[orientation] - sorted.front().bbox.min()[orientation]);
+
+ // Space eaten by bboxes.
+ double span = 0.0;
+ for (auto bbox : sorted) {
+ span += bbox.bbox[orientation].extent();
+ }
+
+ // New distance between each bbox.
+ double step = (dist - span) / (sorted.size() - 1);
+ double pos = sorted.front().bbox.min()[orientation];
+ for (auto bbox : sorted) {
+
+ // Don't move if we are really close.
+ if (!Geom::are_near(pos, bbox.bbox.min()[orientation], 1e-6)) {
+
+ // Compute translation.
+ Geom::Point t(0.0, 0.0);
+ t[orientation] = pos - bbox.bbox.min()[orientation];
+
+ // Translate
+ bbox.item->move_rel(Geom::Translate(t));
+ changed = true;
+ }
+
+ pos += bbox.bbox[orientation].extent();
+ pos += step;
+ }
+
+ } else {
+
+ // Overall anchor span.
+ double dist = sorted.back().anchor - sorted.front().anchor;
+
+ // Distance between anchors.
+ double step = dist / (sorted.size() - 1);
+
+ for (unsigned int i = 0; i < sorted.size() ; i++) {
+ BBoxSort & it(sorted[i]);
+
+ // New anchor position.
+ double 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) {
+ Inkscape::DocumentUndo::done( document, _("Distribute"), INKSCAPE_ICON("dialog-align-and-distribute"));
+ }
+}
+
+class Baseline
+{
+public:
+ Baseline(SPItem *item, Geom::Point base, Geom::Dim2 orientation)
+ : _item (item)
+ , _base (base)
+ , _orientation (orientation)
+ {}
+ SPItem *_item = nullptr;
+ Geom::Point _base;
+ Geom::Dim2 _orientation;
+};
+
+static bool operator< (const Baseline &a, const Baseline &b)
+{
+ return (a._base[a._orientation] < b._base[b._orientation]);
+}
+
+void
+object_distribute_text(const Glib::VariantBase& value, InkscapeApplication *app)
+{
+ Glib::Variant<Glib::ustring> s = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring> >(value);
+ auto token = s.get();
+
+ Geom::Dim2 orientation = Geom::Dim2::X;
+ if (token.find("vertical") != Glib::ustring::npos) {
+ orientation = Geom::Dim2::Y;
+ }
+
+ auto selection = app->get_active_selection();
+ if (selection->size() < 2) {
+ return;
+ }
+
+ // We should not have to do this!
+ auto document = app->get_active_document();
+ selection->setDocument(document);
+
+ std::vector<Baseline> baselines;
+ Geom::Point b_min = Geom::Point ( HUGE_VAL, HUGE_VAL);
+ Geom::Point b_max = Geom::Point (-HUGE_VAL, -HUGE_VAL);
+
+ for (auto item : selection->items()) {
+ if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) {
+ Inkscape::Text::Layout const *layout = te_get_layout(item);
+ std::optional<Geom::Point> 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.emplace_back(Baseline(item, base, orientation));
+ }
+ }
+ }
+
+ if (baselines.size() < 2) {
+ return;
+ }
+
+ std::stable_sort(baselines.begin(), baselines.end());
+
+ double step = (b_max[orientation] - b_min[orientation])/(baselines.size() - 1);
+ int i = 0;
+ for (auto& baseline : baselines) {
+ Geom::Point t(0.0, 0.0);
+ t[orientation] = b_min[orientation] + (step * i) - baseline._base[orientation];
+ baseline._item->move_rel(Geom::Translate(t));
+ ++i;
+ }
+
+ Inkscape::DocumentUndo::done( document, _("Distribute"), INKSCAPE_ICON("dialog-align-and-distribute"));
+}
+
+void
+object_align_text(const Glib::VariantBase& value, InkscapeApplication *app)
+{
+
+ Glib::Variant<Glib::ustring> s = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring> >(value);
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple(" ", s.get());
+
+ // Defaults
+ auto target = ObjectAlignTarget::SELECTION;
+ auto orientation = Geom::Dim2::X;
+ auto direction = Inkscape::Selection::HORIZONTAL;
+
+ // Preference request allows alignment action to remember for key-presses
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (std::find(tokens.begin(), tokens.end(), "pref") != tokens.end()) {
+ tokens.push_back(prefs->getString("/dialogs/align/objects-align-to", "selection"));
+ }
+
+ for (auto const token : tokens) {
+ // Target
+ if (token == "last" ) target = ObjectAlignTarget::LAST;
+ else if (token == "first" ) target = ObjectAlignTarget::FIRST;
+ else if (token == "biggest" ) target = ObjectAlignTarget::BIGGEST;
+ else if (token == "smallest" ) target = ObjectAlignTarget::SMALLEST;
+ else if (token == "page" ) target = ObjectAlignTarget::PAGE;
+ else if (token == "drawing" ) target = ObjectAlignTarget::DRAWING;
+ else if (token == "selection") target = ObjectAlignTarget::SELECTION;
+
+ // Direction
+ if (token == "vertical" ) {
+ orientation = Geom::Dim2::Y;
+ direction = Inkscape::Selection::VERTICAL;
+ }
+ }
+
+ auto selection = app->get_active_selection();
+
+ // We should not have to do this!
+ auto document = app->get_active_document();
+ selection->setDocument(document);
+
+ // Find alignment rectangle. This can come from:
+ // - The bounding box of an object
+ // - The bounding box of a group of objects
+ // - The bounding box of the page, drawing, or selection.
+ SPItem *focus = nullptr;
+ Geom::OptRect b = Geom::OptRect();
+
+ switch (target) {
+ case ObjectAlignTarget::LAST:
+ focus = selection->items().back();
+ break;
+ case ObjectAlignTarget::FIRST:
+ focus = selection->items().front();
+ break;
+ case ObjectAlignTarget::BIGGEST:
+ focus = selection->largestItem(direction);
+ break;
+ case ObjectAlignTarget::SMALLEST:
+ focus = selection->smallestItem(direction);
+ break;
+ case ObjectAlignTarget::PAGE:
+ b = document->pageBounds();
+ break;
+ case ObjectAlignTarget::DRAWING:
+ b = document->getRoot()->desktopPreferredBounds();
+ break;
+ case ObjectAlignTarget::SELECTION:
+ b = selection->preferredBounds();
+ break;
+ default:
+ g_assert_not_reached ();
+ break;
+ };
+
+ Geom::Point ref_point;
+ if (focus) {
+ if (dynamic_cast<SPText *>(focus) || dynamic_cast<SPFlowtext *>(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 : selection->items()) {
+ if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) {
+ Inkscape::Text::Layout const *layout = te_get_layout(item);
+ std::optional<Geom::Point> 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));
+ }
+ }
+ }
+
+ Inkscape::DocumentUndo::done( document, _("Align"), INKSCAPE_ICON("dialog-align-and-distribute"));
+}
+
+/* --------------- Rearrange ----------------- */
+
+class RotateCompare
+{
+public:
+ RotateCompare(Geom::Point& center) : center(center) {}
+
+ bool operator()(const SPItem* a, const SPItem* b) {
+ Geom::Point point_a = a->getCenter() - (center);
+ Geom::Point point_b = b->getCenter() - (center);
+
+ // Sort according to angle.
+ double angle_a = Geom::atan2(point_a);
+ double angle_b = Geom::atan2(point_b);
+ if (angle_a != angle_b) return (angle_a < angle_b);
+
+ // Sort by distance
+ return point_a.length() < point_b.length();
+ }
+
+private:
+ Geom::Point center;
+};
+
+enum SortOrder {
+ SelectionOrder,
+ ZOrder,
+ Rotate
+};
+
+static bool PositionCompare(const SPItem* a, const SPItem* b) {
+ return sp_item_repr_compare_position(a, b) < 0;
+}
+
+void exchange(Inkscape::Selection* selection, SortOrder order)
+{
+ std::vector<SPItem*> items(selection->items().begin(), selection->items().end());
+
+ // Reorder items.
+ switch (order) {
+ case SelectionOrder:
+ break;
+ case ZOrder:
+ std::sort(items.begin(), items.end(), PositionCompare);
+ break;
+ case Rotate:
+ auto center = selection->center();
+ if (center) {
+ std::sort(items.begin(), items.end(), RotateCompare(*center));
+ }
+ break;
+ }
+
+ // Move items.
+ Geom::Point p1 = items.back()->getCenter();
+ for (SPItem *item : items) {
+ Geom::Point p2 = item->getCenter();
+ Geom::Point delta = p1 - p2;
+ item->move_rel(Geom::Translate(delta));
+ p1 = p2;
+ }
+}
+
+/*
+ * The algorithm keeps the size of the bounding box of the centers of all items constant. This
+ * ensures there is no growth or shrinking or drift of the overall area of the items on sequential
+ * randomizations.
+ */
+void randomize(Inkscape::Selection* selection)
+{
+ std::vector<SPItem*> items(selection->items().begin(), selection->items().end());
+
+ // Do 'x' and 'y' independently.
+ for (int i = 0; i < 2; i++) {
+
+ // First, find maximum and minimum centers.
+ double min = std::numeric_limits<double>::max();
+ double max = std::numeric_limits<double>::min();
+
+ for (auto item : items) {
+ double center = item->getCenter()[i];
+ if (min > center) {
+ min = center;
+ }
+ if (max < center) {
+ max = center;
+ }
+ }
+
+
+ // Second, assign minimum/maximum values to two different items randomly.
+ int nitems = items.size();
+ int imin = rand() % nitems;
+ int imax = rand() % nitems;
+ while (imin == imax) {
+ imax = rand() % nitems;
+ }
+
+
+ // Third, find new positions of item centers.
+ int index = 0;
+ for (auto item : items) {
+ double z = 0.0;
+ if (index == imin) {
+ z = min;
+ } else if (index == imax) {
+ z = max;
+ } else {
+ z = g_random_double_range(min, max);
+ }
+
+ double delta = z - item->getCenter()[i];
+ Geom::Point t;
+ t[i] = delta;
+ item->move_rel(Geom::Translate(t));
+
+ ++index;
+ }
+ }
+}
+
+
+void
+object_rearrange(const Glib::VariantBase& value, InkscapeApplication *app)
+{
+ Glib::Variant<Glib::ustring> s = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring> >(value);
+ auto token = s.get();
+
+ auto selection = app->get_active_selection();
+
+ // We should not have to do this!
+ auto document = app->get_active_document();
+ selection->setDocument(document);
+
+ std::vector<SPItem*> items(selection->items().begin(), selection->items().end());
+ if (items.size() < 2) {
+ return;
+ }
+
+ 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);
+
+ // clang-format off
+ if (token == "graph" ) { graphlayout(items); }
+ else if (token == "exchange" ) { exchange(selection, SortOrder::SelectionOrder); }
+ else if (token == "exchangez" ) { exchange(selection, SortOrder::ZOrder); }
+ else if (token == "rotate" ) { exchange(selection, SortOrder::Rotate); }
+ else if (token == "randomize" ) { randomize(selection); }
+ else if (token == "unclump" ) { unclump(items); }
+ else {
+ std::cerr << "object_rearrange: unhandled argument: " << token << std::endl;
+ }
+ // clang-format on
+
+ // Restore compensation setting.
+ prefs->setInt("/options/clonecompensation/value", saved_compensation);
+
+ Inkscape::DocumentUndo::done( document, _("Rearrange"), INKSCAPE_ICON("dialog-align-and-distribute"));
+}
+
+
+void
+object_remove_overlaps(const Glib::VariantBase& value, InkscapeApplication *app)
+{
+ auto selection = app->get_active_selection();
+
+ // We should not have to do this!
+ auto document = app->get_active_document();
+ selection->setDocument(document);
+
+ std::vector<SPItem*> items(selection->items().begin(), selection->items().end());
+ if (items.size() < 2) {
+ return;
+ }
+
+ // We used tuple so as not to convert from double to string and back again (from Align and Distribute dialog).
+ if (value.get_type_string() != "(dd)") {
+ std::cerr << "object_remove_overlaps: wrong variant type: " << value.get_type_string() << " (should be '(dd)')" << std::endl;
+ }
+
+ auto tuple = Glib::VariantBase::cast_dynamic<Glib::Variant<std::tuple<double, double>>>(value);
+ auto [hgap, vgap] = tuple.get();
+
+ 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);
+
+ removeoverlap(items, hgap, vgap);
+
+ // Restore compensation setting.
+ prefs->setInt("/options/clonecompensation/value", saved_compensation);
+
+ Inkscape::DocumentUndo::done( document, _("Remove overlaps"), INKSCAPE_ICON("dialog-align-and-distribute"));
+}
+
+
+std::vector<std::vector<Glib::ustring>> raw_data_object_align =
+{
+ // clang-format off
+ {"app.object-align-on-canvas", N_("Enable on-canvas alignment"), "Object", N_("Enable on-canvas alignment handles." )},
+
+ {"app.object-align", N_("Align objects"), "Object", N_("Align selected objects; usage: [[left|hcenter|right] || [top|vcenter|bottom]] [last|first|biggest|smallest|page|drawing|selection|pref]? group? anchor?")},
+
+ {"app.object-align('left pref')", N_("Align to left edge"), "Object", N_("Align selection horizontally to left edge." )},
+ {"app.object-align('hcenter pref')", N_("Align to horizontal center"), "Object", N_("Align selection horizontally to the center." )},
+ {"app.object-align('right pref')", N_("Align to right edge"), "Object", N_("Align selection horizontally to right edge." )},
+ {"app.object-align('top pref')", N_("Align to top edge"), "Object", N_("Align selection vertically to top edge." )},
+ {"app.object-align('bottom pref')", N_("Align to bottom edge"), "Object", N_("Align selection vertically to bottom edge." )},
+ {"app.object-align('vcenter pref')", N_("Align to vertical center"), "Object", N_("Align selection vertically to the center." )},
+ {"app.object-align('hcenter vcenter pref')", N_("Align to center"), "Object", N_("Align selection to the center." )},
+ {"app.object-align-text", N_("Align text objects"), "Object", N_("Align selected text alignment points; usage: [[vertical | horizontal] [last|first|biggest|smallest|page|drawing|selection]?" )},
+
+ {"app.object-distribute", N_("Distribute objects"), "Object", N_("Distribute selected objects; usage: [hgap | left | hcenter | right | vgap | top | vcenter | bottom]" )},
+ {"app.object-distribute('hgap')", N_("Even horizontal gaps"), "Object", N_("Distribute horizontally with even horizontal gaps." )},
+ {"app.object-distribute('left')", N_("Even left edges"), "Object", N_("Distribute horizontally with even spacing between left edges." )},
+ {"app.object-distribute('hcenter')", N_("Even horizontal centers"), "Object", N_("Distribute horizontally with even spacing between centers." )},
+ {"app.object-distribute('right')", N_("Even right edges"), "Object", N_("Distribute horizontally with even spacing between right edges." )},
+ {"app.object-distribute('vgap')", N_("Even vertical gaps"), "Object", N_("Distribute vertically with even vertical gaps." )},
+ {"app.object-distribute('top')", N_("Even top edges"), "Object", N_("Distribute vertically with even spacing between top edges." )},
+ {"app.object-distribute('vcenter')", N_("Even vertical centers"), "Object", N_("Distribute vertically with even spacing between centers." )},
+ {"app.object-distribute('bottom')", N_("Even bottom edges"), "Object", N_("Distribute vertically with even spacing between bottom edges." )},
+
+ {"app.object-distribute-text", N_("Distribute text objects"), "Object", N_("Distribute text alignment points; usage [vertical | horizontal]" )},
+ {"app.object-distribute-text('horizontal')", N_("Distribute text objects"), "Object", N_("Distribute text alignment points horizontally" )},
+ {"app.object-distribute-text('vertical')", N_("Distribute text objects"), "Object", N_("Distribute text alignment points vertically" )},
+
+ {"app.object-rearrange", N_("Rearrange objects"), "Object", N_("Rearrange selected objects; usage: [graph | exchange | exchangez | rotate | randomize | unclump]" )},
+ {"app.object-rearrange('graph')", N_("Rearrange as graph"), "Object", N_("Nicely arrange selected connector network." )},
+ {"app.object-rearrange('exchange')", N_("Exchange in selection order"), "Object", N_("Exchange positions of selected objects - selection order." )},
+ {"app.object-rearrange('exchangez')", N_("Exchange in z-order"), "Object", N_("Exchange positions of selected objects - stacking order." )},
+ {"app.object-rearrange('rotate')", N_("Exchange around center"), "Object", N_("Exchange positions of selected objects - rotate around center point." )},
+ {"app.object-rearrange('randomize')", N_("Random exchange"), "Object", N_("Randomize centers in both dimensions." )},
+ {"app.object-rearrange('unclump')", N_("Unclump"), "Object", N_("Unclump objects: try to equalize edge-to-edge distances." )},
+
+ {"app.object-remove-overlaps", N_("Remove overlaps"), "Object", N_("Remove overlaps between objects: requires two comma separated numbers (horizontal and vertical gaps)." )},
+ // clang-format on
+};
+
+std::vector<std::vector<Glib::ustring>> hint_data_object_align =
+{
+ // clang-format off
+ {"app.object-align", N_("Enter anchor<space>alignment<space>optional second alignment. Possible anchors: last, first, biggest, smallest, page, drawing, selection, pref; possible alignments: left, hcenter, right, top, vcenter, bottom.")},
+ {"app.object-distribute", N_("Enter distribution type. Possible values: left, hcenter, right, top, vcenter, bottom, hgap, vgap.") },
+ {"app.object-rearrange", N_("Enter arrange method. Possible values: graph, exchange, exchangez, rotate, randomize, unclump.") },
+ {"app.object-remove-overlaps", N_("Enter two comma-separated numbers: horizontal,vertical") },
+ // clang-format on
+};
+
+void
+add_actions_object_align(InkscapeApplication* app)
+{
+ Glib::VariantType String(Glib::VARIANT_TYPE_STRING);
+ std::vector<Glib::VariantType> dd = {Glib::VARIANT_TYPE_DOUBLE, Glib::VARIANT_TYPE_DOUBLE};
+ Glib::VariantType Tuple_DD = Glib::VariantType::create_tuple(dd);
+
+ auto *gapp = app->gio_app();
+
+ auto prefs = Inkscape::Preferences::get();
+ bool on_canvas = prefs->getBool("/dialogs/align/oncanvas");
+
+ // clang-format off
+ gapp->add_action_bool( "object-align-on-canvas", sigc::bind<InkscapeApplication*>(sigc::ptr_fun(&object_align_on_canvas), app), on_canvas);
+ gapp->add_action_with_parameter( "object-align", String, sigc::bind<InkscapeApplication*>(sigc::ptr_fun(&object_align), app));
+ gapp->add_action_with_parameter( "object-align-text", String, sigc::bind<InkscapeApplication*>(sigc::ptr_fun(&object_align_text), app));
+ gapp->add_action_with_parameter( "object-distribute", String, sigc::bind<InkscapeApplication*>(sigc::ptr_fun(&object_distribute), app));
+ gapp->add_action_with_parameter( "object-distribute-text", String, sigc::bind<InkscapeApplication*>(sigc::ptr_fun(&object_distribute_text), app));
+ gapp->add_action_with_parameter( "object-rearrange", String, sigc::bind<InkscapeApplication*>(sigc::ptr_fun(&object_rearrange), app));
+ gapp->add_action_with_parameter( "object-remove-overlaps", Tuple_DD, sigc::bind<InkscapeApplication*>(sigc::ptr_fun(&object_remove_overlaps), app));
+ // clang-format on
+
+ app->get_action_extra_data().add_data(raw_data_object_align);
+ app->get_action_hint_data().add_data(hint_data_object_align);
+}
+
+/*
+ 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 :