diff --git a/src/desktop.cpp b/src/desktop.cpp index 22f195b647c13ad30346a6a57ea4189f9a0802f8..1e76f0e6b41a006760a3173b9aecbc335b747c44 100644 --- a/src/desktop.cpp +++ b/src/desktop.cpp @@ -211,7 +211,8 @@ SPDesktop::init (SPNamedView *nv, Inkscape::UI::Widget::Canvas *acanvas, SPDeskt canvas_group_sketch->set_pickable(false); // Temporary items are not pickable! canvas_group_temp->set_pickable(false); // Temporary items are not pickable! - // The root should never emit events. The "catchall" should get it! (CHECK) + // The root should never emit events. The "catchall" should get it! + // But somehow there are still exceptions, e.g. Ctrl+scroll to zoom. canvas_item_root->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), this)); canvas_catchall->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), this)); diff --git a/src/text-editing.cpp b/src/text-editing.cpp index 375e4ef18282b0fc2e9e498983b8e3467a8761c0..727dbb30440c238b6a0fc744feea75cc14a5947d 100644 --- a/src/text-editing.cpp +++ b/src/text-editing.cpp @@ -45,59 +45,64 @@ static void move_child_nodes(Inkscape::XML::Node *from_repr, Inkscape::XML::Node static bool tidy_xml_tree_recursively(SPObject *root, bool has_text_decoration); -Inkscape::Text::Layout const * te_get_layout (SPItem const *item) +Inkscape::Text::Layout const *te_get_layout(SPItem const *item) { - if (is(item)) { - return &(cast(item)->layout); - } else if (is(item)) { - return &(cast(item)->layout); + if (auto text = cast(item)) { + return &text->layout; + } else if (auto flow = cast(item)) { + return &flow->layout; + } else { + return nullptr; } - return nullptr; } -static void te_update_layout_now (SPItem *item) +static void te_update_layout_now(SPItem *item) { - if (is(item)) - cast(item)->rebuildLayout(); - else if (is(item)) - cast(item)->rebuildLayout(); + if (auto text = cast(item)) { + text->rebuildLayout(); + } else if (auto flow = cast(item)) { + flow->rebuildLayout(); + } else { + return; + } item->updateRepr(); } void te_update_layout_now_recursive(SPItem *item) { - if (is(item)) { - std::vector item_list = cast(item)->item_list(); - for(auto list_item : item_list){ - te_update_layout_now_recursive(list_item); + if (auto group = cast(item)) { + for (auto childitem : group->item_list()) { + te_update_layout_now_recursive(childitem); } - } else if (is(item)) - cast(item)->rebuildLayout(); - else if (is(item)) - cast(item)->rebuildLayout(); + } else if (auto text = cast(item)) { + text->rebuildLayout(); + } else if (auto flow = cast(item)) { + flow->rebuildLayout(); + } else { + return; + } item->updateRepr(); } bool sp_te_output_is_empty(SPItem const *item) { - Inkscape::Text::Layout const *layout = te_get_layout(item); + auto layout = te_get_layout(item); return layout->begin() == layout->end(); } bool sp_te_input_is_empty(SPObject const *item) { - bool empty = true; - if (is(item)) { - empty = cast(item)->string.empty(); - } else { - for (auto& child: item->children) { - if (!sp_te_input_is_empty(&child)) { - empty = false; - break; - } + if (auto str = cast(item)) { + return str->string.empty(); + } + + for (auto &child : item->children) { + if (!sp_te_input_is_empty(&child)) { + return false; } } - return empty; + + return true; } Inkscape::Text::Layout::iterator diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index d4dfdef920de8ee7a94de3bd7d6d60379f2d08da..26802136f303182c7230c0b99d4a15b690cb595e 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -201,6 +201,7 @@ set(ui_SRC widget/dash-selector.cpp widget/entity-entry.cpp widget/entry.cpp + widget/events/canvas-event.cpp widget/export-lists.cpp widget/export-preview.cpp widget/filter-effect-chooser.cpp @@ -479,6 +480,7 @@ set(ui_SRC widget/dash-selector.h widget/entity-entry.h widget/entry.h + widget/events/canvas-event.h widget/export-lists.h widget/export-preview.h widget/fill-style.h diff --git a/src/ui/dialog/knot-properties.cpp b/src/ui/dialog/knot-properties.cpp index 048cd33c4f7e8ec87977d5c1205f0030d2765de9..74eef23467c655077cf98653f022a2e7504f42f3 100644 --- a/src/ui/dialog/knot-properties.cpp +++ b/src/ui/dialog/knot-properties.cpp @@ -137,27 +137,6 @@ KnotPropertiesDialog::_close() ); } -bool KnotPropertiesDialog::_handleKeyEvent(GdkEventKey * /*event*/) -{ - - /*switch (get_latin_keyval(event)) { - case GDK_KEY_Return: - case GDK_KEY_KP_Enter: { - _apply(); - return true; - } - break; - }*/ - return false; -} - -void KnotPropertiesDialog::_handleButtonEvent(GdkEventButton* event) -{ - if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { - _apply(); - } -} - void KnotPropertiesDialog::_setKnotPoint(Geom::Point knotpoint, Glib::ustring const unit_name) { _unit_name = unit_name; diff --git a/src/ui/dialog/knot-properties.h b/src/ui/dialog/knot-properties.h index 18f674500da7084da81f778bcb733293877ab5a6..b475f8b67757bbb76a8f55dab28a4fcd748c0aae 100644 --- a/src/ui/dialog/knot-properties.h +++ b/src/ui/dialog/knot-properties.h @@ -29,9 +29,12 @@ namespace Dialogs { // Used in Measure tool to set ends of "ruler" (via Shift-click)." -class KnotPropertiesDialog : public Gtk::Dialog { - public: - KnotPropertiesDialog(); +class KnotPropertiesDialog : public Gtk::Dialog +{ +public: + KnotPropertiesDialog(); + KnotPropertiesDialog(KnotPropertiesDialog const &) = delete; + KnotPropertiesDialog &operator=(KnotPropertiesDialog const &) = delete; ~KnotPropertiesDialog() override; Glib::ustring getName() const { return "LayerPropertiesDialog"; } @@ -66,23 +69,15 @@ protected: void _close(); void _setKnotPoint(Geom::Point knotpoint, Glib::ustring const unit_name); - void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); - bool _handleKeyEvent(GdkEventKey *event); - void _handleButtonEvent(GdkEventButton* event); friend class Inkscape::UI::Tools::MeasureTool; - -private: - KnotPropertiesDialog(KnotPropertiesDialog const &); // no copy - KnotPropertiesDialog &operator=(KnotPropertiesDialog const &); // no assign }; -} // namespace -} // namespace -} // namespace +} // namespace Dialogs +} // namespace UI +} // namespace Inkscape - -#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_H +#endif // INKSCAPE_DIALOG_KNOT_PROPERTIES_H /* Local Variables: diff --git a/src/ui/tool/control-point.cpp b/src/ui/tool/control-point.cpp index 1c40dfb54e356b7fac47d5b1334ceb1210baeb66..be30c4c494403f1e190b082993aa1db89c486c32 100644 --- a/src/ui/tool/control-point.cpp +++ b/src/ui/tool/control-point.cpp @@ -231,7 +231,8 @@ bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, G switch(event->type) { case GDK_BUTTON_PRESS: - next_release_doubleclick = 0; + case GDK_2BUTTON_PRESS: + next_release_doubleclick = event->type == GDK_2BUTTON_PRESS ? event->button.button : 0; if (event->button.button == 1 && !event_context->is_space_panning()) { // 1st mouse button click. internally, start dragging, but do not emit signals // or change position until drag tolerance is exceeded. @@ -246,11 +247,6 @@ bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, G return true; } return _event_grab; - - case GDK_2BUTTON_PRESS: - // store the button number for next release - next_release_doubleclick = event->button.button; - return true; case GDK_MOTION_NOTIFY: if (_event_grab && ! event_context->is_space_panning()) { diff --git a/src/ui/tools/text-tool.cpp b/src/ui/tools/text-tool.cpp index 9aaffa7d0c80a989e9f0a8b991088ae9de813bb9..b89b2b8046f5acd4ff64acaa71991d4018f2aba2 100644 --- a/src/ui/tools/text-tool.cpp +++ b/src/ui/tools/text-tool.cpp @@ -215,85 +215,75 @@ void TextTool::deleteSelected() DocumentUndo::done(_desktop->getDocument(), _("Delete text"), INKSCAPE_ICON("draw-text")); } -bool TextTool::item_handler(SPItem* item, GdkEvent* event) { - SPItem *item_ungrouped; - - gint ret = FALSE; +bool TextTool::item_handler(SPItem *item, GdkEvent *event) +{ sp_text_context_validate_cursor_iterators(this); - Inkscape::Text::Layout::iterator old_start = this->text_sel_start; switch (event->type) { case GDK_BUTTON_PRESS: if (event->button.button == 1) { - // this var allow too much lees subbselection queries - // reducing it to cursor iteracion, mouseup and down - // find out clicked item, disregarding groups - item_ungrouped = _desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), TRUE); + auto item_ungrouped = _desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), true); if (is(item_ungrouped) || is(item_ungrouped)) { + auto old_start = text_sel_start; _desktop->getSelection()->set(item_ungrouped); - if (this->text) { - // find out click point in document coordinates - Geom::Point p = _desktop->w2d(Geom::Point(event->button.x, event->button.y)); - // set the cursor closest to that point + if (text) { + // Find click point in document coordinates. + auto p = _desktop->w2d(Geom::Point(event->button.x, event->button.y)); + // Set the cursor closest to that point. if (event->button.state & GDK_SHIFT_MASK) { - this->text_sel_start = old_start; - this->text_sel_end = sp_te_get_position_by_coords(this->text, p); + text_sel_start = old_start; + text_sel_end = sp_te_get_position_by_coords(text, p); } else { - this->text_sel_start = this->text_sel_end = sp_te_get_position_by_coords(this->text, p); + text_sel_start = text_sel_end = sp_te_get_position_by_coords(text, p); } - // update display + // Update display. sp_text_context_update_cursor(this); sp_text_context_update_text_selection(this); - this->dragging = 1; + dragging = 1; } - ret = TRUE; + return true; } } break; case GDK_2BUTTON_PRESS: - if (event->button.button == 1 && this->text && this->dragging) { - Inkscape::Text::Layout const *layout = te_get_layout(this->text); - if (layout) { - if (!layout->isStartOfWord(this->text_sel_start)) - this->text_sel_start.prevStartOfWord(); - if (!layout->isEndOfWord(this->text_sel_end)) - this->text_sel_end.nextEndOfWord(); + if (event->button.button == 1 && text) { + if (auto layout = te_get_layout(text)) { + if (!layout->isStartOfWord(text_sel_start)) { + text_sel_start.prevStartOfWord(); + } + if (!layout->isEndOfWord(text_sel_end)) { + text_sel_end.nextEndOfWord(); + } sp_text_context_update_cursor(this); sp_text_context_update_text_selection(this); - this->dragging = 2; - ret = TRUE; + dragging = 2; + return true; } } break; case GDK_3BUTTON_PRESS: - if (event->button.button == 1 && this->text && this->dragging) { - this->text_sel_start.thisStartOfLine(); - this->text_sel_end.thisEndOfLine(); + if (event->button.button == 1 && text) { + text_sel_start.thisStartOfLine(); + text_sel_end.thisEndOfLine(); sp_text_context_update_cursor(this); sp_text_context_update_text_selection(this); - this->dragging = 3; - ret = TRUE; + dragging = 3; + return true; } break; case GDK_BUTTON_RELEASE: - if (event->button.button == 1 && this->dragging) { - this->dragging = 0; - this->discard_delayed_snap_event(); - ret = TRUE; + if (event->button.button == 1 && dragging) { + dragging = 0; + discard_delayed_snap_event(); _desktop->emit_text_cursor_moved(this, this); + return true; } break; - case GDK_MOTION_NOTIFY: - break; default: break; } - if (!ret) { - ret = ToolBase::item_handler(item, event); - } - - return ret; + return ToolBase::item_handler(item, event); } static void sp_text_context_setup_text(TextTool *tc) @@ -654,6 +644,11 @@ bool TextTool::root_handler(GdkEvent* event) { bool preedit_activation = (MOD__CTRL(event) && MOD__SHIFT(event) && !MOD__ALT(event)) && (group0_keyval == GDK_KEY_U || group0_keyval == GDK_KEY_u); + // Fixme: Hack needed to get custom input methods to work properly. + // In GTK4 we should be using gtk_im_context_filter_key() instead. + g_set_object(&event->key.window, _desktop->getCanvas()->get_window()->gobj()); + gdk_event_set_device(event, gtk_get_current_event_device()); + if (this->unimode || !this->imc || preedit_activation || !gtk_im_context_filter_keypress(this->imc, (GdkEventKey*) event)) { // IM did not consume the key, or we're in unimode @@ -1218,6 +1213,11 @@ bool TextTool::root_handler(GdkEvent* event) { } case GDK_KEY_RELEASE: + // Fixme: Hack needed to get custom input methods to work properly. + // In GTK4 we should be using gtk_im_context_filter_key() instead. + g_set_object(&event->key.window, _desktop->getCanvas()->get_window()->gobj()); + gdk_event_set_device(event, gtk_get_current_event_device()); + if (!this->unimode && this->imc && gtk_im_context_filter_keypress(this->imc, (GdkEventKey*) event)) { return TRUE; } diff --git a/src/ui/tools/tool-base.cpp b/src/ui/tools/tool-base.cpp index 59a6470e435b44cd9f68473e99e9ae9a958ed54d..36cd2ecd7a7c39ac0455fb8fd31975fc78239b9a 100644 --- a/src/ui/tools/tool-base.cpp +++ b/src/ui/tools/tool-base.cpp @@ -1336,6 +1336,17 @@ void ToolBase::menu_popup(GdkEvent *event, SPObject *obj) menu->attach_to_widget(*_desktop->getCanvas()); // So actions work! menu->show(); + // Fixme: Hack to get the current window so we can popup a menu. + // The entire menu infrastructure is gone in GTK4, so we'll be using a much saner + // API then anyway. + auto window = _desktop->getCanvas()->get_window()->gobj(); + if (event->type == GDK_BUTTON_PRESS) { + g_set_object(&event->button.window, window); + } else if (event->type == GDK_KEY_PRESS) { + g_set_object(&event->key.window, window); + } + gdk_event_set_device(event, gtk_get_current_event_device()); + switch (event->type) { case GDK_BUTTON_PRESS: case GDK_KEY_PRESS: diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 172ff04a0f8fd84b05237bbfc0da16c93142b6b5..a7f721571fca3feb741c300b070e354a90771bee 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -21,7 +21,6 @@ #include <2geom/convex-hull.h> #include "canvas.h" -#include "canvas-grid.h" #include "color.h" // Background color #include "desktop.h" @@ -29,6 +28,7 @@ #include "preferences.h" #include "ui/util.h" #include "helper/geom.h" +#include "util/callback-converter.h" #include "canvas/prefs.h" #include "canvas/fragment.h" @@ -96,10 +96,6 @@ namespace { * Utilities */ -// GdkEvents can only be safely copied using gdk_event_copy. Since this function allocates, we need the following smart pointer to wrap the result. -struct GdkEventFreer {void operator()(GdkEvent *ev) const {gdk_event_free(ev);}}; -using GdkEventUniqPtr = std::unique_ptr; - // Copies a GdkEvent, returning the result as a smart pointer. auto make_unique_copy(GdkEvent const *ev) { return GdkEventUniqPtr(gdk_event_copy(ev)); } @@ -244,7 +240,9 @@ public: // Event handling. bool process_event(const GdkEvent*); - bool pick_current_item(const GdkEvent*); + void set_pick_event(bool is_leave, double x, double y, unsigned state); + CanvasItem *find_item_at_coords(double x, double y); + bool repick(); bool emit_event(const GdkEvent*); void ensure_geometry_uptodate(); Inkscape::CanvasItem *pre_scroll_grabbed_item; @@ -320,6 +318,26 @@ Canvas::Canvas() Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK ); + scroll_controller = Glib::wrap(gtk_event_controller_scroll_new(Gtk::Widget::gobj(), GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES)); + g_signal_connect(scroll_controller->gobj(), "scroll", Util::make_g_callback<&Canvas::on_scroll>, this); + + click_gesture = Glib::wrap(gtk_gesture_multi_press_new(Gtk::Widget::gobj())); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_gesture->gobj()), 0); // for right click detection + drag_gesture = Glib::wrap(gtk_gesture_drag_new(Gtk::Widget::gobj())); + gtk_gesture_group(click_gesture->gobj(), drag_gesture->gobj()); + g_signal_connect(click_gesture->gobj(), "pressed", Util::make_g_callback<&Canvas::on_button_pressed>, this); + g_signal_connect(drag_gesture->gobj(), "drag-end", Util::make_g_callback<&Canvas::on_button_released>, this); + + key_controller = Glib::wrap(gtk_event_controller_key_new(Gtk::Widget::gobj())); + g_signal_connect(key_controller->gobj(), "focus-in", Util::make_g_callback<&Canvas::on_focus_in>, this); + g_signal_connect(key_controller->gobj(), "key-pressed", Util::make_g_callback<&Canvas::on_key_pressed>, this); + g_signal_connect(key_controller->gobj(), "key-released", Util::make_g_callback<&Canvas::on_key_released>, this); + + motion_controller = Glib::wrap(gtk_event_controller_motion_new(Gtk::Widget::gobj())); + g_signal_connect(motion_controller->gobj(), "motion", Util::make_g_callback<&Canvas::on_motion>, this); + g_signal_connect(motion_controller->gobj(), "enter", Util::make_g_callback<&Canvas::on_enter>, this); + g_signal_connect(motion_controller->gobj(), "leave", Util::make_g_callback<&Canvas::on_leave>, this); + // Updater d->updater = Updater::create(pref_to_updater(d->prefs.update_strategy)); d->updater->reset(); @@ -416,9 +434,9 @@ void CanvasPrivate::activate_graphics() void CanvasPrivate::activate() { // Event handling/item picking - q->_pick_event.type = GDK_LEAVE_NOTIFY; - q->_pick_event.crossing.x = 0; - q->_pick_event.crossing.y = 0; + q->_pick_x = 0; + q->_pick_y = 0; + q->_pick_is_leave = true; q->_in_repick = false; q->_left_grabbed_item = false; @@ -849,14 +867,9 @@ void CanvasPrivate::autoscroll_begin(Geom::IntPoint const &to) displacement -= dpos; if (last_mouse) { - GdkEventMotion event; - memset(&event, 0, sizeof(GdkEventMotion)); - event.type = GDK_MOTION_NOTIFY; - event.x = last_mouse->x(); - event.y = last_mouse->y(); - event.state = q->_state; ensure_geometry_uptodate(); - emit_event(reinterpret_cast(&event)); + MotionEvent event(GDK_CURRENT_TIME, last_mouse->x(), last_mouse->y(), q->_state); + emit_event(event.compatSynthesize().get()); } if (strain_zero && velocity.length() <= 0.1) { @@ -890,54 +903,50 @@ void Canvas::enable_autoscroll() /* * Event handling + * + * Internally, Canvas uses GTK events. With the following event controllers, we create + * a "fake" event and pass it to the canvas for processing. The aim is to slowly migrate + * away from GdkEvent and replace these events with our own internal hierarchy. */ -bool Canvas::on_scroll_event(GdkEventScroll *scroll_event) +/** + * Obtains active modifiers from the current event and stores them + * inside canvas. This is a placeholder until GTK 4, where we should use + * GtkEventController accessors directly. + */ +void Canvas::update_modifiers() { - return d->process_event(reinterpret_cast(scroll_event)); + GdkModifierType state; + if (gtk_get_current_event_state(&state)) { + _state = state; + } } -bool Canvas::on_button_press_event(GdkEventButton *button_event) +bool Canvas::on_scroll(GtkEventControllerScroll *controller, double dx, double dy) { - return on_button_event(button_event); -} + update_modifiers(); -bool Canvas::on_button_release_event(GdkEventButton *button_event) -{ - if (button_event->button == 1) { - d->autoscroll_end(); - } + ScrollEvent event(gtk_get_current_event_time(), dx, dy, _state); - return on_button_event(button_event); + return d->process_event(event.compatSynthesize().get()); } -// Unified handler for press and release events. -bool Canvas::on_button_event(GdkEventButton *button_event) +bool Canvas::on_button_pressed(GtkGestureMultiPress *controller, int n_press, double x, double y) { - // Sanity-check event type. - switch (button_event->type) { - case GDK_BUTTON_PRESS: - case GDK_2BUTTON_PRESS: - case GDK_3BUTTON_PRESS: - case GDK_BUTTON_RELEASE: - break; // Good - default: - std::cerr << "Canvas::on_button_event: illegal event type!" << std::endl; - return false; - } + update_modifiers(); // Drag the split view controller. if (_split_mode == Inkscape::SplitMode::SPLIT) { - auto cursor_position = Geom::IntPoint(button_event->x, button_event->y); - switch (button_event->type) { - case GDK_BUTTON_PRESS: + auto cursor_position = Geom::IntPoint(x, y); + switch (n_press) { + case 1: if (_hover_direction != Inkscape::SplitDirection::NONE) { _split_dragging = true; _split_drag_start = cursor_position; return true; } break; - case GDK_2BUTTON_PRESS: + case 2: if (_hover_direction != Inkscape::SplitDirection::NONE) { _split_direction = _hover_direction; _split_dragging = false; @@ -945,96 +954,119 @@ bool Canvas::on_button_event(GdkEventButton *button_event) return true; } break; - case GDK_BUTTON_RELEASE: - if (!_split_dragging) break; - _split_dragging = false; - - // Check if we are near the edge. If so, revert to normal mode. - if (cursor_position.x() < 5 || - cursor_position.y() < 5 || - cursor_position.x() > get_allocation().get_width() - 5 || - cursor_position.y() > get_allocation().get_height() - 5) - { - // Reset everything. - _split_frac = {0.5, 0.5}; - set_cursor(); - set_split_mode(Inkscape::SplitMode::NORMAL); - - // Update action (turn into utility function?). - auto window = dynamic_cast(get_toplevel()); - if (!window) { - std::cerr << "Canvas::on_motion_notify_event: window missing!" << std::endl; - return true; - } - - auto action = window->lookup_action("canvas-split-mode"); - if (!action) { - std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' missing!" << std::endl; - return true; - } - - auto saction = Glib::RefPtr::cast_dynamic(action); - if (!saction) { - std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' not SimpleAction!" << std::endl; - return true; - } - - saction->change_state(static_cast(Inkscape::SplitMode::NORMAL)); - } - - break; - default: break; } } - return d->process_event(reinterpret_cast(button_event)); + int button = gtk_gesture_single_get_current_button(reinterpret_cast(controller)); + + ButtonPressEvent event(gtk_get_current_event_time(), static_cast(button), n_press, x, y, _state); + + return d->process_event(event.compatSynthesize().get()); } -bool Canvas::on_enter_notify_event(GdkEventCrossing *crossing_event) +bool Canvas::on_button_released(GtkGestureDrag *controller, double offset_x, double offset_y) { - if (crossing_event->window != get_window()->gobj()) { - return false; + double x, y; + gtk_gesture_drag_get_start_point(controller, &x, &y); + x += offset_x; + y += offset_y; + + update_modifiers(); + + // Drag the split view controller. + if (_split_mode == Inkscape::SplitMode::SPLIT && _split_dragging) { + auto cursor_position = Geom::IntPoint(x, y); + + _split_dragging = false; + + // Check if we are near the edge. If so, revert to normal mode. + if (cursor_position.x() < 5 || + cursor_position.y() < 5 || + cursor_position.x() > get_allocation().get_width() - 5 || + cursor_position.y() > get_allocation().get_height() - 5) + { + // Reset everything. + _split_frac = {0.5, 0.5}; + set_cursor(); + set_split_mode(Inkscape::SplitMode::NORMAL); + + // Update action (turn into utility function?). + auto window = dynamic_cast(get_toplevel()); + if (!window) { + std::cerr << "Canvas::on_button_released: window missing!" << std::endl; + return true; + } + + auto action = window->lookup_action("canvas-split-mode"); + if (!action) { + std::cerr << "Canvas::on_button_released: action 'canvas-split-mode' missing!" << std::endl; + return true; + } + + auto saction = Glib::RefPtr::cast_dynamic(action); + if (!saction) { + std::cerr << "Canvas::on_button_released: action 'canvas-split-mode' not SimpleAction!" << std::endl; + return true; + } + + saction->change_state(static_cast(Inkscape::SplitMode::NORMAL)); + } } - return d->process_event(reinterpret_cast(crossing_event)); -} -bool Canvas::on_leave_notify_event(GdkEventCrossing *crossing_event) -{ - if (crossing_event->window != get_window()->gobj()) { - return false; + int button = gtk_gesture_single_get_current_button(reinterpret_cast(controller)); + + if (button == 1) { + d->autoscroll_end(); } - d->last_mouse = {}; - return d->process_event(reinterpret_cast(crossing_event)); + + ButtonReleaseEvent event(gtk_get_current_event_time(), static_cast(button), x, y, _state); + + return d->process_event(event.compatSynthesize().get()); } -bool Canvas::on_focus_in_event(GdkEventFocus *focus_event) +bool Canvas::on_focus_in(GtkEventControllerKey *controller) { + update_modifiers(); grab_focus(); return false; } -bool Canvas::on_key_press_event(GdkEventKey *key_event) +bool Canvas::on_key_pressed(GtkEventControllerKey *controller, unsigned keyval, unsigned keycode, GdkModifierType*) { - return d->process_event(reinterpret_cast(key_event)); + update_modifiers(); + + int group = gtk_event_controller_key_get_group(controller); + + KeyPressEvent event(gtk_get_current_event_time(), group, keycode, keyval, _state); + + return d->process_event(event.compatSynthesize().get()); } -bool Canvas::on_key_release_event(GdkEventKey *key_event) +bool Canvas::on_key_released(GtkEventControllerKey *controller, unsigned keyval, unsigned keycode, GdkModifierType*) { - return d->process_event(reinterpret_cast(key_event)); + update_modifiers(); + + int group = gtk_event_controller_key_get_group(controller); + + KeyReleaseEvent event(gtk_get_current_event_time(), group, keycode, keyval, _state); + + return d->process_event(event.compatSynthesize().get()); } -bool Canvas::on_motion_notify_event(GdkEventMotion *motion_event) +bool Canvas::on_motion(GtkEventControllerMotion *controller, double x, double y) { + update_modifiers(); + // Record the last mouse position. - d->last_mouse = Geom::IntPoint(motion_event->x, motion_event->y); + d->last_mouse = Geom::IntPoint(x, y); // Handle interactions with the split view controller. if (_split_mode == Inkscape::SplitMode::XRAY) { queue_draw(); } else if (_split_mode == Inkscape::SplitMode::SPLIT) { - auto cursor_position = Geom::IntPoint(motion_event->x, motion_event->y); + auto cursor_position = Geom::IntPoint(x, y); // Move controller. if (_split_dragging) { @@ -1095,14 +1127,39 @@ bool Canvas::on_motion_notify_event(GdkEventMotion *motion_event) } // Avoid embarrassing neverending autoscroll in case the button-released handler somehow doesn't fire. - if (!(motion_event->state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK))) { + if (!(_state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK))) { d->autoscroll_end(); } - return d->process_event(reinterpret_cast(motion_event)); + MotionEvent event(gtk_get_current_event_time(), x, y, _state); + + return d->process_event(event.compatSynthesize().get()); +} + +bool Canvas::on_enter(GtkEventControllerMotion *controller, double x, double y) +{ + update_modifiers(); + + EnterEvent event(gtk_get_current_event_time(), x, y, _state); + + return d->process_event(event.compatSynthesize().get()); } -// Unified handler for all events. +bool Canvas::on_leave(GtkEventControllerMotion *controller) +{ + d->last_mouse = {}; + update_modifiers(); + + LeaveEvent event(gtk_get_current_event_time(), _state); + + return d->process_event(event.compatSynthesize().get()); +} + +/** + * Unified handler for all events. + * @param event Event to process + * @return True if event was handled + */ bool CanvasPrivate::process_event(const GdkEvent *event) { framecheck_whole_function(this) @@ -1141,8 +1198,7 @@ bool CanvasPrivate::process_event(const GdkEvent *event) bool retval = emit_event(event); // ...then repick. - q->_state = event->scroll.state; - pick_current_item(event); + repick(); return retval; } @@ -1154,8 +1210,8 @@ bool CanvasPrivate::process_event(const GdkEvent *event) pre_scroll_grabbed_item = nullptr; // Pick the current item as if the button were not pressed... - q->_state = event->button.state; - pick_current_item(event); + set_pick_event(false, event->button.x, event->button.y, event->button.state); + repick(); // ...then process the event. q->_state ^= calc_button_mask(); @@ -1167,31 +1223,31 @@ bool CanvasPrivate::process_event(const GdkEvent *event) pre_scroll_grabbed_item = nullptr; // Process the event as if the button were pressed... - q->_state = event->button.state; bool retval = emit_event(event); // ...then repick after the button has been released. auto event_copy = make_unique_copy(event); event_copy->button.state ^= calc_button_mask(); q->_state = event_copy->button.state; - pick_current_item(event_copy.get()); + set_pick_event(false, event_copy->button.x, event_copy->button.y, event_copy->button.state); + repick(); return retval; } case GDK_ENTER_NOTIFY: pre_scroll_grabbed_item = nullptr; - q->_state = event->crossing.state; - return pick_current_item(event); + set_pick_event(true, event->crossing.x, event->crossing.y, event->crossing.state); + return repick(); case GDK_LEAVE_NOTIFY: pre_scroll_grabbed_item = nullptr; - q->_state = event->crossing.state; // This is needed to remove alignment or distribution snap indicators. if (q->_desktop) { q->_desktop->snapindicator->remove_snaptarget(); } - return pick_current_item(event); + set_pick_event(false, event->crossing.x, event->crossing.y, event->crossing.state); + return repick(); case GDK_KEY_PRESS: case GDK_KEY_RELEASE: @@ -1199,8 +1255,8 @@ bool CanvasPrivate::process_event(const GdkEvent *event) case GDK_MOTION_NOTIFY: pre_scroll_grabbed_item = nullptr; - q->_state = event->motion.state; - pick_current_item(event); + set_pick_event(false, event->motion.x, event->motion.y, event->motion.state); + repick(); return emit_event(event); default: @@ -1208,121 +1264,95 @@ bool CanvasPrivate::process_event(const GdkEvent *event) } } -// This function is called by 'process_event' to manipulate the state variables relating -// to the current object under the mouse, for example, to generate enter and leave events. -// -// This routine reacts to events from the canvas. Its main purpose is to find the canvas item -// closest to the cursor where the event occurred and then send the event (sometimes modified) to -// that item. The event then bubbles up the canvas item tree until an object handles it. If the -// widget is redrawn, this routine may be called again for the same event. -// -// Canvas items register their interest by connecting to the "event" signal. -// Example in desktop.cpp: -// canvas_catchall->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), this)); -bool CanvasPrivate::pick_current_item(const GdkEvent *event) +/** + * Saves the provided coordinates and modifiers to the canvas for use + * by CanvasPrivate::repick. + * + * @param is_leave True if the cursor has left the canvas, false otherwise + * @param x Horizontal position of cursor + * @param y Vertical position of cursor + * @param state Modifier keys and button state + */ +void CanvasPrivate::set_pick_event(bool is_leave, double x, double y, unsigned state) +{ + // Save the event in the canvas. This is used to synthesize enter and + // leave events in case the current item changes. It is also used to + // re-pick the current item if the current one gets deleted. + + q->_pick_x = x; + q->_pick_y = y; + q->_pick_is_leave = is_leave; + q->_pick_state = state; + + // TODO: Remove this once we're sure repicking never happens + if (q->_in_repick) { + assert(false); + } +} + +/** + * Internal helper method to retrieve the canvas item under the given + * coordinates, taking into account the current render mode and other + * characteristics. + * + * @param x Horizontal position of cursor + * @param y Vertical position of cursor + * @return The current canvas item, or nullptr + */ +CanvasItem *CanvasPrivate::find_item_at_coords(double x, double y) +{ + // Look at where the cursor is to see if one should pick with outline mode. + bool outline = q->canvas_point_in_outline_zone({ x, y }); + + // Convert to world coordinates. + auto p = Geom::Point(x, y) + q->_pos; + if (stores.mode() == Stores::Mode::Decoupled) { + p *= q->_affine.inverse() * canvasitem_ctx->affine(); + } + + q->_drawing->getCanvasItemDrawing()->set_pick_outline(outline); + return canvasitem_ctx->root()->pick_item(p); +} + +/** + * This function is called by 'process_event' to manipulate the state variables relating + * to the current object under the mouse. Additionally, it will generate enter and leave events + * for canvas items if the current item changes. + * + * This routine reacts to events from the canvas. Its main purpose is to find the canvas item + * closest to the cursor where the event occurred and synthesise enter and leave events for the + * current and previously selected items. + * + * The coordinates and state used for picking is set by the CanvasPrivate::set_pick_event method + * and used here. This routine may be called any number of times for the same event. This is useful + * for cases in which re-picking is necessary but the cursor position has not changed (e.g. scrolling). + * + * @return True if the enter or leave event was handled + */ +bool CanvasPrivate::repick() { // Ensure requested geometry updates are performed first. ensure_geometry_uptodate(); - int button_down = 0; + bool button_down = false; if (!q->_all_enter_events) { // Only set true in connector-tool.cpp. // If a button is down, we'll perform enter and leave events on the // current item, but not enter on any other item. This is more or // less like X pointer grabbing for canvas items. - button_down = q->_state & (GDK_BUTTON1_MASK | - GDK_BUTTON2_MASK | - GDK_BUTTON3_MASK | - GDK_BUTTON4_MASK | - GDK_BUTTON5_MASK); - if (!button_down) q->_left_grabbed_item = false; - } - - // Save the event in the canvas. This is used to synthesize enter and - // leave events in case the current item changes. It is also used to - // re-pick the current item if the current one gets deleted. Also, - // synthesize an enter event. - if (event != &q->_pick_event) { - if (event->type == GDK_MOTION_NOTIFY || event->type == GDK_SCROLL || event->type == GDK_BUTTON_RELEASE) { - // Convert to GDK_ENTER_NOTIFY - - // These fields have the same offsets in all types of events. - q->_pick_event.crossing.type = GDK_ENTER_NOTIFY; - q->_pick_event.crossing.window = event->motion.window; - q->_pick_event.crossing.send_event = event->motion.send_event; - q->_pick_event.crossing.subwindow = nullptr; - q->_pick_event.crossing.x = event->motion.x; - q->_pick_event.crossing.y = event->motion.y; - q->_pick_event.crossing.mode = GDK_CROSSING_NORMAL; - q->_pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR; - q->_pick_event.crossing.focus = false; - - // These fields don't have the same offsets in all types of events. - switch (event->type) - { - case GDK_MOTION_NOTIFY: - q->_pick_event.crossing.state = event->motion.state; - q->_pick_event.crossing.x_root = event->motion.x_root; - q->_pick_event.crossing.y_root = event->motion.y_root; - break; - case GDK_SCROLL: - q->_pick_event.crossing.state = event->scroll.state; - q->_pick_event.crossing.x_root = event->scroll.x_root; - q->_pick_event.crossing.y_root = event->scroll.y_root; - break; - case GDK_BUTTON_RELEASE: - q->_pick_event.crossing.state = event->button.state; - q->_pick_event.crossing.x_root = event->button.x_root; - q->_pick_event.crossing.y_root = event->button.y_root; - break; - default: - assert(false); - } - - } else { - q->_pick_event = *event; + button_down = q->_state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK | GDK_BUTTON4_MASK | GDK_BUTTON5_MASK); + if (!button_down) { + q->_left_grabbed_item = false; } } - if (q->_in_repick) { - // Don't do anything else if this is a recursive call. - return false; - } - // Find new item q->_current_canvas_item_new = nullptr; - if (q->_pick_event.type != GDK_LEAVE_NOTIFY && canvasitem_ctx->root()->is_visible()) { - // Leave notify means there is no current item. - // Find closest item. - double x = 0.0; - double y = 0.0; - - if (q->_pick_event.type == GDK_ENTER_NOTIFY) { - x = q->_pick_event.crossing.x; - y = q->_pick_event.crossing.y; - } else { - x = q->_pick_event.motion.x; - y = q->_pick_event.motion.y; - } - - // Look at where the cursor is to see if one should pick with outline mode. - bool outline = q->canvas_point_in_outline_zone({ x, y }); - - // Convert to world coordinates. - auto p = Geom::Point(x, y) + q->_pos; - if (stores.mode() == Stores::Mode::Decoupled) { - p *= q->_affine.inverse() * canvasitem_ctx->affine(); - } - - q->_drawing->getCanvasItemDrawing()->set_pick_outline(outline); - q->_current_canvas_item_new = canvasitem_ctx->root()->pick_item(p); - // if (q->_current_canvas_item_new) { - // std::cout << " PICKING: FOUND ITEM: " << q->_current_canvas_item_new->get_name() << std::endl; - // } else { - // std::cout << " PICKING: DID NOT FIND ITEM" << std::endl; - // } + // A 'leave' pick event means there is no new item (i.e. cursor has left the canvas) + if (!q->_pick_is_leave && canvasitem_ctx->root()->is_visible()) { + q->_current_canvas_item_new = find_item_at_coords(q->_pick_x, q->_pick_y); } if (q->_current_canvas_item_new == q->_current_canvas_item && !q->_left_grabbed_item) { @@ -1336,17 +1366,13 @@ bool CanvasPrivate::pick_current_item(const GdkEvent *event) q->_current_canvas_item != nullptr && !q->_left_grabbed_item ) { - GdkEvent new_event; - new_event = q->_pick_event; - new_event.type = GDK_LEAVE_NOTIFY; - new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; - new_event.crossing.subwindow = nullptr; + LeaveEvent leaveEvent(GDK_CURRENT_TIME, q->_pick_state); q->_in_repick = true; - retval = emit_event(&new_event); + retval = emit_event(leaveEvent.compatSynthesize().get()); q->_in_repick = false; } - if (q->_all_enter_events == false) { + if (!q->_all_enter_events) { // new_current_item may have been set to nullptr during the call to emitEvent() above. if (q->_current_canvas_item_new != q->_current_canvas_item && button_down) { q->_left_grabbed_item = true; @@ -1359,18 +1385,30 @@ bool CanvasPrivate::pick_current_item(const GdkEvent *event) q->_current_canvas_item = q->_current_canvas_item_new; if (q->_current_canvas_item != nullptr) { - GdkEvent new_event; - new_event = q->_pick_event; - new_event.type = GDK_ENTER_NOTIFY; - new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; - new_event.crossing.subwindow = nullptr; - retval = emit_event(&new_event); + EnterEvent enterEvent(GDK_CURRENT_TIME, q->_pick_x, q->_pick_y, q->_pick_state); + q->_in_repick = true; + retval = emit_event(enterEvent.compatSynthesize().get()); + q->_in_repick = false; } return retval; } -// Fires an event at the canvas, after a little pre-processing. Returns true if handled. +/** + * Fires an event at the canvas, after a little pre-processing. The event then bubbles up the + * CanvasItem tree until an object or tool handles it. + * + * If the event is not handled, the method returns false and the event is passed back to GTK where + * it propagates back up the widget tree in the 'bubble' propagation phase. + * + * Canvas items register their interest by connecting to the "event" signal. + * + * Example in desktop.cpp: + * canvas_catchall->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), this)); + * + * @param event Event to dispatch to the canvas + * @return Returns true if handled. + */ bool CanvasPrivate::emit_event(const GdkEvent *event) { // Handle grabbed items. diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index bc50848e68a9f89d5e4b89c0440c3e7772f18756..272b37e90a0ab5cc2f827e3d8d1e80a19a962d97 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -22,6 +22,7 @@ #include <2geom/int-rect.h> #include "display/rendermode.h" #include "optglarea.h" +#include "events/canvas-event.h" class SPDesktop; @@ -134,17 +135,29 @@ protected: void get_preferred_width_vfunc (int &minimum_width, int &natural_width ) const override; void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; + // Event controllers + Glib::RefPtr scroll_controller, key_controller, motion_controller; + Glib::RefPtr click_gesture, drag_gesture; + // Event handlers - bool on_scroll_event (GdkEventScroll* ) override; - bool on_button_event (GdkEventButton* ); - bool on_button_press_event (GdkEventButton* ) override; - bool on_button_release_event(GdkEventButton* ) override; - bool on_enter_notify_event (GdkEventCrossing*) override; - bool on_leave_notify_event (GdkEventCrossing*) override; - bool on_focus_in_event (GdkEventFocus* ) override; - bool on_key_press_event (GdkEventKey* ) override; - bool on_key_release_event (GdkEventKey* ) override; - bool on_motion_notify_event (GdkEventMotion* ) override; + void update_modifiers(); + + // EventControllerScroll + bool on_scroll(GtkEventControllerScroll *controller, double dx, double dy); + + // GtkGestureMultiPress + bool on_button_pressed (GtkGestureMultiPress *controller, int n_press, double x, double y); + bool on_button_released(GtkGestureDrag *controller, double offset_x, double offset_y); + + // EventControllerMotion + bool on_motion(GtkEventControllerMotion *controller, double x, double y); + bool on_enter (GtkEventControllerMotion *controller, double x, double y); + bool on_leave (GtkEventControllerMotion *controller); + + // EventControllerKey + bool on_focus_in (GtkEventControllerKey *controller); + bool on_key_pressed (GtkEventControllerKey *controller, unsigned keyval, unsigned keycode, GdkModifierType *state); + bool on_key_released(GtkEventControllerKey *controller, unsigned keyval, unsigned keycode, GdkModifierType *state); void on_realize() override; void on_unrealize() override; @@ -179,12 +192,18 @@ private: /* Internal state */ // Event handling/item picking - GdkEvent _pick_event; ///< Event used to find currently selected item. - bool _in_repick; ///< For tracking recursion of pick_current_item(). + // State used to find 'current' item + double _pick_x; + double _pick_y; + unsigned _pick_state; // TODO: Merge with _state + bool _pick_is_leave; // TODO: Get rid of? + + // State for repick operation + bool _in_repick; ///< For tracking recursion of repick(). bool _left_grabbed_item; ///< ? bool _all_enter_events; ///< Keep all enter events. Only set true in connector-tool.cpp. bool _is_dragging; ///< Used in selection-chemistry to block undo/redo. - int _state; ///< Last known modifier state (SHIFT, CTRL, etc.). + unsigned _state; ///< Last known modifier state (SHIFT, CTRL, etc.). Inkscape::CanvasItem *_current_canvas_item; ///< Item containing cursor, nullptr if none. Inkscape::CanvasItem *_current_canvas_item_new; ///< Item to become _current_item, nullptr if none. diff --git a/src/ui/widget/events/canvas-event.cpp b/src/ui/widget/events/canvas-event.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d90af7a60f22c9de17c95a0f9441781158fa620a --- /dev/null +++ b/src/ui/widget/events/canvas-event.cpp @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Matthew Jakeman + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "canvas-event.h" +#include + +namespace Inkscape::UI::Widget { + +// Todo: Remove in C++23. +template +static auto to_underlying(T t) +{ + return static_cast>(t); +} + +CanvasEvent::CanvasEvent(uint32_t time) + : _time(time) +{} + +ButtonEvent::ButtonEvent(uint32_t time, EventButton button, double x, double y, unsigned state) + : CanvasEvent(time) + , _button(button) + , _x(x) + , _y(y) + , _state(state) +{} + +ButtonPressEvent::ButtonPressEvent(uint32_t time, EventButton button, int n_press, double x, double y, unsigned state) + : ButtonEvent(time, button, x, y, state) + , _n_press(n_press) +{} + +GdkEventUniqPtr ButtonPressEvent::compatSynthesize() +{ + GdkEventType type; + switch (_n_press % 3) { + default: + case 1: type = GDK_BUTTON_PRESS; break; + case 2: type = GDK_2BUTTON_PRESS; break; + case 0: type = GDK_3BUTTON_PRESS; break; + } + + auto event = GdkEventUniqPtr(gdk_event_new(type)); + + auto button_event = reinterpret_cast(event.get()); + button_event->x = _x; + button_event->y = _y; + button_event->state = _state; + button_event->button = to_underlying(_button); + button_event->time = _time; + + return event; +} + +ButtonReleaseEvent::ButtonReleaseEvent(uint32_t time, EventButton button, double x, double y, unsigned state) + : ButtonEvent(time, button, x, y, state) +{} + +GdkEventUniqPtr ButtonReleaseEvent::compatSynthesize() +{ + auto event = GdkEventUniqPtr(gdk_event_new(GDK_BUTTON_RELEASE)); + + auto button_event = reinterpret_cast(event.get()); + button_event->x = _x; + button_event->y = _y; + button_event->state = _state; + button_event->button = to_underlying(_button); + button_event->time = _time; + + return event; +} + +PointerEvent::PointerEvent(uint32_t time, unsigned state) + : CanvasEvent(time) + , _state(state) +{} + +MotionEvent::MotionEvent(uint32_t time, double x, double y, unsigned state) + : PointerEvent(time, state) + , _x(x) + , _y(y) +{} + +GdkEventUniqPtr MotionEvent::compatSynthesize() +{ + auto event = GdkEventUniqPtr(gdk_event_new(GDK_MOTION_NOTIFY)); + + auto motion_event = reinterpret_cast(event.get()); + motion_event->x = _x; + motion_event->y = _y; + motion_event->state = _state; + motion_event->time = _time; + + return event; +} + +EnterEvent::EnterEvent(uint32_t time, double x, double y, unsigned state) + : PointerEvent(time, state) + , _x(x) + , _y(y) +{} + +GdkEventUniqPtr EnterEvent::compatSynthesize() +{ + auto event = GdkEventUniqPtr(gdk_event_new(GDK_ENTER_NOTIFY)); + + auto crossing_event = reinterpret_cast(event.get()); + crossing_event->x = _x; + crossing_event->y = _y; + crossing_event->state = _state; + crossing_event->time = _time; + + return event; +} + +LeaveEvent::LeaveEvent(uint32_t time, unsigned state) + : PointerEvent(time, state) +{} + +GdkEventUniqPtr LeaveEvent::compatSynthesize() +{ + auto event = GdkEventUniqPtr(gdk_event_new(GDK_LEAVE_NOTIFY)); + + auto crossing_event = reinterpret_cast(event.get()); + crossing_event->x = 0.0f; // x not relevant + crossing_event->y = 0.0f; // y not relevant + crossing_event->state = _state; + crossing_event->time = _time; + + return event; +} + +KeyEvent::KeyEvent(uint32_t time, uint8_t group, uint16_t keycode, uint32_t keyval, unsigned state) + : CanvasEvent(time) + , _group(group) + , _hardware_keycode(keycode) + , _keyval(keyval) + , _state(state) +{} + +KeyPressEvent::KeyPressEvent(uint32_t time, uint8_t group, uint16_t keycode, uint32_t keyval, unsigned state) + : KeyEvent(time, group, keycode, keyval, state) +{} + +GdkEventUniqPtr KeyPressEvent::compatSynthesize() +{ + auto event = GdkEventUniqPtr(gdk_event_new(GDK_KEY_PRESS)); + + auto key_event = reinterpret_cast(event.get()); + key_event->keyval = _keyval; + key_event->hardware_keycode = _hardware_keycode; + key_event->state = _state; + key_event->group = _group; + key_event->time = _time; + + return event; +} + +KeyReleaseEvent::KeyReleaseEvent(uint32_t time, uint8_t group, uint16_t keycode, uint32_t keyval, unsigned state) + : KeyEvent(time, group, keycode, keyval, state) +{} + +GdkEventUniqPtr KeyReleaseEvent::compatSynthesize() +{ + auto event = GdkEventUniqPtr(gdk_event_new(GDK_KEY_RELEASE)); + + auto key_event = reinterpret_cast(event.get()); + key_event->keyval = _keyval; + key_event->hardware_keycode = _hardware_keycode; + key_event->state = _state; + key_event->group = _group; + key_event->time = _time; + + return event; +} + +ScrollEvent::ScrollEvent(uint32_t time, double dx, double dy, unsigned state) + : CanvasEvent(time) + , _dx(dx) + , _dy(dy) + , _state(state) +{} + +GdkEventUniqPtr ScrollEvent::compatSynthesize() +{ + auto event = GdkEventUniqPtr(gdk_event_new(GDK_SCROLL)); + + auto scroll_event = reinterpret_cast(event.get()); + + scroll_event->delta_x = _dx; + scroll_event->delta_y = _dy; + scroll_event->direction = GDK_SCROLL_SMOOTH; + scroll_event->state = _state; + scroll_event->time = _time; + + return event; +} + +} // namespace Inkscape::UI::Widget diff --git a/src/ui/widget/events/canvas-event.h b/src/ui/widget/events/canvas-event.h new file mode 100644 index 0000000000000000000000000000000000000000..5d6f3fc4f5141a2a665d0f98b88bdbbba6611657 --- /dev/null +++ b/src/ui/widget/events/canvas-event.h @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Matthew Jakeman + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_UI_WIDGET_EVENTS_CANVAS_EVENT_H +#define INKSCAPE_UI_WIDGET_EVENTS_CANVAS_EVENT_H + +#include +#include +#include + +namespace Inkscape::UI::Widget { + +// Smart pointer for wrapping GdkEvents. +struct GdkEventFreer { void operator()(GdkEvent *ev) const { gdk_event_free(ev); } }; +using GdkEventUniqPtr = std::unique_ptr; + +/** + * An internal event hierarchy for Inkscape's Canvas. + * + * When events from GDK reach the Canvas widget, we translate them + * into a set of Inkscape specific events which can be emitted, suspended, + * modified, and re-fired as necessary. + * + * For now, each CanvasEvent maps directly to a GdkEvent, although this + * may change in the future (e.g. special casing support for touch). + * + * - GdkCrossingEvent -> CrossingEvent + * - GdkMotionEvent -> MotionEvent + * - GdkButtonEvent -> ButtonEvent + * - GdkKeyEvent -> KeyEvent + */ +enum class EventType +{ + ENTER, + LEAVE, + MOTION, + BUTTON_PRESS, + BUTTON_RELEASE, + KEY_PRESS, + KEY_RELEASE, + SCROLL +}; + +/** + * Which mouse button the event pertains to. This is mapped to + * GDK's own representation of button numbers so you can + * directly cast e.g. `(EventButton) event->button`. + */ +enum class EventButton +{ + PRIMARY = 1, + MIDDLE = 2, + SECONDARY = 3 +}; + +/** + * Abstract base class for events + */ +class CanvasEvent +{ +public: + CanvasEvent(uint32_t time); + virtual ~CanvasEvent() {} + + virtual EventType getEventType() const = 0; + virtual GdkEventUniqPtr compatSynthesize() = 0; + + uint32_t getTime() const { return _time; } + +protected: + uint32_t _time; +}; + +/** + * Abstract event for mouse button (left/right/middle). May also + * be used for touch interactions. + */ +class ButtonEvent : public CanvasEvent +{ +public: + ButtonEvent(uint32_t time, EventButton button, double x, double y, unsigned state); + + double getEventX() const { return _x; } + double getEventY() const { return _y; } + unsigned getModifiers() const { return _state; } + EventButton getButton() const { return _button; } + +protected: + double _x; + double _y; + unsigned _state; + EventButton _button; +}; + +/** + * A mouse button (left/right/middle) is pressed. May also + * be used for touch interactions. + */ +class ButtonPressEvent : public ButtonEvent +{ +public: + ButtonPressEvent(uint32_t time, EventButton button, int n_press, double x, double y, unsigned state); + EventType getEventType() const override { return EventType::BUTTON_PRESS; } + GdkEventUniqPtr compatSynthesize() override; + + int getNumPress() const { return _n_press; }; + +private: + int _n_press; +}; + +/** + * A mouse button (left/right/middle) is released. May also + * be used for touch interactions. + */ +class ButtonReleaseEvent : public ButtonEvent +{ +public: + ButtonReleaseEvent(uint32_t time, EventButton button, double x, double y, unsigned state); + EventType getEventType() const override { return EventType::BUTTON_RELEASE; } + GdkEventUniqPtr compatSynthesize() override; +}; + +/** + * A key has been pressed. + */ +class KeyEvent : public CanvasEvent +{ +public: + KeyEvent(uint32_t time, uint8_t group, uint16_t keycode, uint32_t keyval, unsigned state); + + uint8_t getGroup() const { return _group; } + uint16_t getHardwareKeycode() const { return _hardware_keycode; } + uint32_t getKeyval() const { return _keyval; } + unsigned getModifiers() const { return _state; } + +protected: + uint8_t _group; + uint16_t _hardware_keycode; + uint32_t _keyval; + unsigned _state; +}; + +/** + * A key has been pressed. + */ +class KeyPressEvent : public KeyEvent +{ +public: + KeyPressEvent(uint32_t time, uint8_t group, uint16_t keycode, uint32_t keyval, unsigned state); + EventType getEventType() const override { return EventType::KEY_PRESS; } + GdkEventUniqPtr compatSynthesize() override; +}; + +/** + * A key has been released. + */ +class KeyReleaseEvent : public KeyEvent +{ +public: + KeyReleaseEvent(uint32_t time, uint8_t group, uint16_t keycode, uint32_t keyval, unsigned state); + EventType getEventType() const override { return EventType::KEY_RELEASE; } + GdkEventUniqPtr compatSynthesize() override; +}; + +/** + * The pointer has moved, entered, or exited a widget or item. + */ +class PointerEvent : public CanvasEvent +{ +public: + PointerEvent(uint32_t time, unsigned state); + + unsigned getModifiers() const { return _state; } + +protected: + unsigned _state; +}; + +/** + * Movement of the mouse pointer. May also be used + * for touch interactions. + */ +class MotionEvent : public PointerEvent +{ +public: + MotionEvent(uint32_t time, double x, double y, unsigned state); + EventType getEventType() const override { return EventType::MOTION; } + GdkEventUniqPtr compatSynthesize() override; + + double getEventX() const { return _x; } + double getEventY() const { return _y; } + +private: + double _x; + double _y; +}; + +/** + * The pointer has entered a widget or item. + */ +class EnterEvent : public PointerEvent +{ +public: + EnterEvent(uint32_t time, double x, double y, unsigned state); + EventType getEventType() const override { return EventType::ENTER; } + GdkEventUniqPtr compatSynthesize() override; + + double getEventX() const { return _x; } + double getEventY() const { return _y; } + +private: + double _x; + double _y; +}; + +/** + * The pointer has exited a widget or item. + * + * Note the coordinates will always be (0, 0) for this event. + */ +class LeaveEvent : public PointerEvent +{ +public: + LeaveEvent(uint32_t time, unsigned state); + EventType getEventType() const override { return EventType::LEAVE; } + GdkEventUniqPtr compatSynthesize() override; +}; + +/** + * Scroll the item or widget by the provided amount + */ +class ScrollEvent : public CanvasEvent +{ +public: + ScrollEvent(uint32_t time, double dx, double dy, unsigned state); + EventType getEventType() const override { return EventType::SCROLL; } + GdkEventUniqPtr compatSynthesize() override; + + unsigned getModifiers() const { return _state; } + double getDeltaX() const { return _dx; } + double getDeltaY() const { return _dy; } + +private: + double _dx; + double _dy; + unsigned _state; +}; + +} // namespace Inkscape::UI::Widget + +#endif // INKSCAPE_UI_WIDGET_EVENTS_CANVAS_EVENT_H diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 7c76d101c16d59cc497e358a4729babbe8b7d453..73e25640125814eec0d821fdbdcb1db5c7c4e93d 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -23,6 +23,7 @@ set(util_SRC action-accel.h cached_map.h cast.h + callback-converter.h const_char_ptr.h document-fonts.h enums.h diff --git a/src/util/callback-converter.h b/src/util/callback-converter.h new file mode 100644 index 0000000000000000000000000000000000000000..9a0f34666b04eeaf0f6d5fe9a7164683e00789e8 --- /dev/null +++ b/src/util/callback-converter.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Callback converter for interfacing with C APIs. + */ +/* + * Author: PBS + * Copyright (C) 2022 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_UTIL_CALLBACK_CONVERTER_H +#define INKSCAPE_UTIL_CALLBACK_CONVERTER_H + +#include + +namespace Inkscape::Util { +namespace detail { + +template +struct CallbackConverter +{ + template + struct Helper; + + template + struct Helper + { + static constexpr auto result = +[] (Args... args, void *data) -> Ret { + return (reinterpret_cast(data)->*Fp)(std::forward(args)...); + }; + }; + + static constexpr auto result = Helper::result; +}; + +} // namespace detail + +/** + * Given a member function, make_c_callback produces a pure function with an extra void* argument at the end, + * into which an object pointer can be passed. Calling the pure function then invokes the original + * function on this object. In other words + * + * make_c_callback<&X::f>(..., &x) + * + * is equivalent to + * + * x->f(...); + * + * This is useful for passing member functions as callbacks to C code. + * + * Note: Actually they're not completely equivalent in that some extra forwarding might go on. + * Specifically, if your member function takes a T (by value) then the resulting callback + * will also take a T by value (because make_c_callback always exactly preserves argument types). That means + * your T will have to be moved from the wrapping function's argument into the wrapped function's + * argument. This won't make much difference if you only use this with C-compatible types. + */ +template +constexpr auto make_c_callback = detail::CallbackConverter::result; + +/** + * A worse version of make_c_callback that also casts the result to a GCallback, losing even more type-safety. + * Commonly needed to interface with Glib and GTK. (See make_c_callback for more details.) + */ +template +inline auto make_g_callback = reinterpret_cast(make_c_callback); // inline instead of constexpr due to reinterpret_cast + +} // namespace Inkscape::Util + +#endif // INKSCAPE_UTIL_CALLBACK_CONVERTER_H