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 = SP_ACTIVE_DESKTOP;
120 if (!desktop) return;
122 Inkscape::Selection *selection = sp_desktop_selection(desktop);
123 if (!selection) return;
125 using Inkscape::Util::GSListConstIterator;
126 std::list<SPItem *> selected;
127 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
128 if (selected.empty()) return;
130 NR::Point mp; //Anchor point
131 AlignAndDistribute::AlignTarget target = _dialog.getAlignTarget();
132 const Coeffs &a= _allCoeffs[_index];
133 switch (target)
134 {
135 case AlignAndDistribute::LAST:
136 case AlignAndDistribute::FIRST:
137 case AlignAndDistribute::BIGGEST:
138 case AlignAndDistribute::SMALLEST:
139 {
140 //Check 2 or more selected objects
141 std::list<SPItem *>::iterator second(selected.begin());
142 ++second;
143 if (second == selected.end())
144 return;
145 //Find the master (anchor on which the other objects are aligned)
146 std::list<SPItem *>::iterator master(
147 _dialog.find_master (
148 selected,
149 (a.mx0 != 0.0) ||
150 (a.mx1 != 0.0) )
151 );
152 //remove the master from the selection
153 SPItem * thing = *master;
154 selected.erase(master);
155 //Compute the anchor point
156 NR::Maybe<NR::Rect> b = sp_item_bbox_desktop (thing);
157 if (b) {
158 mp = NR::Point(a.mx0 * b->min()[NR::X] + a.mx1 * b->max()[NR::X],
159 a.my0 * b->min()[NR::Y] + a.my1 * b->max()[NR::Y]);
160 } else {
161 return;
162 }
163 break;
164 }
166 case AlignAndDistribute::PAGE:
167 mp = NR::Point(a.mx1 * sp_document_width(sp_desktop_document(desktop)),
168 a.my1 * sp_document_height(sp_desktop_document(desktop)));
169 break;
171 case AlignAndDistribute::DRAWING:
172 {
173 NR::Maybe<NR::Rect> b = sp_item_bbox_desktop
174 ( (SPItem *) sp_document_root (sp_desktop_document (desktop)) );
175 if (b) {
176 mp = NR::Point(a.mx0 * b->min()[NR::X] + a.mx1 * b->max()[NR::X],
177 a.my0 * b->min()[NR::Y] + a.my1 * b->max()[NR::Y]);
178 } else {
179 return;
180 }
181 break;
182 }
184 case AlignAndDistribute::SELECTION:
185 {
186 NR::Maybe<NR::Rect> b = selection->bounds();
187 if (b) {
188 mp = NR::Point(a.mx0 * b->min()[NR::X] + a.mx1 * b->max()[NR::X],
189 a.my0 * b->min()[NR::Y] + a.my1 * b->max()[NR::Y]);
190 } else {
191 return;
192 }
193 break;
194 }
196 default:
197 g_assert_not_reached ();
198 break;
199 }; // end of switch
201 // Top hack: temporarily set clone compensation to unmoved, so that we can align/distribute
202 // clones with their original (and the move of the original does not disturb the
203 // clones). The only problem with this is that if there are outside-of-selection clones of
204 // a selected original, they will be unmoved too, possibly contrary to user's
205 // expecation. However this is a minor point compared to making align/distribute always
206 // work as expected, and "unmoved" is the default option anyway.
207 int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
208 prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
210 bool changed = false;
211 //Move each item in the selected list
212 for (std::list<SPItem *>::iterator it(selected.begin());
213 it != selected.end();
214 it++)
215 {
216 sp_document_ensure_up_to_date(sp_desktop_document (desktop));
217 NR::Maybe<NR::Rect> b = sp_item_bbox_desktop (*it);
218 if (b) {
219 NR::Point const sp(a.sx0 * b->min()[NR::X] + a.sx1 * b->max()[NR::X],
220 a.sy0 * b->min()[NR::Y] + a.sy1 * b->max()[NR::Y]);
221 NR::Point const mp_rel( mp - sp );
222 if (LInfty(mp_rel) > 1e-9) {
223 sp_item_move_rel(*it, NR::translate(mp_rel));
224 changed = true;
225 }
226 }
227 }
229 // restore compensation setting
230 prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
232 if (changed) {
233 sp_document_done ( sp_desktop_document (desktop) , SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
234 _("Align"));
235 }
238 }
239 guint _index;
240 AlignAndDistribute &_dialog;
242 static const Coeffs _allCoeffs[10];
244 };
245 ActionAlign::Coeffs const ActionAlign::_allCoeffs[10] = {
246 {1., 0., 0., 0., 0., 1., 0., 0.},
247 {1., 0., 0., 0., 1., 0., 0., 0.},
248 {.5, .5, 0., 0., .5, .5, 0., 0.},
249 {0., 1., 0., 0., 0., 1., 0., 0.},
250 {0., 1., 0., 0., 1., 0., 0., 0.},
251 {0., 0., 0., 1., 0., 0., 1., 0.},
252 {0., 0., 0., 1., 0., 0., 0., 1.},
253 {0., 0., .5, .5, 0., 0., .5, .5},
254 {0., 0., 1., 0., 0., 0., 1., 0.},
255 {0., 0., 1., 0., 0., 0., 0., 1.}
256 };
258 struct BBoxSort
259 {
260 SPItem *item;
261 float anchor;
262 NR::Rect bbox;
263 BBoxSort(SPItem *pItem, NR::Rect bounds, NR::Dim2 orientation, double kBegin, double kEnd) :
264 item(pItem),
265 bbox (bounds)
266 {
267 anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation];
268 }
269 BBoxSort(const BBoxSort &rhs):
270 //NOTE : this copy ctor is called O(sort) when sorting the vector
271 //this is bad. The vector should be a vector of pointers.
272 //But I'll wait the bohem GC before doing that
273 item(rhs.item), anchor(rhs.anchor), bbox(rhs.bbox) {
274 }
275 };
276 bool operator< (const BBoxSort &a, const BBoxSort &b)
277 {
278 return (a.anchor < b.anchor);
279 }
281 class ActionDistribute : public Action {
282 public :
283 ActionDistribute(const Glib::ustring &id,
284 const Glib::ustring &tiptext,
285 guint row, guint column,
286 AlignAndDistribute &dialog,
287 bool onInterSpace,
288 NR::Dim2 orientation,
289 double kBegin, double kEnd
290 ):
291 Action(id, tiptext, row, column,
292 dialog.distribute_table(), dialog.tooltips(), dialog),
293 _dialog(dialog),
294 _onInterSpace(onInterSpace),
295 _orientation(orientation),
296 _kBegin(kBegin),
297 _kEnd( kEnd)
298 {}
300 private :
301 virtual void on_button_click() {
302 //Retreive selected objects
303 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
304 if (!desktop) return;
306 Inkscape::Selection *selection = sp_desktop_selection(desktop);
307 if (!selection) return;
309 using Inkscape::Util::GSListConstIterator;
310 std::list<SPItem *> selected;
311 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
312 if (selected.empty()) return;
314 //Check 2 or more selected objects
315 std::list<SPItem *>::iterator second(selected.begin());
316 ++second;
317 if (second == selected.end()) return;
320 std::vector< BBoxSort > sorted;
321 for (std::list<SPItem *>::iterator it(selected.begin());
322 it != selected.end();
323 ++it)
324 {
325 NR::Maybe<NR::Rect> bbox = sp_item_bbox_desktop(*it);
326 if (bbox) {
327 sorted.push_back(BBoxSort(*it, *bbox, _orientation, _kBegin, _kEnd));
328 }
329 }
330 //sort bbox by anchors
331 std::sort(sorted.begin(), sorted.end());
333 // see comment in ActionAlign above
334 int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
335 prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
337 unsigned int len = sorted.size();
338 bool changed = false;
339 if (_onInterSpace)
340 {
341 //overall bboxes span
342 float dist = (sorted.back().bbox.max()[_orientation] -
343 sorted.front().bbox.min()[_orientation]);
344 //space eaten by bboxes
345 float span = 0;
346 for (unsigned int i = 0; i < len; i++)
347 {
348 span += sorted[i].bbox.extent(_orientation);
349 }
350 //new distance between each bbox
351 float step = (dist - span) / (len - 1);
352 float pos = sorted.front().bbox.min()[_orientation];
353 for ( std::vector<BBoxSort> ::iterator it (sorted.begin());
354 it < sorted.end();
355 it ++ )
356 {
357 if (!NR_DF_TEST_CLOSE (pos, it->bbox.min()[_orientation], 1e-6)) {
358 NR::Point t(0.0, 0.0);
359 t[_orientation] = pos - it->bbox.min()[_orientation];
360 sp_item_move_rel(it->item, NR::translate(t));
361 changed = true;
362 }
363 pos += it->bbox.extent(_orientation);
364 pos += step;
365 }
366 }
367 else
368 {
369 //overall anchor span
370 float dist = sorted.back().anchor - sorted.front().anchor;
371 //distance between anchors
372 float step = dist / (len - 1);
374 for ( unsigned int i = 0; i < len ; i ++ )
375 {
376 BBoxSort & it(sorted[i]);
377 //new anchor position
378 float pos = sorted.front().anchor + i * step;
379 //Don't move if we are really close
380 if (!NR_DF_TEST_CLOSE (pos, it.anchor, 1e-6)) {
381 //Compute translation
382 NR::Point t(0.0, 0.0);
383 t[_orientation] = pos - it.anchor;
384 //translate
385 sp_item_move_rel(it.item, NR::translate(t));
386 changed = true;
387 }
388 }
389 }
391 // restore compensation setting
392 prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
394 if (changed) {
395 sp_document_done ( sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
396 _("Distribute"));
397 }
398 }
399 guint _index;
400 AlignAndDistribute &_dialog;
401 bool _onInterSpace;
402 NR::Dim2 _orientation;
404 double _kBegin;
405 double _kEnd;
407 };
410 class ActionNode : public Action {
411 public :
412 ActionNode(const Glib::ustring &id,
413 const Glib::ustring &tiptext,
414 guint column,
415 AlignAndDistribute &dialog,
416 NR::Dim2 orientation, bool distribute):
417 Action(id, tiptext, 0, column,
418 dialog.nodes_table(), dialog.tooltips(), dialog),
419 _orientation(orientation),
420 _distribute(distribute)
421 {}
423 private :
424 NR::Dim2 _orientation;
425 bool _distribute;
426 virtual void on_button_click()
427 {
429 if (!SP_ACTIVE_DESKTOP) return;
430 SPEventContext *event_context = sp_desktop_event_context(SP_ACTIVE_DESKTOP);
431 if (!SP_IS_NODE_CONTEXT (event_context)) return ;
433 if (_distribute)
434 SP_NODE_CONTEXT (event_context)->shape_editor->distribute(_orientation);
435 else
436 SP_NODE_CONTEXT (event_context)->shape_editor->align(_orientation);
438 }
439 };
441 class ActionRemoveOverlaps : public Action {
442 private:
443 Gtk::Label removeOverlapXGapLabel;
444 Gtk::Label removeOverlapYGapLabel;
445 Gtk::SpinButton removeOverlapXGap;
446 Gtk::SpinButton removeOverlapYGap;
448 public:
449 ActionRemoveOverlaps(Glib::ustring const &id,
450 Glib::ustring const &tiptext,
451 guint row,
452 guint column,
453 AlignAndDistribute &dialog) :
454 Action(id, tiptext, row, column + 4,
455 dialog.removeOverlap_table(), dialog.tooltips(), dialog)
456 {
457 dialog.removeOverlap_table().set_col_spacings(3);
459 removeOverlapXGap.set_digits(1);
460 removeOverlapXGap.set_size_request(60, -1);
461 removeOverlapXGap.set_increments(1.0, 5.0);
462 removeOverlapXGap.set_range(-1000.0, 1000.0);
463 removeOverlapXGap.set_value(0);
464 dialog.tooltips().set_tip(removeOverlapXGap,
465 _("Minimum horizontal gap (in px units) between bounding boxes"));
466 /* TRANSLATORS: Horizontal gap */
467 removeOverlapXGapLabel.set_label(_("H:"));
469 removeOverlapYGap.set_digits(1);
470 removeOverlapYGap.set_size_request(60, -1);
471 removeOverlapYGap.set_increments(1.0, 5.0);
472 removeOverlapYGap.set_range(-1000.0, 1000.0);
473 removeOverlapYGap.set_value(0);
474 dialog.tooltips().set_tip(removeOverlapYGap,
475 _("Minimum vertical gap (in px units) between bounding boxes"));
476 /* TRANSLATORS: Vertical gap */
477 removeOverlapYGapLabel.set_label(_("V:"));
479 dialog.removeOverlap_table().attach(removeOverlapXGapLabel, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL);
480 dialog.removeOverlap_table().attach(removeOverlapXGap, column+1, column+2, row, row+1, Gtk::FILL, Gtk::FILL);
481 dialog.removeOverlap_table().attach(removeOverlapYGapLabel, column+2, column+3, row, row+1, Gtk::FILL, Gtk::FILL);
482 dialog.removeOverlap_table().attach(removeOverlapYGap, column+3, column+4, row, row+1, Gtk::FILL, Gtk::FILL);
484 }
486 private :
487 virtual void on_button_click()
488 {
489 if (!SP_ACTIVE_DESKTOP) return;
491 // see comment in ActionAlign above
492 int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
493 prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
495 // xGap and yGap are the minimum space required between bounding rectangles.
496 double const xGap = removeOverlapXGap.get_value();
497 double const yGap = removeOverlapYGap.get_value();
498 removeoverlap(sp_desktop_selection(SP_ACTIVE_DESKTOP)->itemList(),
499 xGap, yGap);
501 // restore compensation setting
502 prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
504 sp_document_done(sp_desktop_document(SP_ACTIVE_DESKTOP), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
505 _("Remove overlaps"));
506 }
507 };
509 class ActionGraphLayout : public Action {
510 public:
511 ActionGraphLayout(Glib::ustring const &id,
512 Glib::ustring const &tiptext,
513 guint row,
514 guint column,
515 AlignAndDistribute &dialog) :
516 Action(id, tiptext, row, column + 4,
517 dialog.graphLayout_table(), dialog.tooltips(), dialog)
518 {}
520 private :
521 virtual void on_button_click()
522 {
523 if (!SP_ACTIVE_DESKTOP) return;
525 // see comment in ActionAlign above
526 int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
527 prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
529 graphlayout(sp_desktop_selection(SP_ACTIVE_DESKTOP)->itemList());
531 // restore compensation setting
532 prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
534 sp_document_done(sp_desktop_document(SP_ACTIVE_DESKTOP), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
535 _("Arrange connector network"));
536 }
537 };
539 class ActionUnclump : public Action {
540 public :
541 ActionUnclump(const Glib::ustring &id,
542 const Glib::ustring &tiptext,
543 guint row,
544 guint column,
545 AlignAndDistribute &dialog):
546 Action(id, tiptext, row, column,
547 dialog.distribute_table(), dialog.tooltips(), dialog)
548 {}
550 private :
551 virtual void on_button_click()
552 {
553 if (!SP_ACTIVE_DESKTOP) return;
555 // see comment in ActionAlign above
556 int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
557 prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
559 unclump ((GSList *) sp_desktop_selection(SP_ACTIVE_DESKTOP)->itemList());
561 // restore compensation setting
562 prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
564 sp_document_done (sp_desktop_document (SP_ACTIVE_DESKTOP), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
565 _("Unclump"));
566 }
567 };
569 class ActionRandomize : public Action {
570 public :
571 ActionRandomize(const Glib::ustring &id,
572 const Glib::ustring &tiptext,
573 guint row,
574 guint column,
575 AlignAndDistribute &dialog):
576 Action(id, tiptext, row, column,
577 dialog.distribute_table(), dialog.tooltips(), dialog)
578 {}
580 private :
581 virtual void on_button_click()
582 {
583 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
584 if (!desktop) return;
586 Inkscape::Selection *selection = sp_desktop_selection(desktop);
587 if (!selection) return;
589 using Inkscape::Util::GSListConstIterator;
590 std::list<SPItem *> selected;
591 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
592 if (selected.empty()) return;
594 //Check 2 or more selected objects
595 if (selected.size() < 2) return;
597 NR::Maybe<NR::Rect> sel_bbox = selection->bounds();
598 if (!sel_bbox) {
599 return;
600 }
602 // This bbox is cached between calls to randomize, so that there's no growth nor shrink
603 // nor drift on sequential randomizations. Discard cache on global (or better active
604 // desktop's) selection_change signal.
605 if (!_dialog.randomize_bbox) {
606 _dialog.randomize_bbox = *sel_bbox;
607 }
609 // see comment in ActionAlign above
610 int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
611 prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
613 for (std::list<SPItem *>::iterator it(selected.begin());
614 it != selected.end();
615 ++it)
616 {
617 sp_document_ensure_up_to_date(sp_desktop_document (desktop));
618 NR::Maybe<NR::Rect> item_box = sp_item_bbox_desktop (*it);
619 if (item_box) {
620 // find new center, staying within bbox
621 double x = _dialog.randomize_bbox->min()[NR::X] + item_box->extent(NR::X)/2 +
622 g_random_double_range (0, _dialog.randomize_bbox->extent(NR::X) - item_box->extent(NR::X));
623 double y = _dialog.randomize_bbox->min()[NR::Y] + item_box->extent(NR::Y)/2 +
624 g_random_double_range (0, _dialog.randomize_bbox->extent(NR::Y) - item_box->extent(NR::Y));
625 // displacement is the new center minus old:
626 NR::Point t = NR::Point (x, y) - 0.5*(item_box->max() + item_box->min());
627 sp_item_move_rel(*it, NR::translate(t));
628 }
629 }
631 // restore compensation setting
632 prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
634 sp_document_done (sp_desktop_document (SP_ACTIVE_DESKTOP), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
635 _("Randomize positions"));
636 }
637 };
639 struct Baselines
640 {
641 SPItem *_item;
642 NR::Point _base;
643 NR::Dim2 _orientation;
644 Baselines(SPItem *item, NR::Point base, NR::Dim2 orientation) :
645 _item (item),
646 _base (base),
647 _orientation (orientation)
648 {}
649 };
651 bool operator< (const Baselines &a, const Baselines &b)
652 {
653 return (a._base[a._orientation] < b._base[b._orientation]);
654 }
656 class ActionBaseline : public Action {
657 public :
658 ActionBaseline(const Glib::ustring &id,
659 const Glib::ustring &tiptext,
660 guint row,
661 guint column,
662 AlignAndDistribute &dialog,
663 Gtk::Table &table,
664 NR::Dim2 orientation, bool distribute):
665 Action(id, tiptext, row, column,
666 table, dialog.tooltips(), dialog),
667 _orientation(orientation),
668 _distribute(distribute)
669 {}
671 private :
672 NR::Dim2 _orientation;
673 bool _distribute;
674 virtual void on_button_click()
675 {
676 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
677 if (!desktop) return;
679 Inkscape::Selection *selection = sp_desktop_selection(desktop);
680 if (!selection) return;
682 using Inkscape::Util::GSListConstIterator;
683 std::list<SPItem *> selected;
684 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
685 if (selected.empty()) return;
687 //Check 2 or more selected objects
688 if (selected.size() < 2) return;
690 NR::Point b_min = NR::Point (HUGE_VAL, HUGE_VAL);
691 NR::Point b_max = NR::Point (-HUGE_VAL, -HUGE_VAL);
693 std::vector<Baselines> sorted;
695 for (std::list<SPItem *>::iterator it(selected.begin());
696 it != selected.end();
697 ++it)
698 {
699 if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
700 Inkscape::Text::Layout const *layout = te_get_layout(*it);
701 NR::Point base = layout->characterAnchorPoint(layout->begin()) * sp_item_i2d_affine(*it);
702 if (base[NR::X] < b_min[NR::X]) b_min[NR::X] = base[NR::X];
703 if (base[NR::Y] < b_min[NR::Y]) b_min[NR::Y] = base[NR::Y];
704 if (base[NR::X] > b_max[NR::X]) b_max[NR::X] = base[NR::X];
705 if (base[NR::Y] > b_max[NR::Y]) b_max[NR::Y] = base[NR::Y];
707 Baselines b (*it, base, _orientation);
708 sorted.push_back(b);
709 }
710 }
712 if (sorted.size() <= 1) return;
714 //sort baselines
715 std::sort(sorted.begin(), sorted.end());
717 bool changed = false;
719 if (_distribute) {
720 double step = (b_max[_orientation] - b_min[_orientation])/(sorted.size() - 1);
721 for (unsigned int i = 0; i < sorted.size(); i++) {
722 SPItem *item = sorted[i]._item;
723 NR::Point base = sorted[i]._base;
724 NR::Point t(0.0, 0.0);
725 t[_orientation] = b_min[_orientation] + step * i - base[_orientation];
726 sp_item_move_rel(item, NR::translate(t));
727 changed = true;
728 }
730 if (changed) {
731 sp_document_done (sp_desktop_document (SP_ACTIVE_DESKTOP), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
732 _("Distribute text baselines"));
733 }
735 } else {
736 for (std::list<SPItem *>::iterator it(selected.begin());
737 it != selected.end();
738 ++it)
739 {
740 if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
741 Inkscape::Text::Layout const *layout = te_get_layout(*it);
742 NR::Point base = layout->characterAnchorPoint(layout->begin()) * sp_item_i2d_affine(*it);
743 NR::Point t(0.0, 0.0);
744 t[_orientation] = b_min[_orientation] - base[_orientation];
745 sp_item_move_rel(*it, NR::translate(t));
746 changed = true;
747 }
748 }
750 if (changed) {
751 sp_document_done (sp_desktop_document (SP_ACTIVE_DESKTOP), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
752 _("Align text baselines"));
753 }
754 }
755 }
756 };
760 void on_tool_changed(Inkscape::Application *inkscape, SPEventContext *context, AlignAndDistribute *daad)
761 {
762 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
763 if (desktop && sp_desktop_event_context(desktop))
764 daad->setMode(tools_active(desktop) == TOOLS_NODES);
765 }
767 void on_selection_changed(Inkscape::Application *inkscape, Inkscape::Selection *selection, AlignAndDistribute *daad)
768 {
769 daad->randomize_bbox = NR::Nothing();
770 }
772 /////////////////////////////////////////////////////////
777 AlignAndDistribute::AlignAndDistribute(Behavior::BehaviorFactory behavior_factory)
778 : Dialog (behavior_factory, "dialogs.align", SP_VERB_DIALOG_ALIGN_DISTRIBUTE),
779 randomize_bbox(NR::Nothing()),
780 _alignFrame(_("Align")),
781 _distributeFrame(_("Distribute")),
782 _removeOverlapFrame(_("Remove overlaps")),
783 _graphLayoutFrame(_("Connector network layout")),
784 _nodesFrame(_("Nodes")),
785 _alignTable(2, 6, true),
786 _distributeTable(3, 6, true),
787 _removeOverlapTable(1, 5, false),
788 _graphLayoutTable(1, 5, false),
789 _nodesTable(1, 4, true),
790 _anchorLabel(_("Relative to: "))
791 {
793 //Instanciate the align buttons
794 addAlignButton("al_left_out",
795 _("Align right sides of objects to left side of anchor"),
796 0, 0);
797 addAlignButton("al_left_in",
798 _("Align left sides"),
799 0, 1);
800 addAlignButton("al_center_hor",
801 _("Center on vertical axis"),
802 0, 2);
803 addAlignButton("al_right_in",
804 _("Align right sides"),
805 0, 3);
806 addAlignButton("al_right_out",
807 _("Align left sides of objects to right side of anchor"),
808 0, 4);
809 addAlignButton("al_top_out",
810 _("Align bottoms of objects to top of anchor"),
811 1, 0);
812 addAlignButton("al_top_in",
813 _("Align tops"),
814 1, 1);
815 addAlignButton("al_center_ver",
816 _("Center on horizontal axis"),
817 1, 2);
818 addAlignButton("al_bottom_in",
819 _("Align bottoms"),
820 1, 3);
821 addAlignButton("al_bottom_out",
822 _("Align tops of objects to bottom of anchor"),
823 1, 4);
825 //Baseline aligns
826 addBaselineButton("al_baselines_vert",
827 _("Align baseline anchors of texts vertically"),
828 0, 5, this->align_table(), NR::X, false);
829 addBaselineButton("al_baselines_hor",
830 _("Align baseline anchors of texts horizontally"),
831 1, 5, this->align_table(), NR::Y, false);
833 //The distribute buttons
834 addDistributeButton("distribute_hdist",
835 _("Make horizontal gaps between objects equal"),
836 0, 4, true, NR::X, .5, .5);
838 addDistributeButton("distribute_left",
839 _("Distribute left sides equidistantly"),
840 0, 1, false, NR::X, 1., 0.);
841 addDistributeButton("distribute_hcentre",
842 _("Distribute centers equidistantly horizontally"),
843 0, 2, false, NR::X, .5, .5);
844 addDistributeButton("distribute_right",
845 _("Distribute right sides equidistantly"),
846 0, 3, false, NR::X, 0., 1.);
848 addDistributeButton("distribute_vdist",
849 _("Make vertical gaps between objects equal"),
850 1, 4, true, NR::Y, .5, .5);
852 addDistributeButton("distribute_top",
853 _("Distribute tops equidistantly"),
854 1, 1, false, NR::Y, 0, 1);
855 addDistributeButton("distribute_vcentre",
856 _("Distribute centers equidistantly vertically"),
857 1, 2, false, NR::Y, .5, .5);
858 addDistributeButton("distribute_bottom",
859 _("Distribute bottoms equidistantly"),
860 1, 3, false, NR::Y, 1., 0.);
862 //Baseline distribs
863 addBaselineButton("distribute_baselines_hor",
864 _("Distribute baseline anchors of texts horizontally"),
865 0, 5, this->distribute_table(), NR::X, true);
866 addBaselineButton("distribute_baselines_vert",
867 _("Distribute baseline anchors of texts vertically"),
868 1, 5, this->distribute_table(), NR::Y, true);
870 //Randomize & Unclump
871 addRandomizeButton("distribute_randomize",
872 _("Randomize centers in both dimensions"),
873 2, 2);
874 addUnclumpButton("unclump",
875 _("Unclump objects: try to equalize edge-to-edge distances"),
876 2, 4);
878 //Remove overlaps
879 addRemoveOverlapsButton("remove_overlaps",
880 _("Move objects as little as possible so that their bounding boxes do not overlap"),
881 0, 0);
882 //Graph Layout
883 addGraphLayoutButton("graph_layout",
884 _("Nicely arrange selected connector network"),
885 0, 0);
887 //Node Mode buttons
888 addNodeButton("node_halign",
889 _("Align selected nodes horizontally"),
890 0, NR::X, false);
891 addNodeButton("node_valign",
892 _("Align selected nodes vertically"),
893 1, NR::Y, false);
894 addNodeButton("node_hdistribute",
895 _("Distribute selected nodes horizontally"),
896 2, NR::X, true);
897 addNodeButton("node_vdistribute",
898 _("Distribute selected nodes vertically"),
899 3, NR::Y, true);
901 //Rest of the widgetry
903 _combo.append_text(_("Last selected"));
904 _combo.append_text(_("First selected"));
905 _combo.append_text(_("Biggest item"));
906 _combo.append_text(_("Smallest item"));
907 _combo.append_text(_("Page"));
908 _combo.append_text(_("Drawing"));
909 _combo.append_text(_("Selection"));
911 _combo.set_active(6);
912 _combo.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_ref_change));
914 _anchorBox.pack_start(_anchorLabel);
915 _anchorBox.pack_start(_combo);
917 _alignBox.pack_start(_anchorBox);
918 _alignBox.pack_start(_alignTable);
920 _alignFrame.add(_alignBox);
921 _distributeFrame.add(_distributeTable);
922 _removeOverlapFrame.add(_removeOverlapTable);
923 _graphLayoutFrame.add(_graphLayoutTable);
924 _nodesFrame.add(_nodesTable);
926 // Top level vbox
927 Gtk::VBox *vbox = get_vbox();
928 vbox->set_spacing(4);
930 // Notebook for individual transformations
932 vbox->pack_start(_alignFrame, true, true);
933 vbox->pack_start(_distributeFrame, true, true);
934 vbox->pack_start(_removeOverlapFrame, true, true);
935 vbox->pack_start(_graphLayoutFrame, true, true);
936 vbox->pack_start(_nodesFrame, true, true);
938 //Connect to the global tool change signal
939 g_signal_connect (G_OBJECT (INKSCAPE), "set_eventcontext", G_CALLBACK (on_tool_changed), this);
941 // Connect to the global selection change, to invalidate cached randomize_bbox
942 g_signal_connect (G_OBJECT (INKSCAPE), "change_selection", G_CALLBACK (on_selection_changed), this);
943 randomize_bbox = NR::Nothing();
945 show_all_children();
947 on_tool_changed (NULL, NULL, this); // set current mode
948 }
950 AlignAndDistribute::~AlignAndDistribute()
951 {
952 sp_signal_disconnect_by_data (G_OBJECT (INKSCAPE), this);
954 for (std::list<Action *>::iterator it = _actionList.begin();
955 it != _actionList.end();
956 it ++)
957 delete *it;
958 }
960 void AlignAndDistribute::on_ref_change(){
961 //Make blink the master
962 }
967 void AlignAndDistribute::setMode(bool nodeEdit)
968 {
969 //Act on widgets used in node mode
970 void ( Gtk::Widget::*mNode) () = nodeEdit ?
971 &Gtk::Widget::show_all : &Gtk::Widget::hide_all;
973 //Act on widgets used in selection mode
974 void ( Gtk::Widget::*mSel) () = nodeEdit ?
975 &Gtk::Widget::hide_all : &Gtk::Widget::show_all;
978 ((_alignFrame).*(mSel))();
979 ((_distributeFrame).*(mSel))();
980 ((_removeOverlapFrame).*(mSel))();
981 ((_graphLayoutFrame).*(mSel))();
982 ((_nodesFrame).*(mNode))();
984 }
985 void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext,
986 guint row, guint col)
987 {
988 _actionList.push_back(
989 new ActionAlign(
990 id, tiptext, row, col,
991 *this , col + row * 5));
992 }
993 void AlignAndDistribute::addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext,
994 guint row, guint col, bool onInterSpace,
995 NR::Dim2 orientation, float kBegin, float kEnd)
996 {
997 _actionList.push_back(
998 new ActionDistribute(
999 id, tiptext, row, col, *this ,
1000 onInterSpace, orientation,
1001 kBegin, kEnd
1002 )
1003 );
1004 }
1006 void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1007 guint col, NR::Dim2 orientation, bool distribute)
1008 {
1009 _actionList.push_back(
1010 new ActionNode(
1011 id, tiptext, col,
1012 *this, orientation, distribute));
1013 }
1015 void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext,
1016 guint row, guint col)
1017 {
1018 _actionList.push_back(
1019 new ActionRemoveOverlaps(
1020 id, tiptext, row, col, *this)
1021 );
1022 }
1024 void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext,
1025 guint row, guint col)
1026 {
1027 _actionList.push_back(
1028 new ActionGraphLayout(
1029 id, tiptext, row, col, *this)
1030 );
1031 }
1033 void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext,
1034 guint row, guint col)
1035 {
1036 _actionList.push_back(
1037 new ActionUnclump(
1038 id, tiptext, row, col, *this)
1039 );
1040 }
1042 void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1043 guint row, guint col)
1044 {
1045 _actionList.push_back(
1046 new ActionRandomize(
1047 id, tiptext, row, col, *this)
1048 );
1049 }
1051 void AlignAndDistribute::addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext,
1052 guint row, guint col, Gtk::Table &table, NR::Dim2 orientation, bool distribute)
1053 {
1054 _actionList.push_back(
1055 new ActionBaseline(
1056 id, tiptext, row, col,
1057 *this, table, orientation, distribute));
1058 }
1063 std::list<SPItem *>::iterator AlignAndDistribute::find_master( std::list<SPItem *> &list, bool horizontal){
1064 std::list<SPItem *>::iterator master = list.end();
1065 switch (getAlignTarget()) {
1066 case LAST:
1067 return list.begin();
1068 break;
1070 case FIRST:
1071 return --(list.end());
1072 break;
1074 case BIGGEST:
1075 {
1076 gdouble max = -1e18;
1077 for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1078 NR::Maybe<NR::Rect> b = sp_item_bbox_desktop (*it);
1079 if (b) {
1080 gdouble dim = b->extent(horizontal ? NR::X : NR::Y);
1081 if (dim > max) {
1082 max = dim;
1083 master = it;
1084 }
1085 }
1086 }
1087 return master;
1088 break;
1089 }
1091 case SMALLEST:
1092 {
1093 gdouble max = 1e18;
1094 for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1095 NR::Maybe<NR::Rect> b = sp_item_bbox_desktop (*it);
1096 if (b) {
1097 gdouble dim = b->extent(horizontal ? NR::X : NR::Y);
1098 if (dim < max) {
1099 max = dim;
1100 master = it;
1101 }
1102 }
1103 }
1104 return master;
1105 break;
1106 }
1108 default:
1109 g_assert_not_reached ();
1110 break;
1112 } // end of switch statement
1113 return master;
1114 }
1116 AlignAndDistribute::AlignTarget AlignAndDistribute::getAlignTarget()const {
1117 return AlignTarget(_combo.get_active_row_number());
1118 }
1122 } // namespace Dialog
1123 } // namespace UI
1124 } // namespace Inkscape
1126 /*
1127 Local Variables:
1128 mode:c++
1129 c-file-style:"stroustrup"
1130 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1131 indent-tabs-mode:nil
1132 fill-column:99
1133 End:
1134 */
1135 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :