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)
270 {
271 anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation];
272 }
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)
278 {
279 }
281 bool operator< (const BBoxSort &a, const BBoxSort &b)
282 {
283 return (a.anchor < b.anchor);
284 }
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
645 {
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)
657 {
658 return (a._base[a._orientation] < b._base[b._orientation]);
659 }
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)
766 {
767 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
768 if (desktop && sp_desktop_event_context(desktop))
769 daad->setMode(tools_active(desktop) == TOOLS_NODES);
770 }
772 void on_selection_changed(Inkscape::Application */*inkscape*/, Inkscape::Selection */*selection*/, AlignAndDistribute *daad)
773 {
774 daad->randomize_bbox = boost::optional<Geom::Rect>();
775 }
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: "))
797 {
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
959 }
961 AlignAndDistribute::~AlignAndDistribute()
962 {
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;
969 }
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
976 }
978 void AlignAndDistribute::on_selgrp_toggled(){
980 prefs_set_int_attribute("dialogs.align", "sel-as-groups", _selgrp.get_active());
982 //Make blink the master
983 }
988 void AlignAndDistribute::setMode(bool nodeEdit)
989 {
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))();
1005 }
1006 void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext,
1007 guint row, guint col)
1008 {
1009 _actionList.push_back(
1010 new ActionAlign(
1011 id, tiptext, row, col,
1012 *this , col + row * 5));
1013 }
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)
1017 {
1018 _actionList.push_back(
1019 new ActionDistribute(
1020 id, tiptext, row, col, *this ,
1021 onInterSpace, orientation,
1022 kBegin, kEnd
1023 )
1024 );
1025 }
1027 void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1028 guint col, Geom::Dim2 orientation, bool distribute)
1029 {
1030 _actionList.push_back(
1031 new ActionNode(
1032 id, tiptext, col,
1033 *this, orientation, distribute));
1034 }
1036 void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext,
1037 guint row, guint col)
1038 {
1039 _actionList.push_back(
1040 new ActionRemoveOverlaps(
1041 id, tiptext, row, col, *this)
1042 );
1043 }
1045 void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext,
1046 guint row, guint col)
1047 {
1048 _actionList.push_back(
1049 new ActionGraphLayout(
1050 id, tiptext, row, col, *this)
1051 );
1052 }
1054 void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext,
1055 guint row, guint col)
1056 {
1057 _actionList.push_back(
1058 new ActionUnclump(
1059 id, tiptext, row, col, *this)
1060 );
1061 }
1063 void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1064 guint row, guint col)
1065 {
1066 _actionList.push_back(
1067 new ActionRandomize(
1068 id, tiptext, row, col, *this)
1069 );
1070 }
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)
1074 {
1075 _actionList.push_back(
1076 new ActionBaseline(
1077 id, tiptext, row, col,
1078 *this, table, orientation, distribute));
1079 }
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;
1135 }
1137 AlignAndDistribute::AlignTarget AlignAndDistribute::getAlignTarget()const {
1138 return AlignTarget(_combo.get_active_row_number());
1139 }
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 :