d7e3d1766fd715e93a39d57063cf81a5858e48cc
1 /** @file
2 * @brief Align and Distribute dialog - implementation
3 */
4 /* Authors:
5 * Bryce W. Harrington <bryce@bryceharrington.org>
6 * Aubanel MONNIER <aubi@libertysurf.fr>
7 * Frank Felfe <innerspace@iname.com>
8 * Lauris Kaplinski <lauris@kaplinski.com>
9 * Tim Dwyer <tgdwyer@gmail.com>
10 *
11 * Copyright (C) 1999-2004, 2005 Authors
12 *
13 * Released under GNU GPL. Read the file 'COPYING' for more information.
14 */
17 #ifdef HAVE_CONFIG_H
18 # include <config.h>
19 #endif
21 #include <gtkmm/spinbutton.h>
23 #include "desktop-handles.h"
24 #include "unclump.h"
25 #include "document.h"
26 #include "enums.h"
27 #include "graphlayout.h"
28 #include "inkscape.h"
29 #include "macros.h"
30 #include "preferences.h"
31 #include "removeoverlap.h"
32 #include "selection.h"
33 #include "sp-flowtext.h"
34 #include "sp-item-transform.h"
35 #include "sp-text.h"
36 #include "text-editing.h"
37 #include "tools-switch.h"
38 #include "ui/icon-names.h"
39 #include "ui/tool/node-tool.h"
40 #include "ui/tool/multi-path-manipulator.h"
41 #include "util/glib-list-iterators.h"
42 #include "verbs.h"
43 #include "widgets/icon.h"
45 #include "align-and-distribute.h"
47 namespace Inkscape {
48 namespace UI {
49 namespace Dialog {
51 /////////helper classes//////////////////////////////////
53 class Action {
54 public :
55 Action(const Glib::ustring &id,
56 const Glib::ustring &tiptext,
57 guint row, guint column,
58 Gtk::Table &parent,
59 Gtk::Tooltips &tooltips,
60 AlignAndDistribute &dialog):
61 _dialog(dialog),
62 _id(id),
63 _parent(parent)
64 {
65 Gtk::Widget* pIcon = Gtk::manage( sp_icon_get_icon( _id, Inkscape::ICON_SIZE_LARGE_TOOLBAR) );
66 Gtk::Button * pButton = Gtk::manage(new Gtk::Button());
67 pButton->set_relief(Gtk::RELIEF_NONE);
68 pIcon->show();
69 pButton->add(*pIcon);
70 pButton->show();
72 pButton->signal_clicked()
73 .connect(sigc::mem_fun(*this, &Action::on_button_click));
74 tooltips.set_tip(*pButton, tiptext);
75 parent.attach(*pButton, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL);
76 }
77 virtual ~Action(){}
79 AlignAndDistribute &_dialog;
81 private :
82 virtual void on_button_click(){}
84 Glib::ustring _id;
85 Gtk::Table &_parent;
86 };
89 class ActionAlign : public Action {
90 public :
91 struct Coeffs {
92 double mx0, mx1, my0, my1;
93 double sx0, sx1, sy0, sy1;
94 };
95 ActionAlign(const Glib::ustring &id,
96 const Glib::ustring &tiptext,
97 guint row, guint column,
98 AlignAndDistribute &dialog,
99 guint coeffIndex):
100 Action(id, tiptext, row, column,
101 dialog.align_table(), dialog.tooltips(), dialog),
102 _index(coeffIndex),
103 _dialog(dialog)
104 {}
106 private :
108 virtual void on_button_click() {
109 //Retreive selected objects
110 SPDesktop *desktop = _dialog.getDesktop();
111 if (!desktop) return;
113 Inkscape::Selection *selection = sp_desktop_selection(desktop);
114 if (!selection) return;
116 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
117 bool sel_as_group = prefs->getBool("/dialogs/align/sel-as-groups");
119 using Inkscape::Util::GSListConstIterator;
120 std::list<SPItem *> selected;
121 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
122 if (selected.empty()) return;
124 Geom::Point mp; //Anchor point
125 AlignAndDistribute::AlignTarget target = _dialog.getAlignTarget();
126 const Coeffs &a= _allCoeffs[_index];
127 switch (target)
128 {
129 case AlignAndDistribute::LAST:
130 case AlignAndDistribute::FIRST:
131 case AlignAndDistribute::BIGGEST:
132 case AlignAndDistribute::SMALLEST:
133 {
134 //Check 2 or more selected objects
135 std::list<SPItem *>::iterator second(selected.begin());
136 ++second;
137 if (second == selected.end())
138 return;
139 //Find the master (anchor on which the other objects are aligned)
140 std::list<SPItem *>::iterator master(
141 _dialog.find_master (
142 selected,
143 (a.mx0 != 0.0) ||
144 (a.mx1 != 0.0) )
145 );
146 //remove the master from the selection
147 SPItem * thing = *master;
148 // TODO: either uncomment or remove the following commented lines, depending on which
149 // behaviour of moving objects makes most sense; also cf. discussion at
150 // https://bugs.launchpad.net/inkscape/+bug/255933
151 /*if (!sel_as_group) { */
152 selected.erase(master);
153 /*}*/
154 //Compute the anchor point
155 Geom::OptRect b = thing->getBboxDesktop ();
156 if (b) {
157 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
158 a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
159 } else {
160 return;
161 }
162 break;
163 }
165 case AlignAndDistribute::PAGE:
166 mp = Geom::Point(a.mx1 * sp_document_width(sp_desktop_document(desktop)),
167 a.my1 * sp_document_height(sp_desktop_document(desktop)));
168 break;
170 case AlignAndDistribute::DRAWING:
171 {
172 Geom::OptRect b = static_cast<SPItem *>( sp_document_root (sp_desktop_document (desktop)))->getBboxDesktop();
173 if (b) {
174 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
175 a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
176 } else {
177 return;
178 }
179 break;
180 }
182 case AlignAndDistribute::SELECTION:
183 {
184 Geom::OptRect b = selection->bounds();
185 if (b) {
186 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
187 a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
188 } else {
189 return;
190 }
191 break;
192 }
194 default:
195 g_assert_not_reached ();
196 break;
197 }; // end of switch
199 // Top hack: temporarily set clone compensation to unmoved, so that we can align/distribute
200 // clones with their original (and the move of the original does not disturb the
201 // clones). The only problem with this is that if there are outside-of-selection clones of
202 // a selected original, they will be unmoved too, possibly contrary to user's
203 // expecation. However this is a minor point compared to making align/distribute always
204 // work as expected, and "unmoved" is the default option anyway.
205 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
206 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
208 bool changed = false;
209 Geom::OptRect b;
210 if (sel_as_group)
211 b = selection->bounds();
213 //Move each item in the selected list separately
214 for (std::list<SPItem *>::iterator it(selected.begin());
215 it != selected.end();
216 it++)
217 {
218 sp_document_ensure_up_to_date(sp_desktop_document (desktop));
219 if (!sel_as_group)
220 b = (*it)->getBboxDesktop();
221 if (b) {
222 Geom::Point const sp(a.sx0 * b->min()[Geom::X] + a.sx1 * b->max()[Geom::X],
223 a.sy0 * b->min()[Geom::Y] + a.sy1 * b->max()[Geom::Y]);
224 Geom::Point const mp_rel( mp - sp );
225 if (LInfty(mp_rel) > 1e-9) {
226 sp_item_move_rel(*it, Geom::Translate(mp_rel));
227 changed = true;
228 }
229 }
230 }
232 // restore compensation setting
233 prefs->setInt("/options/clonecompensation/value", saved_compensation);
235 if (changed) {
236 sp_document_done ( sp_desktop_document (desktop) , SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
237 _("Align"));
238 }
241 }
242 guint _index;
243 AlignAndDistribute &_dialog;
245 static const Coeffs _allCoeffs[10];
247 };
248 ActionAlign::Coeffs const ActionAlign::_allCoeffs[10] = {
249 {1., 0., 0., 0., 0., 1., 0., 0.},
250 {1., 0., 0., 0., 1., 0., 0., 0.},
251 {.5, .5, 0., 0., .5, .5, 0., 0.},
252 {0., 1., 0., 0., 0., 1., 0., 0.},
253 {0., 1., 0., 0., 1., 0., 0., 0.},
254 {0., 0., 0., 1., 0., 0., 1., 0.},
255 {0., 0., 0., 1., 0., 0., 0., 1.},
256 {0., 0., .5, .5, 0., 0., .5, .5},
257 {0., 0., 1., 0., 0., 0., 1., 0.},
258 {0., 0., 1., 0., 0., 0., 0., 1.}
259 };
261 BBoxSort::BBoxSort(SPItem *pItem, Geom::Rect bounds, Geom::Dim2 orientation, double kBegin, double kEnd) :
262 item(pItem),
263 bbox (bounds)
264 {
265 anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation];
266 }
267 BBoxSort::BBoxSort(const BBoxSort &rhs) :
268 //NOTE : this copy ctor is called O(sort) when sorting the vector
269 //this is bad. The vector should be a vector of pointers.
270 //But I'll wait the bohem GC before doing that
271 item(rhs.item), anchor(rhs.anchor), bbox(rhs.bbox)
272 {
273 }
275 bool operator< (const BBoxSort &a, const BBoxSort &b)
276 {
277 return (a.anchor < b.anchor);
278 }
280 class ActionDistribute : public Action {
281 public :
282 ActionDistribute(const Glib::ustring &id,
283 const Glib::ustring &tiptext,
284 guint row, guint column,
285 AlignAndDistribute &dialog,
286 bool onInterSpace,
287 Geom::Dim2 orientation,
288 double kBegin, double kEnd
289 ):
290 Action(id, tiptext, row, column,
291 dialog.distribute_table(), dialog.tooltips(), dialog),
292 _dialog(dialog),
293 _onInterSpace(onInterSpace),
294 _orientation(orientation),
295 _kBegin(kBegin),
296 _kEnd( kEnd)
297 {}
299 private :
300 virtual void on_button_click() {
301 //Retreive selected objects
302 SPDesktop *desktop = _dialog.getDesktop();
303 if (!desktop) return;
305 Inkscape::Selection *selection = sp_desktop_selection(desktop);
306 if (!selection) return;
308 using Inkscape::Util::GSListConstIterator;
309 std::list<SPItem *> selected;
310 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
311 if (selected.empty()) return;
313 //Check 2 or more selected objects
314 std::list<SPItem *>::iterator second(selected.begin());
315 ++second;
316 if (second == selected.end()) return;
319 std::vector< BBoxSort > sorted;
320 for (std::list<SPItem *>::iterator it(selected.begin());
321 it != selected.end();
322 ++it)
323 {
324 Geom::OptRect bbox = (*it)->getBboxDesktop();
325 if (bbox) {
326 sorted.push_back(BBoxSort(*it, *bbox, _orientation, _kBegin, _kEnd));
327 }
328 }
329 //sort bbox by anchors
330 std::sort(sorted.begin(), sorted.end());
332 // see comment in ActionAlign above
333 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
334 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
335 prefs->setInt("/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[_orientation].extent();
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 Geom::Point t(0.0, 0.0);
359 t[_orientation] = pos - it->bbox.min()[_orientation];
360 sp_item_move_rel(it->item, Geom::Translate(t));
361 changed = true;
362 }
363 pos += it->bbox[_orientation].extent();
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 Geom::Point t(0.0, 0.0);
383 t[_orientation] = pos - it.anchor;
384 //translate
385 sp_item_move_rel(it.item, Geom::Translate(t));
386 changed = true;
387 }
388 }
389 }
391 // restore compensation setting
392 prefs->setInt("/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 Geom::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 Geom::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 Geom::Dim2 _orientation;
425 bool _distribute;
426 virtual void on_button_click()
427 {
429 if (!_dialog.getDesktop()) return;
430 SPEventContext *event_context = sp_desktop_event_context(_dialog.getDesktop());
431 if (!INK_IS_NODE_TOOL (event_context)) return;
432 InkNodeTool *nt = INK_NODE_TOOL(event_context);
434 if (_distribute)
435 nt->_multipath->distributeNodes(_orientation);
436 else
437 nt->_multipath->alignNodes(_orientation);
439 }
440 };
442 class ActionRemoveOverlaps : public Action {
443 private:
444 Gtk::Label removeOverlapXGapLabel;
445 Gtk::Label removeOverlapYGapLabel;
446 Gtk::SpinButton removeOverlapXGap;
447 Gtk::SpinButton removeOverlapYGap;
449 public:
450 ActionRemoveOverlaps(Glib::ustring const &id,
451 Glib::ustring const &tiptext,
452 guint row,
453 guint column,
454 AlignAndDistribute &dialog) :
455 Action(id, tiptext, row, column + 4,
456 dialog.removeOverlap_table(), dialog.tooltips(), dialog)
457 {
458 dialog.removeOverlap_table().set_col_spacings(3);
460 removeOverlapXGap.set_digits(1);
461 removeOverlapXGap.set_size_request(60, -1);
462 removeOverlapXGap.set_increments(1.0, 0);
463 removeOverlapXGap.set_range(-1000.0, 1000.0);
464 removeOverlapXGap.set_value(0);
465 dialog.tooltips().set_tip(removeOverlapXGap,
466 _("Minimum horizontal gap (in px units) between bounding boxes"));
467 //TRANSLATORS: only translate "string" in "context|string".
468 // For more details, see http://developer.gnome.org/doc/API/2.0/glib/glib-I18N.html#Q-:CAPS
469 // "H:" stands for horizontal gap
470 removeOverlapXGapLabel.set_label(Q_("gap|H:"));
472 removeOverlapYGap.set_digits(1);
473 removeOverlapYGap.set_size_request(60, -1);
474 removeOverlapYGap.set_increments(1.0, 0);
475 removeOverlapYGap.set_range(-1000.0, 1000.0);
476 removeOverlapYGap.set_value(0);
477 dialog.tooltips().set_tip(removeOverlapYGap,
478 _("Minimum vertical gap (in px units) between bounding boxes"));
479 /* TRANSLATORS: Vertical gap */
480 removeOverlapYGapLabel.set_label(_("V:"));
482 dialog.removeOverlap_table().attach(removeOverlapXGapLabel, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL);
483 dialog.removeOverlap_table().attach(removeOverlapXGap, column+1, column+2, row, row+1, Gtk::FILL, Gtk::FILL);
484 dialog.removeOverlap_table().attach(removeOverlapYGapLabel, column+2, column+3, row, row+1, Gtk::FILL, Gtk::FILL);
485 dialog.removeOverlap_table().attach(removeOverlapYGap, column+3, column+4, row, row+1, Gtk::FILL, Gtk::FILL);
487 }
489 private :
490 virtual void on_button_click()
491 {
492 if (!_dialog.getDesktop()) return;
494 // see comment in ActionAlign above
495 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
496 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
497 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
499 // xGap and yGap are the minimum space required between bounding rectangles.
500 double const xGap = removeOverlapXGap.get_value();
501 double const yGap = removeOverlapYGap.get_value();
502 removeoverlap(sp_desktop_selection(_dialog.getDesktop())->itemList(),
503 xGap, yGap);
505 // restore compensation setting
506 prefs->setInt("/options/clonecompensation/value", saved_compensation);
508 sp_document_done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
509 _("Remove overlaps"));
510 }
511 };
513 class ActionGraphLayout : public Action {
514 public:
515 ActionGraphLayout(Glib::ustring const &id,
516 Glib::ustring const &tiptext,
517 guint row,
518 guint column,
519 AlignAndDistribute &dialog) :
520 Action(id, tiptext, row, column + 4,
521 dialog.graphLayout_table(), dialog.tooltips(), dialog)
522 {}
524 private :
525 virtual void on_button_click()
526 {
527 if (!_dialog.getDesktop()) return;
529 // see comment in ActionAlign above
530 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
531 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
532 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
534 graphlayout(sp_desktop_selection(_dialog.getDesktop())->itemList());
536 // restore compensation setting
537 prefs->setInt("/options/clonecompensation/value", saved_compensation);
539 sp_document_done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
540 _("Arrange connector network"));
541 }
542 };
544 class ActionUnclump : public Action {
545 public :
546 ActionUnclump(const Glib::ustring &id,
547 const Glib::ustring &tiptext,
548 guint row,
549 guint column,
550 AlignAndDistribute &dialog):
551 Action(id, tiptext, row, column,
552 dialog.distribute_table(), dialog.tooltips(), dialog)
553 {}
555 private :
556 virtual void on_button_click()
557 {
558 if (!_dialog.getDesktop()) return;
560 // see comment in ActionAlign above
561 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
562 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
563 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
565 unclump ((GSList *) sp_desktop_selection(_dialog.getDesktop())->itemList());
567 // restore compensation setting
568 prefs->setInt("/options/clonecompensation/value", saved_compensation);
570 sp_document_done (sp_desktop_document (_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
571 _("Unclump"));
572 }
573 };
575 class ActionRandomize : public Action {
576 public :
577 ActionRandomize(const Glib::ustring &id,
578 const Glib::ustring &tiptext,
579 guint row,
580 guint column,
581 AlignAndDistribute &dialog):
582 Action(id, tiptext, row, column,
583 dialog.distribute_table(), dialog.tooltips(), dialog)
584 {}
586 private :
587 virtual void on_button_click()
588 {
589 SPDesktop *desktop = _dialog.getDesktop();
590 if (!desktop) return;
592 Inkscape::Selection *selection = sp_desktop_selection(desktop);
593 if (!selection) return;
595 using Inkscape::Util::GSListConstIterator;
596 std::list<SPItem *> selected;
597 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
598 if (selected.empty()) return;
600 //Check 2 or more selected objects
601 if (selected.size() < 2) return;
603 Geom::OptRect sel_bbox = selection->bounds();
604 if (!sel_bbox) {
605 return;
606 }
608 // This bbox is cached between calls to randomize, so that there's no growth nor shrink
609 // nor drift on sequential randomizations. Discard cache on global (or better active
610 // desktop's) selection_change signal.
611 if (!_dialog.randomize_bbox) {
612 _dialog.randomize_bbox = *sel_bbox;
613 }
615 // see comment in ActionAlign above
616 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
617 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
618 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
620 for (std::list<SPItem *>::iterator it(selected.begin());
621 it != selected.end();
622 ++it)
623 {
624 sp_document_ensure_up_to_date(sp_desktop_document (desktop));
625 Geom::OptRect item_box = (*it)->getBboxDesktop ();
626 if (item_box) {
627 // find new center, staying within bbox
628 double x = _dialog.randomize_bbox->min()[Geom::X] + (*item_box)[Geom::X].extent() /2 +
629 g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::X].extent() - (*item_box)[Geom::X].extent());
630 double y = _dialog.randomize_bbox->min()[Geom::Y] + (*item_box)[Geom::Y].extent()/2 +
631 g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::Y].extent() - (*item_box)[Geom::Y].extent());
632 // displacement is the new center minus old:
633 Geom::Point t = Geom::Point (x, y) - 0.5*(item_box->max() + item_box->min());
634 sp_item_move_rel(*it, Geom::Translate(t));
635 }
636 }
638 // restore compensation setting
639 prefs->setInt("/options/clonecompensation/value", saved_compensation);
641 sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
642 _("Randomize positions"));
643 }
644 };
646 struct Baselines
647 {
648 SPItem *_item;
649 Geom::Point _base;
650 Geom::Dim2 _orientation;
651 Baselines(SPItem *item, Geom::Point base, Geom::Dim2 orientation) :
652 _item (item),
653 _base (base),
654 _orientation (orientation)
655 {}
656 };
658 bool operator< (const Baselines &a, const Baselines &b)
659 {
660 return (a._base[a._orientation] < b._base[b._orientation]);
661 }
663 class ActionBaseline : public Action {
664 public :
665 ActionBaseline(const Glib::ustring &id,
666 const Glib::ustring &tiptext,
667 guint row,
668 guint column,
669 AlignAndDistribute &dialog,
670 Gtk::Table &table,
671 Geom::Dim2 orientation, bool distribute):
672 Action(id, tiptext, row, column,
673 table, dialog.tooltips(), dialog),
674 _orientation(orientation),
675 _distribute(distribute)
676 {}
678 private :
679 Geom::Dim2 _orientation;
680 bool _distribute;
681 virtual void on_button_click()
682 {
683 SPDesktop *desktop = _dialog.getDesktop();
684 if (!desktop) return;
686 Inkscape::Selection *selection = sp_desktop_selection(desktop);
687 if (!selection) return;
689 using Inkscape::Util::GSListConstIterator;
690 std::list<SPItem *> selected;
691 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
692 if (selected.empty()) return;
694 //Check 2 or more selected objects
695 if (selected.size() < 2) return;
697 Geom::Point b_min = Geom::Point (HUGE_VAL, HUGE_VAL);
698 Geom::Point b_max = Geom::Point (-HUGE_VAL, -HUGE_VAL);
700 std::vector<Baselines> sorted;
702 for (std::list<SPItem *>::iterator it(selected.begin());
703 it != selected.end();
704 ++it)
705 {
706 if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
707 Inkscape::Text::Layout const *layout = te_get_layout(*it);
708 boost::optional<Geom::Point> pt = layout->baselineAnchorPoint();
709 if (pt) {
710 Geom::Point base = *pt * (*it)->i2d_affine();
711 if (base[Geom::X] < b_min[Geom::X]) b_min[Geom::X] = base[Geom::X];
712 if (base[Geom::Y] < b_min[Geom::Y]) b_min[Geom::Y] = base[Geom::Y];
713 if (base[Geom::X] > b_max[Geom::X]) b_max[Geom::X] = base[Geom::X];
714 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 }
719 }
721 if (sorted.size() <= 1) return;
723 //sort baselines
724 std::sort(sorted.begin(), sorted.end());
726 bool changed = false;
728 if (_distribute) {
729 double step = (b_max[_orientation] - b_min[_orientation])/(sorted.size() - 1);
730 for (unsigned int i = 0; i < sorted.size(); i++) {
731 SPItem *item = sorted[i]._item;
732 Geom::Point base = sorted[i]._base;
733 Geom::Point t(0.0, 0.0);
734 t[_orientation] = b_min[_orientation] + step * i - base[_orientation];
735 sp_item_move_rel(item, Geom::Translate(t));
736 changed = true;
737 }
739 if (changed) {
740 sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
741 _("Distribute text baselines"));
742 }
744 } else {
745 for (std::list<SPItem *>::iterator it(selected.begin());
746 it != selected.end();
747 ++it)
748 {
749 if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
750 Inkscape::Text::Layout const *layout = te_get_layout(*it);
751 boost::optional<Geom::Point> pt = layout->baselineAnchorPoint();
752 if (pt) {
753 Geom::Point base = *pt * (*it)->i2d_affine();
754 Geom::Point t(0.0, 0.0);
755 t[_orientation] = b_min[_orientation] - base[_orientation];
756 sp_item_move_rel(*it, Geom::Translate(t));
757 changed = true;
758 }
759 }
760 }
762 if (changed) {
763 sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
764 _("Align text baselines"));
765 }
766 }
767 }
768 };
772 void on_tool_changed(Inkscape::Application */*inkscape*/, SPEventContext */*context*/, AlignAndDistribute *daad)
773 {
774 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
775 if (desktop && sp_desktop_event_context(desktop))
776 daad->setMode(tools_active(desktop) == TOOLS_NODES);
777 }
779 void on_selection_changed(Inkscape::Application */*inkscape*/, Inkscape::Selection */*selection*/, AlignAndDistribute *daad)
780 {
781 daad->randomize_bbox = Geom::OptRect();
782 }
784 /////////////////////////////////////////////////////////
789 AlignAndDistribute::AlignAndDistribute()
790 : UI::Widget::Panel ("", "/dialogs/align", SP_VERB_DIALOG_ALIGN_DISTRIBUTE),
791 randomize_bbox(),
792 _alignFrame(_("Align")),
793 _distributeFrame(_("Distribute")),
794 _removeOverlapFrame(_("Remove overlaps")),
795 _graphLayoutFrame(_("Connector network layout")),
796 _nodesFrame(_("Nodes")),
797 _alignTable(2, 6, true),
798 _distributeTable(3, 6, true),
799 _removeOverlapTable(1, 5, false),
800 _graphLayoutTable(1, 5, false),
801 _nodesTable(1, 4, true),
802 _anchorLabel(_("Relative to: ")),
803 _selgrpLabel(_("Treat selection as group: "))
804 {
805 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
807 //Instanciate the align buttons
808 addAlignButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_RIGHT_TO_ANCHOR,
809 _("Align right edges of objects to the left edge of the anchor"),
810 0, 0);
811 addAlignButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_LEFT,
812 _("Align left edges"),
813 0, 1);
814 addAlignButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_CENTER,
815 _("Center on vertical axis"),
816 0, 2);
817 addAlignButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_RIGHT,
818 _("Align right sides"),
819 0, 3);
820 addAlignButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_LEFT_TO_ANCHOR,
821 _("Align left edges of objects to the right edge of the anchor"),
822 0, 4);
823 addAlignButton(INKSCAPE_ICON_ALIGN_VERTICAL_BOTTOM_TO_ANCHOR,
824 _("Align bottom edges of objects to the top edge of the anchor"),
825 1, 0);
826 addAlignButton(INKSCAPE_ICON_ALIGN_VERTICAL_TOP,
827 _("Align top edges"),
828 1, 1);
829 addAlignButton(INKSCAPE_ICON_ALIGN_VERTICAL_CENTER,
830 _("Center on horizontal axis"),
831 1, 2);
832 addAlignButton(INKSCAPE_ICON_ALIGN_VERTICAL_BOTTOM,
833 _("Align bottom edges"),
834 1, 3);
835 addAlignButton(INKSCAPE_ICON_ALIGN_VERTICAL_TOP_TO_ANCHOR,
836 _("Align top edges of objects to the bottom edge of the anchor"),
837 1, 4);
839 //Baseline aligns
840 addBaselineButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_BASELINE,
841 _("Align baseline anchors of texts horizontally"),
842 0, 5, this->align_table(), Geom::X, false);
843 addBaselineButton(INKSCAPE_ICON_ALIGN_VERTICAL_BASELINE,
844 _("Align baselines of texts"),
845 1, 5, this->align_table(), Geom::Y, false);
847 //The distribute buttons
848 addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_HORIZONTAL_GAPS,
849 _("Make horizontal gaps between objects equal"),
850 0, 4, true, Geom::X, .5, .5);
852 addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_HORIZONTAL_LEFT,
853 _("Distribute left edges equidistantly"),
854 0, 1, false, Geom::X, 1., 0.);
855 addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_HORIZONTAL_CENTER,
856 _("Distribute centers equidistantly horizontally"),
857 0, 2, false, Geom::X, .5, .5);
858 addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_HORIZONTAL_RIGHT,
859 _("Distribute right edges equidistantly"),
860 0, 3, false, Geom::X, 0., 1.);
862 addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_VERTICAL_GAPS,
863 _("Make vertical gaps between objects equal"),
864 1, 4, true, Geom::Y, .5, .5);
866 addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_VERTICAL_TOP,
867 _("Distribute top edges equidistantly"),
868 1, 1, false, Geom::Y, 0, 1);
869 addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_VERTICAL_CENTER,
870 _("Distribute centers equidistantly vertically"),
871 1, 2, false, Geom::Y, .5, .5);
872 addDistributeButton(INKSCAPE_ICON_DISTRIBUTE_VERTICAL_BOTTOM,
873 _("Distribute bottom edges equidistantly"),
874 1, 3, false, Geom::Y, 1., 0.);
876 //Baseline distribs
877 addBaselineButton(INKSCAPE_ICON_DISTRIBUTE_HORIZONTAL_BASELINE,
878 _("Distribute baseline anchors of texts horizontally"),
879 0, 5, this->distribute_table(), Geom::X, true);
880 addBaselineButton(INKSCAPE_ICON_DISTRIBUTE_VERTICAL_BASELINE,
881 _("Distribute baselines of texts vertically"),
882 1, 5, this->distribute_table(), Geom::Y, true);
884 //Randomize & Unclump
885 addRandomizeButton(INKSCAPE_ICON_DISTRIBUTE_RANDOMIZE,
886 _("Randomize centers in both dimensions"),
887 2, 2);
888 addUnclumpButton(INKSCAPE_ICON_DISTRIBUTE_UNCLUMP,
889 _("Unclump objects: try to equalize edge-to-edge distances"),
890 2, 4);
892 //Remove overlaps
893 addRemoveOverlapsButton(INKSCAPE_ICON_DISTRIBUTE_REMOVE_OVERLAPS,
894 _("Move objects as little as possible so that their bounding boxes do not overlap"),
895 0, 0);
896 //Graph Layout
897 addGraphLayoutButton(INKSCAPE_ICON_DISTRIBUTE_GRAPH,
898 _("Nicely arrange selected connector network"),
899 0, 0);
901 //Node Mode buttons
902 // NOTE: "align nodes vertically" means "move nodes vertically until they align on a common
903 // _horizontal_ line". This is analogous to what the "align-vertical-center" icon means.
904 // There is no doubt some ambiguity. For this reason the descriptions are different.
905 addNodeButton(INKSCAPE_ICON_ALIGN_VERTICAL_NODES,
906 _("Align selected nodes to a common horizontal line"),
907 0, Geom::X, false);
908 addNodeButton(INKSCAPE_ICON_ALIGN_HORIZONTAL_NODES,
909 _("Align selected nodes to a common vertical line"),
910 1, Geom::Y, false);
911 addNodeButton(INKSCAPE_ICON_DISTRIBUTE_HORIZONTAL_NODE,
912 _("Distribute selected nodes horizontally"),
913 2, Geom::X, true);
914 addNodeButton(INKSCAPE_ICON_DISTRIBUTE_VERTICAL_NODE,
915 _("Distribute selected nodes vertically"),
916 3, Geom::Y, true);
918 //Rest of the widgetry
920 _combo.append_text(_("Last selected"));
921 _combo.append_text(_("First selected"));
922 _combo.append_text(_("Biggest object"));
923 _combo.append_text(_("Smallest object"));
924 _combo.append_text(_("Page"));
925 _combo.append_text(_("Drawing"));
926 _combo.append_text(_("Selection"));
928 _combo.set_active(prefs->getInt("/dialogs/align/align-to", 6));
929 _combo.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_ref_change));
931 _anchorBox.pack_start(_anchorLabel);
932 _anchorBox.pack_start(_combo);
934 _selgrpBox.pack_start(_selgrpLabel);
935 _selgrpBox.pack_start(_selgrp);
936 _selgrp.set_active(prefs->getBool("/dialogs/align/sel-as-groups"));
937 _selgrp.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_selgrp_toggled));
939 _alignBox.pack_start(_anchorBox);
940 _alignBox.pack_start(_selgrpBox);
941 _alignBox.pack_start(_alignTable);
943 _alignFrame.add(_alignBox);
944 _distributeFrame.add(_distributeTable);
945 _removeOverlapFrame.add(_removeOverlapTable);
946 _graphLayoutFrame.add(_graphLayoutTable);
947 _nodesFrame.add(_nodesTable);
949 Gtk::Box *contents = _getContents();
950 contents->set_spacing(4);
952 // Notebook for individual transformations
954 contents->pack_start(_alignFrame, true, true);
955 contents->pack_start(_distributeFrame, true, true);
956 contents->pack_start(_removeOverlapFrame, true, true);
957 contents->pack_start(_graphLayoutFrame, true, true);
958 contents->pack_start(_nodesFrame, true, true);
960 //Connect to the global tool change signal
961 g_signal_connect (G_OBJECT (INKSCAPE), "set_eventcontext", G_CALLBACK (on_tool_changed), this);
963 // Connect to the global selection change, to invalidate cached randomize_bbox
964 g_signal_connect (G_OBJECT (INKSCAPE), "change_selection", G_CALLBACK (on_selection_changed), this);
965 randomize_bbox = Geom::OptRect();
967 show_all_children();
969 on_tool_changed (NULL, NULL, this); // set current mode
970 }
972 AlignAndDistribute::~AlignAndDistribute()
973 {
974 sp_signal_disconnect_by_data (G_OBJECT (INKSCAPE), this);
976 for (std::list<Action *>::iterator it = _actionList.begin();
977 it != _actionList.end();
978 it ++)
979 delete *it;
980 }
982 void AlignAndDistribute::on_ref_change(){
983 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
984 prefs->setInt("/dialogs/align/align-to", _combo.get_active_row_number());
986 //Make blink the master
987 }
989 void AlignAndDistribute::on_selgrp_toggled(){
990 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
991 prefs->setInt("/dialogs/align/sel-as-groups", _selgrp.get_active());
993 //Make blink the master
994 }
999 void AlignAndDistribute::setMode(bool nodeEdit)
1000 {
1001 //Act on widgets used in node mode
1002 void ( Gtk::Widget::*mNode) () = nodeEdit ?
1003 &Gtk::Widget::show_all : &Gtk::Widget::hide_all;
1005 //Act on widgets used in selection mode
1006 void ( Gtk::Widget::*mSel) () = nodeEdit ?
1007 &Gtk::Widget::hide_all : &Gtk::Widget::show_all;
1010 ((_alignFrame).*(mSel))();
1011 ((_distributeFrame).*(mSel))();
1012 ((_removeOverlapFrame).*(mSel))();
1013 ((_graphLayoutFrame).*(mSel))();
1014 ((_nodesFrame).*(mNode))();
1016 }
1017 void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext,
1018 guint row, guint col)
1019 {
1020 _actionList.push_back(
1021 new ActionAlign(
1022 id, tiptext, row, col,
1023 *this , col + row * 5));
1024 }
1025 void AlignAndDistribute::addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1026 guint row, guint col, bool onInterSpace,
1027 Geom::Dim2 orientation, float kBegin, float kEnd)
1028 {
1029 _actionList.push_back(
1030 new ActionDistribute(
1031 id, tiptext, row, col, *this ,
1032 onInterSpace, orientation,
1033 kBegin, kEnd
1034 )
1035 );
1036 }
1038 void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1039 guint col, Geom::Dim2 orientation, bool distribute)
1040 {
1041 _actionList.push_back(
1042 new ActionNode(
1043 id, tiptext, col,
1044 *this, orientation, distribute));
1045 }
1047 void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext,
1048 guint row, guint col)
1049 {
1050 _actionList.push_back(
1051 new ActionRemoveOverlaps(
1052 id, tiptext, row, col, *this)
1053 );
1054 }
1056 void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext,
1057 guint row, guint col)
1058 {
1059 _actionList.push_back(
1060 new ActionGraphLayout(
1061 id, tiptext, row, col, *this)
1062 );
1063 }
1065 void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext,
1066 guint row, guint col)
1067 {
1068 _actionList.push_back(
1069 new ActionUnclump(
1070 id, tiptext, row, col, *this)
1071 );
1072 }
1074 void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1075 guint row, guint col)
1076 {
1077 _actionList.push_back(
1078 new ActionRandomize(
1079 id, tiptext, row, col, *this)
1080 );
1081 }
1083 void AlignAndDistribute::addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext,
1084 guint row, guint col, Gtk::Table &table, Geom::Dim2 orientation, bool distribute)
1085 {
1086 _actionList.push_back(
1087 new ActionBaseline(
1088 id, tiptext, row, col,
1089 *this, table, orientation, distribute));
1090 }
1095 std::list<SPItem *>::iterator AlignAndDistribute::find_master( std::list<SPItem *> &list, bool horizontal){
1096 std::list<SPItem *>::iterator master = list.end();
1097 switch (getAlignTarget()) {
1098 case LAST:
1099 return list.begin();
1100 break;
1102 case FIRST:
1103 return --(list.end());
1104 break;
1106 case BIGGEST:
1107 {
1108 gdouble max = -1e18;
1109 for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1110 Geom::OptRect b = (*it)->getBboxDesktop ();
1111 if (b) {
1112 gdouble dim = (*b)[horizontal ? Geom::X : Geom::Y].extent();
1113 if (dim > max) {
1114 max = dim;
1115 master = it;
1116 }
1117 }
1118 }
1119 return master;
1120 break;
1121 }
1123 case SMALLEST:
1124 {
1125 gdouble max = 1e18;
1126 for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1127 Geom::OptRect b = (*it)->getBboxDesktop ();
1128 if (b) {
1129 gdouble dim = (*b)[horizontal ? Geom::X : Geom::Y].extent();
1130 if (dim < max) {
1131 max = dim;
1132 master = it;
1133 }
1134 }
1135 }
1136 return master;
1137 break;
1138 }
1140 default:
1141 g_assert_not_reached ();
1142 break;
1144 } // end of switch statement
1145 return master;
1146 }
1148 AlignAndDistribute::AlignTarget AlignAndDistribute::getAlignTarget()const {
1149 return AlignTarget(_combo.get_active_row_number());
1150 }
1154 } // namespace Dialog
1155 } // namespace UI
1156 } // namespace Inkscape
1158 /*
1159 Local Variables:
1160 mode:c++
1161 c-file-style:"stroustrup"
1162 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1163 indent-tabs-mode:nil
1164 fill-column:99
1165 End:
1166 */
1167 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :