Code

Add option to align & distribute dialog to treat the selection as a group (closes...
[inkscape.git] / src / ui / dialog / align-and-distribute.cpp
1 /**
2  * \brief Align and Distribute dialog
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 "verbs.h"
23 #include "dialogs/unclump.h"
24 #include "removeoverlap/removeoverlap.h"
25 #include "graphlayout/graphlayout.h"
27 #include <gtkmm/spinbutton.h>
32 #include "util/glib-list-iterators.h"
34 #include "widgets/icon.h"
36 #include "inkscape.h"
37 #include "document.h"
38 #include "selection.h"
39 #include "desktop-handles.h"
40 #include "macros.h"
41 #include "sp-item-transform.h"
42 #include "prefs-utils.h"
43 #include "enums.h"
45 #include "sp-text.h"
46 #include "sp-flowtext.h"
47 #include "text-editing.h"
49 #include "node-context.h"  //For access to ShapeEditor
50 #include "shape-editor.h" //For node align/distribute methods
52 #include "tools-switch.h"
54 #include "align-and-distribute.h"
56 namespace Inkscape {
57 namespace UI {
58 namespace Dialog {
60 /////////helper classes//////////////////////////////////
62 class Action {
63 public :
64     Action(const Glib::ustring &id,
65            const Glib::ustring &tiptext,
66            guint row, guint column,
67            Gtk::Table &parent,
68            Gtk::Tooltips &tooltips,
69            AlignAndDistribute &dialog):
70         _dialog(dialog),
71         _id(id),
72         _parent(parent)
73     {
74         Gtk::Widget*  pIcon = Gtk::manage( sp_icon_get_icon( _id, Inkscape::ICON_SIZE_LARGE_TOOLBAR) );
75         Gtk::Button * pButton = Gtk::manage(new Gtk::Button());
76         pButton->set_relief(Gtk::RELIEF_NONE);
77         pIcon->show();
78         pButton->add(*pIcon);
79         pButton->show();
81         pButton->signal_clicked()
82             .connect(sigc::mem_fun(*this, &Action::on_button_click));
83         tooltips.set_tip(*pButton, tiptext);
84         parent.attach(*pButton, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL);
85     }
86     virtual ~Action(){}
88     AlignAndDistribute &_dialog;
90 private :
91     virtual void on_button_click(){}
93     Glib::ustring _id;
94     Gtk::Table &_parent;
95 };
98 class ActionAlign : public Action {
99 public :
100     struct Coeffs {
101        double mx0, mx1, my0, my1;
102        double sx0, sx1, sy0, sy1;
103     };
104     ActionAlign(const Glib::ustring &id,
105                 const Glib::ustring &tiptext,
106                 guint row, guint column,
107                 AlignAndDistribute &dialog,
108                 guint coeffIndex):
109         Action(id, tiptext, row, column,
110                dialog.align_table(), dialog.tooltips(), dialog),
111         _index(coeffIndex),
112         _dialog(dialog)
113     {}
115 private :
117     virtual void on_button_click() {
118         //Retreive selected objects
119         SPDesktop *desktop = _dialog.getDesktop();
120         if (!desktop) return;
122         Inkscape::Selection *selection = sp_desktop_selection(desktop);
123         if (!selection) return;
125         bool sel_as_group = (prefs_get_int_attribute("dialogs.align", "sel-as-groups", 0) != 0);
127         using Inkscape::Util::GSListConstIterator;
128         std::list<SPItem *> selected;
129         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
130         if (selected.empty()) return;
132         Geom::Point mp; //Anchor point
133         AlignAndDistribute::AlignTarget target = _dialog.getAlignTarget();
134         const Coeffs &a= _allCoeffs[_index];
135         switch (target)
136         {
137         case AlignAndDistribute::LAST:
138         case AlignAndDistribute::FIRST:
139         case AlignAndDistribute::BIGGEST:
140         case AlignAndDistribute::SMALLEST:
141         {
142             //Check 2 or more selected objects
143             std::list<SPItem *>::iterator second(selected.begin());
144             ++second;
145             if (second == selected.end())
146                 return;
147             //Find the master (anchor on which the other objects are aligned)
148             std::list<SPItem *>::iterator master(
149                 _dialog.find_master (
150                     selected,
151                     (a.mx0 != 0.0) ||
152                     (a.mx1 != 0.0) )
153                 );
154             //remove the master from the selection
155             SPItem * thing = *master;
156             if (!sel_as_group) {
157                 selected.erase(master);
158             }
159             //Compute the anchor point
160             boost::optional<NR::Rect> b = sp_item_bbox_desktop (thing);
161             if (b) {
162                 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
163                                a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
164             } else {
165                 return;
166             }
167             break;
168         }
170         case AlignAndDistribute::PAGE:
171             mp = Geom::Point(a.mx1 * sp_document_width(sp_desktop_document(desktop)),
172                            a.my1 * sp_document_height(sp_desktop_document(desktop)));
173             break;
175         case AlignAndDistribute::DRAWING:
176         {
177             boost::optional<NR::Rect> b = sp_item_bbox_desktop
178                 ( (SPItem *) sp_document_root (sp_desktop_document (desktop)) );
179             if (b) {
180                 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
181                                a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
182             } else {
183                 return;
184             }
185             break;
186         }
188         case AlignAndDistribute::SELECTION:
189         {
190             boost::optional<NR::Rect> b =  selection->bounds();
191             if (b) {
192                 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
193                                a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
194             } else {
195                 return;
196             }
197             break;
198         }
200         default:
201             g_assert_not_reached ();
202             break;
203         };  // end of switch
205         // Top hack: temporarily set clone compensation to unmoved, so that we can align/distribute
206         // clones with their original (and the move of the original does not disturb the
207         // clones). The only problem with this is that if there are outside-of-selection clones of
208         // a selected original, they will be unmoved too, possibly contrary to user's
209         // expecation. However this is a minor point compared to making align/distribute always
210         // work as expected, and "unmoved" is the default option anyway.
211         int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
212         prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
214         bool changed = false;
215         boost::optional<NR::Rect> b;
216         if (sel_as_group)
217             b = selection->bounds();
219         //Move each item in the selected list separately
220         for (std::list<SPItem *>::iterator it(selected.begin());
221              it != selected.end();
222              it++)
223         {
224             sp_document_ensure_up_to_date(sp_desktop_document (desktop));
225             if (!sel_as_group)
226                 b = sp_item_bbox_desktop (*it);
227             if (b) {
228                 Geom::Point const sp(a.sx0 * b->min()[Geom::X] + a.sx1 * b->max()[Geom::X],
229                                      a.sy0 * b->min()[Geom::Y] + a.sy1 * b->max()[Geom::Y]);
230                 Geom::Point const mp_rel( mp - sp );
231                 if (LInfty(mp_rel) > 1e-9) {
232                     sp_item_move_rel(*it, NR::translate(mp_rel));
233                     changed = true;
234                 }
235             }
236         }
238         // restore compensation setting
239         prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
241         if (changed) {
242             sp_document_done ( sp_desktop_document (desktop) , SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
243                                _("Align"));
244         }
247     }
248     guint _index;
249     AlignAndDistribute &_dialog;
251     static const Coeffs _allCoeffs[10];
253 };
254 ActionAlign::Coeffs const ActionAlign::_allCoeffs[10] = {
255     {1., 0., 0., 0., 0., 1., 0., 0.},
256     {1., 0., 0., 0., 1., 0., 0., 0.},
257     {.5, .5, 0., 0., .5, .5, 0., 0.},
258     {0., 1., 0., 0., 0., 1., 0., 0.},
259     {0., 1., 0., 0., 1., 0., 0., 0.},
260     {0., 0., 0., 1., 0., 0., 1., 0.},
261     {0., 0., 0., 1., 0., 0., 0., 1.},
262     {0., 0., .5, .5, 0., 0., .5, .5},
263     {0., 0., 1., 0., 0., 0., 1., 0.},
264     {0., 0., 1., 0., 0., 0., 0., 1.}
265 };
267 BBoxSort::BBoxSort(SPItem *pItem, Geom::Rect bounds, Geom::Dim2 orientation, double kBegin, double kEnd) :
268         item(pItem),
269         bbox (bounds)
271         anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation];
273 BBoxSort::BBoxSort(const BBoxSort &rhs) :
274         //NOTE :  this copy ctor is called O(sort) when sorting the vector
275         //this is bad. The vector should be a vector of pointers.
276         //But I'll wait the bohem GC before doing that
277         item(rhs.item), anchor(rhs.anchor), bbox(rhs.bbox) 
281 bool operator< (const BBoxSort &a, const BBoxSort &b)
283     return (a.anchor < b.anchor);
286 class ActionDistribute : public Action {
287 public :
288     ActionDistribute(const Glib::ustring &id,
289                      const Glib::ustring &tiptext,
290                      guint row, guint column,
291                      AlignAndDistribute &dialog,
292                      bool onInterSpace,
293                      Geom::Dim2 orientation,
294                      double kBegin, double kEnd
295         ):
296         Action(id, tiptext, row, column,
297                dialog.distribute_table(), dialog.tooltips(), dialog),
298         _dialog(dialog),
299         _onInterSpace(onInterSpace),
300         _orientation(orientation),
301         _kBegin(kBegin),
302         _kEnd( kEnd)
303     {}
305 private :
306     virtual void on_button_click() {
307         //Retreive selected objects
308         SPDesktop *desktop = _dialog.getDesktop();
309         if (!desktop) return;
311         Inkscape::Selection *selection = sp_desktop_selection(desktop);
312         if (!selection) return;
314         using Inkscape::Util::GSListConstIterator;
315         std::list<SPItem *> selected;
316         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
317         if (selected.empty()) return;
319         //Check 2 or more selected objects
320         std::list<SPItem *>::iterator second(selected.begin());
321         ++second;
322         if (second == selected.end()) return;
325         std::vector< BBoxSort  > sorted;
326         for (std::list<SPItem *>::iterator it(selected.begin());
327             it != selected.end();
328             ++it)
329         {
330             boost::optional<NR::Rect> bbox = sp_item_bbox_desktop(*it);
331             if (bbox) {
332                 sorted.push_back(BBoxSort(*it, to_2geom(*bbox), _orientation, _kBegin, _kEnd));
333             }
334         }
335         //sort bbox by anchors
336         std::sort(sorted.begin(), sorted.end());
338         // see comment in ActionAlign above
339         int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
340         prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
342         unsigned int len = sorted.size();
343         bool changed = false;
344         if (_onInterSpace)
345         {
346             //overall bboxes span
347             float dist = (sorted.back().bbox.max()[_orientation] -
348                           sorted.front().bbox.min()[_orientation]);
349             //space eaten by bboxes
350             float span = 0;
351             for (unsigned int i = 0; i < len; i++)
352             {
353                 span += sorted[i].bbox[_orientation].extent();
354             }
355             //new distance between each bbox
356             float step = (dist - span) / (len - 1);
357             float pos = sorted.front().bbox.min()[_orientation];
358             for ( std::vector<BBoxSort> ::iterator it (sorted.begin());
359                   it < sorted.end();
360                   it ++ )
361             {
362                 if (!NR_DF_TEST_CLOSE (pos, it->bbox.min()[_orientation], 1e-6)) {
363                     Geom::Point t(0.0, 0.0);
364                     t[_orientation] = pos - it->bbox.min()[_orientation];
365                     sp_item_move_rel(it->item, NR::translate(t));
366                     changed = true;
367                 }
368                 pos += it->bbox[_orientation].extent();
369                 pos += step;
370             }
371         }
372         else
373         {
374             //overall anchor span
375             float dist = sorted.back().anchor - sorted.front().anchor;
376             //distance between anchors
377             float step = dist / (len - 1);
379             for ( unsigned int i = 0; i < len ; i ++ )
380             {
381                 BBoxSort & it(sorted[i]);
382                 //new anchor position
383                 float pos = sorted.front().anchor + i * step;
384                 //Don't move if we are really close
385                 if (!NR_DF_TEST_CLOSE (pos, it.anchor, 1e-6)) {
386                     //Compute translation
387                     Geom::Point t(0.0, 0.0);
388                     t[_orientation] = pos - it.anchor;
389                     //translate
390                     sp_item_move_rel(it.item, NR::translate(t));
391                     changed = true;
392                 }
393             }
394         }
396         // restore compensation setting
397         prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
399         if (changed) {
400             sp_document_done ( sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
401                                _("Distribute"));
402         }
403     }
404     guint _index;
405     AlignAndDistribute &_dialog;
406     bool _onInterSpace;
407     Geom::Dim2 _orientation;
409     double _kBegin;
410     double _kEnd;
412 };
415 class ActionNode : public Action {
416 public :
417     ActionNode(const Glib::ustring &id,
418                const Glib::ustring &tiptext,
419                guint column,
420                AlignAndDistribute &dialog,
421                Geom::Dim2 orientation, bool distribute):
422         Action(id, tiptext, 0, column,
423                dialog.nodes_table(), dialog.tooltips(), dialog),
424         _orientation(orientation),
425         _distribute(distribute)
426     {}
428 private :
429     Geom::Dim2 _orientation;
430     bool _distribute;
431     virtual void on_button_click()
432     {
434         if (!_dialog.getDesktop()) return;
435         SPEventContext *event_context = sp_desktop_event_context(_dialog.getDesktop());
436         if (!SP_IS_NODE_CONTEXT (event_context)) return ;
438         if (_distribute)
439             SP_NODE_CONTEXT (event_context)->shape_editor->distribute((NR::Dim2)_orientation);
440         else
441             SP_NODE_CONTEXT (event_context)->shape_editor->align((NR::Dim2)_orientation);
443     }
444 };
446 class ActionRemoveOverlaps : public Action {
447 private:
448     Gtk::Label removeOverlapXGapLabel;
449     Gtk::Label removeOverlapYGapLabel;
450     Gtk::SpinButton removeOverlapXGap;
451     Gtk::SpinButton removeOverlapYGap;
453 public:
454     ActionRemoveOverlaps(Glib::ustring const &id,
455                          Glib::ustring const &tiptext,
456                          guint row,
457                          guint column,
458                          AlignAndDistribute &dialog) :
459         Action(id, tiptext, row, column + 4,
460                dialog.removeOverlap_table(), dialog.tooltips(), dialog)
461     {
462         dialog.removeOverlap_table().set_col_spacings(3);
464         removeOverlapXGap.set_digits(1);
465         removeOverlapXGap.set_size_request(60, -1);
466         removeOverlapXGap.set_increments(1.0, 5.0);
467         removeOverlapXGap.set_range(-1000.0, 1000.0);
468         removeOverlapXGap.set_value(0);
469         dialog.tooltips().set_tip(removeOverlapXGap,
470                                   _("Minimum horizontal gap (in px units) between bounding boxes"));
471         /* TRANSLATORS: Horizontal gap. Only put "H:" equivalent in the translation */
472         removeOverlapXGapLabel.set_label(Q_("gap|H:"));
474         removeOverlapYGap.set_digits(1);
475         removeOverlapYGap.set_size_request(60, -1);
476         removeOverlapYGap.set_increments(1.0, 5.0);
477         removeOverlapYGap.set_range(-1000.0, 1000.0);
478         removeOverlapYGap.set_value(0);
479         dialog.tooltips().set_tip(removeOverlapYGap,
480                                   _("Minimum vertical gap (in px units) between bounding boxes"));
481         /* TRANSLATORS: Vertical gap */
482         removeOverlapYGapLabel.set_label(_("V:"));
484         dialog.removeOverlap_table().attach(removeOverlapXGapLabel, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL);
485         dialog.removeOverlap_table().attach(removeOverlapXGap, column+1, column+2, row, row+1, Gtk::FILL, Gtk::FILL);
486         dialog.removeOverlap_table().attach(removeOverlapYGapLabel, column+2, column+3, row, row+1, Gtk::FILL, Gtk::FILL);
487         dialog.removeOverlap_table().attach(removeOverlapYGap, column+3, column+4, row, row+1, Gtk::FILL, Gtk::FILL);
489     }
491 private :
492     virtual void on_button_click()
493     {
494         if (!_dialog.getDesktop()) return;
496         // see comment in ActionAlign above
497         int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
498         prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
500         // xGap and yGap are the minimum space required between bounding rectangles.
501         double const xGap = removeOverlapXGap.get_value();
502         double const yGap = removeOverlapYGap.get_value();
503         removeoverlap(sp_desktop_selection(_dialog.getDesktop())->itemList(),
504                       xGap, yGap);
506         // restore compensation setting
507         prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
509         sp_document_done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
510                          _("Remove overlaps"));
511     }
512 };
514 class ActionGraphLayout : public Action {
515 public:
516     ActionGraphLayout(Glib::ustring const &id,
517                          Glib::ustring const &tiptext,
518                          guint row,
519                          guint column,
520                          AlignAndDistribute &dialog) :
521         Action(id, tiptext, row, column + 4,
522                dialog.graphLayout_table(), dialog.tooltips(), dialog)
523     {}
525 private :
526     virtual void on_button_click()
527     {
528         if (!_dialog.getDesktop()) return;
530         // see comment in ActionAlign above
531         int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
532         prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
534         graphlayout(sp_desktop_selection(_dialog.getDesktop())->itemList());
536         // restore compensation setting
537         prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
539         sp_document_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         int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
562         prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
564         unclump ((GSList *) sp_desktop_selection(_dialog.getDesktop())->itemList());
566         // restore compensation setting
567         prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
569         sp_document_done (sp_desktop_document (_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
570                           _("Unclump"));
571     }
572 };
574 class ActionRandomize : public Action {
575 public :
576     ActionRandomize(const Glib::ustring &id,
577                const Glib::ustring &tiptext,
578                guint row,
579                guint column,
580                AlignAndDistribute &dialog):
581         Action(id, tiptext, row, column,
582                dialog.distribute_table(), dialog.tooltips(), dialog)
583     {}
585 private :
586     virtual void on_button_click()
587     {
588         SPDesktop *desktop = _dialog.getDesktop();
589         if (!desktop) return;
591         Inkscape::Selection *selection = sp_desktop_selection(desktop);
592         if (!selection) return;
594         using Inkscape::Util::GSListConstIterator;
595         std::list<SPItem *> selected;
596         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
597         if (selected.empty()) return;
599         //Check 2 or more selected objects
600         if (selected.size() < 2) return;
602         boost::optional<NR::Rect> sel_bbox = selection->bounds();
603         if (!sel_bbox) {
604             return;
605         }
607         // This bbox is cached between calls to randomize, so that there's no growth nor shrink
608         // nor drift on sequential randomizations. Discard cache on global (or better active
609         // desktop's) selection_change signal.
610         if (!_dialog.randomize_bbox) {
611             _dialog.randomize_bbox = to_2geom(*sel_bbox);
612         }
614         // see comment in ActionAlign above
615         int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
616         prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
618         for (std::list<SPItem *>::iterator it(selected.begin());
619             it != selected.end();
620             ++it)
621         {
622             sp_document_ensure_up_to_date(sp_desktop_document (desktop));
623             boost::optional<NR::Rect> item_box = sp_item_bbox_desktop (*it);
624             if (item_box) {
625                 // find new center, staying within bbox
626                 double x = _dialog.randomize_bbox->min()[Geom::X] + (*item_box).extent(Geom::X)/2 +
627                     g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::X].extent() - (*item_box).extent(Geom::X));
628                 double y = _dialog.randomize_bbox->min()[Geom::Y] + (*item_box).extent(Geom::Y)/2 +
629                     g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::Y].extent() - (*item_box).extent(Geom::Y));
630                 // displacement is the new center minus old:
631                 NR::Point t = NR::Point (x, y) - 0.5*(item_box->max() + item_box->min());
632                 sp_item_move_rel(*it, NR::translate(t));
633             }
634         }
636         // restore compensation setting
637         prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
639         sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
640                           _("Randomize positions"));
641     }
642 };
644 struct Baselines
646     SPItem *_item;
647     Geom::Point _base;
648     Geom::Dim2 _orientation;
649     Baselines(SPItem *item, Geom::Point base, Geom::Dim2 orientation) :
650         _item (item),
651         _base (base),
652         _orientation (orientation)
653     {}
654 };
656 bool operator< (const Baselines &a, const Baselines &b)
658     return (a._base[a._orientation] < b._base[b._orientation]);
661 class ActionBaseline : public Action {
662 public :
663     ActionBaseline(const Glib::ustring &id,
664                const Glib::ustring &tiptext,
665                guint row,
666                guint column,
667                AlignAndDistribute &dialog,
668                Gtk::Table &table,
669                Geom::Dim2 orientation, bool distribute):
670         Action(id, tiptext, row, column,
671                table, dialog.tooltips(), dialog),
672         _orientation(orientation),
673         _distribute(distribute)
674     {}
676 private :
677     Geom::Dim2 _orientation;
678     bool _distribute;
679     virtual void on_button_click()
680     {
681         SPDesktop *desktop = _dialog.getDesktop();
682         if (!desktop) return;
684         Inkscape::Selection *selection = sp_desktop_selection(desktop);
685         if (!selection) return;
687         using Inkscape::Util::GSListConstIterator;
688         std::list<SPItem *> selected;
689         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
690         if (selected.empty()) return;
692         //Check 2 or more selected objects
693         if (selected.size() < 2) return;
695         Geom::Point b_min = Geom::Point (HUGE_VAL, HUGE_VAL);
696         Geom::Point b_max = Geom::Point (-HUGE_VAL, -HUGE_VAL);
698         std::vector<Baselines> sorted;
700         for (std::list<SPItem *>::iterator it(selected.begin());
701             it != selected.end();
702             ++it)
703         {
704             if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
705                 Inkscape::Text::Layout const *layout = te_get_layout(*it);
706                 Geom::Point base = layout->characterAnchorPoint(layout->begin()) * sp_item_i2d_affine(*it);
707                 if (base[Geom::X] < b_min[Geom::X]) b_min[Geom::X] = base[Geom::X];
708                 if (base[Geom::Y] < b_min[Geom::Y]) b_min[Geom::Y] = base[Geom::Y];
709                 if (base[Geom::X] > b_max[Geom::X]) b_max[Geom::X] = base[Geom::X];
710                 if (base[Geom::Y] > b_max[Geom::Y]) b_max[Geom::Y] = base[Geom::Y];
712                 Baselines b (*it, base, _orientation);
713                 sorted.push_back(b);
714             }
715         }
717         if (sorted.size() <= 1) return;
719         //sort baselines
720         std::sort(sorted.begin(), sorted.end());
722         bool changed = false;
724         if (_distribute) {
725             double step = (b_max[_orientation] - b_min[_orientation])/(sorted.size() - 1);
726             for (unsigned int i = 0; i < sorted.size(); i++) {
727                 SPItem *item = sorted[i]._item;
728                 Geom::Point base = sorted[i]._base;
729                 Geom::Point t(0.0, 0.0);
730                 t[_orientation] = b_min[_orientation] + step * i - base[_orientation];
731                 sp_item_move_rel(item, NR::translate(t));
732                 changed = true;
733             }
735             if (changed) {
736                 sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
737                                   _("Distribute text baselines"));
738             }
740         } else {
741             for (std::list<SPItem *>::iterator it(selected.begin());
742                  it != selected.end();
743                  ++it)
744             {
745                 if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
746                     Inkscape::Text::Layout const *layout = te_get_layout(*it);
747                     Geom::Point base = layout->characterAnchorPoint(layout->begin()) * sp_item_i2d_affine(*it);
748                     Geom::Point t(0.0, 0.0);
749                     t[_orientation] = b_min[_orientation] - base[_orientation];
750                     sp_item_move_rel(*it, NR::translate(t));
751                     changed = true;
752                 }
753             }
755             if (changed) {
756                 sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
757                                   _("Align text baselines"));
758             }
759         }
760     }
761 };
765 void on_tool_changed(Inkscape::Application */*inkscape*/, SPEventContext */*context*/, AlignAndDistribute *daad)
767     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
768     if (desktop && sp_desktop_event_context(desktop))
769         daad->setMode(tools_active(desktop) == TOOLS_NODES);
772 void on_selection_changed(Inkscape::Application */*inkscape*/, Inkscape::Selection */*selection*/, AlignAndDistribute *daad)
774     daad->randomize_bbox = boost::optional<Geom::Rect>();
777 /////////////////////////////////////////////////////////
782 AlignAndDistribute::AlignAndDistribute()
783     : UI::Widget::Panel ("", "dialogs.align", SP_VERB_DIALOG_ALIGN_DISTRIBUTE),
784       randomize_bbox(),
785       _alignFrame(_("Align")),
786       _distributeFrame(_("Distribute")),
787       _removeOverlapFrame(_("Remove overlaps")),
788       _graphLayoutFrame(_("Connector network layout")),
789       _nodesFrame(_("Nodes")),
790       _alignTable(2, 6, true),
791       _distributeTable(3, 6, true),
792       _removeOverlapTable(1, 5, false),
793       _graphLayoutTable(1, 5, false),
794       _nodesTable(1, 4, true),
795       _anchorLabel(_("Relative to: ")),
796       _selgrpLabel(_("Treat selection as group: "))
799     //Instanciate the align buttons
800     addAlignButton("al_left_out",
801                    _("Align right sides of objects to left side of anchor"),
802                    0, 0);
803     addAlignButton("al_left_in",
804                    _("Align left sides"),
805                    0, 1);
806     addAlignButton("al_center_hor",
807                    _("Center on vertical axis"),
808                    0, 2);
809     addAlignButton("al_right_in",
810                    _("Align right sides"),
811                    0, 3);
812     addAlignButton("al_right_out",
813                    _("Align left sides of objects to right side of anchor"),
814                    0, 4);
815     addAlignButton("al_top_out",
816                    _("Align bottoms of objects to top of anchor"),
817                    1, 0);
818     addAlignButton("al_top_in",
819                    _("Align tops"),
820                    1, 1);
821     addAlignButton("al_center_ver",
822                    _("Center on horizontal axis"),
823                    1, 2);
824     addAlignButton("al_bottom_in",
825                    _("Align bottoms"),
826                    1, 3);
827     addAlignButton("al_bottom_out",
828                    _("Align tops of objects to bottom of anchor"),
829                    1, 4);
831     //Baseline aligns
832     addBaselineButton("al_baselines_vert",
833                    _("Align baseline anchors of texts vertically"),
834                       0, 5, this->align_table(), Geom::X, false);
835     addBaselineButton("al_baselines_hor",
836                    _("Align baseline anchors of texts horizontally"),
837                      1, 5, this->align_table(), Geom::Y, false);
839     //The distribute buttons
840     addDistributeButton("distribute_hdist",
841                         _("Make horizontal gaps between objects equal"),
842                         0, 4, true, Geom::X, .5, .5);
844     addDistributeButton("distribute_left",
845                         _("Distribute left sides equidistantly"),
846                         0, 1, false, Geom::X, 1., 0.);
847     addDistributeButton("distribute_hcentre",
848                         _("Distribute centers equidistantly horizontally"),
849                         0, 2, false, Geom::X, .5, .5);
850     addDistributeButton("distribute_right",
851                         _("Distribute right sides equidistantly"),
852                         0, 3, false, Geom::X, 0., 1.);
854     addDistributeButton("distribute_vdist",
855                         _("Make vertical gaps between objects equal"),
856                         1, 4, true, Geom::Y, .5, .5);
858     addDistributeButton("distribute_top",
859                         _("Distribute tops equidistantly"),
860                         1, 1, false, Geom::Y, 0, 1);
861     addDistributeButton("distribute_vcentre",
862                         _("Distribute centers equidistantly vertically"),
863                         1, 2, false, Geom::Y, .5, .5);
864     addDistributeButton("distribute_bottom",
865                         _("Distribute bottoms equidistantly"),
866                         1, 3, false, Geom::Y, 1., 0.);
868     //Baseline distribs
869     addBaselineButton("distribute_baselines_hor",
870                    _("Distribute baseline anchors of texts horizontally"),
871                       0, 5, this->distribute_table(), Geom::X, true);
872     addBaselineButton("distribute_baselines_vert",
873                    _("Distribute baseline anchors of texts vertically"),
874                      1, 5, this->distribute_table(), Geom::Y, true);
876     //Randomize & Unclump
877     addRandomizeButton("distribute_randomize",
878                         _("Randomize centers in both dimensions"),
879                         2, 2);
880     addUnclumpButton("unclump",
881                         _("Unclump objects: try to equalize edge-to-edge distances"),
882                         2, 4);
884     //Remove overlaps
885     addRemoveOverlapsButton("remove_overlaps",
886                             _("Move objects as little as possible so that their bounding boxes do not overlap"),
887                             0, 0);
888     //Graph Layout
889     addGraphLayoutButton("graph_layout",
890                             _("Nicely arrange selected connector network"),
891                             0, 0);
893     //Node Mode buttons
894     addNodeButton("node_halign",
895                   _("Align selected nodes horizontally"),
896                   0, Geom::X, false);
897     addNodeButton("node_valign",
898                   _("Align selected nodes vertically"),
899                   1, Geom::Y, false);
900     addNodeButton("node_hdistribute",
901                   _("Distribute selected nodes horizontally"),
902                   2, Geom::X, true);
903     addNodeButton("node_vdistribute",
904                   _("Distribute selected nodes vertically"),
905                   3, Geom::Y, true);
907     //Rest of the widgetry
909     _combo.append_text(_("Last selected"));
910     _combo.append_text(_("First selected"));
911     _combo.append_text(_("Biggest item"));
912     _combo.append_text(_("Smallest item"));
913     _combo.append_text(_("Page"));
914     _combo.append_text(_("Drawing"));
915     _combo.append_text(_("Selection"));
917     _combo.set_active(prefs_get_int_attribute("dialogs.align", "align-to", 6));
918     _combo.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_ref_change));
920     _anchorBox.pack_start(_anchorLabel);
921     _anchorBox.pack_start(_combo);
923     _selgrpBox.pack_start(_selgrpLabel);
924     _selgrpBox.pack_start(_selgrp);
925     _selgrp.set_active(prefs_get_int_attribute("dialogs.align", "sel-as-groups", 0));
926     _selgrp.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_selgrp_toggled));
928     _alignBox.pack_start(_anchorBox);
929     _alignBox.pack_start(_selgrpBox);
930     _alignBox.pack_start(_alignTable);
932     _alignFrame.add(_alignBox);
933     _distributeFrame.add(_distributeTable);
934     _removeOverlapFrame.add(_removeOverlapTable);
935     _graphLayoutFrame.add(_graphLayoutTable);
936     _nodesFrame.add(_nodesTable);
938     Gtk::Box *contents = _getContents();
939     contents->set_spacing(4);
941     // Notebook for individual transformations
943     contents->pack_start(_alignFrame, true, true);
944     contents->pack_start(_distributeFrame, true, true);
945     contents->pack_start(_removeOverlapFrame, true, true);
946     contents->pack_start(_graphLayoutFrame, true, true);
947     contents->pack_start(_nodesFrame, true, true);
949     //Connect to the global tool change signal
950     g_signal_connect (G_OBJECT (INKSCAPE), "set_eventcontext", G_CALLBACK (on_tool_changed), this);
952     // Connect to the global selection change, to invalidate cached randomize_bbox
953     g_signal_connect (G_OBJECT (INKSCAPE), "change_selection", G_CALLBACK (on_selection_changed), this);
954     randomize_bbox = boost::optional<Geom::Rect>();
956     show_all_children();
958     on_tool_changed (NULL, NULL, this); // set current mode
961 AlignAndDistribute::~AlignAndDistribute()
963     sp_signal_disconnect_by_data (G_OBJECT (INKSCAPE), this);
965     for (std::list<Action *>::iterator it = _actionList.begin();
966          it != _actionList.end();
967          it ++)
968         delete *it;
971 void AlignAndDistribute::on_ref_change(){
973     prefs_set_int_attribute("dialogs.align", "align-to", _combo.get_active_row_number());
975     //Make blink the master
978 void AlignAndDistribute::on_selgrp_toggled(){
980     prefs_set_int_attribute("dialogs.align", "sel-as-groups", _selgrp.get_active());
982     //Make blink the master
988 void AlignAndDistribute::setMode(bool nodeEdit)
990     //Act on widgets used in node mode
991     void ( Gtk::Widget::*mNode) ()  = nodeEdit ?
992         &Gtk::Widget::show_all : &Gtk::Widget::hide_all;
994     //Act on widgets used in selection mode
995   void ( Gtk::Widget::*mSel) ()  = nodeEdit ?
996       &Gtk::Widget::hide_all : &Gtk::Widget::show_all;
999     ((_alignFrame).*(mSel))();
1000     ((_distributeFrame).*(mSel))();
1001     ((_removeOverlapFrame).*(mSel))();
1002     ((_graphLayoutFrame).*(mSel))();
1003     ((_nodesFrame).*(mNode))();
1006 void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext,
1007                                  guint row, guint col)
1009     _actionList.push_back(
1010         new ActionAlign(
1011             id, tiptext, row, col,
1012             *this , col + row * 5));
1014 void AlignAndDistribute::addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1015                                       guint row, guint col, bool onInterSpace,
1016                                       Geom::Dim2 orientation, float kBegin, float kEnd)
1018     _actionList.push_back(
1019         new ActionDistribute(
1020             id, tiptext, row, col, *this ,
1021             onInterSpace, orientation,
1022             kBegin, kEnd
1023             )
1024         );
1027 void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1028                    guint col, Geom::Dim2 orientation, bool distribute)
1030     _actionList.push_back(
1031         new ActionNode(
1032             id, tiptext, col,
1033             *this, orientation, distribute));
1036 void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext,
1037                                       guint row, guint col)
1039     _actionList.push_back(
1040         new ActionRemoveOverlaps(
1041             id, tiptext, row, col, *this)
1042         );
1045 void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext,
1046                                       guint row, guint col)
1048     _actionList.push_back(
1049         new ActionGraphLayout(
1050             id, tiptext, row, col, *this)
1051         );
1054 void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext,
1055                                       guint row, guint col)
1057     _actionList.push_back(
1058         new ActionUnclump(
1059             id, tiptext, row, col, *this)
1060         );
1063 void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1064                                       guint row, guint col)
1066     _actionList.push_back(
1067         new ActionRandomize(
1068             id, tiptext, row, col, *this)
1069         );
1072 void AlignAndDistribute::addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext,
1073                                     guint row, guint col, Gtk::Table &table, Geom::Dim2 orientation, bool distribute)
1075     _actionList.push_back(
1076         new ActionBaseline(
1077             id, tiptext, row, col,
1078             *this, table, orientation, distribute));
1084 std::list<SPItem *>::iterator AlignAndDistribute::find_master( std::list<SPItem *> &list, bool horizontal){
1085     std::list<SPItem *>::iterator master = list.end();
1086     switch (getAlignTarget()) {
1087     case LAST:
1088         return list.begin();
1089         break;
1091     case FIRST:
1092         return --(list.end());
1093         break;
1095     case BIGGEST:
1096     {
1097         gdouble max = -1e18;
1098         for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1099             boost::optional<NR::Rect> b = sp_item_bbox_desktop (*it);
1100             if (b) {
1101                 gdouble dim = (*b).extent(horizontal ? Geom::X : Geom::Y);
1102                 if (dim > max) {
1103                     max = dim;
1104                     master = it;
1105                 }
1106             }
1107         }
1108         return master;
1109         break;
1110     }
1112     case SMALLEST:
1113     {
1114         gdouble max = 1e18;
1115         for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1116             boost::optional<NR::Rect> b = sp_item_bbox_desktop (*it);
1117             if (b) {
1118                 gdouble dim = (*b).extent(horizontal ? Geom::X : Geom::Y);
1119                 if (dim < max) {
1120                     max = dim;
1121                     master = it;
1122                 }
1123             }
1124         }
1125         return master;
1126         break;
1127     }
1129     default:
1130         g_assert_not_reached ();
1131         break;
1133     } // end of switch statement
1134     return master;
1137 AlignAndDistribute::AlignTarget AlignAndDistribute::getAlignTarget()const {
1138     return AlignTarget(_combo.get_active_row_number());
1143 } // namespace Dialog
1144 } // namespace UI
1145 } // namespace Inkscape
1147 /*
1148   Local Variables:
1149   mode:c++
1150   c-file-style:"stroustrup"
1151   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1152   indent-tabs-mode:nil
1153   fill-column:99
1154   End:
1155 */
1156 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :