Code

Some NR::Point ==> Geom::Point replacements
[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             // TODO: either uncomment or remove the following commented lines, depending on which
157             //       behaviour of moving objects makes most sense; also cf. discussion at
158             //       https://bugs.launchpad.net/inkscape/+bug/255933
159             /*if (!sel_as_group) { */
160                 selected.erase(master);
161             /*}*/
162             //Compute the anchor point
163             boost::optional<NR::Rect> b = sp_item_bbox_desktop (thing);
164             if (b) {
165                 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
166                                a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
167             } else {
168                 return;
169             }
170             break;
171         }
173         case AlignAndDistribute::PAGE:
174             mp = Geom::Point(a.mx1 * sp_document_width(sp_desktop_document(desktop)),
175                            a.my1 * sp_document_height(sp_desktop_document(desktop)));
176             break;
178         case AlignAndDistribute::DRAWING:
179         {
180             boost::optional<NR::Rect> b = sp_item_bbox_desktop
181                 ( (SPItem *) sp_document_root (sp_desktop_document (desktop)) );
182             if (b) {
183                 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
184                                a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
185             } else {
186                 return;
187             }
188             break;
189         }
191         case AlignAndDistribute::SELECTION:
192         {
193             boost::optional<NR::Rect> b =  selection->bounds();
194             if (b) {
195                 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
196                                a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
197             } else {
198                 return;
199             }
200             break;
201         }
203         default:
204             g_assert_not_reached ();
205             break;
206         };  // end of switch
208         // Top hack: temporarily set clone compensation to unmoved, so that we can align/distribute
209         // clones with their original (and the move of the original does not disturb the
210         // clones). The only problem with this is that if there are outside-of-selection clones of
211         // a selected original, they will be unmoved too, possibly contrary to user's
212         // expecation. However this is a minor point compared to making align/distribute always
213         // work as expected, and "unmoved" is the default option anyway.
214         int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
215         prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
217         bool changed = false;
218         boost::optional<NR::Rect> b;
219         if (sel_as_group)
220             b = selection->bounds();
222         //Move each item in the selected list separately
223         for (std::list<SPItem *>::iterator it(selected.begin());
224              it != selected.end();
225              it++)
226         {
227             sp_document_ensure_up_to_date(sp_desktop_document (desktop));
228             if (!sel_as_group)
229                 b = sp_item_bbox_desktop (*it);
230             if (b) {
231                 Geom::Point const sp(a.sx0 * b->min()[Geom::X] + a.sx1 * b->max()[Geom::X],
232                                      a.sy0 * b->min()[Geom::Y] + a.sy1 * b->max()[Geom::Y]);
233                 Geom::Point const mp_rel( mp - sp );
234                 if (LInfty(mp_rel) > 1e-9) {
235                     sp_item_move_rel(*it, NR::translate(mp_rel));
236                     changed = true;
237                 }
238             }
239         }
241         // restore compensation setting
242         prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
244         if (changed) {
245             sp_document_done ( sp_desktop_document (desktop) , SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
246                                _("Align"));
247         }
250     }
251     guint _index;
252     AlignAndDistribute &_dialog;
254     static const Coeffs _allCoeffs[10];
256 };
257 ActionAlign::Coeffs const ActionAlign::_allCoeffs[10] = {
258     {1., 0., 0., 0., 0., 1., 0., 0.},
259     {1., 0., 0., 0., 1., 0., 0., 0.},
260     {.5, .5, 0., 0., .5, .5, 0., 0.},
261     {0., 1., 0., 0., 0., 1., 0., 0.},
262     {0., 1., 0., 0., 1., 0., 0., 0.},
263     {0., 0., 0., 1., 0., 0., 1., 0.},
264     {0., 0., 0., 1., 0., 0., 0., 1.},
265     {0., 0., .5, .5, 0., 0., .5, .5},
266     {0., 0., 1., 0., 0., 0., 1., 0.},
267     {0., 0., 1., 0., 0., 0., 0., 1.}
268 };
270 BBoxSort::BBoxSort(SPItem *pItem, Geom::Rect bounds, Geom::Dim2 orientation, double kBegin, double kEnd) :
271         item(pItem),
272         bbox (bounds)
274         anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation];
276 BBoxSort::BBoxSort(const BBoxSort &rhs) :
277         //NOTE :  this copy ctor is called O(sort) when sorting the vector
278         //this is bad. The vector should be a vector of pointers.
279         //But I'll wait the bohem GC before doing that
280         item(rhs.item), anchor(rhs.anchor), bbox(rhs.bbox) 
284 bool operator< (const BBoxSort &a, const BBoxSort &b)
286     return (a.anchor < b.anchor);
289 class ActionDistribute : public Action {
290 public :
291     ActionDistribute(const Glib::ustring &id,
292                      const Glib::ustring &tiptext,
293                      guint row, guint column,
294                      AlignAndDistribute &dialog,
295                      bool onInterSpace,
296                      Geom::Dim2 orientation,
297                      double kBegin, double kEnd
298         ):
299         Action(id, tiptext, row, column,
300                dialog.distribute_table(), dialog.tooltips(), dialog),
301         _dialog(dialog),
302         _onInterSpace(onInterSpace),
303         _orientation(orientation),
304         _kBegin(kBegin),
305         _kEnd( kEnd)
306     {}
308 private :
309     virtual void on_button_click() {
310         //Retreive selected objects
311         SPDesktop *desktop = _dialog.getDesktop();
312         if (!desktop) return;
314         Inkscape::Selection *selection = sp_desktop_selection(desktop);
315         if (!selection) return;
317         using Inkscape::Util::GSListConstIterator;
318         std::list<SPItem *> selected;
319         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
320         if (selected.empty()) return;
322         //Check 2 or more selected objects
323         std::list<SPItem *>::iterator second(selected.begin());
324         ++second;
325         if (second == selected.end()) return;
328         std::vector< BBoxSort  > sorted;
329         for (std::list<SPItem *>::iterator it(selected.begin());
330             it != selected.end();
331             ++it)
332         {
333             boost::optional<NR::Rect> bbox = sp_item_bbox_desktop(*it);
334             if (bbox) {
335                 sorted.push_back(BBoxSort(*it, to_2geom(*bbox), _orientation, _kBegin, _kEnd));
336             }
337         }
338         //sort bbox by anchors
339         std::sort(sorted.begin(), sorted.end());
341         // see comment in ActionAlign above
342         int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
343         prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
345         unsigned int len = sorted.size();
346         bool changed = false;
347         if (_onInterSpace)
348         {
349             //overall bboxes span
350             float dist = (sorted.back().bbox.max()[_orientation] -
351                           sorted.front().bbox.min()[_orientation]);
352             //space eaten by bboxes
353             float span = 0;
354             for (unsigned int i = 0; i < len; i++)
355             {
356                 span += sorted[i].bbox[_orientation].extent();
357             }
358             //new distance between each bbox
359             float step = (dist - span) / (len - 1);
360             float pos = sorted.front().bbox.min()[_orientation];
361             for ( std::vector<BBoxSort> ::iterator it (sorted.begin());
362                   it < sorted.end();
363                   it ++ )
364             {
365                 if (!NR_DF_TEST_CLOSE (pos, it->bbox.min()[_orientation], 1e-6)) {
366                     Geom::Point t(0.0, 0.0);
367                     t[_orientation] = pos - it->bbox.min()[_orientation];
368                     sp_item_move_rel(it->item, NR::translate(t));
369                     changed = true;
370                 }
371                 pos += it->bbox[_orientation].extent();
372                 pos += step;
373             }
374         }
375         else
376         {
377             //overall anchor span
378             float dist = sorted.back().anchor - sorted.front().anchor;
379             //distance between anchors
380             float step = dist / (len - 1);
382             for ( unsigned int i = 0; i < len ; i ++ )
383             {
384                 BBoxSort & it(sorted[i]);
385                 //new anchor position
386                 float pos = sorted.front().anchor + i * step;
387                 //Don't move if we are really close
388                 if (!NR_DF_TEST_CLOSE (pos, it.anchor, 1e-6)) {
389                     //Compute translation
390                     Geom::Point t(0.0, 0.0);
391                     t[_orientation] = pos - it.anchor;
392                     //translate
393                     sp_item_move_rel(it.item, NR::translate(t));
394                     changed = true;
395                 }
396             }
397         }
399         // restore compensation setting
400         prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
402         if (changed) {
403             sp_document_done ( sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
404                                _("Distribute"));
405         }
406     }
407     guint _index;
408     AlignAndDistribute &_dialog;
409     bool _onInterSpace;
410     Geom::Dim2 _orientation;
412     double _kBegin;
413     double _kEnd;
415 };
418 class ActionNode : public Action {
419 public :
420     ActionNode(const Glib::ustring &id,
421                const Glib::ustring &tiptext,
422                guint column,
423                AlignAndDistribute &dialog,
424                Geom::Dim2 orientation, bool distribute):
425         Action(id, tiptext, 0, column,
426                dialog.nodes_table(), dialog.tooltips(), dialog),
427         _orientation(orientation),
428         _distribute(distribute)
429     {}
431 private :
432     Geom::Dim2 _orientation;
433     bool _distribute;
434     virtual void on_button_click()
435     {
437         if (!_dialog.getDesktop()) return;
438         SPEventContext *event_context = sp_desktop_event_context(_dialog.getDesktop());
439         if (!SP_IS_NODE_CONTEXT (event_context)) return ;
441         if (_distribute)
442             SP_NODE_CONTEXT (event_context)->shape_editor->distribute((Geom::Dim2)_orientation);
443         else
444             SP_NODE_CONTEXT (event_context)->shape_editor->align((Geom::Dim2)_orientation);
446     }
447 };
449 class ActionRemoveOverlaps : public Action {
450 private:
451     Gtk::Label removeOverlapXGapLabel;
452     Gtk::Label removeOverlapYGapLabel;
453     Gtk::SpinButton removeOverlapXGap;
454     Gtk::SpinButton removeOverlapYGap;
456 public:
457     ActionRemoveOverlaps(Glib::ustring const &id,
458                          Glib::ustring const &tiptext,
459                          guint row,
460                          guint column,
461                          AlignAndDistribute &dialog) :
462         Action(id, tiptext, row, column + 4,
463                dialog.removeOverlap_table(), dialog.tooltips(), dialog)
464     {
465         dialog.removeOverlap_table().set_col_spacings(3);
467         removeOverlapXGap.set_digits(1);
468         removeOverlapXGap.set_size_request(60, -1);
469         removeOverlapXGap.set_increments(1.0, 5.0);
470         removeOverlapXGap.set_range(-1000.0, 1000.0);
471         removeOverlapXGap.set_value(0);
472         dialog.tooltips().set_tip(removeOverlapXGap,
473                                   _("Minimum horizontal gap (in px units) between bounding boxes"));
474         /* TRANSLATORS: Horizontal gap. Only put "H:" equivalent in the translation */
475         removeOverlapXGapLabel.set_label(Q_("gap|H:"));
477         removeOverlapYGap.set_digits(1);
478         removeOverlapYGap.set_size_request(60, -1);
479         removeOverlapYGap.set_increments(1.0, 5.0);
480         removeOverlapYGap.set_range(-1000.0, 1000.0);
481         removeOverlapYGap.set_value(0);
482         dialog.tooltips().set_tip(removeOverlapYGap,
483                                   _("Minimum vertical gap (in px units) between bounding boxes"));
484         /* TRANSLATORS: Vertical gap */
485         removeOverlapYGapLabel.set_label(_("V:"));
487         dialog.removeOverlap_table().attach(removeOverlapXGapLabel, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL);
488         dialog.removeOverlap_table().attach(removeOverlapXGap, column+1, column+2, row, row+1, Gtk::FILL, Gtk::FILL);
489         dialog.removeOverlap_table().attach(removeOverlapYGapLabel, column+2, column+3, row, row+1, Gtk::FILL, Gtk::FILL);
490         dialog.removeOverlap_table().attach(removeOverlapYGap, column+3, column+4, row, row+1, Gtk::FILL, Gtk::FILL);
492     }
494 private :
495     virtual void on_button_click()
496     {
497         if (!_dialog.getDesktop()) return;
499         // see comment in ActionAlign above
500         int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
501         prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
503         // xGap and yGap are the minimum space required between bounding rectangles.
504         double const xGap = removeOverlapXGap.get_value();
505         double const yGap = removeOverlapYGap.get_value();
506         removeoverlap(sp_desktop_selection(_dialog.getDesktop())->itemList(),
507                       xGap, yGap);
509         // restore compensation setting
510         prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
512         sp_document_done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
513                          _("Remove overlaps"));
514     }
515 };
517 class ActionGraphLayout : public Action {
518 public:
519     ActionGraphLayout(Glib::ustring const &id,
520                          Glib::ustring const &tiptext,
521                          guint row,
522                          guint column,
523                          AlignAndDistribute &dialog) :
524         Action(id, tiptext, row, column + 4,
525                dialog.graphLayout_table(), dialog.tooltips(), dialog)
526     {}
528 private :
529     virtual void on_button_click()
530     {
531         if (!_dialog.getDesktop()) return;
533         // see comment in ActionAlign above
534         int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
535         prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
537         graphlayout(sp_desktop_selection(_dialog.getDesktop())->itemList());
539         // restore compensation setting
540         prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
542         sp_document_done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
543                          _("Arrange connector network"));
544     }
545 };
547 class ActionUnclump : public Action {
548 public :
549     ActionUnclump(const Glib::ustring &id,
550                const Glib::ustring &tiptext,
551                guint row,
552                guint column,
553                AlignAndDistribute &dialog):
554         Action(id, tiptext, row, column,
555                dialog.distribute_table(), dialog.tooltips(), dialog)
556     {}
558 private :
559     virtual void on_button_click()
560     {
561         if (!_dialog.getDesktop()) return;
563         // see comment in ActionAlign above
564         int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
565         prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
567         unclump ((GSList *) sp_desktop_selection(_dialog.getDesktop())->itemList());
569         // restore compensation setting
570         prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
572         sp_document_done (sp_desktop_document (_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
573                           _("Unclump"));
574     }
575 };
577 class ActionRandomize : public Action {
578 public :
579     ActionRandomize(const Glib::ustring &id,
580                const Glib::ustring &tiptext,
581                guint row,
582                guint column,
583                AlignAndDistribute &dialog):
584         Action(id, tiptext, row, column,
585                dialog.distribute_table(), dialog.tooltips(), dialog)
586     {}
588 private :
589     virtual void on_button_click()
590     {
591         SPDesktop *desktop = _dialog.getDesktop();
592         if (!desktop) return;
594         Inkscape::Selection *selection = sp_desktop_selection(desktop);
595         if (!selection) return;
597         using Inkscape::Util::GSListConstIterator;
598         std::list<SPItem *> selected;
599         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
600         if (selected.empty()) return;
602         //Check 2 or more selected objects
603         if (selected.size() < 2) return;
605         boost::optional<NR::Rect> sel_bbox = selection->bounds();
606         if (!sel_bbox) {
607             return;
608         }
610         // This bbox is cached between calls to randomize, so that there's no growth nor shrink
611         // nor drift on sequential randomizations. Discard cache on global (or better active
612         // desktop's) selection_change signal.
613         if (!_dialog.randomize_bbox) {
614             _dialog.randomize_bbox = to_2geom(*sel_bbox);
615         }
617         // see comment in ActionAlign above
618         int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
619         prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
621         for (std::list<SPItem *>::iterator it(selected.begin());
622             it != selected.end();
623             ++it)
624         {
625             sp_document_ensure_up_to_date(sp_desktop_document (desktop));
626             boost::optional<NR::Rect> item_box = sp_item_bbox_desktop (*it);
627             if (item_box) {
628                 // find new center, staying within bbox
629                 double x = _dialog.randomize_bbox->min()[Geom::X] + (*item_box).extent(Geom::X)/2 +
630                     g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::X].extent() - (*item_box).extent(Geom::X));
631                 double y = _dialog.randomize_bbox->min()[Geom::Y] + (*item_box).extent(Geom::Y)/2 +
632                     g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::Y].extent() - (*item_box).extent(Geom::Y));
633                 // displacement is the new center minus old:
634                 NR::Point t = NR::Point (x, y) - 0.5*(item_box->max() + item_box->min());
635                 sp_item_move_rel(*it, NR::translate(t));
636             }
637         }
639         // restore compensation setting
640         prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
642         sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
643                           _("Randomize positions"));
644     }
645 };
647 struct Baselines
649     SPItem *_item;
650     Geom::Point _base;
651     Geom::Dim2 _orientation;
652     Baselines(SPItem *item, Geom::Point base, Geom::Dim2 orientation) :
653         _item (item),
654         _base (base),
655         _orientation (orientation)
656     {}
657 };
659 bool operator< (const Baselines &a, const Baselines &b)
661     return (a._base[a._orientation] < b._base[b._orientation]);
664 class ActionBaseline : public Action {
665 public :
666     ActionBaseline(const Glib::ustring &id,
667                const Glib::ustring &tiptext,
668                guint row,
669                guint column,
670                AlignAndDistribute &dialog,
671                Gtk::Table &table,
672                Geom::Dim2 orientation, bool distribute):
673         Action(id, tiptext, row, column,
674                table, dialog.tooltips(), dialog),
675         _orientation(orientation),
676         _distribute(distribute)
677     {}
679 private :
680     Geom::Dim2 _orientation;
681     bool _distribute;
682     virtual void on_button_click()
683     {
684         SPDesktop *desktop = _dialog.getDesktop();
685         if (!desktop) return;
687         Inkscape::Selection *selection = sp_desktop_selection(desktop);
688         if (!selection) return;
690         using Inkscape::Util::GSListConstIterator;
691         std::list<SPItem *> selected;
692         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
693         if (selected.empty()) return;
695         //Check 2 or more selected objects
696         if (selected.size() < 2) return;
698         Geom::Point b_min = Geom::Point (HUGE_VAL, HUGE_VAL);
699         Geom::Point b_max = Geom::Point (-HUGE_VAL, -HUGE_VAL);
701         std::vector<Baselines> sorted;
703         for (std::list<SPItem *>::iterator it(selected.begin());
704             it != selected.end();
705             ++it)
706         {
707             if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
708                 Inkscape::Text::Layout const *layout = te_get_layout(*it);
709                 Geom::Point base = layout->characterAnchorPoint(layout->begin()) * sp_item_i2d_affine(*it);
710                 if (base[Geom::X] < b_min[Geom::X]) b_min[Geom::X] = base[Geom::X];
711                 if (base[Geom::Y] < b_min[Geom::Y]) b_min[Geom::Y] = base[Geom::Y];
712                 if (base[Geom::X] > b_max[Geom::X]) b_max[Geom::X] = base[Geom::X];
713                 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         }
720         if (sorted.size() <= 1) return;
722         //sort baselines
723         std::sort(sorted.begin(), sorted.end());
725         bool changed = false;
727         if (_distribute) {
728             double step = (b_max[_orientation] - b_min[_orientation])/(sorted.size() - 1);
729             for (unsigned int i = 0; i < sorted.size(); i++) {
730                 SPItem *item = sorted[i]._item;
731                 Geom::Point base = sorted[i]._base;
732                 Geom::Point t(0.0, 0.0);
733                 t[_orientation] = b_min[_orientation] + step * i - base[_orientation];
734                 sp_item_move_rel(item, NR::translate(t));
735                 changed = true;
736             }
738             if (changed) {
739                 sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
740                                   _("Distribute text baselines"));
741             }
743         } else {
744             for (std::list<SPItem *>::iterator it(selected.begin());
745                  it != selected.end();
746                  ++it)
747             {
748                 if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
749                     Inkscape::Text::Layout const *layout = te_get_layout(*it);
750                     Geom::Point base = layout->characterAnchorPoint(layout->begin()) * sp_item_i2d_affine(*it);
751                     Geom::Point t(0.0, 0.0);
752                     t[_orientation] = b_min[_orientation] - base[_orientation];
753                     sp_item_move_rel(*it, NR::translate(t));
754                     changed = true;
755                 }
756             }
758             if (changed) {
759                 sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
760                                   _("Align text baselines"));
761             }
762         }
763     }
764 };
768 void on_tool_changed(Inkscape::Application */*inkscape*/, SPEventContext */*context*/, AlignAndDistribute *daad)
770     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
771     if (desktop && sp_desktop_event_context(desktop))
772         daad->setMode(tools_active(desktop) == TOOLS_NODES);
775 void on_selection_changed(Inkscape::Application */*inkscape*/, Inkscape::Selection */*selection*/, AlignAndDistribute *daad)
777     daad->randomize_bbox = boost::optional<Geom::Rect>();
780 /////////////////////////////////////////////////////////
785 AlignAndDistribute::AlignAndDistribute()
786     : UI::Widget::Panel ("", "dialogs.align", SP_VERB_DIALOG_ALIGN_DISTRIBUTE),
787       randomize_bbox(),
788       _alignFrame(_("Align")),
789       _distributeFrame(_("Distribute")),
790       _removeOverlapFrame(_("Remove overlaps")),
791       _graphLayoutFrame(_("Connector network layout")),
792       _nodesFrame(_("Nodes")),
793       _alignTable(2, 6, true),
794       _distributeTable(3, 6, true),
795       _removeOverlapTable(1, 5, false),
796       _graphLayoutTable(1, 5, false),
797       _nodesTable(1, 4, true),
798       _anchorLabel(_("Relative to: ")),
799       _selgrpLabel(_("Treat selection as group: "))
802     //Instanciate the align buttons
803     addAlignButton("al_left_out",
804                    _("Align right sides of objects to left side of anchor"),
805                    0, 0);
806     addAlignButton("al_left_in",
807                    _("Align left sides"),
808                    0, 1);
809     addAlignButton("al_center_hor",
810                    _("Center on vertical axis"),
811                    0, 2);
812     addAlignButton("al_right_in",
813                    _("Align right sides"),
814                    0, 3);
815     addAlignButton("al_right_out",
816                    _("Align left sides of objects to right side of anchor"),
817                    0, 4);
818     addAlignButton("al_top_out",
819                    _("Align bottoms of objects to top of anchor"),
820                    1, 0);
821     addAlignButton("al_top_in",
822                    _("Align tops"),
823                    1, 1);
824     addAlignButton("al_center_ver",
825                    _("Center on horizontal axis"),
826                    1, 2);
827     addAlignButton("al_bottom_in",
828                    _("Align bottoms"),
829                    1, 3);
830     addAlignButton("al_bottom_out",
831                    _("Align tops of objects to bottom of anchor"),
832                    1, 4);
834     //Baseline aligns
835     addBaselineButton("al_baselines_vert",
836                    _("Align baseline anchors of texts vertically"),
837                       0, 5, this->align_table(), Geom::X, false);
838     addBaselineButton("al_baselines_hor",
839                    _("Align baseline anchors of texts horizontally"),
840                      1, 5, this->align_table(), Geom::Y, false);
842     //The distribute buttons
843     addDistributeButton("distribute_hdist",
844                         _("Make horizontal gaps between objects equal"),
845                         0, 4, true, Geom::X, .5, .5);
847     addDistributeButton("distribute_left",
848                         _("Distribute left sides equidistantly"),
849                         0, 1, false, Geom::X, 1., 0.);
850     addDistributeButton("distribute_hcentre",
851                         _("Distribute centers equidistantly horizontally"),
852                         0, 2, false, Geom::X, .5, .5);
853     addDistributeButton("distribute_right",
854                         _("Distribute right sides equidistantly"),
855                         0, 3, false, Geom::X, 0., 1.);
857     addDistributeButton("distribute_vdist",
858                         _("Make vertical gaps between objects equal"),
859                         1, 4, true, Geom::Y, .5, .5);
861     addDistributeButton("distribute_top",
862                         _("Distribute tops equidistantly"),
863                         1, 1, false, Geom::Y, 0, 1);
864     addDistributeButton("distribute_vcentre",
865                         _("Distribute centers equidistantly vertically"),
866                         1, 2, false, Geom::Y, .5, .5);
867     addDistributeButton("distribute_bottom",
868                         _("Distribute bottoms equidistantly"),
869                         1, 3, false, Geom::Y, 1., 0.);
871     //Baseline distribs
872     addBaselineButton("distribute_baselines_hor",
873                    _("Distribute baseline anchors of texts horizontally"),
874                       0, 5, this->distribute_table(), Geom::X, true);
875     addBaselineButton("distribute_baselines_vert",
876                    _("Distribute baseline anchors of texts vertically"),
877                      1, 5, this->distribute_table(), Geom::Y, true);
879     //Randomize & Unclump
880     addRandomizeButton("distribute_randomize",
881                         _("Randomize centers in both dimensions"),
882                         2, 2);
883     addUnclumpButton("unclump",
884                         _("Unclump objects: try to equalize edge-to-edge distances"),
885                         2, 4);
887     //Remove overlaps
888     addRemoveOverlapsButton("remove_overlaps",
889                             _("Move objects as little as possible so that their bounding boxes do not overlap"),
890                             0, 0);
891     //Graph Layout
892     addGraphLayoutButton("graph_layout",
893                             _("Nicely arrange selected connector network"),
894                             0, 0);
896     //Node Mode buttons
897     addNodeButton("node_halign",
898                   _("Align selected nodes horizontally"),
899                   0, Geom::X, false);
900     addNodeButton("node_valign",
901                   _("Align selected nodes vertically"),
902                   1, Geom::Y, false);
903     addNodeButton("node_hdistribute",
904                   _("Distribute selected nodes horizontally"),
905                   2, Geom::X, true);
906     addNodeButton("node_vdistribute",
907                   _("Distribute selected nodes vertically"),
908                   3, Geom::Y, true);
910     //Rest of the widgetry
912     _combo.append_text(_("Last selected"));
913     _combo.append_text(_("First selected"));
914     _combo.append_text(_("Biggest item"));
915     _combo.append_text(_("Smallest item"));
916     _combo.append_text(_("Page"));
917     _combo.append_text(_("Drawing"));
918     _combo.append_text(_("Selection"));
920     _combo.set_active(prefs_get_int_attribute("dialogs.align", "align-to", 6));
921     _combo.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_ref_change));
923     _anchorBox.pack_start(_anchorLabel);
924     _anchorBox.pack_start(_combo);
926     _selgrpBox.pack_start(_selgrpLabel);
927     _selgrpBox.pack_start(_selgrp);
928     _selgrp.set_active(prefs_get_int_attribute("dialogs.align", "sel-as-groups", 0));
929     _selgrp.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_selgrp_toggled));
931     _alignBox.pack_start(_anchorBox);
932     _alignBox.pack_start(_selgrpBox);
933     _alignBox.pack_start(_alignTable);
935     _alignFrame.add(_alignBox);
936     _distributeFrame.add(_distributeTable);
937     _removeOverlapFrame.add(_removeOverlapTable);
938     _graphLayoutFrame.add(_graphLayoutTable);
939     _nodesFrame.add(_nodesTable);
941     Gtk::Box *contents = _getContents();
942     contents->set_spacing(4);
944     // Notebook for individual transformations
946     contents->pack_start(_alignFrame, true, true);
947     contents->pack_start(_distributeFrame, true, true);
948     contents->pack_start(_removeOverlapFrame, true, true);
949     contents->pack_start(_graphLayoutFrame, true, true);
950     contents->pack_start(_nodesFrame, true, true);
952     //Connect to the global tool change signal
953     g_signal_connect (G_OBJECT (INKSCAPE), "set_eventcontext", G_CALLBACK (on_tool_changed), this);
955     // Connect to the global selection change, to invalidate cached randomize_bbox
956     g_signal_connect (G_OBJECT (INKSCAPE), "change_selection", G_CALLBACK (on_selection_changed), this);
957     randomize_bbox = boost::optional<Geom::Rect>();
959     show_all_children();
961     on_tool_changed (NULL, NULL, this); // set current mode
964 AlignAndDistribute::~AlignAndDistribute()
966     sp_signal_disconnect_by_data (G_OBJECT (INKSCAPE), this);
968     for (std::list<Action *>::iterator it = _actionList.begin();
969          it != _actionList.end();
970          it ++)
971         delete *it;
974 void AlignAndDistribute::on_ref_change(){
976     prefs_set_int_attribute("dialogs.align", "align-to", _combo.get_active_row_number());
978     //Make blink the master
981 void AlignAndDistribute::on_selgrp_toggled(){
983     prefs_set_int_attribute("dialogs.align", "sel-as-groups", _selgrp.get_active());
985     //Make blink the master
991 void AlignAndDistribute::setMode(bool nodeEdit)
993     //Act on widgets used in node mode
994     void ( Gtk::Widget::*mNode) ()  = nodeEdit ?
995         &Gtk::Widget::show_all : &Gtk::Widget::hide_all;
997     //Act on widgets used in selection mode
998   void ( Gtk::Widget::*mSel) ()  = nodeEdit ?
999       &Gtk::Widget::hide_all : &Gtk::Widget::show_all;
1002     ((_alignFrame).*(mSel))();
1003     ((_distributeFrame).*(mSel))();
1004     ((_removeOverlapFrame).*(mSel))();
1005     ((_graphLayoutFrame).*(mSel))();
1006     ((_nodesFrame).*(mNode))();
1009 void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext,
1010                                  guint row, guint col)
1012     _actionList.push_back(
1013         new ActionAlign(
1014             id, tiptext, row, col,
1015             *this , col + row * 5));
1017 void AlignAndDistribute::addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1018                                       guint row, guint col, bool onInterSpace,
1019                                       Geom::Dim2 orientation, float kBegin, float kEnd)
1021     _actionList.push_back(
1022         new ActionDistribute(
1023             id, tiptext, row, col, *this ,
1024             onInterSpace, orientation,
1025             kBegin, kEnd
1026             )
1027         );
1030 void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1031                    guint col, Geom::Dim2 orientation, bool distribute)
1033     _actionList.push_back(
1034         new ActionNode(
1035             id, tiptext, col,
1036             *this, orientation, distribute));
1039 void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext,
1040                                       guint row, guint col)
1042     _actionList.push_back(
1043         new ActionRemoveOverlaps(
1044             id, tiptext, row, col, *this)
1045         );
1048 void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext,
1049                                       guint row, guint col)
1051     _actionList.push_back(
1052         new ActionGraphLayout(
1053             id, tiptext, row, col, *this)
1054         );
1057 void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext,
1058                                       guint row, guint col)
1060     _actionList.push_back(
1061         new ActionUnclump(
1062             id, tiptext, row, col, *this)
1063         );
1066 void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1067                                       guint row, guint col)
1069     _actionList.push_back(
1070         new ActionRandomize(
1071             id, tiptext, row, col, *this)
1072         );
1075 void AlignAndDistribute::addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext,
1076                                     guint row, guint col, Gtk::Table &table, Geom::Dim2 orientation, bool distribute)
1078     _actionList.push_back(
1079         new ActionBaseline(
1080             id, tiptext, row, col,
1081             *this, table, orientation, distribute));
1087 std::list<SPItem *>::iterator AlignAndDistribute::find_master( std::list<SPItem *> &list, bool horizontal){
1088     std::list<SPItem *>::iterator master = list.end();
1089     switch (getAlignTarget()) {
1090     case LAST:
1091         return list.begin();
1092         break;
1094     case FIRST:
1095         return --(list.end());
1096         break;
1098     case BIGGEST:
1099     {
1100         gdouble max = -1e18;
1101         for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1102             boost::optional<NR::Rect> b = sp_item_bbox_desktop (*it);
1103             if (b) {
1104                 gdouble dim = (*b).extent(horizontal ? Geom::X : Geom::Y);
1105                 if (dim > max) {
1106                     max = dim;
1107                     master = it;
1108                 }
1109             }
1110         }
1111         return master;
1112         break;
1113     }
1115     case SMALLEST:
1116     {
1117         gdouble max = 1e18;
1118         for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1119             boost::optional<NR::Rect> b = sp_item_bbox_desktop (*it);
1120             if (b) {
1121                 gdouble dim = (*b).extent(horizontal ? Geom::X : Geom::Y);
1122                 if (dim < max) {
1123                     max = dim;
1124                     master = it;
1125                 }
1126             }
1127         }
1128         return master;
1129         break;
1130     }
1132     default:
1133         g_assert_not_reached ();
1134         break;
1136     } // end of switch statement
1137     return master;
1140 AlignAndDistribute::AlignTarget AlignAndDistribute::getAlignTarget()const {
1141     return AlignTarget(_combo.get_active_row_number());
1146 } // namespace Dialog
1147 } // namespace UI
1148 } // namespace Inkscape
1150 /*
1151   Local Variables:
1152   mode:c++
1153   c-file-style:"stroustrup"
1154   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1155   indent-tabs-mode:nil
1156   fill-column:99
1157   End:
1158 */
1159 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :