1 /** @file
2 * @brief New node tool - implementation
3 */
4 /* Authors:
5 * Krzysztof KosiĆski <tweenk@gmail.com>
6 *
7 * Copyright (C) 2009 Authors
8 * Released under GNU GPL, read the file 'COPYING' for more information
9 */
11 #include <glib.h>
12 #include <glib/gi18n.h>
13 #include "desktop.h"
14 #include "desktop-handles.h"
15 #include "display/canvas-bpath.h"
16 #include "display/curve.h"
17 #include "display/sp-canvas.h"
18 #include "document.h"
19 #include "message-context.h"
20 #include "selection.h"
21 #include "shape-editor.h" // temporary!
22 #include "sp-clippath.h"
23 #include "sp-item-group.h"
24 #include "sp-mask.h"
25 #include "sp-object-group.h"
26 #include "sp-path.h"
27 #include "ui/tool/node-tool.h"
28 #include "ui/tool/control-point-selection.h"
29 #include "ui/tool/curve-drag-point.h"
30 #include "ui/tool/event-utils.h"
31 #include "ui/tool/manipulator.h"
32 #include "ui/tool/multi-path-manipulator.h"
33 #include "ui/tool/path-manipulator.h"
34 #include "ui/tool/selector.h"
36 #include "pixmaps/cursor-node.xpm"
37 #include "pixmaps/cursor-node-d.xpm"
39 namespace {
40 SPCanvasGroup *create_control_group(SPDesktop *d);
41 void ink_node_tool_class_init(InkNodeToolClass *klass);
42 void ink_node_tool_init(InkNodeTool *node_context);
43 void ink_node_tool_dispose(GObject *object);
45 void ink_node_tool_setup(SPEventContext *ec);
46 gint ink_node_tool_root_handler(SPEventContext *event_context, GdkEvent *event);
47 gint ink_node_tool_item_handler(SPEventContext *event_context, SPItem *item, GdkEvent *event);
48 void ink_node_tool_set(SPEventContext *ec, Inkscape::Preferences::Entry *value);
50 void ink_node_tool_update_tip(InkNodeTool *nt, GdkEvent *event);
51 void ink_node_tool_selection_changed(InkNodeTool *nt, Inkscape::Selection *sel);
52 void ink_node_tool_select_area(InkNodeTool *nt, Geom::Rect const &, GdkEventButton *);
53 void ink_node_tool_select_point(InkNodeTool *nt, Geom::Point const &, GdkEventButton *);
54 void ink_node_tool_mouseover_changed(InkNodeTool *nt, Inkscape::UI::ControlPoint *p);
55 } // anonymous namespace
57 GType ink_node_tool_get_type()
58 {
59 static GType type = 0;
60 if (!type) {
61 GTypeInfo info = {
62 sizeof(InkNodeToolClass),
63 NULL, NULL,
64 (GClassInitFunc) ink_node_tool_class_init,
65 NULL, NULL,
66 sizeof(InkNodeTool),
67 4,
68 (GInstanceInitFunc) ink_node_tool_init,
69 NULL, /* value_table */
70 };
71 type = g_type_register_static(SP_TYPE_EVENT_CONTEXT, "InkNodeTool", &info, (GTypeFlags)0);
72 }
73 return type;
74 }
76 namespace {
78 SPCanvasGroup *create_control_group(SPDesktop *d)
79 {
80 return reinterpret_cast<SPCanvasGroup*>(sp_canvas_item_new(
81 sp_desktop_controls(d), SP_TYPE_CANVAS_GROUP, NULL));
82 }
84 void destroy_group(SPCanvasGroup *g)
85 {
86 gtk_object_destroy(GTK_OBJECT(g));
87 }
89 void ink_node_tool_class_init(InkNodeToolClass *klass)
90 {
91 GObjectClass *object_class = (GObjectClass *) klass;
92 SPEventContextClass *event_context_class = (SPEventContextClass *) klass;
94 object_class->dispose = ink_node_tool_dispose;
96 event_context_class->setup = ink_node_tool_setup;
97 event_context_class->set = ink_node_tool_set;
98 event_context_class->root_handler = ink_node_tool_root_handler;
99 event_context_class->item_handler = ink_node_tool_item_handler;
100 }
102 void ink_node_tool_init(InkNodeTool *nt)
103 {
104 SPEventContext *event_context = SP_EVENT_CONTEXT(nt);
106 event_context->cursor_shape = cursor_node_xpm;
107 event_context->hot_x = 1;
108 event_context->hot_y = 1;
110 new (&nt->_selection_changed_connection) sigc::connection();
111 new (&nt->_mouseover_changed_connection) sigc::connection();
112 //new (&nt->_mgroup) Inkscape::UI::ManipulatorGroup(nt->desktop);
113 new (&nt->_selected_nodes) CSelPtr();
114 new (&nt->_multipath) MultiPathPtr();
115 new (&nt->_selector) SelectorPtr();
116 new (&nt->_path_data) PathSharedDataPtr();
117 }
119 void ink_node_tool_dispose(GObject *object)
120 {
121 InkNodeTool *nt = INK_NODE_TOOL(object);
123 nt->enableGrDrag(false);
125 nt->_selection_changed_connection.disconnect();
126 nt->_mouseover_changed_connection.disconnect();
127 nt->_multipath.~MultiPathPtr();
128 nt->_selected_nodes.~CSelPtr();
129 nt->_selector.~SelectorPtr();
131 Inkscape::UI::PathSharedData &data = *nt->_path_data;
132 destroy_group(data.node_data.node_group);
133 destroy_group(data.node_data.handle_group);
134 destroy_group(data.node_data.handle_line_group);
135 destroy_group(data.outline_group);
136 destroy_group(data.dragpoint_group);
137 destroy_group(nt->_transform_handle_group);
139 nt->_path_data.~PathSharedDataPtr();
140 nt->_selection_changed_connection.~connection();
141 nt->_mouseover_changed_connection.~connection();
143 if (nt->_node_message_context) {
144 delete nt->_node_message_context;
145 }
146 if (nt->shape_editor) {
147 nt->shape_editor->unset_item(SH_KNOTHOLDER);
148 delete nt->shape_editor;
149 }
151 G_OBJECT_CLASS(g_type_class_peek(g_type_parent(INK_TYPE_NODE_TOOL)))->dispose(object);
152 }
154 void ink_node_tool_setup(SPEventContext *ec)
155 {
156 InkNodeTool *nt = INK_NODE_TOOL(ec);
158 SPEventContextClass *parent = (SPEventContextClass *) g_type_class_peek(g_type_parent(INK_TYPE_NODE_TOOL));
159 if (parent->setup) parent->setup(ec);
161 nt->_node_message_context = new Inkscape::MessageContext((ec->desktop)->messageStack());
163 nt->_path_data.reset(new Inkscape::UI::PathSharedData());
164 Inkscape::UI::PathSharedData &data = *nt->_path_data;
165 data.node_data.desktop = nt->desktop;
167 // selector has to be created here, so that its hidden control point is on the bottom
168 nt->_selector.reset(new Inkscape::UI::Selector(nt->desktop));
170 // Prepare canvas groups for controls. This guarantees correct z-order, so that
171 // for example a dragpoint won't obscure a node
172 data.outline_group = create_control_group(nt->desktop);
173 data.node_data.handle_line_group = create_control_group(nt->desktop);
174 data.dragpoint_group = create_control_group(nt->desktop);
175 nt->_transform_handle_group = create_control_group(nt->desktop);
176 data.node_data.node_group = create_control_group(nt->desktop);
177 data.node_data.handle_group = create_control_group(nt->desktop);
179 Inkscape::Selection *selection = sp_desktop_selection (ec->desktop);
180 nt->_selection_changed_connection.disconnect();
181 nt->_selection_changed_connection =
182 selection->connectChanged(
183 sigc::bind<0>(
184 sigc::ptr_fun(&ink_node_tool_selection_changed),
185 nt));
186 nt->_mouseover_changed_connection.disconnect();
187 nt->_mouseover_changed_connection =
188 Inkscape::UI::ControlPoint::signal_mouseover_change.connect(
189 sigc::bind<0>(
190 sigc::ptr_fun(&ink_node_tool_mouseover_changed),
191 nt));
193 nt->_selected_nodes.reset(
194 new Inkscape::UI::ControlPointSelection(nt->desktop, nt->_transform_handle_group));
195 data.node_data.selection = nt->_selected_nodes.get();
196 nt->_multipath.reset(new Inkscape::UI::MultiPathManipulator(data,
197 nt->_selection_changed_connection));
199 nt->_selector->signal_point.connect(
200 sigc::bind<0>(
201 sigc::ptr_fun(&ink_node_tool_select_point),
202 nt));
203 nt->_selector->signal_area.connect(
204 sigc::bind<0>(
205 sigc::ptr_fun(&ink_node_tool_select_area),
206 nt));
208 nt->_multipath->signal_coords_changed.connect(
209 sigc::bind(
210 sigc::mem_fun(*nt->desktop, &SPDesktop::emitToolSubselectionChanged),
211 (void*) 0));
212 nt->_selected_nodes->signal_point_changed.connect(
213 sigc::hide( sigc::hide(
214 sigc::bind(
215 sigc::bind(
216 sigc::ptr_fun(ink_node_tool_update_tip),
217 (GdkEvent*)0),
218 nt))));
220 nt->cursor_drag = false;
221 nt->show_transform_handles = true;
222 nt->single_node_transform_handles = false;
223 nt->flash_tempitem = NULL;
224 nt->flashed_item = NULL;
225 // TODO remove this!
226 nt->shape_editor = new ShapeEditor(nt->desktop);
228 // read prefs before adding items to selection to prevent momentarily showing the outline
229 sp_event_context_read(nt, "show_handles");
230 sp_event_context_read(nt, "show_outline");
231 sp_event_context_read(nt, "show_path_direction");
232 sp_event_context_read(nt, "show_transform_handles");
233 sp_event_context_read(nt, "single_node_transform_handles");
234 sp_event_context_read(nt, "edit_clipping_paths");
235 sp_event_context_read(nt, "edit_masks");
237 ink_node_tool_selection_changed(nt, selection);
238 ink_node_tool_update_tip(nt, NULL);
240 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
241 if (prefs->getBool("/tools/nodes/selcue")) {
242 ec->enableSelectionCue();
243 }
244 if (prefs->getBool("/tools/nodes/gradientdrag")) {
245 ec->enableGrDrag();
246 }
248 nt->desktop->emitToolSubselectionChanged(NULL); // sets the coord entry fields to inactive
249 }
251 void ink_node_tool_set(SPEventContext *ec, Inkscape::Preferences::Entry *value)
252 {
253 InkNodeTool *nt = INK_NODE_TOOL(ec);
254 Glib::ustring entry_name = value->getEntryName();
256 if (entry_name == "show_handles") {
257 nt->_multipath->showHandles(value->getBool(true));
258 } else if (entry_name == "show_outline") {
259 nt->show_outline = value->getBool();
260 nt->_multipath->showOutline(nt->show_outline);
261 } else if (entry_name == "show_path_direction") {
262 nt->show_path_direction = value->getBool();
263 nt->_multipath->showPathDirection(nt->show_path_direction);
264 } else if (entry_name == "show_transform_handles") {
265 nt->show_transform_handles = value->getBool(true);
266 nt->_selected_nodes->showTransformHandles(
267 nt->show_transform_handles, nt->single_node_transform_handles);
268 } else if (entry_name == "single_node_transform_handles") {
269 nt->single_node_transform_handles = value->getBool();
270 nt->_selected_nodes->showTransformHandles(
271 nt->show_transform_handles, nt->single_node_transform_handles);
272 } else if (entry_name == "edit_clipping_paths") {
273 nt->edit_clipping_paths = value->getBool();
274 ink_node_tool_selection_changed(nt, nt->desktop->selection);
275 } else if (entry_name == "edit_masks") {
276 nt->edit_masks = value->getBool();
277 ink_node_tool_selection_changed(nt, nt->desktop->selection);
278 } else {
279 SPEventContextClass *parent_class =
280 (SPEventContextClass *) g_type_class_peek(g_type_parent(INK_TYPE_NODE_TOOL));
281 if (parent_class->set)
282 parent_class->set(ec, value);
283 }
284 }
286 void store_clip_mask_items(SPItem *clipped, SPObject *obj, std::map<SPItem*,
287 std::pair<Geom::Matrix, guint32> > &s, Geom::Matrix const &postm, guint32 color)
288 {
289 if (!obj) return;
290 if (SP_IS_GROUP(obj) || SP_IS_OBJECTGROUP(obj)) {
291 //TODO is checking for obj->children != NULL above better?
292 for (SPObject *c = obj->children; c; c = c->next) {
293 store_clip_mask_items(clipped, c, s, postm, color);
294 }
295 } else if (SP_IS_ITEM(obj)) {
296 s.insert(std::make_pair(SP_ITEM(obj),
297 std::make_pair(sp_item_i2d_affine(clipped) * postm, color)));
298 }
299 }
301 struct IsPath {
302 bool operator()(SPItem *i) const { return SP_IS_PATH(i); }
303 };
305 void ink_node_tool_selection_changed(InkNodeTool *nt, Inkscape::Selection *sel)
306 {
307 using namespace Inkscape::UI;
308 // TODO this is ugly!!!
309 typedef std::map<SPItem*, std::pair<Geom::Matrix, guint32> > TransMap;
310 typedef std::map<SPPath*, std::pair<Geom::Matrix, guint32> > PathMap;
311 GSList const *ilist = sel->itemList();
312 TransMap items;
313 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
315 for (GSList *i = const_cast<GSList*>(ilist); i; i = i->next) {
316 SPObject *obj = static_cast<SPObject*>(i->data);
317 if (SP_IS_ITEM(obj)) {
318 items.insert(std::make_pair(SP_ITEM(obj),
319 std::make_pair(Geom::identity(),
320 prefs->getColor("/tools/nodes/outline_color", 0xff0000ff))));
321 if (nt->edit_clipping_paths && SP_ITEM(i->data)->clip_ref) {
322 store_clip_mask_items(SP_ITEM(i->data),
323 SP_OBJECT(SP_ITEM(i->data)->clip_ref->getObject()), items,
324 nt->desktop->dt2doc(),
325 prefs->getColor("/tools/nodes/clipping_path_color", 0x00ff00ff));
326 }
327 if (nt->edit_masks && SP_ITEM(i->data)->mask_ref) {
328 store_clip_mask_items(SP_ITEM(i->data),
329 SP_OBJECT(SP_ITEM(i->data)->mask_ref->getObject()), items,
330 nt->desktop->dt2doc(),
331 prefs->getColor("/tools/nodes/mask_color", 0x0000ffff));
332 }
333 }
334 }
336 // ugly hack: set the first editable non-path item for knotholder
337 // maybe use multiple ShapeEditors for now, to allow editing many shapes at once?
338 bool something_set = false;
339 for (TransMap::iterator i = items.begin(); i != items.end(); ++i) {
340 SPItem *obj = i->first;
341 if (SP_IS_SHAPE(obj) && !SP_IS_PATH(obj)) {
342 nt->shape_editor->set_item(obj, SH_KNOTHOLDER);
343 something_set = true;
344 break;
345 }
346 }
347 if (!something_set) {
348 nt->shape_editor->unset_item(SH_KNOTHOLDER);
349 }
351 PathMap p;
352 for (TransMap::iterator i = items.begin(); i != items.end(); ++i) {
353 if (SP_IS_PATH(i->first)) {
354 p.insert(std::make_pair(SP_PATH(i->first),
355 std::make_pair(i->second.first, i->second.second)));
356 }
357 }
359 nt->_multipath->setItems(p);
360 ink_node_tool_update_tip(nt, NULL);
361 nt->desktop->updateNow();
362 }
364 gint ink_node_tool_root_handler(SPEventContext *event_context, GdkEvent *event)
365 {
366 /* things to handle here:
367 * 1. selection of items
368 * 2. passing events to manipulators
369 * 3. some keybindings
370 */
371 using namespace Inkscape::UI; // pull in event helpers
373 SPDesktop *desktop = event_context->desktop;
374 Inkscape::Selection *selection = desktop->selection;
375 InkNodeTool *nt = static_cast<InkNodeTool*>(event_context);
376 static Inkscape::Preferences *prefs = Inkscape::Preferences::get();
378 if (nt->_multipath->event(event)) return true;
379 if (nt->_selector->event(event)) return true;
380 if (nt->_selected_nodes->event(event)) return true;
382 switch (event->type)
383 {
384 case GDK_MOTION_NOTIFY:
385 // create outline
386 if (prefs->getBool("/tools/nodes/pathflash_enabled")) {
387 if (prefs->getBool("/tools/nodes/pathflash_unselected") && !nt->_multipath->empty())
388 break;
390 SPItem *over_item = sp_event_context_find_item (desktop, event_point(event->button),
391 FALSE, TRUE);
392 if (over_item == nt->flashed_item) break;
393 if (nt->flash_tempitem) {
394 desktop->remove_temporary_canvasitem(nt->flash_tempitem);
395 nt->flash_tempitem = NULL;
396 nt->flashed_item = NULL;
397 }
398 if (!SP_IS_PATH(over_item)) break; // for now, handle only paths
400 nt->flashed_item = over_item;
401 SPCurve *c = sp_path_get_curve_for_edit(SP_PATH(over_item));
402 c->transform(sp_item_i2d_affine(over_item));
403 SPCanvasItem *flash = sp_canvas_bpath_new(sp_desktop_tempgroup(desktop), c);
404 sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(flash),
405 prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff), 1.0,
406 SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT);
407 sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(flash), 0, SP_WIND_RULE_NONZERO);
408 nt->flash_tempitem = desktop->add_temporary_canvasitem(flash,
409 prefs->getInt("/tools/nodes/pathflash_timeout", 500));
410 c->unref();
411 }
412 return true;
413 case GDK_KEY_PRESS:
414 switch (get_group0_keyval(&event->key))
415 {
416 case GDK_Escape: // deselect everything
417 if (nt->_selected_nodes->empty()) {
418 selection->clear();
419 } else {
420 nt->_selected_nodes->clear();
421 }
422 ink_node_tool_update_tip(nt, event);
423 return TRUE;
424 case GDK_a:
425 if (held_control(event->key)) {
426 if (held_alt(event->key)) {
427 nt->_multipath->selectAll();
428 } else {
429 // select all nodes in subpaths that have something selected
430 // if nothing is selected, select everything
431 nt->_multipath->selectSubpaths();
432 }
433 ink_node_tool_update_tip(nt, event);
434 return TRUE;
435 }
436 break;
437 default:
438 break;
439 }
440 ink_node_tool_update_tip(nt, event);
441 break;
442 case GDK_KEY_RELEASE:
443 ink_node_tool_update_tip(nt, event);
444 break;
445 default: break;
446 }
448 SPEventContextClass *parent_class = (SPEventContextClass *) g_type_class_peek(g_type_parent(INK_TYPE_NODE_TOOL));
449 if (parent_class->root_handler)
450 return parent_class->root_handler(event_context, event);
451 return FALSE;
452 }
454 void ink_node_tool_update_tip(InkNodeTool *nt, GdkEvent *event)
455 {
456 using namespace Inkscape::UI;
457 if (event && (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE)) {
458 unsigned new_state = state_after_event(event);
459 if (new_state == event->key.state) return;
460 if (state_held_shift(new_state)) {
461 nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE,
462 C_("Node tool tip", "<b>Shift:</b> drag to add nodes to the selection, "
463 "click to toggle object selection"));
464 return;
465 }
466 }
467 unsigned sz = nt->_selected_nodes->size();
468 if (sz != 0) {
469 char *dyntip = g_strdup_printf(C_("Node tool tip",
470 "Selected <b>%d nodes</b>. Drag to select nodes, click to select a single object "
471 "or unselect all objects"), sz);
472 nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, dyntip);
473 g_free(dyntip);
474 } else if (nt->_multipath->empty()) {
475 nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE,
476 C_("Node tool tip", "Drag or click to select objects to edit"));
477 } else {
478 nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE,
479 C_("Node tool tip", "Drag to select nodes, click to select an object "
480 "or clear the selection"));
481 }
482 }
484 gint ink_node_tool_item_handler(SPEventContext *event_context, SPItem *item, GdkEvent *event)
485 {
486 SPEventContextClass *parent_class =
487 (SPEventContextClass *) g_type_class_peek(g_type_parent(INK_TYPE_NODE_TOOL));
488 if (parent_class->item_handler)
489 return parent_class->item_handler(event_context, item, event);
490 return FALSE;
491 }
493 void ink_node_tool_select_area(InkNodeTool *nt, Geom::Rect const &sel, GdkEventButton *event)
494 {
495 using namespace Inkscape::UI;
496 if (nt->_multipath->empty()) {
497 // if multipath is empty, select rubberbanded items rather than nodes
498 Inkscape::Selection *selection = nt->desktop->selection;
499 GSList *items = sp_document_items_in_box(
500 sp_desktop_document(nt->desktop), nt->desktop->dkey, sel);
501 selection->setList(items);
502 g_slist_free(items);
503 } else {
504 nt->_multipath->selectArea(sel, !held_shift(*event));
505 }
506 }
507 void ink_node_tool_select_point(InkNodeTool *nt, Geom::Point const &sel, GdkEventButton *event)
508 {
509 using namespace Inkscape::UI; // pull in event helpers
510 if (!event) return;
511 if (event->button != 1) return;
513 Inkscape::Selection *selection = nt->desktop->selection;
515 SPItem *item_clicked = sp_event_context_find_item (nt->desktop, event_point(*event),
516 (event->state & GDK_MOD1_MASK) && !(event->state & GDK_CONTROL_MASK), TRUE);
518 if (item_clicked == NULL) { // nothing under cursor
519 // if no Shift, deselect
520 if (!(event->state & GDK_SHIFT_MASK)) {
521 selection->clear();
522 }
523 return;
524 }
525 if (held_shift(*event)) {
526 selection->toggle(item_clicked);
527 } else {
528 selection->set(item_clicked);
529 }
530 nt->desktop->updateNow();
531 }
533 void ink_node_tool_mouseover_changed(InkNodeTool *nt, Inkscape::UI::ControlPoint *p)
534 {
535 using Inkscape::UI::CurveDragPoint;
536 CurveDragPoint *cdp = dynamic_cast<CurveDragPoint*>(p);
537 if (cdp && !nt->cursor_drag) {
538 nt->cursor_shape = cursor_node_d_xpm;
539 nt->hot_x = 1;
540 nt->hot_y = 1;
541 sp_event_context_update_cursor(nt);
542 nt->cursor_drag = true;
543 } else if (!cdp && nt->cursor_drag) {
544 nt->cursor_shape = cursor_node_xpm;
545 nt->hot_x = 1;
546 nt->hot_y = 1;
547 sp_event_context_update_cursor(nt);
548 nt->cursor_drag = false;
549 }
550 }
552 } // anonymous namespace
554 /*
555 Local Variables:
556 mode:c++
557 c-file-style:"stroustrup"
558 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
559 indent-tabs-mode:nil
560 fill-column:99
561 End:
562 */
563 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :