Code

A simple layout document as to what, why and how is cppification.
[inkscape.git] / src / ui / dialog / align-and-distribute.cpp
1 /** @file
2  * @brief Align and Distribute dialog - implementation
3  */
4 /* Authors:
5  *   Bryce W. Harrington <bryce@bryceharrington.org>
6  *   Aubanel MONNIER <aubi@libertysurf.fr>
7  *   Frank Felfe <innerspace@iname.com>
8  *   Lauris Kaplinski <lauris@kaplinski.com>
9  *   Tim Dwyer <tgdwyer@gmail.com>
10  *
11  * Copyright (C) 1999-2004, 2005 Authors
12  *
13  * Released under GNU GPL.  Read the file 'COPYING' for more information.
14  */
17 #ifdef HAVE_CONFIG_H
18 # include <config.h>
19 #endif
21 #include <gtkmm/spinbutton.h>
23 #include "desktop-handles.h"
24 #include "unclump.h"
25 #include "document.h"
26 #include "enums.h"
27 #include "graphlayout.h"
28 #include "inkscape.h"
29 #include "macros.h"
30 #include "preferences.h"
31 #include "removeoverlap.h"
32 #include "selection.h"
33 #include "sp-flowtext.h"
34 #include "sp-item-transform.h"
35 #include "sp-text.h"
36 #include "text-editing.h"
37 #include "tools-switch.h"
38 #include "ui/icon-names.h"
39 #include "ui/tool/node-tool.h"
40 #include "ui/tool/multi-path-manipulator.h"
41 #include "util/glib-list-iterators.h"
42 #include "verbs.h"
43 #include "widgets/icon.h"
45 #include "align-and-distribute.h"
47 namespace Inkscape {
48 namespace UI {
49 namespace Dialog {
51 /////////helper classes//////////////////////////////////
53 class Action {
54 public :
55     Action(const Glib::ustring &id,
56            const Glib::ustring &tiptext,
57            guint row, guint column,
58            Gtk::Table &parent,
59            Gtk::Tooltips &tooltips,
60            AlignAndDistribute &dialog):
61         _dialog(dialog),
62         _id(id),
63         _parent(parent)
64     {
65         Gtk::Widget*  pIcon = Gtk::manage( sp_icon_get_icon( _id, Inkscape::ICON_SIZE_LARGE_TOOLBAR) );
66         Gtk::Button * pButton = Gtk::manage(new Gtk::Button());
67         pButton->set_relief(Gtk::RELIEF_NONE);
68         pIcon->show();
69         pButton->add(*pIcon);
70         pButton->show();
72         pButton->signal_clicked()
73             .connect(sigc::mem_fun(*this, &Action::on_button_click));
74         tooltips.set_tip(*pButton, tiptext);
75         parent.attach(*pButton, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL);
76     }
77     virtual ~Action(){}
79     AlignAndDistribute &_dialog;
81 private :
82     virtual void on_button_click(){}
84     Glib::ustring _id;
85     Gtk::Table &_parent;
86 };
89 class ActionAlign : public Action {
90 public :
91     struct Coeffs {
92        double mx0, mx1, my0, my1;
93        double sx0, sx1, sy0, sy1;
94     };
95     ActionAlign(const Glib::ustring &id,
96                 const Glib::ustring &tiptext,
97                 guint row, guint column,
98                 AlignAndDistribute &dialog,
99                 guint coeffIndex):
100         Action(id, tiptext, row, column,
101                dialog.align_table(), dialog.tooltips(), dialog),
102         _index(coeffIndex),
103         _dialog(dialog)
104     {}
106 private :
108     virtual void on_button_click() {
109         //Retreive selected objects
110         SPDesktop *desktop = _dialog.getDesktop();
111         if (!desktop) return;
113         Inkscape::Selection *selection = sp_desktop_selection(desktop);
114         if (!selection) return;
116         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
117         bool sel_as_group = prefs->getBool("/dialogs/align/sel-as-groups");
119         using Inkscape::Util::GSListConstIterator;
120         std::list<SPItem *> selected;
121         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
122         if (selected.empty()) return;
124         Geom::Point mp; //Anchor point
125         AlignAndDistribute::AlignTarget target = _dialog.getAlignTarget();
126         const Coeffs &a= _allCoeffs[_index];
127         switch (target)
128         {
129         case AlignAndDistribute::LAST:
130         case AlignAndDistribute::FIRST:
131         case AlignAndDistribute::BIGGEST:
132         case AlignAndDistribute::SMALLEST:
133         {
134             //Check 2 or more selected objects
135             std::list<SPItem *>::iterator second(selected.begin());
136             ++second;
137             if (second == selected.end())
138                 return;
139             //Find the master (anchor on which the other objects are aligned)
140             std::list<SPItem *>::iterator master(
141                 _dialog.find_master (
142                     selected,
143                     (a.mx0 != 0.0) ||
144                     (a.mx1 != 0.0) )
145                 );
146             //remove the master from the selection
147             SPItem * thing = *master;
148             // TODO: either uncomment or remove the following commented lines, depending on which
149             //       behaviour of moving objects makes most sense; also cf. discussion at
150             //       https://bugs.launchpad.net/inkscape/+bug/255933
151             /*if (!sel_as_group) { */
152                 selected.erase(master);
153             /*}*/
154             //Compute the anchor point
155             Geom::OptRect b = thing->getBboxDesktop ();
156             if (b) {
157                 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
158                                a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
159             } else {
160                 return;
161             }
162             break;
163         }
165         case AlignAndDistribute::PAGE:
166             mp = Geom::Point(a.mx1 * sp_desktop_document(desktop)->getWidth(),
167                            a.my1 * sp_desktop_document(desktop)->getHeight());
168             break;
170         case AlignAndDistribute::DRAWING:
171         {
172             Geom::OptRect b = static_cast<SPItem *>( sp_document_root (sp_desktop_document (desktop)))->getBboxDesktop();
173             if (b) {
174                 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
175                                a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
176             } else {
177                 return;
178             }
179             break;
180         }
182         case AlignAndDistribute::SELECTION:
183         {
184             Geom::OptRect b =  selection->bounds();
185             if (b) {
186                 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
187                                a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
188             } else {
189                 return;
190             }
191             break;
192         }
194         default:
195             g_assert_not_reached ();
196             break;
197         };  // end of switch
199         // Top hack: temporarily set clone compensation to unmoved, so that we can align/distribute
200         // clones with their original (and the move of the original does not disturb the
201         // clones). The only problem with this is that if there are outside-of-selection clones of
202         // a selected original, they will be unmoved too, possibly contrary to user's
203         // expecation. However this is a minor point compared to making align/distribute always
204         // work as expected, and "unmoved" is the default option anyway.
205         int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
206         prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
208         bool changed = false;
209         Geom::OptRect b;
210         if (sel_as_group)
211             b = selection->bounds();
213         //Move each item in the selected list separately
214         for (std::list<SPItem *>::iterator it(selected.begin());
215              it != selected.end();
216              it++)
217         {
218             sp_desktop_document (desktop)->ensure_up_to_date();
219             if (!sel_as_group)
220                 b = (*it)->getBboxDesktop();
221             if (b) {
222                 Geom::Point const sp(a.sx0 * b->min()[Geom::X] + a.sx1 * b->max()[Geom::X],
223                                      a.sy0 * b->min()[Geom::Y] + a.sy1 * b->max()[Geom::Y]);
224                 Geom::Point const mp_rel( mp - sp );
225                 if (LInfty(mp_rel) > 1e-9) {
226                     sp_item_move_rel(*it, Geom::Translate(mp_rel));
227                     changed = true;
228                 }
229             }
230         }
232         // restore compensation setting
233         prefs->setInt("/options/clonecompensation/value", saved_compensation);
235         if (changed) {
236             SPDocumentUndo::done ( sp_desktop_document (desktop) , SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
237                                _("Align"));
238         }
241     }
242     guint _index;
243     AlignAndDistribute &_dialog;
245     static const Coeffs _allCoeffs[10];
247 };
248 ActionAlign::Coeffs const ActionAlign::_allCoeffs[10] = {
249     {1., 0., 0., 0., 0., 1., 0., 0.},
250     {1., 0., 0., 0., 1., 0., 0., 0.},
251     {.5, .5, 0., 0., .5, .5, 0., 0.},
252     {0., 1., 0., 0., 0., 1., 0., 0.},
253     {0., 1., 0., 0., 1., 0., 0., 0.},
254     {0., 0., 0., 1., 0., 0., 1., 0.},
255     {0., 0., 0., 1., 0., 0., 0., 1.},
256     {0., 0., .5, .5, 0., 0., .5, .5},
257     {0., 0., 1., 0., 0., 0., 1., 0.},
258     {0., 0., 1., 0., 0., 0., 0., 1.}
259 };
261 BBoxSort::BBoxSort(SPItem *pItem, Geom::Rect bounds, Geom::Dim2 orientation, double kBegin, double kEnd) :
262         item(pItem),
263         bbox (bounds)
265         anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation];
267 BBoxSort::BBoxSort(const BBoxSort &rhs) :
268         //NOTE :  this copy ctor is called O(sort) when sorting the vector
269         //this is bad. The vector should be a vector of pointers.
270         //But I'll wait the bohem GC before doing that
271         item(rhs.item), anchor(rhs.anchor), bbox(rhs.bbox) 
275 bool operator< (const BBoxSort &a, const BBoxSort &b)
277     return (a.anchor < b.anchor);
280 class ActionDistribute : public Action {
281 public :
282     ActionDistribute(const Glib::ustring &id,
283                      const Glib::ustring &tiptext,
284                      guint row, guint column,
285                      AlignAndDistribute &dialog,
286                      bool onInterSpace,
287                      Geom::Dim2 orientation,
288                      double kBegin, double kEnd
289         ):
290         Action(id, tiptext, row, column,
291                dialog.distribute_table(), dialog.tooltips(), dialog),
292         _dialog(dialog),
293         _onInterSpace(onInterSpace),
294         _orientation(orientation),
295         _kBegin(kBegin),
296         _kEnd( kEnd)
297     {}
299 private :
300     virtual void on_button_click() {
301         //Retreive selected objects
302         SPDesktop *desktop = _dialog.getDesktop();
303         if (!desktop) return;
305         Inkscape::Selection *selection = sp_desktop_selection(desktop);
306         if (!selection) return;
308         using Inkscape::Util::GSListConstIterator;
309         std::list<SPItem *> selected;
310         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
311         if (selected.empty()) return;
313         //Check 2 or more selected objects
314         std::list<SPItem *>::iterator second(selected.begin());
315         ++second;
316         if (second == selected.end()) return;
319         std::vector< BBoxSort  > sorted;
320         for (std::list<SPItem *>::iterator it(selected.begin());
321             it != selected.end();
322             ++it)
323         {
324             Geom::OptRect bbox = (*it)->getBboxDesktop();
325             if (bbox) {
326                 sorted.push_back(BBoxSort(*it, *bbox, _orientation, _kBegin, _kEnd));
327             }
328         }
329         //sort bbox by anchors
330         std::sort(sorted.begin(), sorted.end());
332         // see comment in ActionAlign above
333         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
334         int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
335         prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
337         unsigned int len = sorted.size();
338         bool changed = false;
339         if (_onInterSpace)
340         {
341             //overall bboxes span
342             float dist = (sorted.back().bbox.max()[_orientation] -
343                           sorted.front().bbox.min()[_orientation]);
344             //space eaten by bboxes
345             float span = 0;
346             for (unsigned int i = 0; i < len; i++)
347             {
348                 span += sorted[i].bbox[_orientation].extent();
349             }
350             //new distance between each bbox
351             float step = (dist - span) / (len - 1);
352             float pos = sorted.front().bbox.min()[_orientation];
353             for ( std::vector<BBoxSort> ::iterator it (sorted.begin());
354                   it < sorted.end();
355                   it ++ )
356             {
357                 if (!NR_DF_TEST_CLOSE (pos, it->bbox.min()[_orientation], 1e-6)) {
358                     Geom::Point t(0.0, 0.0);
359                     t[_orientation] = pos - it->bbox.min()[_orientation];
360                     sp_item_move_rel(it->item, Geom::Translate(t));
361                     changed = true;
362                 }
363                 pos += it->bbox[_orientation].extent();
364                 pos += step;
365             }
366         }
367         else
368         {
369             //overall anchor span
370             float dist = sorted.back().anchor - sorted.front().anchor;
371             //distance between anchors
372             float step = dist / (len - 1);
374             for ( unsigned int i = 0; i < len ; i ++ )
375             {
376                 BBoxSort & it(sorted[i]);
377                 //new anchor position
378                 float pos = sorted.front().anchor + i * step;
379                 //Don't move if we are really close
380                 if (!NR_DF_TEST_CLOSE (pos, it.anchor, 1e-6)) {
381                     //Compute translation
382                     Geom::Point t(0.0, 0.0);
383                     t[_orientation] = pos - it.anchor;
384                     //translate
385                     sp_item_move_rel(it.item, Geom::Translate(t));
386                     changed = true;
387                 }
388             }
389         }
391         // restore compensation setting
392         prefs->setInt("/options/clonecompensation/value", saved_compensation);
394         if (changed) {
395             SPDocumentUndo::done ( sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
396                                _("Distribute"));
397         }
398     }
399     guint _index;
400     AlignAndDistribute &_dialog;
401     bool _onInterSpace;
402     Geom::Dim2 _orientation;
404     double _kBegin;
405     double _kEnd;
407 };
410 class ActionNode : public Action {
411 public :
412     ActionNode(const Glib::ustring &id,
413                const Glib::ustring &tiptext,
414                guint column,
415                AlignAndDistribute &dialog,
416                Geom::Dim2 orientation, bool distribute):
417         Action(id, tiptext, 0, column,
418                dialog.nodes_table(), dialog.tooltips(), dialog),
419         _orientation(orientation),
420         _distribute(distribute)
421     {}
423 private :
424     Geom::Dim2 _orientation;
425     bool _distribute;
426     virtual void on_button_click()
427     {
429         if (!_dialog.getDesktop()) return;
430         SPEventContext *event_context = sp_desktop_event_context(_dialog.getDesktop());
431         if (!INK_IS_NODE_TOOL (event_context)) return;
432         InkNodeTool *nt = INK_NODE_TOOL(event_context);
434         if (_distribute)
435             nt->_multipath->distributeNodes(_orientation);
436         else
437             nt->_multipath->alignNodes(_orientation);
439     }
440 };
442 class ActionRemoveOverlaps : public Action {
443 private:
444     Gtk::Label removeOverlapXGapLabel;
445     Gtk::Label removeOverlapYGapLabel;
446     Gtk::SpinButton removeOverlapXGap;
447     Gtk::SpinButton removeOverlapYGap;
449 public:
450     ActionRemoveOverlaps(Glib::ustring const &id,
451                          Glib::ustring const &tiptext,
452                          guint row,
453                          guint column,
454                          AlignAndDistribute &dialog) :
455         Action(id, tiptext, row, column + 4,
456                dialog.removeOverlap_table(), dialog.tooltips(), dialog)
457     {
458         dialog.removeOverlap_table().set_col_spacings(3);
460         removeOverlapXGap.set_digits(1);
461         removeOverlapXGap.set_size_request(60, -1);
462         removeOverlapXGap.set_increments(1.0, 0);
463         removeOverlapXGap.set_range(-1000.0, 1000.0);
464         removeOverlapXGap.set_value(0);
465         dialog.tooltips().set_tip(removeOverlapXGap,
466                                   _("Minimum horizontal gap (in px units) between bounding boxes"));
467         //TRANSLATORS: only translate "string" in "context|string".
468         // For more details, see http://developer.gnome.org/doc/API/2.0/glib/glib-I18N.html#Q-:CAPS
469         // "H:" stands for horizontal gap
470         removeOverlapXGapLabel.set_label(Q_("gap|H:"));
472         removeOverlapYGap.set_digits(1);
473         removeOverlapYGap.set_size_request(60, -1);
474         removeOverlapYGap.set_increments(1.0, 0);
475         removeOverlapYGap.set_range(-1000.0, 1000.0);
476         removeOverlapYGap.set_value(0);
477         dialog.tooltips().set_tip(removeOverlapYGap,
478                                   _("Minimum vertical gap (in px units) between bounding boxes"));
479         /* TRANSLATORS: Vertical gap */
480         removeOverlapYGapLabel.set_label(_("V:"));
482         dialog.removeOverlap_table().attach(removeOverlapXGapLabel, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL);
483         dialog.removeOverlap_table().attach(removeOverlapXGap, column+1, column+2, row, row+1, Gtk::FILL, Gtk::FILL);
484         dialog.removeOverlap_table().attach(removeOverlapYGapLabel, column+2, column+3, row, row+1, Gtk::FILL, Gtk::FILL);
485         dialog.removeOverlap_table().attach(removeOverlapYGap, column+3, column+4, row, row+1, Gtk::FILL, Gtk::FILL);
487     }
489 private :
490     virtual void on_button_click()
491     {
492         if (!_dialog.getDesktop()) return;
494         // see comment in ActionAlign above
495         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
496         int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
497         prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
499         // xGap and yGap are the minimum space required between bounding rectangles.
500         double const xGap = removeOverlapXGap.get_value();
501         double const yGap = removeOverlapYGap.get_value();
502         removeoverlap(sp_desktop_selection(_dialog.getDesktop())->itemList(),
503                       xGap, yGap);
505         // restore compensation setting
506         prefs->setInt("/options/clonecompensation/value", saved_compensation);
508         SPDocumentUndo::done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
509                          _("Remove overlaps"));
510     }
511 };
513 class ActionGraphLayout : public Action {
514 public:
515     ActionGraphLayout(Glib::ustring const &id,
516                          Glib::ustring const &tiptext,
517                          guint row,
518                          guint column,
519                          AlignAndDistribute &dialog) :
520         Action(id, tiptext, row, column + 4,
521                dialog.graphLayout_table(), dialog.tooltips(), dialog)
522     {}
524 private :
525     virtual void on_button_click()
526     {
527         if (!_dialog.getDesktop()) return;
529         // see comment in ActionAlign above
530         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
531         int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
532         prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
534         graphlayout(sp_desktop_selection(_dialog.getDesktop())->itemList());
536         // restore compensation setting
537         prefs->setInt("/options/clonecompensation/value", saved_compensation);
539         SPDocumentUndo::done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
540                          _("Arrange connector network"));
541     }
542 };
544 class ActionUnclump : public Action {
545 public :
546     ActionUnclump(const Glib::ustring &id,
547                const Glib::ustring &tiptext,
548                guint row,
549                guint column,
550                AlignAndDistribute &dialog):
551         Action(id, tiptext, row, column,
552                dialog.distribute_table(), dialog.tooltips(), dialog)
553     {}
555 private :
556     virtual void on_button_click()
557     {
558         if (!_dialog.getDesktop()) return;
560         // see comment in ActionAlign above
561         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
562         int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
563         prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
565         unclump ((GSList *) sp_desktop_selection(_dialog.getDesktop())->itemList());
567         // restore compensation setting
568         prefs->setInt("/options/clonecompensation/value", saved_compensation);
570         SPDocumentUndo::done (sp_desktop_document (_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
571                           _("Unclump"));
572     }
573 };
575 class ActionRandomize : public Action {
576 public :
577     ActionRandomize(const Glib::ustring &id,
578                const Glib::ustring &tiptext,
579                guint row,
580                guint column,
581                AlignAndDistribute &dialog):
582         Action(id, tiptext, row, column,
583                dialog.distribute_table(), dialog.tooltips(), dialog)
584     {}
586 private :
587     virtual void on_button_click()
588     {
589         SPDesktop *desktop = _dialog.getDesktop();
590         if (!desktop) return;
592         Inkscape::Selection *selection = sp_desktop_selection(desktop);
593         if (!selection) return;
595         using Inkscape::Util::GSListConstIterator;
596         std::list<SPItem *> selected;
597         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
598         if (selected.empty()) return;
600         //Check 2 or more selected objects
601         if (selected.size() < 2) return;
603         Geom::OptRect sel_bbox = selection->bounds();
604         if (!sel_bbox) {
605             return;
606         }
608         // This bbox is cached between calls to randomize, so that there's no growth nor shrink
609         // nor drift on sequential randomizations. Discard cache on global (or better active
610         // desktop's) selection_change signal.
611         if (!_dialog.randomize_bbox) {
612             _dialog.randomize_bbox = *sel_bbox;
613         }
615         // see comment in ActionAlign above
616         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
617         int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
618         prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
620         for (std::list<SPItem *>::iterator it(selected.begin());
621             it != selected.end();
622             ++it)
623         {
624             sp_desktop_document (desktop)->ensure_up_to_date();
625             Geom::OptRect item_box = (*it)->getBboxDesktop ();
626             if (item_box) {
627                 // find new center, staying within bbox
628                 double x = _dialog.randomize_bbox->min()[Geom::X] + (*item_box)[Geom::X].extent() /2 +
629                     g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::X].extent() - (*item_box)[Geom::X].extent());
630                 double y = _dialog.randomize_bbox->min()[Geom::Y] + (*item_box)[Geom::Y].extent()/2 +
631                     g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::Y].extent() - (*item_box)[Geom::Y].extent());
632                 // displacement is the new center minus old:
633                 Geom::Point t = Geom::Point (x, y) - 0.5*(item_box->max() + item_box->min());
634                 sp_item_move_rel(*it, Geom::Translate(t));
635             }
636         }
638         // restore compensation setting
639         prefs->setInt("/options/clonecompensation/value", saved_compensation);
641         SPDocumentUndo::done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
642                           _("Randomize positions"));
643     }
644 };
646 struct Baselines
648     SPItem *_item;
649     Geom::Point _base;
650     Geom::Dim2 _orientation;
651     Baselines(SPItem *item, Geom::Point base, Geom::Dim2 orientation) :
652         _item (item),
653         _base (base),
654         _orientation (orientation)
655     {}
656 };
658 bool operator< (const Baselines &a, const Baselines &b)
660     return (a._base[a._orientation] < b._base[b._orientation]);
663 class ActionBaseline : public Action {
664 public :
665     ActionBaseline(const Glib::ustring &id,
666                const Glib::ustring &tiptext,
667                guint row,
668                guint column,
669                AlignAndDistribute &dialog,
670                Gtk::Table &table,
671                Geom::Dim2 orientation, bool distribute):
672         Action(id, tiptext, row, column,
673                table, dialog.tooltips(), dialog),
674         _orientation(orientation),
675         _distribute(distribute)
676     {}
678 private :
679     Geom::Dim2 _orientation;
680     bool _distribute;
681     virtual void on_button_click()
682     {
683         SPDesktop *desktop = _dialog.getDesktop();
684         if (!desktop) return;
686         Inkscape::Selection *selection = sp_desktop_selection(desktop);
687         if (!selection) return;
689         using Inkscape::Util::GSListConstIterator;
690         std::list<SPItem *> selected;
691         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
692         if (selected.empty()) return;
694         //Check 2 or more selected objects
695         if (selected.size() < 2) return;
697         Geom::Point b_min = Geom::Point (HUGE_VAL, HUGE_VAL);
698         Geom::Point b_max = Geom::Point (-HUGE_VAL, -HUGE_VAL);
700         std::vector<Baselines> sorted;
702         for (std::list<SPItem *>::iterator it(selected.begin());
703             it != selected.end();
704             ++it)
705         {
706             if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
707                 Inkscape::Text::Layout const *layout = te_get_layout(*it);
708                 boost::optional<Geom::Point> pt = layout->baselineAnchorPoint();
709                 if (pt) {
710                     Geom::Point base = *pt * (*it)->i2d_affine();
711                     if (base[Geom::X] < b_min[Geom::X]) b_min[Geom::X] = base[Geom::X];
712                     if (base[Geom::Y] < b_min[Geom::Y]) b_min[Geom::Y] = base[Geom::Y];
713                     if (base[Geom::X] > b_max[Geom::X]) b_max[Geom::X] = base[Geom::X];
714                     if (base[Geom::Y] > b_max[Geom::Y]) b_max[Geom::Y] = base[Geom::Y];
715                     Baselines b (*it, base, _orientation);
716                     sorted.push_back(b);
717                 }
718             }
719         }
721         if (sorted.size() <= 1) return;
723         //sort baselines
724         std::sort(sorted.begin(), sorted.end());
726         bool changed = false;
728         if (_distribute) {
729             double step = (b_max[_orientation] - b_min[_orientation])/(sorted.size() - 1);
730             for (unsigned int i = 0; i < sorted.size(); i++) {
731                 SPItem *item = sorted[i]._item;
732                 Geom::Point base = sorted[i]._base;
733                 Geom::Point t(0.0, 0.0);
734                 t[_orientation] = b_min[_orientation] + step * i - base[_orientation];
735                 sp_item_move_rel(item, Geom::Translate(t));
736                 changed = true;
737             }
739             if (changed) {
740                 SPDocumentUndo::done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
741                                   _("Distribute text baselines"));
742             }
744         } else {
745             for (std::list<SPItem *>::iterator it(selected.begin());
746                  it != selected.end();
747                  ++it)
748             {
749                 if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
750                     Inkscape::Text::Layout const *layout = te_get_layout(*it);
751                     boost::optional<Geom::Point> pt = layout->baselineAnchorPoint();
752                     if (pt) {
753                         Geom::Point base = *pt * (*it)->i2d_affine();
754                         Geom::Point t(0.0, 0.0);
755                         t[_orientation] = b_min[_orientation] - base[_orientation];
756                         sp_item_move_rel(*it, Geom::Translate(t));
757                         changed = true;
758                     }
759                 }
760             }
762             if (changed) {
763                 SPDocumentUndo::done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
764                                   _("Align text baselines"));
765             }
766         }
767     }
768 };
772 void on_tool_changed(Inkscape::Application */*inkscape*/, SPEventContext */*context*/, AlignAndDistribute *daad)
774     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
775     if (desktop && sp_desktop_event_context(desktop))
776         daad->setMode(tools_active(desktop) == TOOLS_NODES);
779 void on_selection_changed(Inkscape::Application */*inkscape*/, Inkscape::Selection */*selection*/, AlignAndDistribute *daad)
781     daad->randomize_bbox = Geom::OptRect();
784 /////////////////////////////////////////////////////////
789 AlignAndDistribute::AlignAndDistribute()
790     : UI::Widget::Panel ("", "/dialogs/align", SP_VERB_DIALOG_ALIGN_DISTRIBUTE),
791       randomize_bbox(),
792       _alignFrame(_("Align")),
793       _distributeFrame(_("Distribute")),
794       _removeOverlapFrame(_("Remove overlaps")),
795       _graphLayoutFrame(_("Connector network layout")),
796       _nodesFrame(_("Nodes")),
797       _alignTable(2, 6, true),
798       _distributeTable(3, 6, true),
799       _removeOverlapTable(1, 5, false),
800       _graphLayoutTable(1, 5, false),
801       _nodesTable(1, 4, true),
802       _anchorLabel(_("Relative to: ")),
803       _selgrpLabel(_("Treat selection as group: "))
805     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
807     //Instanciate the align buttons
808     addAlignButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_RIGHT_TO_ANCHOR,
809                    _("Align right edges of objects to the left edge of the anchor"),
810                    0, 0);
811     addAlignButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_LEFT,
812                    _("Align left edges"),
813                    0, 1);
814     addAlignButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_CENTER,
815                    _("Center on vertical axis"),
816                    0, 2);
817     addAlignButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_RIGHT,
818                    _("Align right sides"),
819                    0, 3);
820     addAlignButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_LEFT_TO_ANCHOR,
821                    _("Align left edges of objects to the right edge of the anchor"),
822                    0, 4);
823     addAlignButton(INKSCAPE_ICON_ALIGN_VERTICAL_BOTTOM_TO_ANCHOR,
824                    _("Align bottom edges of objects to the top edge of the anchor"),
825                    1, 0);
826     addAlignButton(INKSCAPE_ICON_ALIGN_VERTICAL_TOP,
827                    _("Align top edges"),
828                    1, 1);
829     addAlignButton(INKSCAPE_ICON_ALIGN_VERTICAL_CENTER,
830                    _("Center on horizontal axis"),
831                    1, 2);
832     addAlignButton(INKSCAPE_ICON_ALIGN_VERTICAL_BOTTOM,
833                    _("Align bottom edges"),
834                    1, 3);
835     addAlignButton(INKSCAPE_ICON_ALIGN_VERTICAL_TOP_TO_ANCHOR,
836                    _("Align top edges of objects to the bottom edge of the anchor"),
837                    1, 4);
839     //Baseline aligns
840     addBaselineButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_BASELINE,
841                    _("Align baseline anchors of texts horizontally"),
842                       0, 5, this->align_table(), Geom::X, false);
843     addBaselineButton(INKSCAPE_ICON_ALIGN_VERTICAL_BASELINE,
844                    _("Align baselines of texts"),
845                      1, 5, this->align_table(), Geom::Y, false);
847     //The distribute buttons
848     addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_HORIZONTAL_GAPS,
849                         _("Make horizontal gaps between objects equal"),
850                         0, 4, true, Geom::X, .5, .5);
852     addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_HORIZONTAL_LEFT,
853                         _("Distribute left edges equidistantly"),
854                         0, 1, false, Geom::X, 1., 0.);
855     addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_HORIZONTAL_CENTER,
856                         _("Distribute centers equidistantly horizontally"),
857                         0, 2, false, Geom::X, .5, .5);
858     addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_HORIZONTAL_RIGHT,
859                         _("Distribute right edges equidistantly"),
860                         0, 3, false, Geom::X, 0., 1.);
862     addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_VERTICAL_GAPS,
863                         _("Make vertical gaps between objects equal"),
864                         1, 4, true, Geom::Y, .5, .5);
866     addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_VERTICAL_TOP,
867                         _("Distribute top edges equidistantly"),
868                         1, 1, false, Geom::Y, 0, 1);
869     addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_VERTICAL_CENTER,
870                         _("Distribute centers equidistantly vertically"),
871                         1, 2, false, Geom::Y, .5, .5);
872     addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_VERTICAL_BOTTOM,
873                         _("Distribute bottom edges equidistantly"),
874                         1, 3, false, Geom::Y, 1., 0.);
876     //Baseline distribs
877     addBaselineButton(INKSCAPE_ICON_DISTRIBUTE_HORIZONTAL_BASELINE,
878                    _("Distribute baseline anchors of texts horizontally"),
879                       0, 5, this->distribute_table(), Geom::X, true);
880     addBaselineButton(INKSCAPE_ICON_DISTRIBUTE_VERTICAL_BASELINE,
881                    _("Distribute baselines of texts vertically"),
882                      1, 5, this->distribute_table(), Geom::Y, true);
884     //Randomize & Unclump
885     addRandomizeButton(INKSCAPE_ICON_DISTRIBUTE_RANDOMIZE,
886                         _("Randomize centers in both dimensions"),
887                         2, 2);
888     addUnclumpButton(INKSCAPE_ICON_DISTRIBUTE_UNCLUMP,
889                         _("Unclump objects: try to equalize edge-to-edge distances"),
890                         2, 4);
892     //Remove overlaps
893     addRemoveOverlapsButton(INKSCAPE_ICON_DISTRIBUTE_REMOVE_OVERLAPS,
894                             _("Move objects as little as possible so that their bounding boxes do not overlap"),
895                             0, 0);
896     //Graph Layout
897     addGraphLayoutButton(INKSCAPE_ICON_DISTRIBUTE_GRAPH,
898                             _("Nicely arrange selected connector network"),
899                             0, 0);
901     //Node Mode buttons
902     // NOTE: "align nodes vertically" means "move nodes vertically until they align on a common
903     // _horizontal_ line". This is analogous to what the "align-vertical-center" icon means.
904     // There is no doubt some ambiguity. For this reason the descriptions are different.
905     addNodeButton(INKSCAPE_ICON_ALIGN_VERTICAL_NODES,
906                   _("Align selected nodes to a common horizontal line"),
907                   0, Geom::X, false);
908     addNodeButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_NODES,
909                   _("Align selected nodes to a common vertical line"),
910                   1, Geom::Y, false);
911     addNodeButton(INKSCAPE_ICON_DISTRIBUTE_HORIZONTAL_NODE,
912                   _("Distribute selected nodes horizontally"),
913                   2, Geom::X, true);
914     addNodeButton(INKSCAPE_ICON_DISTRIBUTE_VERTICAL_NODE,
915                   _("Distribute selected nodes vertically"),
916                   3, Geom::Y, true);
918     //Rest of the widgetry
920     _combo.append_text(_("Last selected"));
921     _combo.append_text(_("First selected"));
922     _combo.append_text(_("Biggest object"));
923     _combo.append_text(_("Smallest object"));
924     _combo.append_text(_("Page"));
925     _combo.append_text(_("Drawing"));
926     _combo.append_text(_("Selection"));
928     _combo.set_active(prefs->getInt("/dialogs/align/align-to", 6));
929     _combo.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_ref_change));
931     _anchorBox.pack_start(_anchorLabel);
932     _anchorBox.pack_start(_combo);
934     _selgrpBox.pack_start(_selgrpLabel);
935     _selgrpBox.pack_start(_selgrp);
936     _selgrp.set_active(prefs->getBool("/dialogs/align/sel-as-groups"));
937     _selgrp.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_selgrp_toggled));
939     _alignBox.pack_start(_anchorBox);
940     _alignBox.pack_start(_selgrpBox);
941     _alignBox.pack_start(_alignTable);
943     _alignFrame.add(_alignBox);
944     _distributeFrame.add(_distributeTable);
945     _removeOverlapFrame.add(_removeOverlapTable);
946     _graphLayoutFrame.add(_graphLayoutTable);
947     _nodesFrame.add(_nodesTable);
949     Gtk::Box *contents = _getContents();
950     contents->set_spacing(4);
952     // Notebook for individual transformations
954     contents->pack_start(_alignFrame, true, true);
955     contents->pack_start(_distributeFrame, true, true);
956     contents->pack_start(_removeOverlapFrame, true, true);
957     contents->pack_start(_graphLayoutFrame, true, true);
958     contents->pack_start(_nodesFrame, true, true);
960     //Connect to the global tool change signal
961     g_signal_connect (G_OBJECT (INKSCAPE), "set_eventcontext", G_CALLBACK (on_tool_changed), this);
963     // Connect to the global selection change, to invalidate cached randomize_bbox
964     g_signal_connect (G_OBJECT (INKSCAPE), "change_selection", G_CALLBACK (on_selection_changed), this);
965     randomize_bbox = Geom::OptRect();
967     show_all_children();
969     on_tool_changed (NULL, NULL, this); // set current mode
972 AlignAndDistribute::~AlignAndDistribute()
974     sp_signal_disconnect_by_data (G_OBJECT (INKSCAPE), this);
976     for (std::list<Action *>::iterator it = _actionList.begin();
977          it != _actionList.end();
978          it ++)
979         delete *it;
982 void AlignAndDistribute::on_ref_change(){
983     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
984     prefs->setInt("/dialogs/align/align-to", _combo.get_active_row_number());
986     //Make blink the master
989 void AlignAndDistribute::on_selgrp_toggled(){
990     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
991     prefs->setInt("/dialogs/align/sel-as-groups", _selgrp.get_active());
993     //Make blink the master
999 void AlignAndDistribute::setMode(bool nodeEdit)
1001     //Act on widgets used in node mode
1002     void ( Gtk::Widget::*mNode) ()  = nodeEdit ?
1003         &Gtk::Widget::show_all : &Gtk::Widget::hide_all;
1005     //Act on widgets used in selection mode
1006   void ( Gtk::Widget::*mSel) ()  = nodeEdit ?
1007       &Gtk::Widget::hide_all : &Gtk::Widget::show_all;
1010     ((_alignFrame).*(mSel))();
1011     ((_distributeFrame).*(mSel))();
1012     ((_removeOverlapFrame).*(mSel))();
1013     ((_graphLayoutFrame).*(mSel))();
1014     ((_nodesFrame).*(mNode))();
1017 void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext,
1018                                  guint row, guint col)
1020     _actionList.push_back(
1021         new ActionAlign(
1022             id, tiptext, row, col,
1023             *this , col + row * 5));
1025 void AlignAndDistribute::addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1026                                       guint row, guint col, bool onInterSpace,
1027                                       Geom::Dim2 orientation, float kBegin, float kEnd)
1029     _actionList.push_back(
1030         new ActionDistribute(
1031             id, tiptext, row, col, *this ,
1032             onInterSpace, orientation,
1033             kBegin, kEnd
1034             )
1035         );
1038 void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1039                    guint col, Geom::Dim2 orientation, bool distribute)
1041     _actionList.push_back(
1042         new ActionNode(
1043             id, tiptext, col,
1044             *this, orientation, distribute));
1047 void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext,
1048                                       guint row, guint col)
1050     _actionList.push_back(
1051         new ActionRemoveOverlaps(
1052             id, tiptext, row, col, *this)
1053         );
1056 void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext,
1057                                       guint row, guint col)
1059     _actionList.push_back(
1060         new ActionGraphLayout(
1061             id, tiptext, row, col, *this)
1062         );
1065 void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext,
1066                                       guint row, guint col)
1068     _actionList.push_back(
1069         new ActionUnclump(
1070             id, tiptext, row, col, *this)
1071         );
1074 void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1075                                       guint row, guint col)
1077     _actionList.push_back(
1078         new ActionRandomize(
1079             id, tiptext, row, col, *this)
1080         );
1083 void AlignAndDistribute::addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext,
1084                                     guint row, guint col, Gtk::Table &table, Geom::Dim2 orientation, bool distribute)
1086     _actionList.push_back(
1087         new ActionBaseline(
1088             id, tiptext, row, col,
1089             *this, table, orientation, distribute));
1095 std::list<SPItem *>::iterator AlignAndDistribute::find_master( std::list<SPItem *> &list, bool horizontal){
1096     std::list<SPItem *>::iterator master = list.end();
1097     switch (getAlignTarget()) {
1098     case LAST:
1099         return list.begin();
1100         break;
1102     case FIRST:
1103         return --(list.end());
1104         break;
1106     case BIGGEST:
1107     {
1108         gdouble max = -1e18;
1109         for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1110             Geom::OptRect b = (*it)->getBboxDesktop ();
1111             if (b) {
1112                 gdouble dim = (*b)[horizontal ? Geom::X : Geom::Y].extent();
1113                 if (dim > max) {
1114                     max = dim;
1115                     master = it;
1116                 }
1117             }
1118         }
1119         return master;
1120         break;
1121     }
1123     case SMALLEST:
1124     {
1125         gdouble max = 1e18;
1126         for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1127             Geom::OptRect b = (*it)->getBboxDesktop ();
1128             if (b) {
1129                 gdouble dim = (*b)[horizontal ? Geom::X : Geom::Y].extent();
1130                 if (dim < max) {
1131                     max = dim;
1132                     master = it;
1133                 }
1134             }
1135         }
1136         return master;
1137         break;
1138     }
1140     default:
1141         g_assert_not_reached ();
1142         break;
1144     } // end of switch statement
1145     return master;
1148 AlignAndDistribute::AlignTarget AlignAndDistribute::getAlignTarget()const {
1149     return AlignTarget(_combo.get_active_row_number());
1154 } // namespace Dialog
1155 } // namespace UI
1156 } // namespace Inkscape
1158 /*
1159   Local Variables:
1160   mode:c++
1161   c-file-style:"stroustrup"
1162   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1163   indent-tabs-mode:nil
1164   fill-column:99
1165   End:
1166 */
1167 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :