diff options
Diffstat (limited to 'src/ui/dialog/align-and-distribute.cpp')
-rw-r--r-- | src/ui/dialog/align-and-distribute.cpp | 1339 |
1 files changed, 1339 insertions, 0 deletions
diff --git a/src/ui/dialog/align-and-distribute.cpp b/src/ui/dialog/align-and-distribute.cpp new file mode 100644 index 0000000..31605a0 --- /dev/null +++ b/src/ui/dialog/align-and-distribute.cpp @@ -0,0 +1,1339 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Align and Distribute dialog - implementation. + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Aubanel MONNIER <aubi@libertysurf.fr> + * Frank Felfe <innerspace@iname.com> + * Lauris Kaplinski <lauris@kaplinski.com> + * Tim Dwyer <tgdwyer@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> + +#include <2geom/transforms.h> + +#include <utility> + +#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<SPItem*> 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<SPItem*> selected(selection->items().begin(), selection->items().end()); + if (selected.empty()) return; + + //Check 2 or more selected objects + std::vector<SPItem*>::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<BBoxSort> ::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<SPItem *> 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<SPItem *> 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<Geom::Point> 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<SPItem*> 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<Geom::Point> 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<SPItem*> 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<SPItem*> 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<SPItem*> 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<Baselines> 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<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 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<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)); + 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 : |