Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
control-point.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/* Authors:
3 * Krzysztof KosiƄski <tweenk.pl@gmail.com>
4 * Jon A. Cruz <jon@joncruz.org>
5 *
6 * Copyright (C) 2009 Authors
7 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
8 */
9
10#include <iostream>
11#include <2geom/point.h>
12#include <gdk/gdkkeysyms.h>
13
14#include "desktop.h"
15#include "message-context.h"
18#include "object/sp-namedview.h"
19#include "ui/tools/tool-base.h"
22#include "ui/widget/canvas.h" // autoscroll
24
25namespace Inkscape::UI {
26
27ControlPoint *ControlPoint::mouseovered_point = nullptr;
28
29sigc::signal<void (ControlPoint*)> ControlPoint::signal_mouseover_change;
30
32
34
42
44bool ControlPoint::_event_grab = false;
45
49 : _desktop(d)
50 , _position(initial_pos)
51{
52 _canvas_item_ctrl = make_canvasitem<Inkscape::CanvasItemCtrl>(group ? group : _desktop->getCanvasControls(), type);
53 _canvas_item_ctrl->set_name("CanvasItemCtrl:ControlPoint");
54 _canvas_item_ctrl->set_anchor(anchor);
55
57}
58
60{
61 // avoid storing invalid points in mouseovered_point
62 if (this == mouseovered_point) {
64 }
65
66 _canvas_item_ctrl->set_visible(false);
67}
68
70{
71 _canvas_item_ctrl->set_position(_position);
72 _event_handler_connection = _canvas_item_ctrl->connect_event([this] (CanvasEvent const &event) {
73 // re-routes events into the virtual function TODO: Refactor this nonsense.
74 if (!_desktop) {
75 return false;
76 }
77 return _eventHandler(_desktop->getTool(), event);
78 });
79}
80
82{
83 _position = pos;
84 _canvas_item_ctrl->set_position(_position);
85}
86
88{
89 setPosition(pos);
90}
91
93 move(position() * m);
94}
95
97{
98 return _canvas_item_ctrl->is_visible();
99}
100
102{
103 if (v) {
104 _canvas_item_ctrl->set_visible(true);
105 } else {
106 _canvas_item_ctrl->set_visible(false);
107 }
108}
109
110Glib::ustring ControlPoint::format_tip(char const *format, ...)
111{
112 va_list args;
113 va_start(args, format);
114 char *dyntip = g_strdup_vprintf(format, args);
115 va_end(args);
116 Glib::ustring ret = dyntip;
117 g_free(dyntip);
118 return ret;
119}
120
121
122// ===== Setters =====
123
124void ControlPoint::_setSize(unsigned int size)
125{
126 _canvas_item_ctrl->_set_size(size);
127}
128
133
134// main event callback, which emits all other callbacks.
136{
137 // NOTE the static variables below are shared for all points!
138 // TODO handle clicks and drags from other buttons too
139
140 if (!tool || !_desktop) {
141 return false;
142 } else if (tool->getDesktop() !=_desktop) {
143 g_warning("ControlPoint: desktop pointers not equal!");
144 }
145
146 // offset from the pointer hotspot to the center of the grabbed knot in desktop coords
147 static Geom::Point pointer_offset;
148 // number of last doubleclicked button
149 static unsigned next_release_doubleclick = 0;
150
151 _double_clicked = false;
152
153 auto prefs = Preferences::get();
154 int const drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
155
156 bool ret = false;
157
158 auto key_event_handler = [this] (KeyEvent const &event) {
159 if (mouseovered_point != this){
160 return false;
161 }
162 if (_drag_initiated) {
163 return true; // this prevents the tool from overwriting the drag tip
164 } else if (auto change = event.modifiersChange()) {
165 // we need to return true if there was a tip available, otherwise the tool's
166 // handler will process this event and set the tool's message, overwriting
167 // the point's message
168 return _updateTip(event.modifiers ^ change);
169 }
170 return false;
171 };
172
173 inspect_event(event,
174 [&] (ButtonPressEvent const &event) {
175 if (event.num_press == 1) {
176 next_release_doubleclick = 0;
177 if (event.button == 1 && !tool->is_space_panning()) {
178 // 1st mouse button click. internally, start dragging, but do not emit signals
179 // or change position until drag tolerance is exceeded.
180 _drag_event_origin = event.pos;
181 pointer_offset = _position - _desktop->w2d(_drag_event_origin);
182 _drag_initiated = false;
183 // route all events to this handler
184 _canvas_item_ctrl->grab(grab_event_mask); // cursor is null
185 _event_grab = true;
186 _setState(STATE_CLICKED);
187 ret = true;
188 } else {
189 ret = _event_grab;
190 }
191 } else if (event.num_press == 2) {
192 // store the button number for next release
193 next_release_doubleclick = event.button;
194 ret = true;
195 }
196 },
197
198 [&] (MotionEvent const &event) {
199 if (_event_grab && !tool->is_space_panning()) {
200 _desktop->getSnapIndicator()->remove_snaptarget();
201 bool transferred = false;
202 if (!_drag_initiated) {
203 if (Geom::LInfty(event.pos - _drag_event_origin) <= drag_tolerance) {
204 ret = true;
205 return;
206 }
207
208 // if we are here, it means the tolerance was just exceeded.
209 _drag_origin = _position;
210 transferred = grabbed(event);
211 // _drag_initiated might change during the above virtual call
212 _drag_initiated = true;
213 }
214
215 if (!transferred) {
216 // dragging in progress
217 auto new_pos = _desktop->w2d(event.pos) + pointer_offset;
218 // the new position is passed by reference and can be changed in the handlers.
219 dragged(new_pos, event);
220 move(new_pos);
221 _updateDragTip(event); // update dragging tip after moving to new position
222
223 _desktop->getCanvas()->enable_autoscroll();
224 _desktop->set_coordinate_status(_position);
225 tool->snap_delay_handler(nullptr, this, event, Tools::DelayedSnapEvent::CONTROL_POINT_HANDLER);
226 }
227 ret = true;
228 }
229 },
230
231 [&] (ButtonReleaseEvent const &event) {
232 if (_event_grab && event.button == 1) {
233 // If we have any pending snap event, then invoke it now!
234 // (This is needed because we might not have snapped on the latest GDK_MOTION_NOTIFY event
235 // if the mouse speed was too high. This is inherent to the snap-delay mechanism.
236 // We must snap at some point in time though, and this is our last chance)
237 // PS: For other contexts this is handled already in start_item_handler or start_root_handler
238 // if (_desktop && _desktop->getTool() && _desktop->getTool()->_delayed_snap_event) {
239 tool->process_delayed_snap_event();
240
241 _canvas_item_ctrl->ungrab();
242 _setMouseover(this, event.modifiers);
243 _event_grab = false;
244
245 if (_drag_initiated) {
246 // it is the end of a drag
247 _drag_initiated = false;
248 ungrabbed(&event);
249 ret = true;
250 } else {
251 // it is the end of a click
252 if (next_release_doubleclick) {
253 _double_clicked = true;
254 ret = doubleclicked(event);
255 } else {
256 ret = clicked(event);
257 }
258 }
259 }
260 },
261
262 [&] (EnterEvent const &event) {
263 _setMouseover(this, event.modifiers);
264 return true;
265 },
266
267 [&] (LeaveEvent const &event) {
268 _clearMouseover();
269 return true;
270 },
271
272 // update tips on modifier state change
273 // TODO add ESC keybinding as drag cancel
274 [&] (KeyPressEvent const &event) {
275 switch (Tools::get_latin_keyval(event)) {
276 case GDK_KEY_Escape: {
277 // ignore Escape if this is not a drag
278 if (!_drag_initiated) break;
279
280 // temporarily disable snapping - we might snap to a different place than we were initially
281 tool->discard_delayed_snap_event();
282 auto &snapprefs = _desktop->getNamedView()->snap_manager.snapprefs;
283 bool snap_save = snapprefs.getSnapEnabledGlobally();
284 snapprefs.setSnapEnabledGlobally(false);
285
286 auto new_pos = _drag_origin;
287
288 // make a fake event for dragging
289 // ASSUMPTION: dragging a point without modifiers will never prevent us from moving it
290 // to its original position
291 auto fake = MotionEvent();
292 fake.pos = _drag_event_origin;
293 fake.modifiers = 0; // unconstrained drag
294 fake.time = event.time;
295 fake.control_point_synthesized = true;
296 dragged(new_pos, fake);
297
298 _canvas_item_ctrl->ungrab();
299 _clearMouseover(); // this will also reset state to normal
300 _event_grab = false;
301 _drag_initiated = false;
302
303 ungrabbed(nullptr); // ungrabbed handlers can handle a NULL event
304 snapprefs.setSnapEnabledGlobally(snap_save);
305 ret = true;
306 return;
307 }
308 case GDK_KEY_Tab: {
309 // Downcast from ControlPoint to TransformHandle, if possible
310 // This is an ugly hack; we should have the transform handle intercept the keystrokes itself
311 if (auto th = dynamic_cast<TransformHandle*>(this)) {
312 th->getNextClosestPoint(false);
313 ret = true;
314 return;
315 }
316 break;
317 }
318 case GDK_KEY_ISO_Left_Tab: {
319 // Downcast from ControlPoint to TransformHandle, if possible
320 // This is an ugly hack; we should have the transform handle intercept the keystrokes itself
321 if (auto th = dynamic_cast<TransformHandle*>(this)) {
322 th->getNextClosestPoint(true);
323 ret = true;
324 return;
325 }
326 break;
327 }
328 default:
329 break;
330 }
331
332 ret = key_event_handler(event);
333 },
334
335 [&] (KeyReleaseEvent const &event) {
336 ret = key_event_handler(event);
337 },
338
339 [&] (CanvasEvent const &event) {}
340 );
341
342 // do not propagate events during grab - it might cause problems
343 return ret || _event_grab;
344}
345
346void ControlPoint::_setMouseover(ControlPoint *p, unsigned state)
347{
348 bool visible = p->visible();
349 if (visible) { // invisible points shouldn't get mouseovered
350 p->_setState(STATE_MOUSEOVER);
351 }
352 p->_updateTip(state);
353
354 if (visible && mouseovered_point != p) {
355 mouseovered_point = p;
356 signal_mouseover_change.emit(mouseovered_point);
357 }
358}
359
360bool ControlPoint::_updateTip(unsigned state)
361{
362 Glib::ustring tip = _getTip(state);
363 if (!tip.empty()) {
364 _desktop->getTool()->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE,
365 tip.data());
366 return true;
367 } else {
368 _desktop->getTool()->defaultMessageContext()->clear();
369 return false;
370 }
371}
372
373bool ControlPoint::_updateDragTip(MotionEvent const &event)
374{
375 if (!_hasDragTips()) {
376 return false;
377 }
378 Glib::ustring tip = _getDragTip(event);
379 if (!tip.empty()) {
380 _desktop->getTool()->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE,
381 tip.data());
382 return true;
383 } else {
384 _desktop->getTool()->defaultMessageContext()->clear();
385 return false;
386 }
387}
388
389void ControlPoint::_clearMouseover()
390{
391 if (mouseovered_point) {
392 mouseovered_point->_desktop->getTool()->defaultMessageContext()->clear();
393 mouseovered_point->_setState(STATE_NORMAL);
394 mouseovered_point = nullptr;
395 signal_mouseover_change.emit(mouseovered_point);
396 }
397}
398
399void ControlPoint::transferGrab(ControlPoint *prev_point, MotionEvent const &event)
400{
401 if (!_event_grab) return;
402
403 grabbed(event);
404 prev_point->_canvas_item_ctrl->ungrab();
405 _canvas_item_ctrl->grab(grab_event_mask); // cursor is null
406
407 _drag_initiated = true;
408
409 prev_point->_setState(STATE_NORMAL);
410 _setMouseover(this, event.modifiers);
411}
412
413void ControlPoint::_setState(State state)
414{
415 _canvas_item_ctrl->set_normal(_selected_appearance);
416
417 switch(state) {
418 case STATE_NORMAL:
419 break;
420 case STATE_MOUSEOVER:
421 _canvas_item_ctrl->set_hover();
422 break;
423 case STATE_CLICKED:
424 _canvas_item_ctrl->set_click();
425 break;
426 };
427 _state = state;
428}
429
430void ControlPoint::set_selected_appearance(bool selected) {
431 if (_selected_appearance == selected) return;
432
433 _selected_appearance = selected;
434 _canvas_item_ctrl->set_selected(selected);
435}
436
437// TODO: RENAME
438void ControlPoint::_handleControlStyling()
439{
440 _canvas_item_ctrl->set_size_default();
441}
442
443bool ControlPoint::_is_drag_cancelled(MotionEvent const &event)
444{
445 return event.control_point_synthesized;
446}
447
448// dummy implementations for handlers
449
450bool ControlPoint::grabbed(MotionEvent const &)
451{
452 return false;
453}
454
455void ControlPoint::dragged(Geom::Point &/*new_pos*/, MotionEvent const &/*event*/)
456{
457}
458
459void ControlPoint::ungrabbed(ButtonReleaseEvent const */*event*/)
460{
461}
462
463bool ControlPoint::clicked(ButtonReleaseEvent const &/*event*/)
464{
465 return false;
466}
467
468bool ControlPoint::doubleclicked(ButtonReleaseEvent const &/*event*/)
469{
470 return false;
471}
472
473} // namespace Inkscape::UI
474
475/*
476 Local Variables:
477 mode:c++
478 c-file-style:"stroustrup"
479 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
480 indent-tabs-mode:nil
481 fill-column:99
482 End:
483*/
484// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
Cartesian point / 2D vector and related operations.
Enums for CanvasItems.
Geom::IntRect visible
Definition canvas.cpp:154
Inkscape canvas widget.
3x3 matrix representing an affine transformation.
Definition affine.h:70
Two-dimensional point that doubles as a vector.
Definition point.h:66
static Preferences * get()
Access the singleton Preferences object.
Draggable point, the workhorse of on-canvas editing.
bool _updateTip(unsigned state)
static Geom::Point _drag_origin
Stores the desktop point from which the last drag was initiated.
void _setControlType(Inkscape::CanvasItemCtrlType type)
Geom::Point const & position() const
Current position of the control point.
static sigc::signal< void(ControlPoint *)> signal_mouseover_change
Emitted when the mouseovered point changes.
static Geom::Point _drag_event_origin
Stores the window point over which the cursor was during the last mouse button press.
virtual void move(Geom::Point const &pos)
Move the control point to new position with side effects.
static Glib::ustring format_tip(char const *format,...) G_GNUC_PRINTF(1
ControlPoint(ControlPoint const &other)=delete
static ControlPoint * mouseovered_point
Holds the currently mouseovered control point.
virtual void setVisible(bool v)
Set the visibility of the control point.
virtual void _setState(State state)
Change the state of the knot.
virtual void setPosition(Geom::Point const &pos)
Relocate the control point without side effects.
State
Enumeration representing the possible states of the control point, used to determine its appearance.
SPDesktop *const _desktop
The desktop this control point resides on.
static Glib::ustring virtual bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, CanvasEvent const &event)
sigc::scoped_connection _event_handler_connection
CanvasItemPtr< Inkscape::CanvasItemCtrl > _canvas_item_ctrl
Visual representation of the control point.
virtual void transform(Geom::Affine const &m)
Apply an arbitrary affine transformation to a control point.
void _setSize(unsigned int size)
Geom::Point _position
Current position in desktop coordinates.
Base class for Event processors.
Definition tool-base.h:95
SPDesktop * getDesktop() const
Definition tool-base.h:113
To do: update description of desktop.
Definition desktop.h:149
Inkscape::CanvasItemGroup * getCanvasControls() const
Definition desktop.h:196
Inkscape::UI::Tools::ToolBase * getTool() const
Definition desktop.h:187
Editable view implementation.
SPAnchorType
Definition enums.h:18
constexpr Coord infinity()
Get a value representing infinity.
Definition coord.h:88
Interface for locally managing a current status message.
Coord LInfty(Point const &p)
unsigned get_latin_keyval(GtkEventControllerKey const *const controller, unsigned const keyval, unsigned const keycode, GdkModifierType const state, unsigned *consumed_modifiers)
Return the keyval corresponding to the event controller key in Latin group.
User interface code.
Definition desktop.h:113
static constexpr auto grab_event_mask
Events which should be captured when a handle is being dragged.
void inspect_event(E &&event, Fs... funcs)
Perform pattern-matching on a CanvasEvent.
@ NORMAL_MESSAGE
Definition message.h:26
va_end(args)
int const char va_start(args, fmt)
Provides a class that shows a temporary indicator on the canvas of where the snap was,...
A mouse button (left/right/middle) is pressed.
A mouse button (left/right/middle) is released.
Abstract base class for events.
unsigned modifiers
The modifiers mask immediately before the event.
virtual unsigned modifiersChange() const
Get the change in the modifiers due to this event.
A key has been pressed.
Movement of the mouse pointer.
Affine transform handles component.