c1f4a10a11620a9124eeeef7eb4866abc8441906
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::Rect b = selection->bounds();
187 mp = NR::Point(a.mx0 * b.min()[NR::X] + a.mx1 * b.max()[NR::X],
188 a.my0 * b.min()[NR::Y] + a.my1 * b.max()[NR::Y]);
189 break;
190 }
192 default:
193 g_assert_not_reached ();
194 break;
195 }; // end of switch
197 // Top hack: temporarily set clone compensation to unmoved, so that we can align/distribute
198 // clones with their original (and the move of the original does not disturb the
199 // clones). The only problem with this is that if there are outside-of-selection clones of
200 // a selected original, they will be unmoved too, possibly contrary to user's
201 // expecation. However this is a minor point compared to making align/distribute always
202 // work as expected, and "unmoved" is the default option anyway.
203 int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
204 prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
206 bool changed = false;
207 //Move each item in the selected list
208 for (std::list<SPItem *>::iterator it(selected.begin());
209 it != selected.end();
210 it++)
211 {
212 sp_document_ensure_up_to_date(sp_desktop_document (desktop));
213 NR::Maybe<NR::Rect> b = sp_item_bbox_desktop (*it);
214 if (b) {
215 NR::Point const sp(a.sx0 * b->min()[NR::X] + a.sx1 * b->max()[NR::X],
216 a.sy0 * b->min()[NR::Y] + a.sy1 * b->max()[NR::Y]);
217 NR::Point const mp_rel( mp - sp );
218 if (LInfty(mp_rel) > 1e-9) {
219 sp_item_move_rel(*it, NR::translate(mp_rel));
220 changed = true;
221 }
222 }
223 }
225 // restore compensation setting
226 prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
228 if (changed) {
229 sp_document_done ( sp_desktop_document (desktop) , SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
230 _("Align"));
231 }
234 }
235 guint _index;
236 AlignAndDistribute &_dialog;
238 static const Coeffs _allCoeffs[10];
240 };
241 ActionAlign::Coeffs const ActionAlign::_allCoeffs[10] = {
242 {1., 0., 0., 0., 0., 1., 0., 0.},
243 {1., 0., 0., 0., 1., 0., 0., 0.},
244 {.5, .5, 0., 0., .5, .5, 0., 0.},
245 {0., 1., 0., 0., 0., 1., 0., 0.},
246 {0., 1., 0., 0., 1., 0., 0., 0.},
247 {0., 0., 0., 1., 0., 0., 1., 0.},
248 {0., 0., 0., 1., 0., 0., 0., 1.},
249 {0., 0., .5, .5, 0., 0., .5, .5},
250 {0., 0., 1., 0., 0., 0., 1., 0.},
251 {0., 0., 1., 0., 0., 0., 0., 1.}
252 };
254 struct BBoxSort
255 {
256 SPItem *item;
257 float anchor;
258 NR::Rect bbox;
259 BBoxSort(SPItem *pItem, NR::Rect bounds, NR::Dim2 orientation, double kBegin, double kEnd) :
260 item(pItem),
261 bbox (bounds)
262 {
263 anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation];
264 }
265 BBoxSort(const BBoxSort &rhs):
266 //NOTE : this copy ctor is called O(sort) when sorting the vector
267 //this is bad. The vector should be a vector of pointers.
268 //But I'll wait the bohem GC before doing that
269 item(rhs.item), anchor(rhs.anchor), bbox(rhs.bbox) {
270 }
271 };
272 bool operator< (const BBoxSort &a, const BBoxSort &b)
273 {
274 return (a.anchor < b.anchor);
275 }
277 class ActionDistribute : public Action {
278 public :
279 ActionDistribute(const Glib::ustring &id,
280 const Glib::ustring &tiptext,
281 guint row, guint column,
282 AlignAndDistribute &dialog,
283 bool onInterSpace,
284 NR::Dim2 orientation,
285 double kBegin, double kEnd
286 ):
287 Action(id, tiptext, row, column,
288 dialog.distribute_table(), dialog.tooltips(), dialog),
289 _dialog(dialog),
290 _onInterSpace(onInterSpace),
291 _orientation(orientation),
292 _kBegin(kBegin),
293 _kEnd( kEnd)
294 {}
296 private :
297 virtual void on_button_click() {
298 //Retreive selected objects
299 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
300 if (!desktop) return;
302 Inkscape::Selection *selection = sp_desktop_selection(desktop);
303 if (!selection) return;
305 using Inkscape::Util::GSListConstIterator;
306 std::list<SPItem *> selected;
307 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
308 if (selected.empty()) return;
310 //Check 2 or more selected objects
311 std::list<SPItem *>::iterator second(selected.begin());
312 ++second;
313 if (second == selected.end()) return;
316 std::vector< BBoxSort > sorted;
317 for (std::list<SPItem *>::iterator it(selected.begin());
318 it != selected.end();
319 ++it)
320 {
321 NR::Maybe<NR::Rect> bbox = sp_item_bbox_desktop(*it);
322 if (bbox) {
323 sorted.push_back(BBoxSort(*it, *bbox, _orientation, _kBegin, _kEnd));
324 }
325 }
326 //sort bbox by anchors
327 std::sort(sorted.begin(), sorted.end());
329 // see comment in ActionAlign above
330 int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
331 prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
333 unsigned int len = sorted.size();
334 bool changed = false;
335 if (_onInterSpace)
336 {
337 //overall bboxes span
338 float dist = (sorted.back().bbox.max()[_orientation] -
339 sorted.front().bbox.min()[_orientation]);
340 //space eaten by bboxes
341 float span = 0;
342 for (unsigned int i = 0; i < len; i++)
343 {
344 span += sorted[i].bbox.extent(_orientation);
345 }
346 //new distance between each bbox
347 float step = (dist - span) / (len - 1);
348 float pos = sorted.front().bbox.min()[_orientation];
349 for ( std::vector<BBoxSort> ::iterator it (sorted.begin());
350 it < sorted.end();
351 it ++ )
352 {
353 if (!NR_DF_TEST_CLOSE (pos, it->bbox.min()[_orientation], 1e-6)) {
354 NR::Point t(0.0, 0.0);
355 t[_orientation] = pos - it->bbox.min()[_orientation];
356 sp_item_move_rel(it->item, NR::translate(t));
357 changed = true;
358 }
359 pos += it->bbox.extent(_orientation);
360 pos += step;
361 }
362 }
363 else
364 {
365 //overall anchor span
366 float dist = sorted.back().anchor - sorted.front().anchor;
367 //distance between anchors
368 float step = dist / (len - 1);
370 for ( unsigned int i = 0; i < len ; i ++ )
371 {
372 BBoxSort & it(sorted[i]);
373 //new anchor position
374 float pos = sorted.front().anchor + i * step;
375 //Don't move if we are really close
376 if (!NR_DF_TEST_CLOSE (pos, it.anchor, 1e-6)) {
377 //Compute translation
378 NR::Point t(0.0, 0.0);
379 t[_orientation] = pos - it.anchor;
380 //translate
381 sp_item_move_rel(it.item, NR::translate(t));
382 changed = true;
383 }
384 }
385 }
387 // restore compensation setting
388 prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
390 if (changed) {
391 sp_document_done ( sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
392 _("Distribute"));
393 }
394 }
395 guint _index;
396 AlignAndDistribute &_dialog;
397 bool _onInterSpace;
398 NR::Dim2 _orientation;
400 double _kBegin;
401 double _kEnd;
403 };
406 class ActionNode : public Action {
407 public :
408 ActionNode(const Glib::ustring &id,
409 const Glib::ustring &tiptext,
410 guint column,
411 AlignAndDistribute &dialog,
412 NR::Dim2 orientation, bool distribute):
413 Action(id, tiptext, 0, column,
414 dialog.nodes_table(), dialog.tooltips(), dialog),
415 _orientation(orientation),
416 _distribute(distribute)
417 {}
419 private :
420 NR::Dim2 _orientation;
421 bool _distribute;
422 virtual void on_button_click()
423 {
425 if (!SP_ACTIVE_DESKTOP) return;
426 SPEventContext *event_context = sp_desktop_event_context(SP_ACTIVE_DESKTOP);
427 if (!SP_IS_NODE_CONTEXT (event_context)) return ;
429 if (_distribute)
430 SP_NODE_CONTEXT (event_context)->shape_editor->distribute(_orientation);
431 else
432 SP_NODE_CONTEXT (event_context)->shape_editor->align(_orientation);
434 }
435 };
437 class ActionRemoveOverlaps : public Action {
438 private:
439 Gtk::Label removeOverlapXGapLabel;
440 Gtk::Label removeOverlapYGapLabel;
441 Gtk::SpinButton removeOverlapXGap;
442 Gtk::SpinButton removeOverlapYGap;
444 public:
445 ActionRemoveOverlaps(Glib::ustring const &id,
446 Glib::ustring const &tiptext,
447 guint row,
448 guint column,
449 AlignAndDistribute &dialog) :
450 Action(id, tiptext, row, column + 4,
451 dialog.removeOverlap_table(), dialog.tooltips(), dialog)
452 {
453 dialog.removeOverlap_table().set_col_spacings(3);
455 removeOverlapXGap.set_digits(1);
456 removeOverlapXGap.set_size_request(60, -1);
457 removeOverlapXGap.set_increments(1.0, 5.0);
458 removeOverlapXGap.set_range(-1000.0, 1000.0);
459 removeOverlapXGap.set_value(0);
460 dialog.tooltips().set_tip(removeOverlapXGap,
461 _("Minimum horizontal gap (in px units) between bounding boxes"));
462 /* TRANSLATORS: Horizontal gap */
463 removeOverlapXGapLabel.set_label(_("H:"));
465 removeOverlapYGap.set_digits(1);
466 removeOverlapYGap.set_size_request(60, -1);
467 removeOverlapYGap.set_increments(1.0, 5.0);
468 removeOverlapYGap.set_range(-1000.0, 1000.0);
469 removeOverlapYGap.set_value(0);
470 dialog.tooltips().set_tip(removeOverlapYGap,
471 _("Minimum vertical gap (in px units) between bounding boxes"));
472 /* TRANSLATORS: Vertical gap */
473 removeOverlapYGapLabel.set_label(_("V:"));
475 dialog.removeOverlap_table().attach(removeOverlapXGapLabel, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL);
476 dialog.removeOverlap_table().attach(removeOverlapXGap, column+1, column+2, row, row+1, Gtk::FILL, Gtk::FILL);
477 dialog.removeOverlap_table().attach(removeOverlapYGapLabel, column+2, column+3, row, row+1, Gtk::FILL, Gtk::FILL);
478 dialog.removeOverlap_table().attach(removeOverlapYGap, column+3, column+4, row, row+1, Gtk::FILL, Gtk::FILL);
480 }
482 private :
483 virtual void on_button_click()
484 {
485 if (!SP_ACTIVE_DESKTOP) return;
487 // see comment in ActionAlign above
488 int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
489 prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
491 // xGap and yGap are the minimum space required between bounding rectangles.
492 double const xGap = removeOverlapXGap.get_value();
493 double const yGap = removeOverlapYGap.get_value();
494 removeoverlap(sp_desktop_selection(SP_ACTIVE_DESKTOP)->itemList(),
495 xGap, yGap);
497 // restore compensation setting
498 prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
500 sp_document_done(sp_desktop_document(SP_ACTIVE_DESKTOP), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
501 _("Remove overlaps"));
502 }
503 };
505 class ActionGraphLayout : public Action {
506 public:
507 ActionGraphLayout(Glib::ustring const &id,
508 Glib::ustring const &tiptext,
509 guint row,
510 guint column,
511 AlignAndDistribute &dialog) :
512 Action(id, tiptext, row, column + 4,
513 dialog.graphLayout_table(), dialog.tooltips(), dialog)
514 {}
516 private :
517 virtual void on_button_click()
518 {
519 if (!SP_ACTIVE_DESKTOP) return;
521 // see comment in ActionAlign above
522 int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
523 prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
525 graphlayout(sp_desktop_selection(SP_ACTIVE_DESKTOP)->itemList());
527 // restore compensation setting
528 prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
530 sp_document_done(sp_desktop_document(SP_ACTIVE_DESKTOP), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
531 _("Arrange connector network"));
532 }
533 };
535 class ActionUnclump : public Action {
536 public :
537 ActionUnclump(const Glib::ustring &id,
538 const Glib::ustring &tiptext,
539 guint row,
540 guint column,
541 AlignAndDistribute &dialog):
542 Action(id, tiptext, row, column,
543 dialog.distribute_table(), dialog.tooltips(), dialog)
544 {}
546 private :
547 virtual void on_button_click()
548 {
549 if (!SP_ACTIVE_DESKTOP) return;
551 // see comment in ActionAlign above
552 int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
553 prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
555 unclump ((GSList *) sp_desktop_selection(SP_ACTIVE_DESKTOP)->itemList());
557 // restore compensation setting
558 prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
560 sp_document_done (sp_desktop_document (SP_ACTIVE_DESKTOP), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
561 _("Unclump"));
562 }
563 };
565 class ActionRandomize : public Action {
566 public :
567 ActionRandomize(const Glib::ustring &id,
568 const Glib::ustring &tiptext,
569 guint row,
570 guint column,
571 AlignAndDistribute &dialog):
572 Action(id, tiptext, row, column,
573 dialog.distribute_table(), dialog.tooltips(), dialog)
574 {}
576 private :
577 virtual void on_button_click()
578 {
579 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
580 if (!desktop) return;
582 Inkscape::Selection *selection = sp_desktop_selection(desktop);
583 if (!selection) return;
585 using Inkscape::Util::GSListConstIterator;
586 std::list<SPItem *> selected;
587 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
588 if (selected.empty()) return;
590 //Check 2 or more selected objects
591 if (selected.size() < 2) return;
593 // This bbox is cached between calls to randomize, so that there's no growth nor shrink
594 // nor drift on sequential randomizations. Discard cache on global (or better active
595 // desktop's) selection_change signal.
596 if (!_dialog.randomize_bbox_set) {
597 _dialog.randomize_bbox = selection->bounds();
598 _dialog.randomize_bbox_set = true;
599 }
601 // see comment in ActionAlign above
602 int saved_compensation = prefs_get_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
603 prefs_set_int_attribute("options.clonecompensation", "value", SP_CLONE_COMPENSATION_UNMOVED);
605 for (std::list<SPItem *>::iterator it(selected.begin());
606 it != selected.end();
607 ++it)
608 {
609 sp_document_ensure_up_to_date(sp_desktop_document (desktop));
610 NR::Maybe<NR::Rect> item_box = sp_item_bbox_desktop (*it);
611 if (item_box) {
612 // find new center, staying within bbox
613 double x = _dialog.randomize_bbox.min()[NR::X] + item_box->extent(NR::X)/2 +
614 g_random_double_range (0, _dialog.randomize_bbox.extent(NR::X) - item_box->extent(NR::X));
615 double y = _dialog.randomize_bbox.min()[NR::Y] + item_box->extent(NR::Y)/2 +
616 g_random_double_range (0, _dialog.randomize_bbox.extent(NR::Y) - item_box->extent(NR::Y));
617 // displacement is the new center minus old:
618 NR::Point t = NR::Point (x, y) - 0.5*(item_box->max() + item_box->min());
619 sp_item_move_rel(*it, NR::translate(t));
620 }
621 }
623 // restore compensation setting
624 prefs_set_int_attribute("options.clonecompensation", "value", saved_compensation);
626 sp_document_done (sp_desktop_document (SP_ACTIVE_DESKTOP), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
627 _("Randomize positions"));
628 }
629 };
631 struct Baselines
632 {
633 SPItem *_item;
634 NR::Point _base;
635 NR::Dim2 _orientation;
636 Baselines(SPItem *item, NR::Point base, NR::Dim2 orientation) :
637 _item (item),
638 _base (base),
639 _orientation (orientation)
640 {}
641 };
643 bool operator< (const Baselines &a, const Baselines &b)
644 {
645 return (a._base[a._orientation] < b._base[b._orientation]);
646 }
648 class ActionBaseline : public Action {
649 public :
650 ActionBaseline(const Glib::ustring &id,
651 const Glib::ustring &tiptext,
652 guint row,
653 guint column,
654 AlignAndDistribute &dialog,
655 Gtk::Table &table,
656 NR::Dim2 orientation, bool distribute):
657 Action(id, tiptext, row, column,
658 table, dialog.tooltips(), dialog),
659 _orientation(orientation),
660 _distribute(distribute)
661 {}
663 private :
664 NR::Dim2 _orientation;
665 bool _distribute;
666 virtual void on_button_click()
667 {
668 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
669 if (!desktop) return;
671 Inkscape::Selection *selection = sp_desktop_selection(desktop);
672 if (!selection) return;
674 using Inkscape::Util::GSListConstIterator;
675 std::list<SPItem *> selected;
676 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
677 if (selected.empty()) return;
679 //Check 2 or more selected objects
680 if (selected.size() < 2) return;
682 NR::Point b_min = NR::Point (HUGE_VAL, HUGE_VAL);
683 NR::Point b_max = NR::Point (-HUGE_VAL, -HUGE_VAL);
685 std::vector<Baselines> sorted;
687 for (std::list<SPItem *>::iterator it(selected.begin());
688 it != selected.end();
689 ++it)
690 {
691 if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
692 Inkscape::Text::Layout const *layout = te_get_layout(*it);
693 NR::Point base = layout->characterAnchorPoint(layout->begin()) * sp_item_i2d_affine(*it);
694 if (base[NR::X] < b_min[NR::X]) b_min[NR::X] = base[NR::X];
695 if (base[NR::Y] < b_min[NR::Y]) b_min[NR::Y] = base[NR::Y];
696 if (base[NR::X] > b_max[NR::X]) b_max[NR::X] = base[NR::X];
697 if (base[NR::Y] > b_max[NR::Y]) b_max[NR::Y] = base[NR::Y];
699 Baselines b (*it, base, _orientation);
700 sorted.push_back(b);
701 }
702 }
704 if (sorted.size() <= 1) return;
706 //sort baselines
707 std::sort(sorted.begin(), sorted.end());
709 bool changed = false;
711 if (_distribute) {
712 double step = (b_max[_orientation] - b_min[_orientation])/(sorted.size() - 1);
713 for (unsigned int i = 0; i < sorted.size(); i++) {
714 SPItem *item = sorted[i]._item;
715 NR::Point base = sorted[i]._base;
716 NR::Point t(0.0, 0.0);
717 t[_orientation] = b_min[_orientation] + step * i - base[_orientation];
718 sp_item_move_rel(item, NR::translate(t));
719 changed = true;
720 }
722 if (changed) {
723 sp_document_done (sp_desktop_document (SP_ACTIVE_DESKTOP), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
724 _("Distribute text baselines"));
725 }
727 } else {
728 for (std::list<SPItem *>::iterator it(selected.begin());
729 it != selected.end();
730 ++it)
731 {
732 if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
733 Inkscape::Text::Layout const *layout = te_get_layout(*it);
734 NR::Point base = layout->characterAnchorPoint(layout->begin()) * sp_item_i2d_affine(*it);
735 NR::Point t(0.0, 0.0);
736 t[_orientation] = b_min[_orientation] - base[_orientation];
737 sp_item_move_rel(*it, NR::translate(t));
738 changed = true;
739 }
740 }
742 if (changed) {
743 sp_document_done (sp_desktop_document (SP_ACTIVE_DESKTOP), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
744 _("Align text baselines"));
745 }
746 }
747 }
748 };
752 void on_tool_changed(Inkscape::Application *inkscape, SPEventContext *context, AlignAndDistribute *daad)
753 {
754 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
755 if (desktop && sp_desktop_event_context(desktop))
756 daad->setMode(tools_active(desktop) == TOOLS_NODES);
757 }
759 void on_selection_changed(Inkscape::Application *inkscape, Inkscape::Selection *selection, AlignAndDistribute *daad)
760 {
761 daad->randomize_bbox_set = false;
762 }
764 /////////////////////////////////////////////////////////
769 AlignAndDistribute::AlignAndDistribute()
770 : Dialog ("dialogs.align", SP_VERB_DIALOG_ALIGN_DISTRIBUTE),
771 randomize_bbox (NR::Point (0, 0), NR::Point (0, 0)),
772 _alignFrame(_("Align")),
773 _distributeFrame(_("Distribute")),
774 _removeOverlapFrame(_("Remove overlaps")),
775 _graphLayoutFrame(_("Connector network layout")),
776 _nodesFrame(_("Nodes")),
777 _alignTable(2, 6, true),
778 _distributeTable(3, 6, true),
779 _removeOverlapTable(1, 5, false),
780 _graphLayoutTable(1, 5, false),
781 _nodesTable(1, 4, true),
782 _anchorLabel(_("Relative to: "))
783 {
785 //Instanciate the align buttons
786 addAlignButton("al_left_out",
787 _("Align right sides of objects to left side of anchor"),
788 0, 0);
789 addAlignButton("al_left_in",
790 _("Align left sides"),
791 0, 1);
792 addAlignButton("al_center_hor",
793 _("Center on vertical axis"),
794 0, 2);
795 addAlignButton("al_right_in",
796 _("Align right sides"),
797 0, 3);
798 addAlignButton("al_right_out",
799 _("Align left sides of objects to right side of anchor"),
800 0, 4);
801 addAlignButton("al_top_out",
802 _("Align bottoms of objects to top of anchor"),
803 1, 0);
804 addAlignButton("al_top_in",
805 _("Align tops"),
806 1, 1);
807 addAlignButton("al_center_ver",
808 _("Center on horizontal axis"),
809 1, 2);
810 addAlignButton("al_bottom_in",
811 _("Align bottoms"),
812 1, 3);
813 addAlignButton("al_bottom_out",
814 _("Align tops of objects to bottom of anchor"),
815 1, 4);
817 //Baseline aligns
818 addBaselineButton("al_baselines_vert",
819 _("Align baseline anchors of texts vertically"),
820 0, 5, this->align_table(), NR::X, false);
821 addBaselineButton("al_baselines_hor",
822 _("Align baseline anchors of texts horizontally"),
823 1, 5, this->align_table(), NR::Y, false);
825 //The distribute buttons
826 addDistributeButton("distribute_hdist",
827 _("Make horizontal gaps between objects equal"),
828 0, 4, true, NR::X, .5, .5);
830 addDistributeButton("distribute_left",
831 _("Distribute left sides equidistantly"),
832 0, 1, false, NR::X, 1., 0.);
833 addDistributeButton("distribute_hcentre",
834 _("Distribute centers equidistantly horizontally"),
835 0, 2, false, NR::X, .5, .5);
836 addDistributeButton("distribute_right",
837 _("Distribute right sides equidistantly"),
838 0, 3, false, NR::X, 0., 1.);
840 addDistributeButton("distribute_vdist",
841 _("Make vertical gaps between objects equal"),
842 1, 4, true, NR::Y, .5, .5);
844 addDistributeButton("distribute_top",
845 _("Distribute tops equidistantly"),
846 1, 1, false, NR::Y, 0, 1);
847 addDistributeButton("distribute_vcentre",
848 _("Distribute centers equidistantly vertically"),
849 1, 2, false, NR::Y, .5, .5);
850 addDistributeButton("distribute_bottom",
851 _("Distribute bottoms equidistantly"),
852 1, 3, false, NR::Y, 1., 0.);
854 //Baseline distribs
855 addBaselineButton("distribute_baselines_hor",
856 _("Distribute baseline anchors of texts horizontally"),
857 0, 5, this->distribute_table(), NR::X, true);
858 addBaselineButton("distribute_baselines_vert",
859 _("Distribute baseline anchors of texts vertically"),
860 1, 5, this->distribute_table(), NR::Y, true);
862 //Randomize & Unclump
863 addRandomizeButton("distribute_randomize",
864 _("Randomize centers in both dimensions"),
865 2, 2);
866 addUnclumpButton("unclump",
867 _("Unclump objects: try to equalize edge-to-edge distances"),
868 2, 4);
870 //Remove overlaps
871 addRemoveOverlapsButton("remove_overlaps",
872 _("Move objects as little as possible so that their bounding boxes do not overlap"),
873 0, 0);
874 //Graph Layout
875 addGraphLayoutButton("graph_layout",
876 _("Nicely arrange selected connector network"),
877 0, 0);
879 //Node Mode buttons
880 addNodeButton("node_halign",
881 _("Align selected nodes horizontally"),
882 0, NR::X, false);
883 addNodeButton("node_valign",
884 _("Align selected nodes vertically"),
885 1, NR::Y, false);
886 addNodeButton("node_hdistribute",
887 _("Distribute selected nodes horizontally"),
888 2, NR::X, true);
889 addNodeButton("node_vdistribute",
890 _("Distribute selected nodes vertically"),
891 3, NR::Y, true);
893 //Rest of the widgetry
895 _combo.append_text(_("Last selected"));
896 _combo.append_text(_("First selected"));
897 _combo.append_text(_("Biggest item"));
898 _combo.append_text(_("Smallest item"));
899 _combo.append_text(_("Page"));
900 _combo.append_text(_("Drawing"));
901 _combo.append_text(_("Selection"));
903 _combo.set_active(6);
904 _combo.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_ref_change));
906 _anchorBox.pack_start(_anchorLabel);
907 _anchorBox.pack_start(_combo);
909 _alignBox.pack_start(_anchorBox);
910 _alignBox.pack_start(_alignTable);
912 _alignFrame.add(_alignBox);
913 _distributeFrame.add(_distributeTable);
914 _removeOverlapFrame.add(_removeOverlapTable);
915 _graphLayoutFrame.add(_graphLayoutTable);
916 _nodesFrame.add(_nodesTable);
918 // Top level vbox
919 Gtk::VBox *vbox = get_vbox();
920 vbox->set_spacing(4);
922 // Notebook for individual transformations
924 vbox->pack_start(_alignFrame, true, true);
925 vbox->pack_start(_distributeFrame, true, true);
926 vbox->pack_start(_removeOverlapFrame, true, true);
927 vbox->pack_start(_graphLayoutFrame, true, true);
928 vbox->pack_start(_nodesFrame, true, true);
930 //Connect to the global tool change signal
931 g_signal_connect (G_OBJECT (INKSCAPE), "set_eventcontext", G_CALLBACK (on_tool_changed), this);
933 // Connect to the global selection change, to invalidate cached randomize_bbox
934 g_signal_connect (G_OBJECT (INKSCAPE), "change_selection", G_CALLBACK (on_selection_changed), this);
935 randomize_bbox = NR::Rect (NR::Point (0, 0), NR::Point (0, 0));
936 randomize_bbox_set = false;
938 show_all_children();
940 on_tool_changed (NULL, NULL, this); // set current mode
941 }
943 AlignAndDistribute::~AlignAndDistribute()
944 {
945 sp_signal_disconnect_by_data (G_OBJECT (INKSCAPE), this);
947 for (std::list<Action *>::iterator it = _actionList.begin();
948 it != _actionList.end();
949 it ++)
950 delete *it;
951 }
953 void AlignAndDistribute::on_ref_change(){
954 //Make blink the master
955 }
960 void AlignAndDistribute::setMode(bool nodeEdit)
961 {
962 //Act on widgets used in node mode
963 void ( Gtk::Widget::*mNode) () = nodeEdit ?
964 &Gtk::Widget::show_all : &Gtk::Widget::hide_all;
966 //Act on widgets used in selection mode
967 void ( Gtk::Widget::*mSel) () = nodeEdit ?
968 &Gtk::Widget::hide_all : &Gtk::Widget::show_all;
971 ((_alignFrame).*(mSel))();
972 ((_distributeFrame).*(mSel))();
973 ((_removeOverlapFrame).*(mSel))();
974 ((_graphLayoutFrame).*(mSel))();
975 ((_nodesFrame).*(mNode))();
977 }
978 void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext,
979 guint row, guint col)
980 {
981 _actionList.push_back(
982 new ActionAlign(
983 id, tiptext, row, col,
984 *this , col + row * 5));
985 }
986 void AlignAndDistribute::addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext,
987 guint row, guint col, bool onInterSpace,
988 NR::Dim2 orientation, float kBegin, float kEnd)
989 {
990 _actionList.push_back(
991 new ActionDistribute(
992 id, tiptext, row, col, *this ,
993 onInterSpace, orientation,
994 kBegin, kEnd
995 )
996 );
997 }
999 void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1000 guint col, NR::Dim2 orientation, bool distribute)
1001 {
1002 _actionList.push_back(
1003 new ActionNode(
1004 id, tiptext, col,
1005 *this, orientation, distribute));
1006 }
1008 void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext,
1009 guint row, guint col)
1010 {
1011 _actionList.push_back(
1012 new ActionRemoveOverlaps(
1013 id, tiptext, row, col, *this)
1014 );
1015 }
1017 void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext,
1018 guint row, guint col)
1019 {
1020 _actionList.push_back(
1021 new ActionGraphLayout(
1022 id, tiptext, row, col, *this)
1023 );
1024 }
1026 void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext,
1027 guint row, guint col)
1028 {
1029 _actionList.push_back(
1030 new ActionUnclump(
1031 id, tiptext, row, col, *this)
1032 );
1033 }
1035 void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1036 guint row, guint col)
1037 {
1038 _actionList.push_back(
1039 new ActionRandomize(
1040 id, tiptext, row, col, *this)
1041 );
1042 }
1044 void AlignAndDistribute::addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext,
1045 guint row, guint col, Gtk::Table &table, NR::Dim2 orientation, bool distribute)
1046 {
1047 _actionList.push_back(
1048 new ActionBaseline(
1049 id, tiptext, row, col,
1050 *this, table, orientation, distribute));
1051 }
1056 std::list<SPItem *>::iterator AlignAndDistribute::find_master( std::list<SPItem *> &list, bool horizontal){
1057 std::list<SPItem *>::iterator master = list.end();
1058 switch (getAlignTarget()) {
1059 case LAST:
1060 return list.begin();
1061 break;
1063 case FIRST:
1064 return --(list.end());
1065 break;
1067 case BIGGEST:
1068 {
1069 gdouble max = -1e18;
1070 for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1071 NR::Maybe<NR::Rect> b = sp_item_bbox_desktop (*it);
1072 if (b) {
1073 gdouble dim = b->extent(horizontal ? NR::X : NR::Y);
1074 if (dim > max) {
1075 max = dim;
1076 master = it;
1077 }
1078 }
1079 }
1080 return master;
1081 break;
1082 }
1084 case SMALLEST:
1085 {
1086 gdouble max = 1e18;
1087 for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1088 NR::Maybe<NR::Rect> b = sp_item_bbox_desktop (*it);
1089 if (b) {
1090 gdouble dim = b->extent(horizontal ? NR::X : NR::Y);
1091 if (dim < max) {
1092 max = dim;
1093 master = it;
1094 }
1095 }
1096 }
1097 return master;
1098 break;
1099 }
1101 default:
1102 g_assert_not_reached ();
1103 break;
1105 } // end of switch statement
1106 return master;
1107 }
1109 AlignAndDistribute::AlignTarget AlignAndDistribute::getAlignTarget()const {
1110 return AlignTarget(_combo.get_active_row_number());
1111 }
1115 } // namespace Dialog
1116 } // namespace UI
1117 } // namespace Inkscape
1119 /*
1120 Local Variables:
1121 mode:c++
1122 c-file-style:"stroustrup"
1123 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1124 indent-tabs-mode:nil
1125 fill-column:99
1126 End:
1127 */
1128 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :