Code

From trunk
[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 "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 "preferences.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         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
126         bool sel_as_group = prefs->getBool("/dialogs/align/sel-as-groups");
128         using Inkscape::Util::GSListConstIterator;
129         std::list<SPItem *> selected;
130         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
131         if (selected.empty()) return;
133         Geom::Point mp; //Anchor point
134         AlignAndDistribute::AlignTarget target = _dialog.getAlignTarget();
135         const Coeffs &a= _allCoeffs[_index];
136         switch (target)
137         {
138         case AlignAndDistribute::LAST:
139         case AlignAndDistribute::FIRST:
140         case AlignAndDistribute::BIGGEST:
141         case AlignAndDistribute::SMALLEST:
142         {
143             //Check 2 or more selected objects
144             std::list<SPItem *>::iterator second(selected.begin());
145             ++second;
146             if (second == selected.end())
147                 return;
148             //Find the master (anchor on which the other objects are aligned)
149             std::list<SPItem *>::iterator master(
150                 _dialog.find_master (
151                     selected,
152                     (a.mx0 != 0.0) ||
153                     (a.mx1 != 0.0) )
154                 );
155             //remove the master from the selection
156             SPItem * thing = *master;
157             // TODO: either uncomment or remove the following commented lines, depending on which
158             //       behaviour of moving objects makes most sense; also cf. discussion at
159             //       https://bugs.launchpad.net/inkscape/+bug/255933
160             /*if (!sel_as_group) { */
161                 selected.erase(master);
162             /*}*/
163             //Compute the anchor point
164             boost::optional<Geom::Rect> b = sp_item_bbox_desktop (thing);
165             if (b) {
166                 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
167                                a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
168             } else {
169                 return;
170             }
171             break;
172         }
174         case AlignAndDistribute::PAGE:
175             mp = Geom::Point(a.mx1 * sp_document_width(sp_desktop_document(desktop)),
176                            a.my1 * sp_document_height(sp_desktop_document(desktop)));
177             break;
179         case AlignAndDistribute::DRAWING:
180         {
181             boost::optional<Geom::Rect> b = sp_item_bbox_desktop
182                 ( (SPItem *) sp_document_root (sp_desktop_document (desktop)) );
183             if (b) {
184                 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
185                                a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
186             } else {
187                 return;
188             }
189             break;
190         }
192         case AlignAndDistribute::SELECTION:
193         {
194             boost::optional<Geom::Rect> b =  selection->bounds();
195             if (b) {
196                 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
197                                a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
198             } else {
199                 return;
200             }
201             break;
202         }
204         default:
205             g_assert_not_reached ();
206             break;
207         };  // end of switch
209         // Top hack: temporarily set clone compensation to unmoved, so that we can align/distribute
210         // clones with their original (and the move of the original does not disturb the
211         // clones). The only problem with this is that if there are outside-of-selection clones of
212         // a selected original, they will be unmoved too, possibly contrary to user's
213         // expecation. However this is a minor point compared to making align/distribute always
214         // work as expected, and "unmoved" is the default option anyway.
215         int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
216         prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
218         bool changed = false;
219         boost::optional<Geom::Rect> b;
220         if (sel_as_group)
221             b = selection->bounds();
223         //Move each item in the selected list separately
224         for (std::list<SPItem *>::iterator it(selected.begin());
225              it != selected.end();
226              it++)
227         {
228             sp_document_ensure_up_to_date(sp_desktop_document (desktop));
229             if (!sel_as_group)
230                 b = sp_item_bbox_desktop (*it);
231             if (b) {
232                 Geom::Point const sp(a.sx0 * b->min()[Geom::X] + a.sx1 * b->max()[Geom::X],
233                                      a.sy0 * b->min()[Geom::Y] + a.sy1 * b->max()[Geom::Y]);
234                 Geom::Point const mp_rel( mp - sp );
235                 if (LInfty(mp_rel) > 1e-9) {
236                     sp_item_move_rel(*it, Geom::Translate(mp_rel));
237                     changed = true;
238                 }
239             }
240         }
242         // restore compensation setting
243         prefs->setInt("/options/clonecompensation/value", saved_compensation);
245         if (changed) {
246             sp_document_done ( sp_desktop_document (desktop) , SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
247                                _("Align"));
248         }
251     }
252     guint _index;
253     AlignAndDistribute &_dialog;
255     static const Coeffs _allCoeffs[10];
257 };
258 ActionAlign::Coeffs const ActionAlign::_allCoeffs[10] = {
259     {1., 0., 0., 0., 0., 1., 0., 0.},
260     {1., 0., 0., 0., 1., 0., 0., 0.},
261     {.5, .5, 0., 0., .5, .5, 0., 0.},
262     {0., 1., 0., 0., 0., 1., 0., 0.},
263     {0., 1., 0., 0., 1., 0., 0., 0.},
264     {0., 0., 0., 1., 0., 0., 1., 0.},
265     {0., 0., 0., 1., 0., 0., 0., 1.},
266     {0., 0., .5, .5, 0., 0., .5, .5},
267     {0., 0., 1., 0., 0., 0., 1., 0.},
268     {0., 0., 1., 0., 0., 0., 0., 1.}
269 };
271 BBoxSort::BBoxSort(SPItem *pItem, Geom::Rect bounds, Geom::Dim2 orientation, double kBegin, double kEnd) :
272         item(pItem),
273         bbox (bounds)
275         anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation];
277 BBoxSort::BBoxSort(const BBoxSort &rhs) :
278         //NOTE :  this copy ctor is called O(sort) when sorting the vector
279         //this is bad. The vector should be a vector of pointers.
280         //But I'll wait the bohem GC before doing that
281         item(rhs.item), anchor(rhs.anchor), bbox(rhs.bbox) 
285 bool operator< (const BBoxSort &a, const BBoxSort &b)
287     return (a.anchor < b.anchor);
290 class ActionDistribute : public Action {
291 public :
292     ActionDistribute(const Glib::ustring &id,
293                      const Glib::ustring &tiptext,
294                      guint row, guint column,
295                      AlignAndDistribute &dialog,
296                      bool onInterSpace,
297                      Geom::Dim2 orientation,
298                      double kBegin, double kEnd
299         ):
300         Action(id, tiptext, row, column,
301                dialog.distribute_table(), dialog.tooltips(), dialog),
302         _dialog(dialog),
303         _onInterSpace(onInterSpace),
304         _orientation(orientation),
305         _kBegin(kBegin),
306         _kEnd( kEnd)
307     {}
309 private :
310     virtual void on_button_click() {
311         //Retreive selected objects
312         SPDesktop *desktop = _dialog.getDesktop();
313         if (!desktop) return;
315         Inkscape::Selection *selection = sp_desktop_selection(desktop);
316         if (!selection) return;
318         using Inkscape::Util::GSListConstIterator;
319         std::list<SPItem *> selected;
320         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
321         if (selected.empty()) return;
323         //Check 2 or more selected objects
324         std::list<SPItem *>::iterator second(selected.begin());
325         ++second;
326         if (second == selected.end()) return;
329         std::vector< BBoxSort  > sorted;
330         for (std::list<SPItem *>::iterator it(selected.begin());
331             it != selected.end();
332             ++it)
333         {
334             boost::optional<Geom::Rect> bbox = sp_item_bbox_desktop(*it);
335             if (bbox) {
336                 sorted.push_back(BBoxSort(*it, *bbox, _orientation, _kBegin, _kEnd));
337             }
338         }
339         //sort bbox by anchors
340         std::sort(sorted.begin(), sorted.end());
342         // see comment in ActionAlign above
343         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
344         int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
345         prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
347         unsigned int len = sorted.size();
348         bool changed = false;
349         if (_onInterSpace)
350         {
351             //overall bboxes span
352             float dist = (sorted.back().bbox.max()[_orientation] -
353                           sorted.front().bbox.min()[_orientation]);
354             //space eaten by bboxes
355             float span = 0;
356             for (unsigned int i = 0; i < len; i++)
357             {
358                 span += sorted[i].bbox[_orientation].extent();
359             }
360             //new distance between each bbox
361             float step = (dist - span) / (len - 1);
362             float pos = sorted.front().bbox.min()[_orientation];
363             for ( std::vector<BBoxSort> ::iterator it (sorted.begin());
364                   it < sorted.end();
365                   it ++ )
366             {
367                 if (!NR_DF_TEST_CLOSE (pos, it->bbox.min()[_orientation], 1e-6)) {
368                     Geom::Point t(0.0, 0.0);
369                     t[_orientation] = pos - it->bbox.min()[_orientation];
370                     sp_item_move_rel(it->item, Geom::Translate(t));
371                     changed = true;
372                 }
373                 pos += it->bbox[_orientation].extent();
374                 pos += step;
375             }
376         }
377         else
378         {
379             //overall anchor span
380             float dist = sorted.back().anchor - sorted.front().anchor;
381             //distance between anchors
382             float step = dist / (len - 1);
384             for ( unsigned int i = 0; i < len ; i ++ )
385             {
386                 BBoxSort & it(sorted[i]);
387                 //new anchor position
388                 float pos = sorted.front().anchor + i * step;
389                 //Don't move if we are really close
390                 if (!NR_DF_TEST_CLOSE (pos, it.anchor, 1e-6)) {
391                     //Compute translation
392                     Geom::Point t(0.0, 0.0);
393                     t[_orientation] = pos - it.anchor;
394                     //translate
395                     sp_item_move_rel(it.item, Geom::Translate(t));
396                     changed = true;
397                 }
398             }
399         }
401         // restore compensation setting
402         prefs->setInt("/options/clonecompensation/value", saved_compensation);
404         if (changed) {
405             sp_document_done ( sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
406                                _("Distribute"));
407         }
408     }
409     guint _index;
410     AlignAndDistribute &_dialog;
411     bool _onInterSpace;
412     Geom::Dim2 _orientation;
414     double _kBegin;
415     double _kEnd;
417 };
420 class ActionNode : public Action {
421 public :
422     ActionNode(const Glib::ustring &id,
423                const Glib::ustring &tiptext,
424                guint column,
425                AlignAndDistribute &dialog,
426                Geom::Dim2 orientation, bool distribute):
427         Action(id, tiptext, 0, column,
428                dialog.nodes_table(), dialog.tooltips(), dialog),
429         _orientation(orientation),
430         _distribute(distribute)
431     {}
433 private :
434     Geom::Dim2 _orientation;
435     bool _distribute;
436     virtual void on_button_click()
437     {
439         if (!_dialog.getDesktop()) return;
440         SPEventContext *event_context = sp_desktop_event_context(_dialog.getDesktop());
441         if (!SP_IS_NODE_CONTEXT (event_context)) return ;
443         if (_distribute)
444             SP_NODE_CONTEXT (event_context)->shape_editor->distribute((Geom::Dim2)_orientation);
445         else
446             SP_NODE_CONTEXT (event_context)->shape_editor->align((Geom::Dim2)_orientation);
448     }
449 };
451 class ActionRemoveOverlaps : public Action {
452 private:
453     Gtk::Label removeOverlapXGapLabel;
454     Gtk::Label removeOverlapYGapLabel;
455     Gtk::SpinButton removeOverlapXGap;
456     Gtk::SpinButton removeOverlapYGap;
458 public:
459     ActionRemoveOverlaps(Glib::ustring const &id,
460                          Glib::ustring const &tiptext,
461                          guint row,
462                          guint column,
463                          AlignAndDistribute &dialog) :
464         Action(id, tiptext, row, column + 4,
465                dialog.removeOverlap_table(), dialog.tooltips(), dialog)
466     {
467         dialog.removeOverlap_table().set_col_spacings(3);
469         removeOverlapXGap.set_digits(1);
470         removeOverlapXGap.set_size_request(60, -1);
471         removeOverlapXGap.set_increments(1.0, 5.0);
472         removeOverlapXGap.set_range(-1000.0, 1000.0);
473         removeOverlapXGap.set_value(0);
474         dialog.tooltips().set_tip(removeOverlapXGap,
475                                   _("Minimum horizontal gap (in px units) between bounding boxes"));
476         /* TRANSLATORS: Horizontal gap. Only put "H:" equivalent in the translation */
477         removeOverlapXGapLabel.set_label(Q_("gap|H:"));
479         removeOverlapYGap.set_digits(1);
480         removeOverlapYGap.set_size_request(60, -1);
481         removeOverlapYGap.set_increments(1.0, 5.0);
482         removeOverlapYGap.set_range(-1000.0, 1000.0);
483         removeOverlapYGap.set_value(0);
484         dialog.tooltips().set_tip(removeOverlapYGap,
485                                   _("Minimum vertical gap (in px units) between bounding boxes"));
486         /* TRANSLATORS: Vertical gap */
487         removeOverlapYGapLabel.set_label(_("V:"));
489         dialog.removeOverlap_table().attach(removeOverlapXGapLabel, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL);
490         dialog.removeOverlap_table().attach(removeOverlapXGap, column+1, column+2, row, row+1, Gtk::FILL, Gtk::FILL);
491         dialog.removeOverlap_table().attach(removeOverlapYGapLabel, column+2, column+3, row, row+1, Gtk::FILL, Gtk::FILL);
492         dialog.removeOverlap_table().attach(removeOverlapYGap, column+3, column+4, row, row+1, Gtk::FILL, Gtk::FILL);
494     }
496 private :
497     virtual void on_button_click()
498     {
499         if (!_dialog.getDesktop()) return;
501         // see comment in ActionAlign above
502         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
503         int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
504         prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
506         // xGap and yGap are the minimum space required between bounding rectangles.
507         double const xGap = removeOverlapXGap.get_value();
508         double const yGap = removeOverlapYGap.get_value();
509         removeoverlap(sp_desktop_selection(_dialog.getDesktop())->itemList(),
510                       xGap, yGap);
512         // restore compensation setting
513         prefs->setInt("/options/clonecompensation/value", saved_compensation);
515         sp_document_done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
516                          _("Remove overlaps"));
517     }
518 };
520 class ActionGraphLayout : public Action {
521 public:
522     ActionGraphLayout(Glib::ustring const &id,
523                          Glib::ustring const &tiptext,
524                          guint row,
525                          guint column,
526                          AlignAndDistribute &dialog) :
527         Action(id, tiptext, row, column + 4,
528                dialog.graphLayout_table(), dialog.tooltips(), dialog)
529     {}
531 private :
532     virtual void on_button_click()
533     {
534         if (!_dialog.getDesktop()) return;
536         // see comment in ActionAlign above
537         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
538         int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
539         prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
541         graphlayout(sp_desktop_selection(_dialog.getDesktop())->itemList());
543         // restore compensation setting
544         prefs->setInt("/options/clonecompensation/value", saved_compensation);
546         sp_document_done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
547                          _("Arrange connector network"));
548     }
549 };
551 class ActionUnclump : public Action {
552 public :
553     ActionUnclump(const Glib::ustring &id,
554                const Glib::ustring &tiptext,
555                guint row,
556                guint column,
557                AlignAndDistribute &dialog):
558         Action(id, tiptext, row, column,
559                dialog.distribute_table(), dialog.tooltips(), dialog)
560     {}
562 private :
563     virtual void on_button_click()
564     {
565         if (!_dialog.getDesktop()) return;
567         // see comment in ActionAlign above
568         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
569         int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
570         prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
572         unclump ((GSList *) sp_desktop_selection(_dialog.getDesktop())->itemList());
574         // restore compensation setting
575         prefs->setInt("/options/clonecompensation/value", saved_compensation);
577         sp_document_done (sp_desktop_document (_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
578                           _("Unclump"));
579     }
580 };
582 class ActionRandomize : public Action {
583 public :
584     ActionRandomize(const Glib::ustring &id,
585                const Glib::ustring &tiptext,
586                guint row,
587                guint column,
588                AlignAndDistribute &dialog):
589         Action(id, tiptext, row, column,
590                dialog.distribute_table(), dialog.tooltips(), dialog)
591     {}
593 private :
594     virtual void on_button_click()
595     {
596         SPDesktop *desktop = _dialog.getDesktop();
597         if (!desktop) return;
599         Inkscape::Selection *selection = sp_desktop_selection(desktop);
600         if (!selection) return;
602         using Inkscape::Util::GSListConstIterator;
603         std::list<SPItem *> selected;
604         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
605         if (selected.empty()) return;
607         //Check 2 or more selected objects
608         if (selected.size() < 2) return;
610         boost::optional<Geom::Rect> sel_bbox = selection->bounds();
611         if (!sel_bbox) {
612             return;
613         }
615         // This bbox is cached between calls to randomize, so that there's no growth nor shrink
616         // nor drift on sequential randomizations. Discard cache on global (or better active
617         // desktop's) selection_change signal.
618         if (!_dialog.randomize_bbox) {
619             _dialog.randomize_bbox = *sel_bbox;
620         }
622         // see comment in ActionAlign above
623         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
624         int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
625         prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
627         for (std::list<SPItem *>::iterator it(selected.begin());
628             it != selected.end();
629             ++it)
630         {
631             sp_document_ensure_up_to_date(sp_desktop_document (desktop));
632             boost::optional<Geom::Rect> item_box = sp_item_bbox_desktop (*it);
633             if (item_box) {
634                 // find new center, staying within bbox
635                 double x = _dialog.randomize_bbox->min()[Geom::X] + (*item_box)[Geom::X].extent() /2 +
636                     g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::X].extent() - (*item_box)[Geom::X].extent());
637                 double y = _dialog.randomize_bbox->min()[Geom::Y] + (*item_box)[Geom::Y].extent()/2 +
638                     g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::Y].extent() - (*item_box)[Geom::Y].extent());
639                 // displacement is the new center minus old:
640                 Geom::Point t = Geom::Point (x, y) - 0.5*(item_box->max() + item_box->min());
641                 sp_item_move_rel(*it, Geom::Translate(t));
642             }
643         }
645         // restore compensation setting
646         prefs->setInt("/options/clonecompensation/value", saved_compensation);
648         sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
649                           _("Randomize positions"));
650     }
651 };
653 struct Baselines
655     SPItem *_item;
656     Geom::Point _base;
657     Geom::Dim2 _orientation;
658     Baselines(SPItem *item, Geom::Point base, Geom::Dim2 orientation) :
659         _item (item),
660         _base (base),
661         _orientation (orientation)
662     {}
663 };
665 bool operator< (const Baselines &a, const Baselines &b)
667     return (a._base[a._orientation] < b._base[b._orientation]);
670 class ActionBaseline : public Action {
671 public :
672     ActionBaseline(const Glib::ustring &id,
673                const Glib::ustring &tiptext,
674                guint row,
675                guint column,
676                AlignAndDistribute &dialog,
677                Gtk::Table &table,
678                Geom::Dim2 orientation, bool distribute):
679         Action(id, tiptext, row, column,
680                table, dialog.tooltips(), dialog),
681         _orientation(orientation),
682         _distribute(distribute)
683     {}
685 private :
686     Geom::Dim2 _orientation;
687     bool _distribute;
688     virtual void on_button_click()
689     {
690         SPDesktop *desktop = _dialog.getDesktop();
691         if (!desktop) return;
693         Inkscape::Selection *selection = sp_desktop_selection(desktop);
694         if (!selection) return;
696         using Inkscape::Util::GSListConstIterator;
697         std::list<SPItem *> selected;
698         selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
699         if (selected.empty()) return;
701         //Check 2 or more selected objects
702         if (selected.size() < 2) return;
704         Geom::Point b_min = Geom::Point (HUGE_VAL, HUGE_VAL);
705         Geom::Point b_max = Geom::Point (-HUGE_VAL, -HUGE_VAL);
707         std::vector<Baselines> sorted;
709         for (std::list<SPItem *>::iterator it(selected.begin());
710             it != selected.end();
711             ++it)
712         {
713             if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
714                 Inkscape::Text::Layout const *layout = te_get_layout(*it);
715                 Geom::Point base = layout->characterAnchorPoint(layout->begin()) * sp_item_i2d_affine(*it);
716                 if (base[Geom::X] < b_min[Geom::X]) b_min[Geom::X] = base[Geom::X];
717                 if (base[Geom::Y] < b_min[Geom::Y]) b_min[Geom::Y] = base[Geom::Y];
718                 if (base[Geom::X] > b_max[Geom::X]) b_max[Geom::X] = base[Geom::X];
719                 if (base[Geom::Y] > b_max[Geom::Y]) b_max[Geom::Y] = base[Geom::Y];
721                 Baselines b (*it, base, _orientation);
722                 sorted.push_back(b);
723             }
724         }
726         if (sorted.size() <= 1) return;
728         //sort baselines
729         std::sort(sorted.begin(), sorted.end());
731         bool changed = false;
733         if (_distribute) {
734             double step = (b_max[_orientation] - b_min[_orientation])/(sorted.size() - 1);
735             for (unsigned int i = 0; i < sorted.size(); i++) {
736                 SPItem *item = sorted[i]._item;
737                 Geom::Point base = sorted[i]._base;
738                 Geom::Point t(0.0, 0.0);
739                 t[_orientation] = b_min[_orientation] + step * i - base[_orientation];
740                 sp_item_move_rel(item, Geom::Translate(t));
741                 changed = true;
742             }
744             if (changed) {
745                 sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
746                                   _("Distribute text baselines"));
747             }
749         } else {
750             for (std::list<SPItem *>::iterator it(selected.begin());
751                  it != selected.end();
752                  ++it)
753             {
754                 if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
755                     Inkscape::Text::Layout const *layout = te_get_layout(*it);
756                     Geom::Point base = layout->characterAnchorPoint(layout->begin()) * sp_item_i2d_affine(*it);
757                     Geom::Point t(0.0, 0.0);
758                     t[_orientation] = b_min[_orientation] - base[_orientation];
759                     sp_item_move_rel(*it, Geom::Translate(t));
760                     changed = true;
761                 }
762             }
764             if (changed) {
765                 sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
766                                   _("Align text baselines"));
767             }
768         }
769     }
770 };
774 void on_tool_changed(Inkscape::Application */*inkscape*/, SPEventContext */*context*/, AlignAndDistribute *daad)
776     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
777     if (desktop && sp_desktop_event_context(desktop))
778         daad->setMode(tools_active(desktop) == TOOLS_NODES);
781 void on_selection_changed(Inkscape::Application */*inkscape*/, Inkscape::Selection */*selection*/, AlignAndDistribute *daad)
783     daad->randomize_bbox = boost::optional<Geom::Rect>();
786 /////////////////////////////////////////////////////////
791 AlignAndDistribute::AlignAndDistribute()
792     : UI::Widget::Panel ("", "/dialogs/align", SP_VERB_DIALOG_ALIGN_DISTRIBUTE),
793       randomize_bbox(),
794       _alignFrame(_("Align")),
795       _distributeFrame(_("Distribute")),
796       _removeOverlapFrame(_("Remove overlaps")),
797       _graphLayoutFrame(_("Connector network layout")),
798       _nodesFrame(_("Nodes")),
799       _alignTable(2, 6, true),
800       _distributeTable(3, 6, true),
801       _removeOverlapTable(1, 5, false),
802       _graphLayoutTable(1, 5, false),
803       _nodesTable(1, 4, true),
804       _anchorLabel(_("Relative to: ")),
805       _selgrpLabel(_("Treat selection as group: "))
807     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
809     //Instanciate the align buttons
810     addAlignButton("al_left_out",
811                    _("Align right sides of objects to left side of anchor"),
812                    0, 0);
813     addAlignButton("al_left_in",
814                    _("Align left sides"),
815                    0, 1);
816     addAlignButton("al_center_hor",
817                    _("Center on vertical axis"),
818                    0, 2);
819     addAlignButton("al_right_in",
820                    _("Align right sides"),
821                    0, 3);
822     addAlignButton("al_right_out",
823                    _("Align left sides of objects to right side of anchor"),
824                    0, 4);
825     addAlignButton("al_top_out",
826                    _("Align bottoms of objects to top of anchor"),
827                    1, 0);
828     addAlignButton("al_top_in",
829                    _("Align tops"),
830                    1, 1);
831     addAlignButton("al_center_ver",
832                    _("Center on horizontal axis"),
833                    1, 2);
834     addAlignButton("al_bottom_in",
835                    _("Align bottoms"),
836                    1, 3);
837     addAlignButton("al_bottom_out",
838                    _("Align tops of objects to bottom of anchor"),
839                    1, 4);
841     //Baseline aligns
842     addBaselineButton("al_baselines_vert",
843                    _("Align baseline anchors of texts vertically"),
844                       0, 5, this->align_table(), Geom::X, false);
845     addBaselineButton("al_baselines_hor",
846                    _("Align baseline anchors of texts horizontally"),
847                      1, 5, this->align_table(), Geom::Y, false);
849     //The distribute buttons
850     addDistributeButton("distribute_hdist",
851                         _("Make horizontal gaps between objects equal"),
852                         0, 4, true, Geom::X, .5, .5);
854     addDistributeButton("distribute_left",
855                         _("Distribute left sides equidistantly"),
856                         0, 1, false, Geom::X, 1., 0.);
857     addDistributeButton("distribute_hcentre",
858                         _("Distribute centers equidistantly horizontally"),
859                         0, 2, false, Geom::X, .5, .5);
860     addDistributeButton("distribute_right",
861                         _("Distribute right sides equidistantly"),
862                         0, 3, false, Geom::X, 0., 1.);
864     addDistributeButton("distribute_vdist",
865                         _("Make vertical gaps between objects equal"),
866                         1, 4, true, Geom::Y, .5, .5);
868     addDistributeButton("distribute_top",
869                         _("Distribute tops equidistantly"),
870                         1, 1, false, Geom::Y, 0, 1);
871     addDistributeButton("distribute_vcentre",
872                         _("Distribute centers equidistantly vertically"),
873                         1, 2, false, Geom::Y, .5, .5);
874     addDistributeButton("distribute_bottom",
875                         _("Distribute bottoms equidistantly"),
876                         1, 3, false, Geom::Y, 1., 0.);
878     //Baseline distribs
879     addBaselineButton("distribute_baselines_hor",
880                    _("Distribute baseline anchors of texts horizontally"),
881                       0, 5, this->distribute_table(), Geom::X, true);
882     addBaselineButton("distribute_baselines_vert",
883                    _("Distribute baseline anchors of texts vertically"),
884                      1, 5, this->distribute_table(), Geom::Y, true);
886     //Randomize & Unclump
887     addRandomizeButton("distribute_randomize",
888                         _("Randomize centers in both dimensions"),
889                         2, 2);
890     addUnclumpButton("unclump",
891                         _("Unclump objects: try to equalize edge-to-edge distances"),
892                         2, 4);
894     //Remove overlaps
895     addRemoveOverlapsButton("remove_overlaps",
896                             _("Move objects as little as possible so that their bounding boxes do not overlap"),
897                             0, 0);
898     //Graph Layout
899     addGraphLayoutButton("graph_layout",
900                             _("Nicely arrange selected connector network"),
901                             0, 0);
903     //Node Mode buttons
904     addNodeButton("node_halign",
905                   _("Align selected nodes horizontally"),
906                   0, Geom::X, false);
907     addNodeButton("node_valign",
908                   _("Align selected nodes vertically"),
909                   1, Geom::Y, false);
910     addNodeButton("node_hdistribute",
911                   _("Distribute selected nodes horizontally"),
912                   2, Geom::X, true);
913     addNodeButton("node_vdistribute",
914                   _("Distribute selected nodes vertically"),
915                   3, Geom::Y, true);
917     //Rest of the widgetry
919     _combo.append_text(_("Last selected"));
920     _combo.append_text(_("First selected"));
921     _combo.append_text(_("Biggest item"));
922     _combo.append_text(_("Smallest item"));
923     _combo.append_text(_("Page"));
924     _combo.append_text(_("Drawing"));
925     _combo.append_text(_("Selection"));
927     _combo.set_active(prefs->getInt("/dialogs/align/align-to", 6));
928     _combo.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_ref_change));
930     _anchorBox.pack_start(_anchorLabel);
931     _anchorBox.pack_start(_combo);
933     _selgrpBox.pack_start(_selgrpLabel);
934     _selgrpBox.pack_start(_selgrp);
935     _selgrp.set_active(prefs->getBool("/dialogs/align/sel-as-groups"));
936     _selgrp.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_selgrp_toggled));
938     _alignBox.pack_start(_anchorBox);
939     _alignBox.pack_start(_selgrpBox);
940     _alignBox.pack_start(_alignTable);
942     _alignFrame.add(_alignBox);
943     _distributeFrame.add(_distributeTable);
944     _removeOverlapFrame.add(_removeOverlapTable);
945     _graphLayoutFrame.add(_graphLayoutTable);
946     _nodesFrame.add(_nodesTable);
948     Gtk::Box *contents = _getContents();
949     contents->set_spacing(4);
951     // Notebook for individual transformations
953     contents->pack_start(_alignFrame, true, true);
954     contents->pack_start(_distributeFrame, true, true);
955     contents->pack_start(_removeOverlapFrame, true, true);
956     contents->pack_start(_graphLayoutFrame, true, true);
957     contents->pack_start(_nodesFrame, true, true);
959     //Connect to the global tool change signal
960     g_signal_connect (G_OBJECT (INKSCAPE), "set_eventcontext", G_CALLBACK (on_tool_changed), this);
962     // Connect to the global selection change, to invalidate cached randomize_bbox
963     g_signal_connect (G_OBJECT (INKSCAPE), "change_selection", G_CALLBACK (on_selection_changed), this);
964     randomize_bbox = boost::optional<Geom::Rect>();
966     show_all_children();
968     on_tool_changed (NULL, NULL, this); // set current mode
971 AlignAndDistribute::~AlignAndDistribute()
973     sp_signal_disconnect_by_data (G_OBJECT (INKSCAPE), this);
975     for (std::list<Action *>::iterator it = _actionList.begin();
976          it != _actionList.end();
977          it ++)
978         delete *it;
981 void AlignAndDistribute::on_ref_change(){
982     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
983     prefs->setInt("/dialogs/align/align-to", _combo.get_active_row_number());
985     //Make blink the master
988 void AlignAndDistribute::on_selgrp_toggled(){
989     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
990     prefs->setInt("/dialogs/align/sel-as-groups", _selgrp.get_active());
992     //Make blink the master
998 void AlignAndDistribute::setMode(bool nodeEdit)
1000     //Act on widgets used in node mode
1001     void ( Gtk::Widget::*mNode) ()  = nodeEdit ?
1002         &Gtk::Widget::show_all : &Gtk::Widget::hide_all;
1004     //Act on widgets used in selection mode
1005   void ( Gtk::Widget::*mSel) ()  = nodeEdit ?
1006       &Gtk::Widget::hide_all : &Gtk::Widget::show_all;
1009     ((_alignFrame).*(mSel))();
1010     ((_distributeFrame).*(mSel))();
1011     ((_removeOverlapFrame).*(mSel))();
1012     ((_graphLayoutFrame).*(mSel))();
1013     ((_nodesFrame).*(mNode))();
1016 void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext,
1017                                  guint row, guint col)
1019     _actionList.push_back(
1020         new ActionAlign(
1021             id, tiptext, row, col,
1022             *this , col + row * 5));
1024 void AlignAndDistribute::addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1025                                       guint row, guint col, bool onInterSpace,
1026                                       Geom::Dim2 orientation, float kBegin, float kEnd)
1028     _actionList.push_back(
1029         new ActionDistribute(
1030             id, tiptext, row, col, *this ,
1031             onInterSpace, orientation,
1032             kBegin, kEnd
1033             )
1034         );
1037 void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1038                    guint col, Geom::Dim2 orientation, bool distribute)
1040     _actionList.push_back(
1041         new ActionNode(
1042             id, tiptext, col,
1043             *this, orientation, distribute));
1046 void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext,
1047                                       guint row, guint col)
1049     _actionList.push_back(
1050         new ActionRemoveOverlaps(
1051             id, tiptext, row, col, *this)
1052         );
1055 void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext,
1056                                       guint row, guint col)
1058     _actionList.push_back(
1059         new ActionGraphLayout(
1060             id, tiptext, row, col, *this)
1061         );
1064 void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext,
1065                                       guint row, guint col)
1067     _actionList.push_back(
1068         new ActionUnclump(
1069             id, tiptext, row, col, *this)
1070         );
1073 void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1074                                       guint row, guint col)
1076     _actionList.push_back(
1077         new ActionRandomize(
1078             id, tiptext, row, col, *this)
1079         );
1082 void AlignAndDistribute::addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext,
1083                                     guint row, guint col, Gtk::Table &table, Geom::Dim2 orientation, bool distribute)
1085     _actionList.push_back(
1086         new ActionBaseline(
1087             id, tiptext, row, col,
1088             *this, table, orientation, distribute));
1094 std::list<SPItem *>::iterator AlignAndDistribute::find_master( std::list<SPItem *> &list, bool horizontal){
1095     std::list<SPItem *>::iterator master = list.end();
1096     switch (getAlignTarget()) {
1097     case LAST:
1098         return list.begin();
1099         break;
1101     case FIRST:
1102         return --(list.end());
1103         break;
1105     case BIGGEST:
1106     {
1107         gdouble max = -1e18;
1108         for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1109             boost::optional<Geom::Rect> b = sp_item_bbox_desktop (*it);
1110             if (b) {
1111                 gdouble dim = (*b)[horizontal ? Geom::X : Geom::Y].extent();
1112                 if (dim > max) {
1113                     max = dim;
1114                     master = it;
1115                 }
1116             }
1117         }
1118         return master;
1119         break;
1120     }
1122     case SMALLEST:
1123     {
1124         gdouble max = 1e18;
1125         for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1126             boost::optional<Geom::Rect> b = sp_item_bbox_desktop (*it);
1127             if (b) {
1128                 gdouble dim = (*b)[horizontal ? Geom::X : Geom::Y].extent();
1129                 if (dim < max) {
1130                     max = dim;
1131                     master = it;
1132                 }
1133             }
1134         }
1135         return master;
1136         break;
1137     }
1139     default:
1140         g_assert_not_reached ();
1141         break;
1143     } // end of switch statement
1144     return master;
1147 AlignAndDistribute::AlignTarget AlignAndDistribute::getAlignTarget()const {
1148     return AlignTarget(_combo.get_active_row_number());
1153 } // namespace Dialog
1154 } // namespace UI
1155 } // namespace Inkscape
1157 /*
1158   Local Variables:
1159   mode:c++
1160   c-file-style:"stroustrup"
1161   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1162   indent-tabs-mode:nil
1163   fill-column:99
1164   End:
1165 */
1166 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :