99d3fdda3e521c705952f686f97ce68baad3d66b
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, Geom::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)
273 {
274 anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation];
275 }
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)
281 {
282 }
284 bool operator< (const BBoxSort &a, const BBoxSort &b)
285 {
286 return (a.anchor < b.anchor);
287 }
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, Geom::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, Geom::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, Geom::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
648 {
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)
660 {
661 return (a._base[a._orientation] < b._base[b._orientation]);
662 }
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, Geom::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, Geom::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)
769 {
770 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
771 if (desktop && sp_desktop_event_context(desktop))
772 daad->setMode(tools_active(desktop) == TOOLS_NODES);
773 }
775 void on_selection_changed(Inkscape::Application */*inkscape*/, Inkscape::Selection */*selection*/, AlignAndDistribute *daad)
776 {
777 daad->randomize_bbox = boost::optional<Geom::Rect>();
778 }
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: "))
800 {
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
962 }
964 AlignAndDistribute::~AlignAndDistribute()
965 {
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;
972 }
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
979 }
981 void AlignAndDistribute::on_selgrp_toggled(){
983 prefs_set_int_attribute("dialogs.align", "sel-as-groups", _selgrp.get_active());
985 //Make blink the master
986 }
991 void AlignAndDistribute::setMode(bool nodeEdit)
992 {
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))();
1008 }
1009 void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext,
1010 guint row, guint col)
1011 {
1012 _actionList.push_back(
1013 new ActionAlign(
1014 id, tiptext, row, col,
1015 *this , col + row * 5));
1016 }
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)
1020 {
1021 _actionList.push_back(
1022 new ActionDistribute(
1023 id, tiptext, row, col, *this ,
1024 onInterSpace, orientation,
1025 kBegin, kEnd
1026 )
1027 );
1028 }
1030 void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1031 guint col, Geom::Dim2 orientation, bool distribute)
1032 {
1033 _actionList.push_back(
1034 new ActionNode(
1035 id, tiptext, col,
1036 *this, orientation, distribute));
1037 }
1039 void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext,
1040 guint row, guint col)
1041 {
1042 _actionList.push_back(
1043 new ActionRemoveOverlaps(
1044 id, tiptext, row, col, *this)
1045 );
1046 }
1048 void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext,
1049 guint row, guint col)
1050 {
1051 _actionList.push_back(
1052 new ActionGraphLayout(
1053 id, tiptext, row, col, *this)
1054 );
1055 }
1057 void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext,
1058 guint row, guint col)
1059 {
1060 _actionList.push_back(
1061 new ActionUnclump(
1062 id, tiptext, row, col, *this)
1063 );
1064 }
1066 void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1067 guint row, guint col)
1068 {
1069 _actionList.push_back(
1070 new ActionRandomize(
1071 id, tiptext, row, col, *this)
1072 );
1073 }
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)
1077 {
1078 _actionList.push_back(
1079 new ActionBaseline(
1080 id, tiptext, row, col,
1081 *this, table, orientation, distribute));
1082 }
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;
1138 }
1140 AlignAndDistribute::AlignTarget AlignAndDistribute::getAlignTarget()const {
1141 return AlignTarget(_combo.get_active_row_number());
1142 }
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 :