Code

GSoC node tool
[inkscape.git] / src / ui / tool / node-tool.cpp
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;
102 void ink_node_tool_init(InkNodeTool *nt)
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();
119 void ink_node_tool_dispose(GObject *object)
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();
130     
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);
138     
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);
154 void ink_node_tool_setup(SPEventContext *ec)
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));
192     
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
251 void ink_node_tool_set(SPEventContext *ec, Inkscape::Preferences::Entry *value)
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     }
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)
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     }
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)
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     }
350     
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();
364 gint ink_node_tool_root_handler(SPEventContext *event_context, GdkEvent *event)
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
372     
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();
377     
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     }
447     
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;
454 void ink_node_tool_update_tip(InkNodeTool *nt, GdkEvent *event)
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     }
484 gint ink_node_tool_item_handler(SPEventContext *event_context, SPItem *item, GdkEvent *event)
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;
493 void ink_node_tool_select_area(InkNodeTool *nt, Geom::Rect const &sel, GdkEventButton *event)
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     }
507 void ink_node_tool_select_point(InkNodeTool *nt, Geom::Point const &sel, GdkEventButton *event)
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();
533 void ink_node_tool_mouseover_changed(InkNodeTool *nt, Inkscape::UI::ControlPoint *p)
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     }
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 :