367a744c7d64680a14891c2c2f35ad757b31d976
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 "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 "preferences.h"
43 #include "enums.h"
45 #include "sp-text.h"
46 #include "sp-flowtext.h"
47 #include "text-editing.h"
49 #include "node-context.h" //For access to ShapeEditor
50 #include "shape-editor.h" //For node align/distribute methods
52 #include "tools-switch.h"
54 #include "align-and-distribute.h"
56 namespace Inkscape {
57 namespace UI {
58 namespace Dialog {
60 /////////helper classes//////////////////////////////////
62 class Action {
63 public :
64 Action(const Glib::ustring &id,
65 const Glib::ustring &tiptext,
66 guint row, guint column,
67 Gtk::Table &parent,
68 Gtk::Tooltips &tooltips,
69 AlignAndDistribute &dialog):
70 _dialog(dialog),
71 _id(id),
72 _parent(parent)
73 {
74 Gtk::Widget* pIcon = Gtk::manage( sp_icon_get_icon( _id, Inkscape::ICON_SIZE_LARGE_TOOLBAR) );
75 Gtk::Button * pButton = Gtk::manage(new Gtk::Button());
76 pButton->set_relief(Gtk::RELIEF_NONE);
77 pIcon->show();
78 pButton->add(*pIcon);
79 pButton->show();
81 pButton->signal_clicked()
82 .connect(sigc::mem_fun(*this, &Action::on_button_click));
83 tooltips.set_tip(*pButton, tiptext);
84 parent.attach(*pButton, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL);
85 }
86 virtual ~Action(){}
88 AlignAndDistribute &_dialog;
90 private :
91 virtual void on_button_click(){}
93 Glib::ustring _id;
94 Gtk::Table &_parent;
95 };
98 class ActionAlign : public Action {
99 public :
100 struct Coeffs {
101 double mx0, mx1, my0, my1;
102 double sx0, sx1, sy0, sy1;
103 };
104 ActionAlign(const Glib::ustring &id,
105 const Glib::ustring &tiptext,
106 guint row, guint column,
107 AlignAndDistribute &dialog,
108 guint coeffIndex):
109 Action(id, tiptext, row, column,
110 dialog.align_table(), dialog.tooltips(), dialog),
111 _index(coeffIndex),
112 _dialog(dialog)
113 {}
115 private :
117 virtual void on_button_click() {
118 //Retreive selected objects
119 SPDesktop *desktop = _dialog.getDesktop();
120 if (!desktop) return;
122 Inkscape::Selection *selection = sp_desktop_selection(desktop);
123 if (!selection) return;
125 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
126 bool sel_as_group = prefs->getBool("/dialogs/align/sel-as-groups");
128 using Inkscape::Util::GSListConstIterator;
129 std::list<SPItem *> selected;
130 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
131 if (selected.empty()) return;
133 Geom::Point mp; //Anchor point
134 AlignAndDistribute::AlignTarget target = _dialog.getAlignTarget();
135 const Coeffs &a= _allCoeffs[_index];
136 switch (target)
137 {
138 case AlignAndDistribute::LAST:
139 case AlignAndDistribute::FIRST:
140 case AlignAndDistribute::BIGGEST:
141 case AlignAndDistribute::SMALLEST:
142 {
143 //Check 2 or more selected objects
144 std::list<SPItem *>::iterator second(selected.begin());
145 ++second;
146 if (second == selected.end())
147 return;
148 //Find the master (anchor on which the other objects are aligned)
149 std::list<SPItem *>::iterator master(
150 _dialog.find_master (
151 selected,
152 (a.mx0 != 0.0) ||
153 (a.mx1 != 0.0) )
154 );
155 //remove the master from the selection
156 SPItem * thing = *master;
157 // TODO: either uncomment or remove the following commented lines, depending on which
158 // behaviour of moving objects makes most sense; also cf. discussion at
159 // https://bugs.launchpad.net/inkscape/+bug/255933
160 /*if (!sel_as_group) { */
161 selected.erase(master);
162 /*}*/
163 //Compute the anchor point
164 boost::optional<Geom::Rect> b = sp_item_bbox_desktop (thing);
165 if (b) {
166 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
167 a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
168 } else {
169 return;
170 }
171 break;
172 }
174 case AlignAndDistribute::PAGE:
175 mp = Geom::Point(a.mx1 * sp_document_width(sp_desktop_document(desktop)),
176 a.my1 * sp_document_height(sp_desktop_document(desktop)));
177 break;
179 case AlignAndDistribute::DRAWING:
180 {
181 boost::optional<Geom::Rect> b = sp_item_bbox_desktop
182 ( (SPItem *) sp_document_root (sp_desktop_document (desktop)) );
183 if (b) {
184 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
185 a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
186 } else {
187 return;
188 }
189 break;
190 }
192 case AlignAndDistribute::SELECTION:
193 {
194 boost::optional<Geom::Rect> b = selection->bounds();
195 if (b) {
196 mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X],
197 a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]);
198 } else {
199 return;
200 }
201 break;
202 }
204 default:
205 g_assert_not_reached ();
206 break;
207 }; // end of switch
209 // Top hack: temporarily set clone compensation to unmoved, so that we can align/distribute
210 // clones with their original (and the move of the original does not disturb the
211 // clones). The only problem with this is that if there are outside-of-selection clones of
212 // a selected original, they will be unmoved too, possibly contrary to user's
213 // expecation. However this is a minor point compared to making align/distribute always
214 // work as expected, and "unmoved" is the default option anyway.
215 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
216 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
218 bool changed = false;
219 boost::optional<Geom::Rect> b;
220 if (sel_as_group)
221 b = selection->bounds();
223 //Move each item in the selected list separately
224 for (std::list<SPItem *>::iterator it(selected.begin());
225 it != selected.end();
226 it++)
227 {
228 sp_document_ensure_up_to_date(sp_desktop_document (desktop));
229 if (!sel_as_group)
230 b = sp_item_bbox_desktop (*it);
231 if (b) {
232 Geom::Point const sp(a.sx0 * b->min()[Geom::X] + a.sx1 * b->max()[Geom::X],
233 a.sy0 * b->min()[Geom::Y] + a.sy1 * b->max()[Geom::Y]);
234 Geom::Point const mp_rel( mp - sp );
235 if (LInfty(mp_rel) > 1e-9) {
236 sp_item_move_rel(*it, Geom::Translate(mp_rel));
237 changed = true;
238 }
239 }
240 }
242 // restore compensation setting
243 prefs->setInt("/options/clonecompensation/value", saved_compensation);
245 if (changed) {
246 sp_document_done ( sp_desktop_document (desktop) , SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
247 _("Align"));
248 }
251 }
252 guint _index;
253 AlignAndDistribute &_dialog;
255 static const Coeffs _allCoeffs[10];
257 };
258 ActionAlign::Coeffs const ActionAlign::_allCoeffs[10] = {
259 {1., 0., 0., 0., 0., 1., 0., 0.},
260 {1., 0., 0., 0., 1., 0., 0., 0.},
261 {.5, .5, 0., 0., .5, .5, 0., 0.},
262 {0., 1., 0., 0., 0., 1., 0., 0.},
263 {0., 1., 0., 0., 1., 0., 0., 0.},
264 {0., 0., 0., 1., 0., 0., 1., 0.},
265 {0., 0., 0., 1., 0., 0., 0., 1.},
266 {0., 0., .5, .5, 0., 0., .5, .5},
267 {0., 0., 1., 0., 0., 0., 1., 0.},
268 {0., 0., 1., 0., 0., 0., 0., 1.}
269 };
271 BBoxSort::BBoxSort(SPItem *pItem, Geom::Rect bounds, Geom::Dim2 orientation, double kBegin, double kEnd) :
272 item(pItem),
273 bbox (bounds)
274 {
275 anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation];
276 }
277 BBoxSort::BBoxSort(const BBoxSort &rhs) :
278 //NOTE : this copy ctor is called O(sort) when sorting the vector
279 //this is bad. The vector should be a vector of pointers.
280 //But I'll wait the bohem GC before doing that
281 item(rhs.item), anchor(rhs.anchor), bbox(rhs.bbox)
282 {
283 }
285 bool operator< (const BBoxSort &a, const BBoxSort &b)
286 {
287 return (a.anchor < b.anchor);
288 }
290 class ActionDistribute : public Action {
291 public :
292 ActionDistribute(const Glib::ustring &id,
293 const Glib::ustring &tiptext,
294 guint row, guint column,
295 AlignAndDistribute &dialog,
296 bool onInterSpace,
297 Geom::Dim2 orientation,
298 double kBegin, double kEnd
299 ):
300 Action(id, tiptext, row, column,
301 dialog.distribute_table(), dialog.tooltips(), dialog),
302 _dialog(dialog),
303 _onInterSpace(onInterSpace),
304 _orientation(orientation),
305 _kBegin(kBegin),
306 _kEnd( kEnd)
307 {}
309 private :
310 virtual void on_button_click() {
311 //Retreive selected objects
312 SPDesktop *desktop = _dialog.getDesktop();
313 if (!desktop) return;
315 Inkscape::Selection *selection = sp_desktop_selection(desktop);
316 if (!selection) return;
318 using Inkscape::Util::GSListConstIterator;
319 std::list<SPItem *> selected;
320 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
321 if (selected.empty()) return;
323 //Check 2 or more selected objects
324 std::list<SPItem *>::iterator second(selected.begin());
325 ++second;
326 if (second == selected.end()) return;
329 std::vector< BBoxSort > sorted;
330 for (std::list<SPItem *>::iterator it(selected.begin());
331 it != selected.end();
332 ++it)
333 {
334 boost::optional<Geom::Rect> bbox = sp_item_bbox_desktop(*it);
335 if (bbox) {
336 sorted.push_back(BBoxSort(*it, *bbox, _orientation, _kBegin, _kEnd));
337 }
338 }
339 //sort bbox by anchors
340 std::sort(sorted.begin(), sorted.end());
342 // see comment in ActionAlign above
343 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
344 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
345 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
347 unsigned int len = sorted.size();
348 bool changed = false;
349 if (_onInterSpace)
350 {
351 //overall bboxes span
352 float dist = (sorted.back().bbox.max()[_orientation] -
353 sorted.front().bbox.min()[_orientation]);
354 //space eaten by bboxes
355 float span = 0;
356 for (unsigned int i = 0; i < len; i++)
357 {
358 span += sorted[i].bbox[_orientation].extent();
359 }
360 //new distance between each bbox
361 float step = (dist - span) / (len - 1);
362 float pos = sorted.front().bbox.min()[_orientation];
363 for ( std::vector<BBoxSort> ::iterator it (sorted.begin());
364 it < sorted.end();
365 it ++ )
366 {
367 if (!NR_DF_TEST_CLOSE (pos, it->bbox.min()[_orientation], 1e-6)) {
368 Geom::Point t(0.0, 0.0);
369 t[_orientation] = pos - it->bbox.min()[_orientation];
370 sp_item_move_rel(it->item, Geom::Translate(t));
371 changed = true;
372 }
373 pos += it->bbox[_orientation].extent();
374 pos += step;
375 }
376 }
377 else
378 {
379 //overall anchor span
380 float dist = sorted.back().anchor - sorted.front().anchor;
381 //distance between anchors
382 float step = dist / (len - 1);
384 for ( unsigned int i = 0; i < len ; i ++ )
385 {
386 BBoxSort & it(sorted[i]);
387 //new anchor position
388 float pos = sorted.front().anchor + i * step;
389 //Don't move if we are really close
390 if (!NR_DF_TEST_CLOSE (pos, it.anchor, 1e-6)) {
391 //Compute translation
392 Geom::Point t(0.0, 0.0);
393 t[_orientation] = pos - it.anchor;
394 //translate
395 sp_item_move_rel(it.item, Geom::Translate(t));
396 changed = true;
397 }
398 }
399 }
401 // restore compensation setting
402 prefs->setInt("/options/clonecompensation/value", saved_compensation);
404 if (changed) {
405 sp_document_done ( sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
406 _("Distribute"));
407 }
408 }
409 guint _index;
410 AlignAndDistribute &_dialog;
411 bool _onInterSpace;
412 Geom::Dim2 _orientation;
414 double _kBegin;
415 double _kEnd;
417 };
420 class ActionNode : public Action {
421 public :
422 ActionNode(const Glib::ustring &id,
423 const Glib::ustring &tiptext,
424 guint column,
425 AlignAndDistribute &dialog,
426 Geom::Dim2 orientation, bool distribute):
427 Action(id, tiptext, 0, column,
428 dialog.nodes_table(), dialog.tooltips(), dialog),
429 _orientation(orientation),
430 _distribute(distribute)
431 {}
433 private :
434 Geom::Dim2 _orientation;
435 bool _distribute;
436 virtual void on_button_click()
437 {
439 if (!_dialog.getDesktop()) return;
440 SPEventContext *event_context = sp_desktop_event_context(_dialog.getDesktop());
441 if (!SP_IS_NODE_CONTEXT (event_context)) return ;
443 if (_distribute)
444 SP_NODE_CONTEXT (event_context)->shape_editor->distribute((Geom::Dim2)_orientation);
445 else
446 SP_NODE_CONTEXT (event_context)->shape_editor->align((Geom::Dim2)_orientation);
448 }
449 };
451 class ActionRemoveOverlaps : public Action {
452 private:
453 Gtk::Label removeOverlapXGapLabel;
454 Gtk::Label removeOverlapYGapLabel;
455 Gtk::SpinButton removeOverlapXGap;
456 Gtk::SpinButton removeOverlapYGap;
458 public:
459 ActionRemoveOverlaps(Glib::ustring const &id,
460 Glib::ustring const &tiptext,
461 guint row,
462 guint column,
463 AlignAndDistribute &dialog) :
464 Action(id, tiptext, row, column + 4,
465 dialog.removeOverlap_table(), dialog.tooltips(), dialog)
466 {
467 dialog.removeOverlap_table().set_col_spacings(3);
469 removeOverlapXGap.set_digits(1);
470 removeOverlapXGap.set_size_request(60, -1);
471 removeOverlapXGap.set_increments(1.0, 5.0);
472 removeOverlapXGap.set_range(-1000.0, 1000.0);
473 removeOverlapXGap.set_value(0);
474 dialog.tooltips().set_tip(removeOverlapXGap,
475 _("Minimum horizontal gap (in px units) between bounding boxes"));
476 /* TRANSLATORS: Horizontal gap. Only put "H:" equivalent in the translation */
477 removeOverlapXGapLabel.set_label(Q_("gap|H:"));
479 removeOverlapYGap.set_digits(1);
480 removeOverlapYGap.set_size_request(60, -1);
481 removeOverlapYGap.set_increments(1.0, 5.0);
482 removeOverlapYGap.set_range(-1000.0, 1000.0);
483 removeOverlapYGap.set_value(0);
484 dialog.tooltips().set_tip(removeOverlapYGap,
485 _("Minimum vertical gap (in px units) between bounding boxes"));
486 /* TRANSLATORS: Vertical gap */
487 removeOverlapYGapLabel.set_label(_("V:"));
489 dialog.removeOverlap_table().attach(removeOverlapXGapLabel, column, column+1, row, row+1, Gtk::FILL, Gtk::FILL);
490 dialog.removeOverlap_table().attach(removeOverlapXGap, column+1, column+2, row, row+1, Gtk::FILL, Gtk::FILL);
491 dialog.removeOverlap_table().attach(removeOverlapYGapLabel, column+2, column+3, row, row+1, Gtk::FILL, Gtk::FILL);
492 dialog.removeOverlap_table().attach(removeOverlapYGap, column+3, column+4, row, row+1, Gtk::FILL, Gtk::FILL);
494 }
496 private :
497 virtual void on_button_click()
498 {
499 if (!_dialog.getDesktop()) return;
501 // see comment in ActionAlign above
502 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
503 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
504 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
506 // xGap and yGap are the minimum space required between bounding rectangles.
507 double const xGap = removeOverlapXGap.get_value();
508 double const yGap = removeOverlapYGap.get_value();
509 removeoverlap(sp_desktop_selection(_dialog.getDesktop())->itemList(),
510 xGap, yGap);
512 // restore compensation setting
513 prefs->setInt("/options/clonecompensation/value", saved_compensation);
515 sp_document_done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
516 _("Remove overlaps"));
517 }
518 };
520 class ActionGraphLayout : public Action {
521 public:
522 ActionGraphLayout(Glib::ustring const &id,
523 Glib::ustring const &tiptext,
524 guint row,
525 guint column,
526 AlignAndDistribute &dialog) :
527 Action(id, tiptext, row, column + 4,
528 dialog.graphLayout_table(), dialog.tooltips(), dialog)
529 {}
531 private :
532 virtual void on_button_click()
533 {
534 if (!_dialog.getDesktop()) return;
536 // see comment in ActionAlign above
537 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
538 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
539 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
541 graphlayout(sp_desktop_selection(_dialog.getDesktop())->itemList());
543 // restore compensation setting
544 prefs->setInt("/options/clonecompensation/value", saved_compensation);
546 sp_document_done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
547 _("Arrange connector network"));
548 }
549 };
551 class ActionUnclump : public Action {
552 public :
553 ActionUnclump(const Glib::ustring &id,
554 const Glib::ustring &tiptext,
555 guint row,
556 guint column,
557 AlignAndDistribute &dialog):
558 Action(id, tiptext, row, column,
559 dialog.distribute_table(), dialog.tooltips(), dialog)
560 {}
562 private :
563 virtual void on_button_click()
564 {
565 if (!_dialog.getDesktop()) return;
567 // see comment in ActionAlign above
568 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
569 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
570 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
572 unclump ((GSList *) sp_desktop_selection(_dialog.getDesktop())->itemList());
574 // restore compensation setting
575 prefs->setInt("/options/clonecompensation/value", saved_compensation);
577 sp_document_done (sp_desktop_document (_dialog.getDesktop()), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
578 _("Unclump"));
579 }
580 };
582 class ActionRandomize : public Action {
583 public :
584 ActionRandomize(const Glib::ustring &id,
585 const Glib::ustring &tiptext,
586 guint row,
587 guint column,
588 AlignAndDistribute &dialog):
589 Action(id, tiptext, row, column,
590 dialog.distribute_table(), dialog.tooltips(), dialog)
591 {}
593 private :
594 virtual void on_button_click()
595 {
596 SPDesktop *desktop = _dialog.getDesktop();
597 if (!desktop) return;
599 Inkscape::Selection *selection = sp_desktop_selection(desktop);
600 if (!selection) return;
602 using Inkscape::Util::GSListConstIterator;
603 std::list<SPItem *> selected;
604 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
605 if (selected.empty()) return;
607 //Check 2 or more selected objects
608 if (selected.size() < 2) return;
610 boost::optional<Geom::Rect> sel_bbox = selection->bounds();
611 if (!sel_bbox) {
612 return;
613 }
615 // This bbox is cached between calls to randomize, so that there's no growth nor shrink
616 // nor drift on sequential randomizations. Discard cache on global (or better active
617 // desktop's) selection_change signal.
618 if (!_dialog.randomize_bbox) {
619 _dialog.randomize_bbox = *sel_bbox;
620 }
622 // see comment in ActionAlign above
623 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
624 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
625 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
627 for (std::list<SPItem *>::iterator it(selected.begin());
628 it != selected.end();
629 ++it)
630 {
631 sp_document_ensure_up_to_date(sp_desktop_document (desktop));
632 boost::optional<Geom::Rect> item_box = sp_item_bbox_desktop (*it);
633 if (item_box) {
634 // find new center, staying within bbox
635 double x = _dialog.randomize_bbox->min()[Geom::X] + (*item_box)[Geom::X].extent() /2 +
636 g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::X].extent() - (*item_box)[Geom::X].extent());
637 double y = _dialog.randomize_bbox->min()[Geom::Y] + (*item_box)[Geom::Y].extent()/2 +
638 g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::Y].extent() - (*item_box)[Geom::Y].extent());
639 // displacement is the new center minus old:
640 Geom::Point t = Geom::Point (x, y) - 0.5*(item_box->max() + item_box->min());
641 sp_item_move_rel(*it, Geom::Translate(t));
642 }
643 }
645 // restore compensation setting
646 prefs->setInt("/options/clonecompensation/value", saved_compensation);
648 sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
649 _("Randomize positions"));
650 }
651 };
653 struct Baselines
654 {
655 SPItem *_item;
656 Geom::Point _base;
657 Geom::Dim2 _orientation;
658 Baselines(SPItem *item, Geom::Point base, Geom::Dim2 orientation) :
659 _item (item),
660 _base (base),
661 _orientation (orientation)
662 {}
663 };
665 bool operator< (const Baselines &a, const Baselines &b)
666 {
667 return (a._base[a._orientation] < b._base[b._orientation]);
668 }
670 class ActionBaseline : public Action {
671 public :
672 ActionBaseline(const Glib::ustring &id,
673 const Glib::ustring &tiptext,
674 guint row,
675 guint column,
676 AlignAndDistribute &dialog,
677 Gtk::Table &table,
678 Geom::Dim2 orientation, bool distribute):
679 Action(id, tiptext, row, column,
680 table, dialog.tooltips(), dialog),
681 _orientation(orientation),
682 _distribute(distribute)
683 {}
685 private :
686 Geom::Dim2 _orientation;
687 bool _distribute;
688 virtual void on_button_click()
689 {
690 SPDesktop *desktop = _dialog.getDesktop();
691 if (!desktop) return;
693 Inkscape::Selection *selection = sp_desktop_selection(desktop);
694 if (!selection) return;
696 using Inkscape::Util::GSListConstIterator;
697 std::list<SPItem *> selected;
698 selected.insert<GSListConstIterator<SPItem *> >(selected.end(), selection->itemList(), NULL);
699 if (selected.empty()) return;
701 //Check 2 or more selected objects
702 if (selected.size() < 2) return;
704 Geom::Point b_min = Geom::Point (HUGE_VAL, HUGE_VAL);
705 Geom::Point b_max = Geom::Point (-HUGE_VAL, -HUGE_VAL);
707 std::vector<Baselines> sorted;
709 for (std::list<SPItem *>::iterator it(selected.begin());
710 it != selected.end();
711 ++it)
712 {
713 if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
714 Inkscape::Text::Layout const *layout = te_get_layout(*it);
715 Geom::Point base = layout->characterAnchorPoint(layout->begin()) * sp_item_i2d_affine(*it);
716 if (base[Geom::X] < b_min[Geom::X]) b_min[Geom::X] = base[Geom::X];
717 if (base[Geom::Y] < b_min[Geom::Y]) b_min[Geom::Y] = base[Geom::Y];
718 if (base[Geom::X] > b_max[Geom::X]) b_max[Geom::X] = base[Geom::X];
719 if (base[Geom::Y] > b_max[Geom::Y]) b_max[Geom::Y] = base[Geom::Y];
721 Baselines b (*it, base, _orientation);
722 sorted.push_back(b);
723 }
724 }
726 if (sorted.size() <= 1) return;
728 //sort baselines
729 std::sort(sorted.begin(), sorted.end());
731 bool changed = false;
733 if (_distribute) {
734 double step = (b_max[_orientation] - b_min[_orientation])/(sorted.size() - 1);
735 for (unsigned int i = 0; i < sorted.size(); i++) {
736 SPItem *item = sorted[i]._item;
737 Geom::Point base = sorted[i]._base;
738 Geom::Point t(0.0, 0.0);
739 t[_orientation] = b_min[_orientation] + step * i - base[_orientation];
740 sp_item_move_rel(item, Geom::Translate(t));
741 changed = true;
742 }
744 if (changed) {
745 sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
746 _("Distribute text baselines"));
747 }
749 } else {
750 for (std::list<SPItem *>::iterator it(selected.begin());
751 it != selected.end();
752 ++it)
753 {
754 if (SP_IS_TEXT (*it) || SP_IS_FLOWTEXT (*it)) {
755 Inkscape::Text::Layout const *layout = te_get_layout(*it);
756 Geom::Point base = layout->characterAnchorPoint(layout->begin()) * sp_item_i2d_affine(*it);
757 Geom::Point t(0.0, 0.0);
758 t[_orientation] = b_min[_orientation] - base[_orientation];
759 sp_item_move_rel(*it, Geom::Translate(t));
760 changed = true;
761 }
762 }
764 if (changed) {
765 sp_document_done (sp_desktop_document (desktop), SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
766 _("Align text baselines"));
767 }
768 }
769 }
770 };
774 void on_tool_changed(Inkscape::Application */*inkscape*/, SPEventContext */*context*/, AlignAndDistribute *daad)
775 {
776 SPDesktop *desktop = SP_ACTIVE_DESKTOP;
777 if (desktop && sp_desktop_event_context(desktop))
778 daad->setMode(tools_active(desktop) == TOOLS_NODES);
779 }
781 void on_selection_changed(Inkscape::Application */*inkscape*/, Inkscape::Selection */*selection*/, AlignAndDistribute *daad)
782 {
783 daad->randomize_bbox = boost::optional<Geom::Rect>();
784 }
786 /////////////////////////////////////////////////////////
791 AlignAndDistribute::AlignAndDistribute()
792 : UI::Widget::Panel ("", "/dialogs/align", SP_VERB_DIALOG_ALIGN_DISTRIBUTE),
793 randomize_bbox(),
794 _alignFrame(_("Align")),
795 _distributeFrame(_("Distribute")),
796 _removeOverlapFrame(_("Remove overlaps")),
797 _graphLayoutFrame(_("Connector network layout")),
798 _nodesFrame(_("Nodes")),
799 _alignTable(2, 6, true),
800 _distributeTable(3, 6, true),
801 _removeOverlapTable(1, 5, false),
802 _graphLayoutTable(1, 5, false),
803 _nodesTable(1, 4, true),
804 _anchorLabel(_("Relative to: ")),
805 _selgrpLabel(_("Treat selection as group: "))
806 {
807 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
809 //Instanciate the align buttons
810 addAlignButton("al_left_out",
811 _("Align right sides of objects to left side of anchor"),
812 0, 0);
813 addAlignButton("al_left_in",
814 _("Align left sides"),
815 0, 1);
816 addAlignButton("al_center_hor",
817 _("Center on vertical axis"),
818 0, 2);
819 addAlignButton("al_right_in",
820 _("Align right sides"),
821 0, 3);
822 addAlignButton("al_right_out",
823 _("Align left sides of objects to right side of anchor"),
824 0, 4);
825 addAlignButton("al_top_out",
826 _("Align bottoms of objects to top of anchor"),
827 1, 0);
828 addAlignButton("al_top_in",
829 _("Align tops"),
830 1, 1);
831 addAlignButton("al_center_ver",
832 _("Center on horizontal axis"),
833 1, 2);
834 addAlignButton("al_bottom_in",
835 _("Align bottoms"),
836 1, 3);
837 addAlignButton("al_bottom_out",
838 _("Align tops of objects to bottom of anchor"),
839 1, 4);
841 //Baseline aligns
842 addBaselineButton("al_baselines_vert",
843 _("Align baseline anchors of texts vertically"),
844 0, 5, this->align_table(), Geom::X, false);
845 addBaselineButton("al_baselines_hor",
846 _("Align baseline anchors of texts horizontally"),
847 1, 5, this->align_table(), Geom::Y, false);
849 //The distribute buttons
850 addDistributeButton("distribute_hdist",
851 _("Make horizontal gaps between objects equal"),
852 0, 4, true, Geom::X, .5, .5);
854 addDistributeButton("distribute_left",
855 _("Distribute left sides equidistantly"),
856 0, 1, false, Geom::X, 1., 0.);
857 addDistributeButton("distribute_hcentre",
858 _("Distribute centers equidistantly horizontally"),
859 0, 2, false, Geom::X, .5, .5);
860 addDistributeButton("distribute_right",
861 _("Distribute right sides equidistantly"),
862 0, 3, false, Geom::X, 0., 1.);
864 addDistributeButton("distribute_vdist",
865 _("Make vertical gaps between objects equal"),
866 1, 4, true, Geom::Y, .5, .5);
868 addDistributeButton("distribute_top",
869 _("Distribute tops equidistantly"),
870 1, 1, false, Geom::Y, 0, 1);
871 addDistributeButton("distribute_vcentre",
872 _("Distribute centers equidistantly vertically"),
873 1, 2, false, Geom::Y, .5, .5);
874 addDistributeButton("distribute_bottom",
875 _("Distribute bottoms equidistantly"),
876 1, 3, false, Geom::Y, 1., 0.);
878 //Baseline distribs
879 addBaselineButton("distribute_baselines_hor",
880 _("Distribute baseline anchors of texts horizontally"),
881 0, 5, this->distribute_table(), Geom::X, true);
882 addBaselineButton("distribute_baselines_vert",
883 _("Distribute baseline anchors of texts vertically"),
884 1, 5, this->distribute_table(), Geom::Y, true);
886 //Randomize & Unclump
887 addRandomizeButton("distribute_randomize",
888 _("Randomize centers in both dimensions"),
889 2, 2);
890 addUnclumpButton("unclump",
891 _("Unclump objects: try to equalize edge-to-edge distances"),
892 2, 4);
894 //Remove overlaps
895 addRemoveOverlapsButton("remove_overlaps",
896 _("Move objects as little as possible so that their bounding boxes do not overlap"),
897 0, 0);
898 //Graph Layout
899 addGraphLayoutButton("graph_layout",
900 _("Nicely arrange selected connector network"),
901 0, 0);
903 //Node Mode buttons
904 addNodeButton("node_halign",
905 _("Align selected nodes horizontally"),
906 0, Geom::X, false);
907 addNodeButton("node_valign",
908 _("Align selected nodes vertically"),
909 1, Geom::Y, false);
910 addNodeButton("node_hdistribute",
911 _("Distribute selected nodes horizontally"),
912 2, Geom::X, true);
913 addNodeButton("node_vdistribute",
914 _("Distribute selected nodes vertically"),
915 3, Geom::Y, true);
917 //Rest of the widgetry
919 _combo.append_text(_("Last selected"));
920 _combo.append_text(_("First selected"));
921 _combo.append_text(_("Biggest item"));
922 _combo.append_text(_("Smallest item"));
923 _combo.append_text(_("Page"));
924 _combo.append_text(_("Drawing"));
925 _combo.append_text(_("Selection"));
927 _combo.set_active(prefs->getInt("/dialogs/align/align-to", 6));
928 _combo.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_ref_change));
930 _anchorBox.pack_start(_anchorLabel);
931 _anchorBox.pack_start(_combo);
933 _selgrpBox.pack_start(_selgrpLabel);
934 _selgrpBox.pack_start(_selgrp);
935 _selgrp.set_active(prefs->getBool("/dialogs/align/sel-as-groups"));
936 _selgrp.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_selgrp_toggled));
938 _alignBox.pack_start(_anchorBox);
939 _alignBox.pack_start(_selgrpBox);
940 _alignBox.pack_start(_alignTable);
942 _alignFrame.add(_alignBox);
943 _distributeFrame.add(_distributeTable);
944 _removeOverlapFrame.add(_removeOverlapTable);
945 _graphLayoutFrame.add(_graphLayoutTable);
946 _nodesFrame.add(_nodesTable);
948 Gtk::Box *contents = _getContents();
949 contents->set_spacing(4);
951 // Notebook for individual transformations
953 contents->pack_start(_alignFrame, true, true);
954 contents->pack_start(_distributeFrame, true, true);
955 contents->pack_start(_removeOverlapFrame, true, true);
956 contents->pack_start(_graphLayoutFrame, true, true);
957 contents->pack_start(_nodesFrame, true, true);
959 //Connect to the global tool change signal
960 g_signal_connect (G_OBJECT (INKSCAPE), "set_eventcontext", G_CALLBACK (on_tool_changed), this);
962 // Connect to the global selection change, to invalidate cached randomize_bbox
963 g_signal_connect (G_OBJECT (INKSCAPE), "change_selection", G_CALLBACK (on_selection_changed), this);
964 randomize_bbox = boost::optional<Geom::Rect>();
966 show_all_children();
968 on_tool_changed (NULL, NULL, this); // set current mode
969 }
971 AlignAndDistribute::~AlignAndDistribute()
972 {
973 sp_signal_disconnect_by_data (G_OBJECT (INKSCAPE), this);
975 for (std::list<Action *>::iterator it = _actionList.begin();
976 it != _actionList.end();
977 it ++)
978 delete *it;
979 }
981 void AlignAndDistribute::on_ref_change(){
982 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
983 prefs->setInt("/dialogs/align/align-to", _combo.get_active_row_number());
985 //Make blink the master
986 }
988 void AlignAndDistribute::on_selgrp_toggled(){
989 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
990 prefs->setInt("/dialogs/align/sel-as-groups", _selgrp.get_active());
992 //Make blink the master
993 }
998 void AlignAndDistribute::setMode(bool nodeEdit)
999 {
1000 //Act on widgets used in node mode
1001 void ( Gtk::Widget::*mNode) () = nodeEdit ?
1002 &Gtk::Widget::show_all : &Gtk::Widget::hide_all;
1004 //Act on widgets used in selection mode
1005 void ( Gtk::Widget::*mSel) () = nodeEdit ?
1006 &Gtk::Widget::hide_all : &Gtk::Widget::show_all;
1009 ((_alignFrame).*(mSel))();
1010 ((_distributeFrame).*(mSel))();
1011 ((_removeOverlapFrame).*(mSel))();
1012 ((_graphLayoutFrame).*(mSel))();
1013 ((_nodesFrame).*(mNode))();
1015 }
1016 void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext,
1017 guint row, guint col)
1018 {
1019 _actionList.push_back(
1020 new ActionAlign(
1021 id, tiptext, row, col,
1022 *this , col + row * 5));
1023 }
1024 void AlignAndDistribute::addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1025 guint row, guint col, bool onInterSpace,
1026 Geom::Dim2 orientation, float kBegin, float kEnd)
1027 {
1028 _actionList.push_back(
1029 new ActionDistribute(
1030 id, tiptext, row, col, *this ,
1031 onInterSpace, orientation,
1032 kBegin, kEnd
1033 )
1034 );
1035 }
1037 void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1038 guint col, Geom::Dim2 orientation, bool distribute)
1039 {
1040 _actionList.push_back(
1041 new ActionNode(
1042 id, tiptext, col,
1043 *this, orientation, distribute));
1044 }
1046 void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext,
1047 guint row, guint col)
1048 {
1049 _actionList.push_back(
1050 new ActionRemoveOverlaps(
1051 id, tiptext, row, col, *this)
1052 );
1053 }
1055 void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext,
1056 guint row, guint col)
1057 {
1058 _actionList.push_back(
1059 new ActionGraphLayout(
1060 id, tiptext, row, col, *this)
1061 );
1062 }
1064 void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext,
1065 guint row, guint col)
1066 {
1067 _actionList.push_back(
1068 new ActionUnclump(
1069 id, tiptext, row, col, *this)
1070 );
1071 }
1073 void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext,
1074 guint row, guint col)
1075 {
1076 _actionList.push_back(
1077 new ActionRandomize(
1078 id, tiptext, row, col, *this)
1079 );
1080 }
1082 void AlignAndDistribute::addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext,
1083 guint row, guint col, Gtk::Table &table, Geom::Dim2 orientation, bool distribute)
1084 {
1085 _actionList.push_back(
1086 new ActionBaseline(
1087 id, tiptext, row, col,
1088 *this, table, orientation, distribute));
1089 }
1094 std::list<SPItem *>::iterator AlignAndDistribute::find_master( std::list<SPItem *> &list, bool horizontal){
1095 std::list<SPItem *>::iterator master = list.end();
1096 switch (getAlignTarget()) {
1097 case LAST:
1098 return list.begin();
1099 break;
1101 case FIRST:
1102 return --(list.end());
1103 break;
1105 case BIGGEST:
1106 {
1107 gdouble max = -1e18;
1108 for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1109 boost::optional<Geom::Rect> b = sp_item_bbox_desktop (*it);
1110 if (b) {
1111 gdouble dim = (*b)[horizontal ? Geom::X : Geom::Y].extent();
1112 if (dim > max) {
1113 max = dim;
1114 master = it;
1115 }
1116 }
1117 }
1118 return master;
1119 break;
1120 }
1122 case SMALLEST:
1123 {
1124 gdouble max = 1e18;
1125 for (std::list<SPItem *>::iterator it = list.begin(); it != list.end(); it++) {
1126 boost::optional<Geom::Rect> b = sp_item_bbox_desktop (*it);
1127 if (b) {
1128 gdouble dim = (*b)[horizontal ? Geom::X : Geom::Y].extent();
1129 if (dim < max) {
1130 max = dim;
1131 master = it;
1132 }
1133 }
1134 }
1135 return master;
1136 break;
1137 }
1139 default:
1140 g_assert_not_reached ();
1141 break;
1143 } // end of switch statement
1144 return master;
1145 }
1147 AlignAndDistribute::AlignTarget AlignAndDistribute::getAlignTarget()const {
1148 return AlignTarget(_combo.get_active_row_number());
1149 }
1153 } // namespace Dialog
1154 } // namespace UI
1155 } // namespace Inkscape
1157 /*
1158 Local Variables:
1159 mode:c++
1160 c-file-style:"stroustrup"
1161 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1162 indent-tabs-mode:nil
1163 fill-column:99
1164 End:
1165 */
1166 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :