summaryrefslogtreecommitdiffstats
path: root/src/display/sp-canvas.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/display/sp-canvas.cpp')
-rw-r--r--src/display/sp-canvas.cpp2959
1 files changed, 2959 insertions, 0 deletions
diff --git a/src/display/sp-canvas.cpp b/src/display/sp-canvas.cpp
new file mode 100644
index 0000000..87a9722
--- /dev/null
+++ b/src/display/sp-canvas.cpp
@@ -0,0 +1,2959 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Port of GnomeCanvas for Inkscape needs
+ *
+ * Authors:
+ * Federico Mena <federico@nuclecu.unam.mx>
+ * Raph Levien <raph@gimp.org>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * fred
+ * bbyak
+ * Jon A. Cruz <jon@joncruz.org>
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 1998 The Free Software Foundation
+ * Copyright (C) 2002-2006 authors
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <gdkmm/devicemanager.h>
+#include <gdkmm/display.h>
+#include <gdkmm/rectangle.h>
+#include <gdkmm/seat.h>
+
+#include <cairomm/region.h>
+
+#include "cms-system.h"
+#include "color.h"
+#include "debug/gdk-event-latency-tracker.h"
+#include "desktop.h"
+#include "display/cairo-utils.h"
+#include "display/canvas-arena.h"
+#include "display/rendermode.h"
+#include "display/sp-canvas-group.h"
+#include "display/sp-canvas.h"
+#include "helper/sp-marshal.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "preferences.h"
+#include "sodipodi-ctrlrect.h"
+#include "ui/tools/node-tool.h"
+#include "ui/tools/tool-base.h"
+#include "widgets/desktop-widget.h"
+#include <2geom/affine.h>
+#include <2geom/rect.h>
+
+using Inkscape::Debug::GdkEventLatencyTracker;
+
+
+// Disabled by Mentalguy, many years ago in commit 427a81
+static bool const HAS_BROKEN_MOTION_HINTS = true;
+
+// Define this to visualize the regions to be redrawn
+//#define DEBUG_REDRAW 1;
+
+// Define this to output the time spent in a full idle loop and the number of "tiles" painted
+//#define DEBUG_PERFORMANCE 1;
+
+// Tiles are a way to minimize the number of redraws, eliminating too small redraws.
+// The canvas stores a 2D array of ints, each representing a TILE_SIZExTILE_SIZE pixels tile.
+// If any part of it is dirtied, the entire tile is dirtied (its int is nonzero) and repainted.
+#define TILE_SIZE 16
+
+/**
+ * The SPCanvasGroup vtable.
+ */
+struct SPCanvasGroupClass {
+ SPCanvasItemClass parent_class;
+};
+
+static void ungrab_default_client_pointer()
+{
+ auto const display = Gdk::Display::get_default();
+ auto const seat = display->get_default_seat();
+ seat->ungrab();
+}
+
+/**
+ * A group of items.
+ */
+struct SPCanvasGroup {
+ /**
+ * Adds an item to a canvas group.
+ */
+ void add(SPCanvasItem *item);
+
+ /**
+ * Removes an item from a canvas group.
+ */
+ void remove(SPCanvasItem *item);
+
+ /**
+ * Class initialization function for SPCanvasGroupClass.
+ */
+ static void classInit(SPCanvasGroupClass *klass);
+
+ /**
+ * Callback. Empty.
+ */
+ static void init(SPCanvasGroup *group);
+
+ /**
+ * Callback that destroys all items in group and calls group's virtual
+ * destroy() function.
+ */
+ static void destroy(SPCanvasItem *object);
+
+ /**
+ * Update handler for canvas groups.
+ */
+ static void update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags);
+
+ /**
+ * Point handler for canvas groups.
+ */
+ static double point(SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item);
+
+ /**
+ * Renders all visible canvas group items in buf rectangle.
+ */
+ static void render(SPCanvasItem *item, SPCanvasBuf *buf);
+
+ static void viewboxChanged(SPCanvasItem *item, Geom::IntRect const &new_area);
+
+
+ // Data members: ----------------------------------------------------------
+
+ SPCanvasItem item;
+
+ SPCanvasItemList items;
+};
+
+/**
+ * The SPCanvas vtable.
+ */
+struct SPCanvasClass {
+ GtkWidgetClass parent_class;
+};
+
+namespace {
+
+GdkWindow *getWindow(SPCanvas *canvas)
+{
+ return gtk_widget_get_window(reinterpret_cast<GtkWidget *>(canvas));
+}
+
+
+// SPCanvasItem
+
+enum {
+ ITEM_EVENT,
+ ITEM_LAST_SIGNAL
+};
+
+enum {
+ PROP_0,
+ PROP_VISIBLE
+};
+
+
+void trackLatency(GdkEvent const *event);
+
+enum {
+ DESTROY,
+ LAST_SIGNAL
+};
+
+/**
+ * Callback that removes item from all referrers and destroys it.
+ */
+void sp_canvas_item_dispose(GObject *object);
+void sp_canvas_item_finalize(GObject *object);
+void sp_canvas_item_real_destroy(SPCanvasItem *object);
+
+static guint object_signals[LAST_SIGNAL] = { 0 };
+
+/**
+ * Sets up the newly created SPCanvasItem.
+ *
+ * We make it static for encapsulation reasons since it was nowhere used.
+ */
+void sp_canvas_item_construct(SPCanvasItem *item, SPCanvasGroup *parent, gchar const *first_arg_name, va_list args);
+
+/**
+ * Helper that returns true iff item is descendant of parent.
+ */
+bool is_descendant(SPCanvasItem const *item, SPCanvasItem const *parent);
+
+guint item_signals[ITEM_LAST_SIGNAL] = { 0 };
+
+} // namespace
+
+G_DEFINE_TYPE(SPCanvasItem, sp_canvas_item, G_TYPE_INITIALLY_UNOWNED);
+
+static void
+sp_canvas_item_class_init(SPCanvasItemClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+
+ item_signals[ITEM_EVENT] = g_signal_new ("event",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ ((glong)((guint8*)&(klass->event) - (guint8*)klass)),
+ nullptr, nullptr,
+ sp_marshal_BOOLEAN__POINTER,
+ G_TYPE_BOOLEAN, 1,
+ GDK_TYPE_EVENT);
+
+ gobject_class->dispose = sp_canvas_item_dispose;
+ gobject_class->finalize = sp_canvas_item_finalize;
+ klass->destroy = sp_canvas_item_real_destroy;
+
+ object_signals[DESTROY] =
+ g_signal_new ("destroy",
+ G_TYPE_FROM_CLASS (gobject_class),
+ (GSignalFlags)(G_SIGNAL_RUN_CLEANUP | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS),
+ G_STRUCT_OFFSET (SPCanvasItemClass, destroy),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+}
+
+static void
+sp_canvas_item_init(SPCanvasItem *item)
+{
+ item->xform = Geom::Affine(Geom::identity());
+ item->ctrlResize = 0;
+ item->ctrlType = Inkscape::CTRL_TYPE_UNKNOWN;
+ item->ctrlFlags = Inkscape::CTRL_FLAG_NORMAL;
+
+ // TODO items should not be visible on creation - this causes kludges with items
+ // that should be initially invisible; examples of such items: node handles, the CtrlRect
+ // used for rubberbanding, path outline, etc.
+ item->visible = TRUE;
+ item->in_destruction = false;
+ item->pickable = true;
+}
+
+SPCanvasItem *sp_canvas_item_new(SPCanvasGroup *parent, GType type, gchar const *first_arg_name, ...)
+{
+ va_list args;
+
+ g_return_val_if_fail(parent != nullptr, NULL);
+ g_return_val_if_fail(SP_IS_CANVAS_GROUP(parent), NULL);
+ g_return_val_if_fail(g_type_is_a(type, SP_TYPE_CANVAS_ITEM), NULL);
+
+ SPCanvasItem *item = SP_CANVAS_ITEM(g_object_new(type, nullptr));
+
+ va_start(args, first_arg_name);
+ sp_canvas_item_construct(item, parent, first_arg_name, args);
+ va_end(args);
+
+ return item;
+}
+
+namespace {
+
+void sp_canvas_item_construct(SPCanvasItem *item, SPCanvasGroup *parent, gchar const *first_arg_name, va_list args)
+{
+ g_return_if_fail(SP_IS_CANVAS_GROUP(parent));
+ g_return_if_fail(SP_IS_CANVAS_ITEM(item));
+
+ // manually invoke list_member_hook constructor
+ new (&(item->member_hook_)) boost::intrusive::list_member_hook<>();
+
+ item->parent = SP_CANVAS_ITEM(parent);
+ item->canvas = item->parent->canvas;
+
+ g_object_set_valist(G_OBJECT(item), first_arg_name, args);
+
+ SP_CANVAS_GROUP(item->parent)->add(item);
+
+ sp_canvas_item_request_update(item);
+}
+
+} // namespace
+
+/**
+ * Helper function that requests redraw only if item's visible flag is set.
+ */
+static void redraw_if_visible(SPCanvasItem *item)
+{
+ if (item->visible) {
+ int x0 = (int)(item->x1);
+ int x1 = (int)(item->x2);
+ int y0 = (int)(item->y1);
+ int y1 = (int)(item->y2);
+
+ if (x0 !=0 || x1 !=0 || y0 !=0 || y1 !=0) {
+ item->canvas->requestRedraw((int)(item->x1 - 1), (int)(item->y1 -1), (int)(item->x2 + 1), (int)(item->y2 + 1));
+ }
+ }
+}
+
+void sp_canvas_item_destroy(SPCanvasItem *item)
+{
+ g_return_if_fail(SP_IS_CANVAS_ITEM(item));
+
+ if (!item->in_destruction)
+ g_object_run_dispose(G_OBJECT(item));
+}
+
+namespace {
+void sp_canvas_item_dispose(GObject *object)
+{
+ SPCanvasItem *item = SP_CANVAS_ITEM (object);
+
+ /* guard against reinvocations during
+ * destruction with the in_destruction flag.
+ */
+ if (!item->in_destruction)
+ {
+ item->in_destruction=true;
+
+ // Hack: if this is a ctrlrect, move it to 0,0;
+ // this redraws only the stroke of the rect to be deleted,
+ // avoiding redraw of the entire area
+ if (SP_IS_CTRLRECT(item)) {
+ SP_CTRLRECT(object)->setRectangle(Geom::Rect(Geom::Point(0,0),Geom::Point(0,0)));
+ SP_CTRLRECT(object)->update(item->xform, 0);
+ } else {
+ redraw_if_visible (item);
+ }
+ item->visible = FALSE;
+
+ if (item == item->canvas->_current_item) {
+ item->canvas->_current_item = nullptr;
+ item->canvas->_need_repick = TRUE;
+ }
+
+ if (item == item->canvas->_new_current_item) {
+ item->canvas->_new_current_item = nullptr;
+ item->canvas->_need_repick = TRUE;
+ }
+
+ if (item == item->canvas->_grabbed_item) {
+ item->canvas->_grabbed_item = nullptr;
+ ungrab_default_client_pointer();
+ }
+
+ if (item == item->canvas->_focused_item) {
+ item->canvas->_focused_item = nullptr;
+ }
+
+ if (item->parent) {
+ SP_CANVAS_GROUP(item->parent)->remove(item);
+ }
+
+ g_signal_emit (object, object_signals[DESTROY], 0);
+ item->in_destruction = false;
+ }
+
+ G_OBJECT_CLASS(sp_canvas_item_parent_class)->dispose(object);
+}
+
+void sp_reset_spliter(SPCanvas *canvas)
+{
+ canvas->_spliter = Geom::OptIntRect();
+ canvas->_spliter_area = Geom::OptIntRect();
+ canvas->_spliter_control = Geom::OptIntRect();
+ canvas->_spliter_top = Geom::OptIntRect();
+ canvas->_spliter_bottom = Geom::OptIntRect();
+ canvas->_spliter_left = Geom::OptIntRect();
+ canvas->_spliter_right = Geom::OptIntRect();
+ canvas->_spliter_control_pos = Geom::Point();
+ canvas->_spliter_in_control_pos = Geom::Point();
+ canvas->_split_value = 0.5;
+ canvas->_split_vertical = true;
+ canvas->_split_inverse = false;
+ canvas->_split_hover_vertical = false;
+ canvas->_split_hover_horizontal = false;
+ canvas->_split_hover = false;
+ canvas->_split_pressed = false;
+ canvas->_split_control_pressed = false;
+ canvas->_split_dragging = false;
+}
+
+void sp_canvas_item_real_destroy(SPCanvasItem *object)
+{
+ g_signal_handlers_destroy(object);
+}
+
+void sp_canvas_item_finalize(GObject *gobject)
+{
+ SPCanvasItem *object = SP_CANVAS_ITEM(gobject);
+
+ if (g_object_is_floating (object))
+ {
+ g_warning ("A floating object was finalized. This means that someone\n"
+ "called g_object_unref() on an object that had only a floating\n"
+ "reference; the initial floating reference is not owned by anyone\n"
+ "and must be removed with g_object_ref_sink().");
+ }
+
+ G_OBJECT_CLASS (sp_canvas_item_parent_class)->finalize (gobject);
+}
+} // namespace
+
+/**
+ * Helper function to update item and its children.
+ *
+ * NB! affine is parent2canvas.
+ */
+static void sp_canvas_item_invoke_update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags)
+{
+ // Apply the child item's transform
+ Geom::Affine child_affine = item->xform * affine;
+
+ // apply object flags to child flags
+ int child_flags = flags & ~SP_CANVAS_UPDATE_REQUESTED;
+
+ if (item->need_update) {
+ child_flags |= SP_CANVAS_UPDATE_REQUESTED;
+ }
+
+ if (item->need_affine) {
+ child_flags |= SP_CANVAS_UPDATE_AFFINE;
+ }
+
+ if (child_flags & (SP_CANVAS_UPDATE_REQUESTED | SP_CANVAS_UPDATE_AFFINE)) {
+ if (SP_CANVAS_ITEM_GET_CLASS (item)->update) {
+ SP_CANVAS_ITEM_GET_CLASS (item)->update(item, child_affine, child_flags);
+ }
+ }
+
+ item->need_update = FALSE;
+ item->need_affine = FALSE;
+}
+
+/**
+ * Helper function to invoke the point method of the item.
+ *
+ * The argument x, y should be in the parent's item-relative coordinate
+ * system. This routine applies the _split_inverse of the item's transform,
+ * maintaining the affine invariant.
+ */
+static double sp_canvas_item_invoke_point(SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item)
+{
+ if (SP_CANVAS_ITEM_GET_CLASS(item)->point) {
+ return SP_CANVAS_ITEM_GET_CLASS (item)->point (item, p, actual_item);
+ }
+
+ return Geom::infinity();
+}
+
+/**
+ * Makes the item's affine transformation matrix be equal to the specified
+ * matrix.
+ *
+ * @item: A canvas item.
+ * @affine: An affine transformation matrix.
+ */
+void sp_canvas_item_affine_absolute(SPCanvasItem *item, Geom::Affine const &affine)
+{
+ item->xform = affine;
+
+ if (!item->need_affine) {
+ item->need_affine = TRUE;
+ if (item->parent != nullptr) {
+ sp_canvas_item_request_update (item->parent);
+ } else {
+ item->canvas->requestUpdate();
+ }
+ }
+
+ item->canvas->_need_repick = TRUE;
+}
+
+/**
+ * Raises the item in its parent's stack by the specified number of positions.
+ *
+ * @param item A canvas item.
+ * @param positions Number of steps to raise the item.
+ *
+ * If the number of positions is greater than the distance to the top of the
+ * stack, then the item is put at the top.
+ */
+void sp_canvas_item_raise(SPCanvasItem *item, int positions)
+{
+ g_return_if_fail (item != nullptr);
+ g_return_if_fail (SP_IS_CANVAS_ITEM (item));
+ g_return_if_fail (positions >= 0);
+
+ if (!item->parent || positions == 0) {
+ return;
+ }
+
+ SPCanvasGroup *parent = SP_CANVAS_GROUP (item->parent);
+ auto from = parent->items.iterator_to(*item);
+ auto to = from;
+
+ for (int i = 0; i <= positions && to != parent->items.end(); ++i) {
+ ++to;
+ }
+
+ parent->items.erase(from);
+ parent->items.insert(to, *item);
+
+ redraw_if_visible (item);
+ item->canvas->_need_repick = TRUE;
+}
+
+void sp_canvas_item_raise_to_top(SPCanvasItem *item)
+{
+ g_return_if_fail (item != nullptr);
+ g_return_if_fail (SP_IS_CANVAS_ITEM (item));
+ if (!item->parent)
+ return;
+ SPCanvasGroup *parent = SP_CANVAS_GROUP (item->parent);
+ parent->items.erase(parent->items.iterator_to(*item));
+ parent->items.push_back(*item);
+ redraw_if_visible (item);
+ item->canvas->_need_repick = TRUE;
+}
+
+
+
+/**
+ * Lowers the item in its parent's stack by the specified number of positions.
+ *
+ * @param item A canvas item.
+ * @param positions Number of steps to lower the item.
+ *
+ * If the number of positions is greater than the distance to the bottom of the
+ * stack, then the item is put at the bottom.
+ */
+void sp_canvas_item_lower(SPCanvasItem *item, int positions)
+{
+ g_return_if_fail (item != nullptr);
+ g_return_if_fail (SP_IS_CANVAS_ITEM (item));
+ g_return_if_fail (positions >= 1);
+
+ SPCanvasGroup *parent = SP_CANVAS_GROUP(item->parent);
+
+ if (!parent || positions == 0 || item == &parent->items.front()) {
+ return;
+ }
+
+ auto from = parent->items.iterator_to(*item);
+ auto to = from;
+ g_assert(from != parent->items.end());
+
+ for (int i = 0; i < positions && to != parent->items.begin(); ++i) {
+ --to;
+ }
+
+ parent->items.erase(from);
+ parent->items.insert(to, *item);
+
+ redraw_if_visible (item);
+ item->canvas->_need_repick = TRUE;
+}
+
+void sp_canvas_item_lower_to_bottom(SPCanvasItem *item)
+{
+ g_return_if_fail (item != nullptr);
+ g_return_if_fail (SP_IS_CANVAS_ITEM (item));
+ if (!item->parent)
+ return;
+ SPCanvasGroup *parent = SP_CANVAS_GROUP (item->parent);
+ parent->items.erase(parent->items.iterator_to(*item));
+ parent->items.push_front(*item);
+ redraw_if_visible (item);
+ item->canvas->_need_repick = TRUE;
+}
+
+bool sp_canvas_item_is_visible(SPCanvasItem *item)
+{
+ return item->visible;
+}
+
+/**
+ * Sets visible flag on item and requests a redraw.
+ */
+void sp_canvas_item_show(SPCanvasItem *item)
+{
+ g_return_if_fail (item != nullptr);
+ g_return_if_fail (SP_IS_CANVAS_ITEM (item));
+
+ if (item->visible) {
+ return;
+ }
+
+ item->visible = TRUE;
+
+ int x0 = (int)(item->x1);
+ int x1 = (int)(item->x2);
+ int y0 = (int)(item->y1);
+ int y1 = (int)(item->y2);
+
+ if (x0 !=0 || x1 !=0 || y0 !=0 || y1 !=0) {
+ item->canvas->requestRedraw((int)(item->x1), (int)(item->y1), (int)(item->x2 + 1), (int)(item->y2 + 1));
+ item->canvas->_need_repick = TRUE;
+ }
+}
+
+/**
+ * Clears visible flag on item and requests a redraw.
+ */
+void sp_canvas_item_hide(SPCanvasItem *item)
+{
+ g_return_if_fail (item != nullptr);
+ g_return_if_fail (SP_IS_CANVAS_ITEM (item));
+
+ if (!item->visible) {
+ return;
+ }
+
+ item->visible = FALSE;
+
+ int x0 = (int)(item->x1);
+ int x1 = (int)(item->x2);
+ int y0 = (int)(item->y1);
+ int y1 = (int)(item->y2);
+
+ if (x0 !=0 || x1 !=0 || y0 !=0 || y1 !=0) {
+ item->canvas->requestRedraw((int)item->x1, (int)item->y1, (int)(item->x2 + 1), (int)(item->y2 + 1));
+ item->canvas->_need_repick = TRUE;
+ }
+}
+
+/**
+ * Grab item under cursor.
+ *
+ * \pre !canvas->grabbed_item && item->flags & SP_CANVAS_ITEM_VISIBLE
+ */
+int sp_canvas_item_grab(SPCanvasItem *item, guint event_mask, GdkCursor *cursor, guint32 etime)
+{
+ g_return_val_if_fail (item != nullptr, -1);
+ g_return_val_if_fail (SP_IS_CANVAS_ITEM (item), -1);
+ g_return_val_if_fail (gtk_widget_get_mapped (GTK_WIDGET (item->canvas)), -1);
+
+ if (item->canvas->_grabbed_item) {
+ return -1;
+ }
+
+ // This test disallows grabbing events by an invisible item, which may be useful
+ // sometimes. An example is the hidden control point used for the selector component,
+ // where it is used for object selection and rubberbanding. There seems to be nothing
+ // preventing this except this test, so I removed it.
+ // -- Krzysztof Kosiński, 2009.08.12
+ //if (!(item->flags & SP_CANVAS_ITEM_VISIBLE))
+ // return -1;
+
+ if (HAS_BROKEN_MOTION_HINTS) {
+ event_mask &= ~GDK_POINTER_MOTION_HINT_MASK;
+ }
+
+ // fixme: Top hack (Lauris)
+ // fixme: If we add key masks to event mask, Gdk will abort (Lauris)
+ // fixme: But Canvas actually does get key events, so all we need is routing these here
+ auto display = gdk_display_get_default();
+ auto seat = gdk_display_get_default_seat(display);
+ gdk_seat_grab(seat,
+ getWindow(item->canvas),
+ GDK_SEAT_CAPABILITY_ALL_POINTING,
+ FALSE,
+ cursor,
+ nullptr,
+ nullptr,
+ nullptr);
+
+ item->canvas->_grabbed_item = item;
+ item->canvas->_grabbed_event_mask = event_mask;
+ item->canvas->_current_item = item; // So that events go to the grabbed item
+
+ return 0;
+}
+
+/**
+ * Ungrabs the item, which must have been grabbed in the canvas, and ungrabs the
+ * mouse.
+ *
+ * @param item A canvas item that holds a grab.
+ */
+void sp_canvas_item_ungrab(SPCanvasItem *item)
+{
+ g_return_if_fail (item != nullptr);
+ g_return_if_fail (SP_IS_CANVAS_ITEM (item));
+
+ if (item->canvas->_grabbed_item != item) {
+ return;
+ }
+
+ item->canvas->_grabbed_item = nullptr;
+ ungrab_default_client_pointer();
+}
+
+/**
+ * Returns the product of all transformation matrices from the root item down
+ * to the item.
+ */
+Geom::Affine sp_canvas_item_i2w_affine(SPCanvasItem const *item)
+{
+ g_assert (SP_IS_CANVAS_ITEM (item)); // should we get this?
+
+ Geom::Affine affine = Geom::identity();
+
+ while (item) {
+ affine *= item->xform;
+ item = item->parent;
+ }
+ return affine;
+}
+
+namespace {
+
+bool is_descendant(SPCanvasItem const *item, SPCanvasItem const *parent)
+{
+ while (item) {
+ if (item == parent) {
+ return true;
+ }
+ item = item->parent;
+ }
+
+ return false;
+}
+
+} // namespace
+
+/**
+ * Requests that the canvas queue an update for the specified item.
+ *
+ * To be used only by item implementations.
+ */
+void sp_canvas_item_request_update(SPCanvasItem *item)
+{
+ if (item->need_update) {
+ return;
+ }
+
+ item->need_update = TRUE;
+
+ if (item->parent != nullptr) {
+ // Recurse up the tree
+ sp_canvas_item_request_update (item->parent);
+ } else {
+ // Have reached the top of the tree, make sure the update call gets scheduled.
+ item->canvas->requestUpdate();
+ }
+}
+
+/**
+ * Returns position of item in group.
+ */
+gint sp_canvas_item_order (SPCanvasItem * item)
+{
+ SPCanvasGroup * p = SP_CANVAS_GROUP(item->parent);
+ size_t index = 0;
+ for (auto it = p->items.begin(); it != p->items.end(); ++it, ++index) {
+ if (item == &(*it)) {
+ return index;
+ }
+ }
+
+ return -1;
+}
+
+// SPCanvasGroup
+G_DEFINE_TYPE(SPCanvasGroup, sp_canvas_group, SP_TYPE_CANVAS_ITEM);
+
+static void sp_canvas_group_class_init(SPCanvasGroupClass *klass)
+{
+ SPCanvasItemClass *item_class = reinterpret_cast<SPCanvasItemClass *>(klass);
+
+ item_class->destroy = SPCanvasGroup::destroy;
+ item_class->update = SPCanvasGroup::update;
+ item_class->render = SPCanvasGroup::render;
+ item_class->point = SPCanvasGroup::point;
+ item_class->viewbox_changed = SPCanvasGroup::viewboxChanged;
+}
+
+static void sp_canvas_group_init(SPCanvasGroup * group)
+{
+ new (&group->items) SPCanvasItemList;
+}
+
+void SPCanvasGroup::destroy(SPCanvasItem *object)
+{
+ g_return_if_fail(object != nullptr);
+ g_return_if_fail(SP_IS_CANVAS_GROUP(object));
+
+ SPCanvasGroup *group = SP_CANVAS_GROUP(object);
+
+ for (auto it = group->items.begin(); it != group->items.end();) {
+ SPCanvasItem *item = &(*it);
+ it++;
+ sp_canvas_item_destroy(item);
+ }
+
+ group->items.clear();
+ group->items.~SPCanvasItemList(); // invoke manually
+
+ if (SP_CANVAS_ITEM_CLASS(sp_canvas_group_parent_class)->destroy) {
+ (* SP_CANVAS_ITEM_CLASS(sp_canvas_group_parent_class)->destroy)(object);
+ }
+}
+
+void SPCanvasGroup::update(SPCanvasItem *item, Geom::Affine const &affine, unsigned int flags)
+{
+ SPCanvasGroup *group = SP_CANVAS_GROUP(item);
+ Geom::OptRect bounds;
+
+ for (auto & item : group->items) {
+ SPCanvasItem *i = &item;
+
+ sp_canvas_item_invoke_update (i, affine, flags);
+
+ if ( (i->x2 > i->x1) && (i->y2 > i->y1) ) {
+ bounds.expandTo(Geom::Point(i->x1, i->y1));
+ bounds.expandTo(Geom::Point(i->x2, i->y2));
+ }
+ }
+
+ if (bounds) {
+ item->x1 = bounds->min()[Geom::X];
+ item->y1 = bounds->min()[Geom::Y];
+ item->x2 = bounds->max()[Geom::X];
+ item->y2 = bounds->max()[Geom::Y];
+ } else {
+ // FIXME ?
+ item->x1 = item->x2 = item->y1 = item->y2 = 0;
+ }
+}
+
+double SPCanvasGroup::point(SPCanvasItem *item, Geom::Point p, SPCanvasItem **actual_item)
+{
+ SPCanvasGroup *group = SP_CANVAS_GROUP(item);
+ double const x = p[Geom::X];
+ double const y = p[Geom::Y];
+ int x1 = (int)(x - item->canvas->_close_enough);
+ int y1 = (int)(y - item->canvas->_close_enough);
+ int x2 = (int)(x + item->canvas->_close_enough);
+ int y2 = (int)(y + item->canvas->_close_enough);
+
+ double best = 0.0;
+ *actual_item = nullptr;
+
+ double dist = 0.0;
+ for (auto & it : group->items) {
+ SPCanvasItem *child = &it;
+
+ if ((child->x1 <= x2) && (child->y1 <= y2) && (child->x2 >= x1) && (child->y2 >= y1)) {
+ SPCanvasItem *point_item = nullptr; // cater for incomplete item implementations
+
+ int pickable;
+ if (child->visible && child->pickable && SP_CANVAS_ITEM_GET_CLASS(child)->point) {
+ dist = sp_canvas_item_invoke_point(child, p, &point_item);
+ pickable = TRUE;
+ } else {
+ pickable = FALSE;
+ }
+
+ // TODO: This metric should be improved, because in case of (partly) overlapping items we will now
+ // always select the last one that has been added to the group. We could instead select the one
+ // of which the center is the closest, for example. One can then move to the center
+ // of the item to be focused, and have that one selected. Of course this will only work if the
+ // centers are not coincident, but at least it's better than what we have now.
+ // See the extensive comment in Inkscape::SelTrans::_updateHandles()
+ if (pickable && point_item && ((int) (dist + 0.5) <= item->canvas->_close_enough)) {
+ best = dist;
+ *actual_item = point_item;
+ }
+ }
+ }
+
+ return best;
+}
+
+void SPCanvasGroup::render(SPCanvasItem *item, SPCanvasBuf *buf)
+{
+ SPCanvasGroup *group = SP_CANVAS_GROUP(item);
+
+ for (auto & item : group->items) {
+ SPCanvasItem *child = &item;
+ if (child->visible) {
+ if ((child->x1 < buf->rect.right()) &&
+ (child->y1 < buf->rect.bottom()) &&
+ (child->x2 > buf->rect.left()) &&
+ (child->y2 > buf->rect.top())) {
+ if (SP_CANVAS_ITEM_GET_CLASS(child)->render) {
+ SP_CANVAS_ITEM_GET_CLASS(child)->render(child, buf);
+ }
+ }
+ }
+ }
+}
+
+void SPCanvasGroup::viewboxChanged(SPCanvasItem *item, Geom::IntRect const &new_area)
+{
+ SPCanvasGroup *group = SP_CANVAS_GROUP(item);
+ for (auto & item : group->items) {
+ SPCanvasItem *child = &item;
+ if (child->visible) {
+ if (SP_CANVAS_ITEM_GET_CLASS(child)->viewbox_changed) {
+ SP_CANVAS_ITEM_GET_CLASS(child)->viewbox_changed(child, new_area);
+ }
+ }
+ }
+}
+
+void SPCanvasGroup::add(SPCanvasItem *item)
+{
+ g_object_ref(item);
+ g_object_ref_sink(item);
+
+ items.push_back(*item);
+
+ sp_canvas_item_request_update(item);
+}
+
+void SPCanvasGroup::remove(SPCanvasItem *item)
+{
+ g_return_if_fail(item != nullptr);
+
+ auto position = items.iterator_to(*item);
+ if (position != items.end()) {
+ items.erase(position);
+ }
+
+ // Unparent the child
+ item->parent = nullptr;
+ g_object_unref(item);
+
+}
+
+G_DEFINE_TYPE(SPCanvas, sp_canvas, GTK_TYPE_WIDGET);
+
+static void sp_canvas_finalize(GObject *object)
+{
+ SPCanvas *canvas = SP_CANVAS(object);
+
+#if defined(HAVE_LIBLCMS2)
+ using S = decltype(canvas->_cms_key);
+ canvas->_cms_key.~S();
+#endif
+
+ G_OBJECT_CLASS(sp_canvas_parent_class)->finalize(object);
+}
+
+void sp_canvas_class_init(SPCanvasClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+
+ object_class->dispose = SPCanvas::dispose;
+ object_class->finalize = sp_canvas_finalize;
+
+ widget_class->realize = SPCanvas::handle_realize;
+ widget_class->unrealize = SPCanvas::handle_unrealize;
+ widget_class->get_preferred_width = SPCanvas::handle_get_preferred_width;
+ widget_class->get_preferred_height = SPCanvas::handle_get_preferred_height;
+ widget_class->draw = SPCanvas::handle_draw;
+ widget_class->size_allocate = SPCanvas::handle_size_allocate;
+ widget_class->button_press_event = SPCanvas::handle_button;
+ widget_class->button_release_event = SPCanvas::handle_button;
+ widget_class->motion_notify_event = SPCanvas::handle_motion;
+ widget_class->scroll_event = SPCanvas::handle_scroll;
+ widget_class->key_press_event = SPCanvas::handle_key_event;
+ widget_class->key_release_event = SPCanvas::handle_key_event;
+ widget_class->enter_notify_event = SPCanvas::handle_crossing;
+ widget_class->leave_notify_event = SPCanvas::handle_crossing;
+ widget_class->focus_in_event = SPCanvas::handle_focus_in;
+ widget_class->focus_out_event = SPCanvas::handle_focus_out;
+}
+
+static void sp_canvas_init(SPCanvas *canvas)
+{
+ gtk_widget_set_has_window (GTK_WIDGET (canvas), TRUE);
+ gtk_widget_set_can_focus (GTK_WIDGET (canvas), TRUE);
+
+ canvas->_pick_event.type = GDK_LEAVE_NOTIFY;
+ canvas->_pick_event.crossing.x = 0;
+ canvas->_pick_event.crossing.y = 0;
+
+ // Create the root item as a special case
+ canvas->_root = SP_CANVAS_ITEM(g_object_new(SP_TYPE_CANVAS_GROUP, nullptr));
+ canvas->_root->canvas = canvas;
+
+ g_object_ref (canvas->_root);
+ g_object_ref_sink (canvas->_root);
+
+ canvas->_need_repick = TRUE;
+
+ // See comment at in sp-canvas.h.
+ canvas->_gen_all_enter_events = false;
+
+ canvas->_drawing_disabled = false;
+
+ canvas->_backing_store = nullptr;
+ canvas->_surface_for_similar = nullptr;
+ canvas->_clean_region = cairo_region_create();
+ canvas->_background = cairo_pattern_create_rgb(1, 1, 1);
+ canvas->_background_is_checkerboard = false;
+
+ canvas->_forced_redraw_count = 0;
+ canvas->_forced_redraw_limit = -1;
+ canvas->_in_full_redraw = false;
+ // Split view controls
+ canvas->_spliter = Geom::OptIntRect();
+ canvas->_spliter_area = Geom::OptIntRect();
+ canvas->_spliter_control = Geom::OptIntRect();
+ canvas->_spliter_top = Geom::OptIntRect();
+ canvas->_spliter_bottom = Geom::OptIntRect();
+ canvas->_spliter_left = Geom::OptIntRect();
+ canvas->_spliter_right = Geom::OptIntRect();
+ canvas->_spliter_control_pos = Geom::Point();
+ canvas->_spliter_in_control_pos = Geom::Point();
+ canvas->_xray_rect = Geom::OptIntRect();
+ canvas->_split_value = 0.5;
+ canvas->_split_vertical = true;
+ canvas->_split_inverse = false;
+ canvas->_split_hover_vertical = false;
+ canvas->_split_hover_horizontal = false;
+ canvas->_split_hover = false;
+ canvas->_split_pressed = false;
+ canvas->_split_control_pressed = false;
+ canvas->_split_dragging = false;
+ canvas->_xray_radius = 100;
+ canvas->_xray = false;
+ canvas->_xray_orig = Geom::Point();
+ canvas->_changecursor = 0;
+ canvas->_splits = 0;
+ canvas->_totalelapsed = 0;
+ canvas->_idle_time = g_get_monotonic_time();
+ canvas->_is_dragging = false;
+
+#if defined(HAVE_LIBLCMS2)
+ canvas->_enable_cms_display_adj = false;
+ new (&canvas->_cms_key) Glib::ustring("");
+#endif // defined(HAVE_LIBLCMS2)
+}
+
+void SPCanvas::shutdownTransients()
+{
+ // Reset the clean region
+ dirtyAll();
+
+ if (_grabbed_item) {
+ _grabbed_item = nullptr;
+ ungrab_default_client_pointer();
+ }
+ removeIdle();
+}
+
+void SPCanvas::dispose(GObject *object)
+{
+ SPCanvas *canvas = SP_CANVAS(object);
+
+ if (canvas->_root) {
+ g_object_unref (canvas->_root);
+ canvas->_root = nullptr;
+ }
+ if (canvas->_backing_store) {
+ cairo_surface_destroy(canvas->_backing_store);
+ canvas->_backing_store = nullptr;
+ }
+ if (canvas->_surface_for_similar) {
+ cairo_surface_destroy(canvas->_surface_for_similar);
+ canvas->_surface_for_similar = nullptr;
+ }
+ if (canvas->_clean_region) {
+ cairo_region_destroy(canvas->_clean_region);
+ canvas->_clean_region = nullptr;
+ }
+ if (canvas->_background) {
+ cairo_pattern_destroy(canvas->_background);
+ canvas->_background = nullptr;
+ }
+
+ canvas->shutdownTransients();
+ if (G_OBJECT_CLASS(sp_canvas_parent_class)->dispose) {
+ (* G_OBJECT_CLASS(sp_canvas_parent_class)->dispose)(object);
+ }
+}
+
+namespace {
+
+void trackLatency(GdkEvent const *event)
+{
+ GdkEventLatencyTracker &tracker = GdkEventLatencyTracker::default_tracker();
+ boost::optional<double> latency = tracker.process(event);
+ if (latency && *latency > 2.0) {
+ //g_warning("Event latency reached %f sec (%1.4f)", *latency, tracker.getSkew());
+ }
+}
+
+} // namespace
+
+GtkWidget *SPCanvas::createAA()
+{
+ SPCanvas *canvas = SP_CANVAS(g_object_new(SP_TYPE_CANVAS, nullptr));
+ return GTK_WIDGET(canvas);
+}
+
+void SPCanvas::handle_realize(GtkWidget *widget)
+{
+ GdkWindowAttr attributes;
+ GtkAllocation allocation;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ gtk_widget_get_allocation (widget, &allocation);
+ attributes.x = allocation.x;
+ attributes.y = allocation.y;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.visual = gdk_screen_get_system_visual(gdk_screen_get_default());
+
+ attributes.event_mask = (gtk_widget_get_events (widget) |
+ GDK_EXPOSURE_MASK |
+ GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK |
+ ( HAS_BROKEN_MOTION_HINTS ?
+ 0 : GDK_POINTER_MOTION_HINT_MASK ) |
+ GDK_PROXIMITY_IN_MASK |
+ GDK_PROXIMITY_OUT_MASK |
+ GDK_KEY_PRESS_MASK |
+ GDK_KEY_RELEASE_MASK |
+ GDK_ENTER_NOTIFY_MASK |
+ GDK_LEAVE_NOTIFY_MASK |
+ GDK_SCROLL_MASK |
+ GDK_SMOOTH_SCROLL_MASK |
+ GDK_FOCUS_CHANGE_MASK);
+
+ gint attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
+
+ GdkWindow *window = gdk_window_new (gtk_widget_get_parent_window (widget), &attributes, attributes_mask);
+ gtk_widget_set_window (widget, window);
+ gdk_window_set_user_data (window, widget);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/options/useextinput/value", true)) {
+ gtk_widget_set_events(widget, attributes.event_mask);
+ }
+
+ gtk_widget_set_realized (widget, TRUE);
+}
+
+void SPCanvas::handle_unrealize(GtkWidget *widget)
+{
+ SPCanvas *canvas = SP_CANVAS (widget);
+
+ canvas->_current_item = nullptr;
+ canvas->_grabbed_item = nullptr;
+ canvas->_focused_item = nullptr;
+
+ canvas->shutdownTransients();
+
+ if (GTK_WIDGET_CLASS(sp_canvas_parent_class)->unrealize)
+ (* GTK_WIDGET_CLASS(sp_canvas_parent_class)->unrealize)(widget);
+}
+
+void SPCanvas::handle_get_preferred_width(GtkWidget *widget, gint *minimum_width, gint *natural_width)
+{
+ static_cast<void>(SP_CANVAS (widget));
+ *minimum_width = 256;
+ *natural_width = 256;
+}
+
+void SPCanvas::handle_get_preferred_height(GtkWidget *widget, gint *minimum_height, gint *natural_height)
+{
+ static_cast<void>(SP_CANVAS (widget));
+ *minimum_height = 256;
+ *natural_height = 256;
+}
+
+void SPCanvas::handle_size_allocate(GtkWidget *widget, GtkAllocation *allocation)
+{
+ SPCanvas *canvas = SP_CANVAS (widget);
+ // Allocation does not depend on device scale.
+ GtkAllocation old_allocation;
+ gtk_widget_get_allocation(widget, &old_allocation);
+
+ // For HiDPI monitors.
+ canvas->_device_scale = gtk_widget_get_scale_factor( widget );
+
+ Geom::IntRect new_area = Geom::IntRect::from_xywh(canvas->_x0, canvas->_y0,
+ allocation->width, allocation->height);
+
+ // Resize backing store.
+ cairo_surface_t *new_backing_store = nullptr;
+ if (canvas->_surface_for_similar != nullptr) {
+
+ // Size in device pixels. Does not set device scale.
+ new_backing_store =
+ cairo_surface_create_similar_image(canvas->_surface_for_similar,
+ CAIRO_FORMAT_ARGB32,
+ allocation->width * canvas->_device_scale,
+ allocation->height * canvas->_device_scale);
+ }
+ if (new_backing_store == nullptr) {
+
+ // Size in device pixels. Does not set device scale.
+ new_backing_store =
+ cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
+ allocation->width * canvas->_device_scale,
+ allocation->height * canvas->_device_scale);
+ }
+
+ // Set device scale
+ cairo_surface_set_device_scale(new_backing_store, canvas->_device_scale, canvas->_device_scale);
+
+ if (canvas->_backing_store) {
+ cairo_t *cr = cairo_create(new_backing_store);
+ cairo_translate(cr, -canvas->_x0, -canvas->_y0);
+ cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
+ cairo_set_source(cr, canvas->_background);
+ cairo_paint(cr);
+ cairo_set_source_surface(cr, canvas->_backing_store, canvas->_x0, canvas->_y0);
+ cairo_paint(cr);
+ cairo_destroy(cr);
+ cairo_surface_destroy(canvas->_backing_store);
+ }
+ canvas->_backing_store = new_backing_store;
+
+ // Clip the clean region to the new allocation
+ cairo_rectangle_int_t crect = { canvas->_x0, canvas->_y0, allocation->width, allocation->height };
+ cairo_region_intersect_rectangle(canvas->_clean_region, &crect);
+
+ gtk_widget_set_allocation (widget, allocation);
+
+ if (SP_CANVAS_ITEM_GET_CLASS (canvas->_root)->viewbox_changed)
+ SP_CANVAS_ITEM_GET_CLASS (canvas->_root)->viewbox_changed (canvas->_root, new_area);
+
+ if (gtk_widget_get_realized (widget)) {
+ gdk_window_move_resize (gtk_widget_get_window (widget),
+ allocation->x, allocation->y,
+ allocation->width, allocation->height);
+ }
+ // Schedule redraw of any newly exposed regions
+ canvas->_split_value = 0.5;
+ canvas->_spliter_control_pos = Geom::Point();
+ canvas->requestFullRedraw();
+}
+
+int SPCanvas::emitEvent(GdkEvent *event)
+{
+ guint mask;
+
+ if (_grabbed_item) {
+ switch (event->type) {
+ case GDK_ENTER_NOTIFY:
+ mask = GDK_ENTER_NOTIFY_MASK;
+ break;
+ case GDK_LEAVE_NOTIFY:
+ mask = GDK_LEAVE_NOTIFY_MASK;
+ break;
+ case GDK_MOTION_NOTIFY:
+ mask = GDK_POINTER_MOTION_MASK;
+ break;
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ mask = GDK_BUTTON_PRESS_MASK;
+ break;
+ case GDK_BUTTON_RELEASE:
+ mask = GDK_BUTTON_RELEASE_MASK;
+ break;
+ case GDK_KEY_PRESS:
+ mask = GDK_KEY_PRESS_MASK;
+ break;
+ case GDK_KEY_RELEASE:
+ mask = GDK_KEY_RELEASE_MASK;
+ break;
+ case GDK_SCROLL:
+ mask = GDK_SCROLL_MASK;
+ mask |= GDK_SMOOTH_SCROLL_MASK;
+ break;
+ default:
+ mask = 0;
+ break;
+ }
+
+ if (!(mask & _grabbed_event_mask)) return FALSE;
+ }
+
+ // Convert to world coordinates -- we have two cases because of different
+ // offsets of the fields in the event structures.
+
+ GdkEvent *ev = gdk_event_copy(event);
+ switch (ev->type) {
+ case GDK_ENTER_NOTIFY:
+ case GDK_LEAVE_NOTIFY:
+ ev->crossing.x += _x0;
+ ev->crossing.y += _y0;
+ break;
+ case GDK_MOTION_NOTIFY:
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ case GDK_BUTTON_RELEASE:
+ ev->motion.x += _x0;
+ ev->motion.y += _y0;
+ break;
+ default:
+ break;
+ }
+ // Block Undo and Redo while we drag /anything/
+ if(event->type == GDK_BUTTON_PRESS && event->button.button == 1)
+ _is_dragging = true;
+ else if(event->type == GDK_BUTTON_RELEASE)
+ _is_dragging = false;
+
+ // Choose where we send the event
+
+ // canvas->current_item becomes NULL in some cases under Win32
+ // (e.g. if the pointer leaves the window). So this is a hack that
+ // Lauris applied to SP to get around the problem.
+ //
+ SPCanvasItem* item = nullptr;
+ if (_grabbed_item && !is_descendant(_current_item, _grabbed_item)) {
+ item = _grabbed_item;
+ } else {
+ item = _current_item;
+ }
+
+ if (_focused_item &&
+ ((event->type == GDK_KEY_PRESS) ||
+ (event->type == GDK_KEY_RELEASE) ||
+ (event->type == GDK_FOCUS_CHANGE))) {
+ item = _focused_item;
+ }
+
+ // The event is propagated up the hierarchy (for if someone connected to
+ // a group instead of a leaf event), and emission is stopped if a
+ // handler returns TRUE, just like for GtkWidget events.
+
+ gint finished = FALSE;
+
+ while (item && !finished) {
+ g_object_ref (item);
+ g_signal_emit (G_OBJECT (item), item_signals[ITEM_EVENT], 0, ev, &finished);
+ SPCanvasItem *parent = item->parent;
+ g_object_unref (item);
+ item = parent;
+ }
+
+ gdk_event_free(ev);
+
+ return finished;
+}
+
+int SPCanvas::pickCurrentItem(GdkEvent *event)
+{
+ int button_down = 0;
+
+ if (!_root) // canvas may have already be destroyed by closing desktop during interrupted display!
+ return FALSE;
+
+ int retval = FALSE;
+
+ if (_gen_all_enter_events == false) {
+ // 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 = _state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK |
+ GDK_BUTTON3_MASK | GDK_BUTTON4_MASK | GDK_BUTTON5_MASK);
+
+ if (!button_down) _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 != &_pick_event) {
+ if ((event->type == GDK_MOTION_NOTIFY) || (event->type == GDK_BUTTON_RELEASE)) {
+ // these fields have the same offsets in both types of events
+
+ _pick_event.crossing.type = GDK_ENTER_NOTIFY;
+ _pick_event.crossing.window = event->motion.window;
+ _pick_event.crossing.send_event = event->motion.send_event;
+ _pick_event.crossing.subwindow = nullptr;
+ _pick_event.crossing.x = event->motion.x;
+ _pick_event.crossing.y = event->motion.y;
+ _pick_event.crossing.mode = GDK_CROSSING_NORMAL;
+ _pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR;
+ _pick_event.crossing.focus = FALSE;
+ _pick_event.crossing.state = event->motion.state;
+
+ // these fields don't have the same offsets in both types of events
+
+ if (event->type == GDK_MOTION_NOTIFY) {
+ _pick_event.crossing.x_root = event->motion.x_root;
+ _pick_event.crossing.y_root = event->motion.y_root;
+ } else {
+ _pick_event.crossing.x_root = event->button.x_root;
+ _pick_event.crossing.y_root = event->button.y_root;
+ }
+ } else {
+ _pick_event = *event;
+ }
+ }
+
+ // Don't do anything else if this is a recursive call
+ if (_in_repick) {
+ return retval;
+ }
+
+ // LeaveNotify means that there is no current item, so we don't look for one
+ if (_pick_event.type != GDK_LEAVE_NOTIFY) {
+ // these fields don't have the same offsets in both types of events
+ double x, y;
+
+ if (_pick_event.type == GDK_ENTER_NOTIFY) {
+ x = _pick_event.crossing.x;
+ y = _pick_event.crossing.y;
+ } else {
+ x = _pick_event.motion.x;
+ y = _pick_event.motion.y;
+ }
+
+ // world coords
+ x += _x0;
+ y += _y0;
+
+ // find the closest item
+ if (_root->visible) {
+ sp_canvas_item_invoke_point (_root, Geom::Point(x, y), &_new_current_item);
+ } else {
+ _new_current_item = nullptr;
+ }
+ } else {
+ _new_current_item = nullptr;
+ }
+
+ if ((_new_current_item == _current_item) && !_left_grabbed_item) {
+ return retval; // current item did not change
+ }
+
+ // Synthesize events for old and new current items
+
+ if ((_new_current_item != _current_item) &&
+ _current_item != nullptr && !_left_grabbed_item)
+ {
+ GdkEvent new_event;
+
+ new_event = _pick_event;
+ new_event.type = GDK_LEAVE_NOTIFY;
+
+ new_event.crossing.detail = GDK_NOTIFY_ANCESTOR;
+ new_event.crossing.subwindow = nullptr;
+ _in_repick = TRUE;
+ retval = emitEvent(&new_event);
+ _in_repick = FALSE;
+ }
+
+ if (_gen_all_enter_events == false) {
+ // new_current_item may have been set to NULL during the call to
+ // emitEvent() above
+ if ((_new_current_item != _current_item) && button_down) {
+ _left_grabbed_item = TRUE;
+ return retval;
+ }
+ }
+
+ // Handle the rest of cases
+ _left_grabbed_item = FALSE;
+ _current_item = _new_current_item;
+
+ if (_current_item != nullptr) {
+ GdkEvent new_event;
+
+ new_event = _pick_event;
+ new_event.type = GDK_ENTER_NOTIFY;
+ new_event.crossing.detail = GDK_NOTIFY_ANCESTOR;
+ new_event.crossing.subwindow = nullptr;
+ retval = emitEvent(&new_event);
+ }
+
+ return retval;
+}
+
+gint SPCanvas::handle_doubleclick(GtkWidget *widget, GdkEventButton *event)
+{
+ SPCanvas *canvas = SP_CANVAS(widget);
+ // Maybe we want to use double click on canvas so retain here
+ return 0;
+}
+
+gint SPCanvas::handle_button(GtkWidget *widget, GdkEventButton *event)
+{
+ SPCanvas *canvas = SP_CANVAS (widget);
+
+ int retval = FALSE;
+
+ // dispatch normally regardless of the event's window if an item
+ // has a pointer grab in effect
+ if (!canvas->_grabbed_item &&
+ event->window != getWindow(canvas))
+ return retval;
+
+ int mask;
+ switch (event->button) {
+ case 1:
+ mask = GDK_BUTTON1_MASK;
+ break;
+ case 2:
+ mask = GDK_BUTTON2_MASK;
+ break;
+ case 3:
+ mask = GDK_BUTTON3_MASK;
+ break;
+ case 4:
+ mask = GDK_BUTTON4_MASK;
+ break;
+ case 5:
+ mask = GDK_BUTTON5_MASK;
+ break;
+ default:
+ mask = 0;
+ }
+ static unsigned next_canvas_doubleclick = 0;
+
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ // Pick the current item as if the button were not pressed, and
+ // then process the event.
+ next_canvas_doubleclick = 0;
+ if (!canvas->_split_hover) {
+ canvas->_state = event->state;
+ canvas->pickCurrentItem(reinterpret_cast<GdkEvent *>(event));
+ canvas->_state ^= mask;
+ retval = canvas->emitEvent((GdkEvent *)event);
+ } else {
+ canvas->_split_pressed = true;
+ Geom::IntPoint cursor_pos = Geom::IntPoint(event->x, event->y);
+ canvas->_spliter_in_control_pos = cursor_pos - (*canvas->_spliter_control).midpoint();
+ if (canvas->_spliter && canvas->_spliter_control.contains(cursor_pos) && !canvas->_is_dragging) {
+ canvas->_split_control_pressed = true;
+ }
+ retval = TRUE;
+ }
+ break;
+ case GDK_2BUTTON_PRESS:
+ // Pick the current item as if the button were not pressed, and
+ // then process the event.
+ next_canvas_doubleclick = reinterpret_cast<GdkEvent *>(event)->button.button;
+
+ if (!canvas->_split_hover) {
+ canvas->_state = event->state;
+ canvas->pickCurrentItem(reinterpret_cast<GdkEvent *>(event));
+ canvas->_state ^= mask;
+ retval = canvas->emitEvent((GdkEvent *)event);
+ } else {
+ canvas->_split_pressed = true;
+ retval = TRUE;
+ }
+ break;
+ case GDK_3BUTTON_PRESS:
+ // Pick the current item as if the button were not pressed, and
+ // then process the event.
+ if (!canvas->_split_hover) {
+ canvas->_state = event->state;
+ canvas->pickCurrentItem(reinterpret_cast<GdkEvent *>(event));
+ canvas->_state ^= mask;
+ retval = canvas->emitEvent((GdkEvent *)event);
+ } else {
+ canvas->_split_pressed = true;
+ retval = TRUE;
+ }
+ break;
+
+ case GDK_BUTTON_RELEASE:
+ // Process the event as if the button were pressed, then repick
+ // after the button has been released
+ canvas->_split_pressed = false;
+ if (next_canvas_doubleclick) {
+ GdkEventButton *event2 = reinterpret_cast<GdkEventButton *>(event);
+ handle_doubleclick(GTK_WIDGET(canvas), event2);
+ }
+ if (canvas->_split_hover) {
+ retval = TRUE;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool spliter_clicked = false;
+ bool reset = false;
+ if (!canvas->_split_dragging) {
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(GTK_WIDGET(canvas), &allocation);
+ Geom::Point pos = canvas->_spliter_control_pos;
+ double value = canvas->_split_vertical ? 1 / (allocation.height / (double)pos[Geom::Y])
+ : 1 / (allocation.width / (double)pos[Geom::X]);
+ if (canvas->_split_hover_vertical) {
+ canvas->_split_inverse = !canvas->_split_inverse;
+ spliter_clicked = true;
+ reset = canvas->_split_vertical ? true : false;
+ if (reset) {
+ canvas->_split_value = value;
+ }
+ canvas->_split_vertical = false;
+ } else if (canvas->_split_hover_horizontal) {
+ canvas->_split_inverse = !canvas->_split_inverse;
+ spliter_clicked = true;
+ reset = !canvas->_split_vertical ? true : false;
+ if (reset) {
+ canvas->_split_value = value;
+ }
+ canvas->_split_vertical = true;
+ }
+ if (spliter_clicked) {
+ canvas->requestFullRedraw();
+ }
+ }
+ canvas->_split_control_pressed = false;
+ canvas->_split_dragging = false;
+ } else {
+ canvas->_state = event->state;
+ retval = canvas->emitEvent((GdkEvent *)event);
+ event->state ^= mask;
+ canvas->_state = event->state;
+ canvas->pickCurrentItem(reinterpret_cast<GdkEvent *>(event));
+ event->state ^= mask;
+ }
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+
+ return retval;
+}
+
+gint SPCanvas::handle_scroll(GtkWidget *widget, GdkEventScroll *event)
+{
+ return SP_CANVAS(widget)->emitEvent(reinterpret_cast<GdkEvent *>(event));
+}
+
+static inline void request_motions(GdkWindow *w, GdkEventMotion *event) {
+ gdk_window_get_device_position(w,
+ gdk_event_get_device((GdkEvent *)(event)),
+ nullptr, nullptr, nullptr);
+ gdk_event_request_motions(event);
+}
+
+void SPCanvas::set_cursor(GtkWidget *widget)
+{
+ SPCanvas *canvas = SP_CANVAS(widget);
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ GdkDisplay *display = gdk_display_get_default();
+ GdkCursor *cursor = nullptr;
+ if (canvas->_split_hover_vertical) {
+ if (canvas->_changecursor != 1) {
+ cursor = gdk_cursor_new_from_name(display, "pointer");
+ gdk_window_set_cursor(gtk_widget_get_window(widget), cursor);
+ g_object_unref(cursor);
+ canvas->paintSpliter();
+ canvas->_changecursor = 1;
+ }
+ } else if (canvas->_split_hover_horizontal) {
+ if (canvas->_changecursor != 2) {
+ cursor = gdk_cursor_new_from_name(display, "pointer");
+ gdk_window_set_cursor(gtk_widget_get_window(widget), cursor);
+ g_object_unref(cursor);
+ canvas->paintSpliter();
+ canvas->_changecursor = 2;
+ }
+ } else if (canvas->_split_hover) {
+ if (canvas->_changecursor != 3) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (_split_vertical) {
+ cursor = gdk_cursor_new_from_name(display, "ew-resize");
+ } else {
+ cursor = gdk_cursor_new_from_name(display, "ns-resize");
+ }
+ gdk_window_set_cursor(gtk_widget_get_window(widget), cursor);
+ g_object_unref(cursor);
+ canvas->paintSpliter();
+ canvas->_changecursor = 3;
+ }
+ } else {
+ if (desktop &&
+ desktop->event_context &&
+ !canvas->_split_pressed &&
+ (canvas->_changecursor != 0 && canvas->_changecursor != 4))
+ {
+ desktop->event_context->sp_event_context_update_cursor();
+ canvas->paintSpliter();
+ canvas->_changecursor = 4;
+ }
+ }
+}
+int SPCanvas::handle_motion(GtkWidget *widget, GdkEventMotion *event)
+{
+ int status;
+ SPCanvas *canvas = SP_CANVAS (widget);
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ trackLatency((GdkEvent *)event);
+
+ if (event->window != getWindow(canvas)) {
+ return FALSE;
+ }
+
+ if (canvas->_root == nullptr) // canvas being deleted
+ return FALSE;
+
+ Geom::IntPoint cursor_pos = Geom::IntPoint(event->x, event->y);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (desktop && desktop->splitMode()) {
+ if (canvas->_spliter &&
+ ((*canvas->_spliter).contains(cursor_pos) || canvas->_spliter_control.contains(cursor_pos)) &&
+ !canvas->_is_dragging) {
+ canvas->_split_hover = true;
+ } else {
+ canvas->_split_hover = false;
+ }
+ if (canvas->_spliter_left && canvas->_spliter_right &&
+ ((*canvas->_spliter_left).contains(cursor_pos) || (*canvas->_spliter_right).contains(cursor_pos)) &&
+ !canvas->_is_dragging) {
+ canvas->_split_hover_horizontal = true;
+ } else {
+ canvas->_split_hover_horizontal = false;
+ }
+ if (!canvas->_split_hover_horizontal && canvas->_spliter_top && canvas->_spliter_bottom &&
+ ((*canvas->_spliter_top).contains(cursor_pos) || (*canvas->_spliter_bottom).contains(cursor_pos)) &&
+ !canvas->_is_dragging) {
+ canvas->_split_hover_vertical = true;
+ } else {
+ canvas->_split_hover_vertical = false;
+ }
+
+ canvas->set_cursor(widget);
+ }
+ if (canvas->_split_pressed && desktop && desktop->event_context && desktop->splitMode()) {
+ GtkAllocation allocation;
+ canvas->_split_dragging = true;
+ gtk_widget_get_allocation(GTK_WIDGET(canvas), &allocation);
+ double hide_horiz = 1 / (allocation.width / (double)cursor_pos[Geom::X]);
+ double hide_vert = 1 / (allocation.height / (double)cursor_pos[Geom::Y]);
+ double value = canvas->_split_vertical ? hide_horiz : hide_vert;
+ if (hide_horiz < 0.03 || hide_horiz > 0.97 || hide_vert < 0.03 || hide_vert > 0.97) {
+ if (desktop && desktop->event_context) {
+ desktop->event_context->sp_event_context_update_cursor();
+ desktop->toggleSplitMode();
+ sp_reset_spliter(canvas);
+ }
+ } else {
+ canvas->_split_value = value;
+ if (canvas->_split_control_pressed && !canvas->_is_dragging) {
+ canvas->_spliter_control_pos = cursor_pos - canvas->_spliter_in_control_pos;
+ }
+ }
+ canvas->requestFullRedraw();
+ status = 1;
+ } else {
+ if (desktop && desktop->event_context && desktop->xrayMode()) {
+ sp_reset_spliter(canvas);
+ Geom::Point prev_orig = canvas->_xray_orig;
+ canvas->_xray_orig = desktop->point(true);
+ canvas->_xray_orig *= desktop->current_zoom();
+ if (!SP_ACTIVE_DOCUMENT->is_yaxisdown()) {
+ canvas->_xray_orig[Geom::Y] *= -1.0;
+ }
+ canvas->_xray = true;
+ if (canvas->_xray_orig[Geom::X] != Geom::infinity()) {
+ if (canvas->_xray_rect) {
+ canvas->dirtyRect(*canvas->_xray_rect);
+ canvas->_xray_rect = Geom::OptIntRect();
+ }
+ canvas->addIdle();
+ }
+ status = 1;
+ } else {
+ if (canvas->_xray_rect) {
+ canvas->dirtyRect(*canvas->_xray_rect);
+ canvas->_xray_rect = Geom::OptIntRect();
+ }
+ canvas->_xray = false;
+ }
+ canvas->_state = event->state;
+ canvas->pickCurrentItem(reinterpret_cast<GdkEvent *>(event));
+ status = canvas->emitEvent(reinterpret_cast<GdkEvent *>(event));
+ if (event->is_hint) {
+ request_motions(gtk_widget_get_window(widget), event);
+ }
+ }
+
+ if (desktop) {
+ SPCanvasArena *arena = SP_CANVAS_ARENA(desktop->drawing);
+ if (desktop->splitMode()) {
+ bool contains = canvas->_spliter_area.contains(cursor_pos);
+ bool setoutline = canvas->_split_inverse ? !contains : contains;
+ arena->drawing.setOutlineSensitive(setoutline);
+ } else if (canvas->_xray) {
+ arena->drawing.setOutlineSensitive(true);
+ } else {
+ arena->drawing.setOutlineSensitive(false);
+ }
+ }
+ return status;
+}
+
+void SPCanvas::paintSingleBuffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect, int /*sw*/)
+{
+
+ // Prevent crash if paintSingleBuffer is called before _backing_store is
+ // initialized.
+ if (_backing_store == nullptr)
+ return;
+
+ SPCanvasBuf buf;
+ buf.buf = nullptr;
+ buf.buf_rowstride = 0;
+ buf.rect = paint_rect;
+ buf.canvas_rect = canvas_rect;
+ buf.device_scale = _device_scale;
+ buf.is_empty = true;
+
+ // Make sure the following code does not go outside of _backing_store's data
+ // FIXME for device_scale.
+ assert(cairo_image_surface_get_format(_backing_store) == CAIRO_FORMAT_ARGB32);
+ assert(paint_rect.left() - _x0 >= 0);
+ assert(paint_rect.top() - _y0 >= 0);
+ assert(paint_rect.right() - _x0 <= cairo_image_surface_get_width(_backing_store));
+ assert(paint_rect.bottom() - _y0 <= cairo_image_surface_get_height(_backing_store));
+
+ // Create a temporary surface that draws directly to _backing_store
+ cairo_surface_flush(_backing_store);
+ // cairo_surface_write_to_png( _backing_store, "debug0.png" );
+ unsigned char *data = cairo_image_surface_get_data(_backing_store);
+ int stride = cairo_image_surface_get_stride(_backing_store);
+
+ // Check we are using correct device scale
+ double x_scale = 0;
+ double y_scale = 0;
+ cairo_surface_get_device_scale(_backing_store, &x_scale, &y_scale);
+ assert (_device_scale == (int)x_scale);
+ assert (_device_scale == (int)y_scale);
+
+ // Move to the right row
+ data += stride * (paint_rect.top() - _y0) * (int)y_scale;
+ // Move to the right pixel inside of that row
+ data += 4 * (paint_rect.left() - _x0) * (int)x_scale;
+ cairo_surface_t *imgs =
+ cairo_image_surface_create_for_data(data, CAIRO_FORMAT_ARGB32,
+ paint_rect.width() * _device_scale,
+ paint_rect.height() * _device_scale,
+ stride);
+ cairo_surface_set_device_scale(imgs, _device_scale, _device_scale);
+
+ buf.ct = cairo_create(imgs);
+ cairo_save(buf.ct);
+ cairo_translate(buf.ct, -paint_rect.left(), -paint_rect.top());
+ cairo_set_source(buf.ct, _background);
+ cairo_set_operator(buf.ct, CAIRO_OPERATOR_SOURCE);
+ cairo_paint(buf.ct);
+ cairo_restore(buf.ct);
+ // cairo_surface_write_to_png( imgs, "debug1.png" );
+
+ if (_root->visible) {
+ SP_CANVAS_ITEM_GET_CLASS(_root)->render(_root, &buf);
+ }
+
+ // cairo_surface_write_to_png( imgs, "debug2.png" );
+
+ // output to X
+ cairo_destroy(buf.ct);
+
+#if defined(HAVE_LIBLCMS2)
+ if (_enable_cms_display_adj) {
+ cmsHTRANSFORM transf = nullptr;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool fromDisplay = prefs->getBool( "/options/displayprofile/from_display");
+ if ( fromDisplay ) {
+ transf = Inkscape::CMSSystem::getDisplayPer(_cms_key);
+ } else {
+ transf = Inkscape::CMSSystem::getDisplayTransform();
+ }
+
+ if (transf) {
+ cairo_surface_flush(imgs);
+ unsigned char *px = cairo_image_surface_get_data(imgs);
+ int stride = cairo_image_surface_get_stride(imgs);
+ for (int i=0; i<paint_rect.height(); ++i) {
+ unsigned char *row = px + i*stride;
+ Inkscape::CMSSystem::doTransform(transf, row, row, paint_rect.width());
+ }
+ cairo_surface_mark_dirty(imgs);
+ }
+ }
+#endif // defined(HAVE_LIBLCMS2)
+
+ cairo_surface_mark_dirty(_backing_store);
+ // cairo_surface_write_to_png( _backing_store, "debug3.png" );
+
+ // Mark the painted rectangle clean
+ markRect(paint_rect, 0);
+
+ cairo_surface_destroy(imgs);
+
+ gtk_widget_queue_draw_area(GTK_WIDGET(this), paint_rect.left() -_x0, paint_rect.top() - _y0,
+ paint_rect.width(), paint_rect.height());
+}
+
+void SPCanvas::paintXRayBuffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect)
+{
+
+ // Prevent crash if paintSingleBuffer is called before _backing_store is
+ // initialized.
+ if (_backing_store == nullptr)
+ return;
+ if (!canvas_rect.contains(paint_rect) && !canvas_rect.intersects(paint_rect)) {
+ return;
+ }
+ Geom::IntRect rect_moved = Geom::IntRect::from_xywh(_x0, _y0, _x0 + paint_rect.width(), _y0 + paint_rect.height());
+ SPCanvasBuf buf;
+ buf.buf = nullptr;
+ buf.buf_rowstride = 0;
+ buf.rect = paint_rect;
+ buf.canvas_rect = canvas_rect;
+ buf.device_scale = _device_scale;
+ buf.is_empty = true;
+ // Make sure the following code does not go outside of _backing_store's data
+ // FIXME for device_scale.
+ assert(cairo_image_surface_get_format(_backing_store) == CAIRO_FORMAT_ARGB32);
+ cairo_surface_t *copy_backing = cairo_surface_create_similar_image(_backing_store, CAIRO_FORMAT_ARGB32,
+ paint_rect.width(), paint_rect.height());
+ buf.ct = cairo_create(copy_backing);
+ cairo_t *result = cairo_create(_backing_store);
+ cairo_translate(result, -_x0, -_y0);
+ cairo_save(buf.ct);
+ cairo_set_source_rgba(buf.ct, 1, 1, 1, 0);
+ cairo_fill(buf.ct);
+ cairo_arc(buf.ct, _xray_radius, _xray_radius, _xray_radius, 0, 2 * M_PI);
+ cairo_clip(buf.ct);
+ cairo_paint(buf.ct);
+ cairo_translate(buf.ct, -paint_rect.left(), -paint_rect.top());
+ cairo_set_source(buf.ct, _background);
+ cairo_set_operator(buf.ct, CAIRO_OPERATOR_SOURCE);
+ cairo_paint(buf.ct);
+ cairo_translate(buf.ct, paint_rect.left(), paint_rect.top());
+ // cairo_surface_write_to_png( copy_backing, "debug1.png" );
+
+
+
+ if (_root->visible) {
+ SP_CANVAS_ITEM_GET_CLASS(_root)->render(_root, &buf);
+ }
+ cairo_restore(buf.ct);
+ cairo_arc(buf.ct, _xray_radius, _xray_radius, _xray_radius, 0, 2 * M_PI);
+ cairo_clip(buf.ct);
+ cairo_set_operator(buf.ct, CAIRO_OPERATOR_DEST_IN);
+ cairo_paint(buf.ct);
+ // cairo_surface_write_to_png( copy_backing, "debug2.png" );
+
+ // output to X
+ cairo_arc(buf.ct, _xray_radius, _xray_radius, _xray_radius, 0, 2 * M_PI);
+
+ cairo_set_source_surface(result, copy_backing, paint_rect.left(), paint_rect.top());
+ cairo_set_operator(buf.ct, CAIRO_OPERATOR_IN);
+ cairo_paint(result);
+ cairo_surface_destroy(copy_backing);
+ cairo_destroy(buf.ct);
+ cairo_destroy(result);
+
+ // cairo_surface_write_to_png( _backing_store, "debug3.png" );
+ cairo_surface_mark_dirty(_backing_store);
+ // Mark the painted rectangle un-clean to remove old x-ray when mouse change position
+ _xray_rect = paint_rect;
+ gtk_widget_queue_draw_area(GTK_WIDGET(this), paint_rect.left() - _x0, paint_rect.top() - _y0, paint_rect.width(),
+ paint_rect.height());
+}
+
+void SPCanvas::paintSpliter()
+{
+ // Prevent crash if paintSingleBuffer is called before _backing_store is
+ // initialized.
+ if (_backing_store == nullptr)
+ return;
+ // Todo: scale for HiDPI screens
+ SPCanvas *canvas = SP_CANVAS(this);
+ double ds = canvas->_device_scale;
+ Geom::IntRect linerect = (*canvas->_spliter);
+ Geom::IntPoint c0 = Geom::IntPoint(linerect.corner(0));
+ Geom::IntPoint c1 = Geom::IntPoint(linerect.corner(1));
+ Geom::IntPoint c2 = Geom::IntPoint(linerect.corner(2));
+ Geom::IntPoint c3 = Geom::IntPoint(linerect.corner(3));
+ // We need to draw the line in middle of pixel
+ // https://developer.gnome.org/gtkmm-tutorial/stable/sec-cairo-drawing-lines.html.en:17.2.3
+ double gapx = _split_vertical ? 0.5 : 0;
+ double gapy = _split_vertical ? 0 : 0.5;
+ Geom::Point start = _split_vertical ? Geom::middle_point(c0, c1) : Geom::middle_point(c0, c3);
+ Geom::Point end = _split_vertical ? Geom::middle_point(c2, c3) : Geom::middle_point(c1, c2);
+ Geom::Point middle = Geom::middle_point(start, end);
+ if (canvas->_spliter_control_pos != Geom::Point()) {
+ middle[Geom::X] = _split_vertical ? middle[Geom::X] : canvas->_spliter_control_pos[Geom::X];
+ middle[Geom::Y] = _split_vertical ? canvas->_spliter_control_pos[Geom::Y] : middle[Geom::Y];
+ }
+ canvas->_spliter_control_pos = middle;
+ canvas->_spliter_control = Geom::OptIntRect(Geom::IntPoint(int(middle[0] - (25 * ds)), int(middle[1] - (25 * ds))),
+ Geom::IntPoint(int(middle[0] + (25 * ds)), int(middle[1] + (25 * ds))));
+ canvas->_spliter_top = Geom::OptIntRect(Geom::IntPoint(int(middle[0] - (25 * ds)), int(middle[1] - (25 * ds))),
+ Geom::IntPoint(int(middle[0] + (25 * ds)), int(middle[1] - (10 * ds))));
+ canvas->_spliter_bottom = Geom::OptIntRect(Geom::IntPoint(int(middle[0] - (25 * ds)), int(middle[1] + (25 * ds))),
+ Geom::IntPoint(int(middle[0] + (25 * ds)), int(middle[1] + (10 * ds))));
+ canvas->_spliter_left = Geom::OptIntRect(Geom::IntPoint(int(middle[0] - (25 * ds)), int(middle[1] - (10 * ds))),
+ Geom::IntPoint(int(middle[0]), int(middle[1] + (10 * ds))));
+ canvas->_spliter_right = Geom::OptIntRect(Geom::IntPoint(int(middle[0] + (25 * ds)), int(middle[1] + (10 * ds))),
+ Geom::IntPoint(int(middle[0]), int(middle[1] - (10 * ds))));
+ cairo_t *ct = cairo_create(_backing_store);
+ cairo_set_antialias(ct, CAIRO_ANTIALIAS_BEST);
+ cairo_set_line_width(ct, 1.0 * ds);
+ cairo_line_to(ct, start[0] + gapx, start[1] + gapy);
+ cairo_line_to(ct, end[0] + gapx, end[1] + gapy);
+ cairo_stroke_preserve(ct);
+ if (canvas->_split_hover || canvas->_split_pressed) {
+ cairo_set_source_rgba(ct, 0.15, 0.15, 0.15, 1);
+ } else {
+ cairo_set_source_rgba(ct, 0.3, 0.3, 0.3, 1);
+ }
+ cairo_stroke(ct);
+ /*
+ Get by: https://gitlab.com/snippets/1777221
+ M 40,19.999997 C 39.999998,8.9543032 31.045694,0 20,0 8.9543062,0 1.6568541e-6,8.9543032 0,19.999997
+ 0,31.045692 8.954305,39.999997 20,39.999997 31.045695,39.999997 40,31.045692 40,19.999997 Z M
+ 11.109859,15.230724 2.8492384,19.999997 11.109861,24.769269 Z M 29.249158,15.230724
+ 37.509779,19.999997 29.249158,24.769269 Z M 15.230728,29.03051 20,37.29113 24.769272,29.030509 Z
+ M 15.230728,10.891209 20,2.630586 24.769272,10.891209 Z */
+ double updwidth = _split_vertical ? 0 : linerect.width();
+ double updheight = _split_vertical ? linerect.height() : 0;
+ cairo_translate(ct, middle[0] - (20 * ds), middle[1] - (20 * ds));
+ cairo_scale(ct, ds, ds);
+ cairo_move_to(ct, 40, 19.999997);
+ cairo_curve_to(ct, 39.999998, 8.9543032, 31.045694, 0, 20, 0);
+ cairo_curve_to(ct, 8.9543062, 0, 0, 8.9543032, 0, 19.999997);
+ cairo_curve_to(ct, 0, 31.045692, 8.954305, 39.999997, 20, 39.999997);
+ cairo_curve_to(ct, 31.045695, 39.999997, 40, 31.045692, 40, 19.999997);
+ cairo_close_path(ct);
+ cairo_fill(ct);
+ cairo_move_to(ct, 15.230728, 10.891209);
+ cairo_line_to(ct, 20, 2.630586);
+ cairo_line_to(ct, 24.769272, 10.891209);
+ cairo_close_path(ct);
+ if (canvas->_split_hover_vertical) {
+ cairo_set_source_rgba(ct, 0.90, 0.90, 0.90, 1);
+ } else {
+ cairo_set_source_rgba(ct, 0.6, 0.6, 0.6, 1);
+ }
+ cairo_fill(ct);
+ cairo_move_to(ct, 15.230728, 29.03051);
+ cairo_line_to(ct, 20, 37.29113);
+ cairo_line_to(ct, 24.769272, 29.030509);
+ cairo_close_path(ct);
+ cairo_fill(ct);
+ cairo_move_to(ct, 11.109859, 15.230724);
+ cairo_line_to(ct, 2.8492384, 19.999997);
+ cairo_line_to(ct, 11.109861, 24.769269);
+ cairo_close_path(ct);
+ if (canvas->_split_hover_horizontal) {
+ cairo_set_source_rgba(ct, 0.90, 0.90, 0.90, 1);
+ } else {
+ cairo_set_source_rgba(ct, 0.6, 0.6, 0.6, 1);
+ }
+ cairo_fill(ct);
+ cairo_move_to(ct, 29.249158, 15.230724);
+ cairo_line_to(ct, 37.509779, 19.999997);
+ cairo_line_to(ct, 29.249158, 24.769269);
+ cairo_close_path(ct);
+ cairo_fill(ct);
+ cairo_scale(ct, 1 / ds, 1 / ds);
+ cairo_translate(ct, -middle[0] - (20 * ds), -middle[1] - (20 * ds));
+ cairo_restore(ct);
+ cairo_destroy(ct);
+ gtk_widget_queue_draw_area(GTK_WIDGET(this), start[0] - (21 * ds), start[1] - (21 * ds), updwidth + (42 * ds),
+ updheight + (42 * ds));
+}
+
+struct PaintRectSetup {
+ Geom::IntRect canvas_rect;
+ gint64 start_time;
+ int max_pixels;
+ Geom::Point mouse_loc;
+};
+
+int SPCanvas::paintRectInternal(PaintRectSetup const *setup, Geom::IntRect const &this_rect)
+{
+ gint64 now = g_get_monotonic_time();
+ gint64 elapsed = now - setup->start_time;
+
+ // Allow only very fast buffers to be run together;
+ // as soon as the total redraw time exceeds 1ms, cancel;
+ // this returns control to the idle loop and allows Inkscape to process user input
+ // (potentially interrupting the redraw); as soon as Inkscape has some more idle time,
+ if (elapsed > 1000) {
+
+ // Interrupting redraw isn't always good.
+ // For example, when you drag one node of a big path, only the buffer containing
+ // the mouse cursor will be redrawn again and again, and the rest of the path
+ // will remain stale because Inkscape never has enough idle time to redraw all
+ // of the screen. To work around this, such operations set a forced_redraw_limit > 0.
+ // If this limit is set, and if we have aborted redraw more times than is allowed,
+ // interrupting is blocked and we're forced to redraw full screen once
+ // (after which we can again interrupt forced_redraw_limit times).
+ if (_forced_redraw_limit < 0 ||
+ _forced_redraw_count < _forced_redraw_limit) {
+
+ if (_forced_redraw_limit != -1) {
+ _forced_redraw_count++;
+ }
+ return false;
+ }
+ _forced_redraw_count = 0;
+ }
+
+ // Find the optimal buffer dimensions
+ int bw = this_rect.width();
+ int bh = this_rect.height();
+ // we dont want to stop the idle process if the area is empty
+ if ((bw < 1) || (bh < 1))
+ return 1;
+
+ if (bw * bh < setup->max_pixels) {
+ // We are small enough
+ /*
+ GdkRectangle r;
+ r.x = this_rect.x0 - setup->canvas->x0;
+ r.y = this_rect.y0 - setup->canvas->y0;
+ r.width = this_rect.x1 - this_rect.x0;
+ r.height = this_rect.y1 - this_rect.y0;
+
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(setup->canvas));
+ gdk_window_begin_paint_rect(window, &r);
+ */
+
+ paintSingleBuffer(this_rect, setup->canvas_rect, bw);
+ _splits++;
+ //gdk_window_end_paint(window);
+ return 1;
+ }
+
+ Geom::IntRect lo, hi;
+
+/*
+This test determines the redraw strategy:
+
+bw < bh (strips mode) splits across the smaller dimension of the rect and therefore (on
+horizontally-stretched windows) results in redrawing in horizontal strips (from cursor point, in
+both directions if the cursor is in the middle). This is traditional for Inkscape since old days,
+and seems to be faster for drawings with many smaller objects at zoom-out.
+
+bw > bh (chunks mode) splits across the larger dimension of the rect and therefore paints in
+almost-square chunks, again from the cursor point. It's sometimes faster for drawings with few slow
+(e.g. blurred) objects crossing the entire screen. It also appears to be somewhat psychologically
+faster.
+
+The default for now is the strips mode.
+*/
+ if (bw < bh || bh < 2 * TILE_SIZE) {
+ int mid = this_rect[Geom::X].middle();
+ // Make sure that mid lies on a tile boundary
+ mid = (mid / TILE_SIZE) * TILE_SIZE;
+
+ lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom());
+ hi = Geom::IntRect(mid, this_rect.top(), this_rect.right(), this_rect.bottom());
+
+ if (setup->mouse_loc[Geom::X] < mid) {
+ // Always paint towards the mouse first
+ return paintRectInternal(setup, lo)
+ && paintRectInternal(setup, hi);
+ } else {
+ return paintRectInternal(setup, hi)
+ && paintRectInternal(setup, lo);
+ }
+ } else {
+ int mid = this_rect[Geom::Y].middle();
+ // Make sure that mid lies on a tile boundary
+ mid = (mid / TILE_SIZE) * TILE_SIZE;
+
+ lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid);
+ hi = Geom::IntRect(this_rect.left(), mid, this_rect.right(), this_rect.bottom());
+
+ if (setup->mouse_loc[Geom::Y] < mid) {
+ // Always paint towards the mouse first
+ return paintRectInternal(setup, lo)
+ && paintRectInternal(setup, hi);
+ } else {
+ return paintRectInternal(setup, hi)
+ && paintRectInternal(setup, lo);
+ }
+ }
+}
+
+
+bool SPCanvas::paintRect(int xx0, int yy0, int xx1, int yy1)
+{
+ GtkAllocation allocation;
+ g_return_val_if_fail (!_need_update, false);
+
+ gtk_widget_get_allocation(GTK_WIDGET(this), &allocation);
+
+ // Find window rectangle in 'world coordinates'.
+ Geom::IntRect canvas_rect = Geom::IntRect::from_xywh(_x0, _y0,
+ allocation.width, allocation.height);
+ Geom::IntRect paint_rect(xx0, yy0, xx1, yy1);
+
+ Geom::OptIntRect area = paint_rect & canvas_rect;
+ // we dont want to stop the idle process if the area is empty
+ if (!area || area->hasZeroArea()) {
+ return true;
+ }
+ paint_rect = *area;
+
+ PaintRectSetup setup;
+ setup.canvas_rect = canvas_rect;
+
+ // Save the mouse location
+ gint x, y;
+
+ auto const display = Gdk::Display::get_default();
+ auto const seat = display->get_default_seat();
+ auto const device = seat->get_pointer();
+
+ gdk_window_get_device_position(gtk_widget_get_window(GTK_WIDGET(this)),
+ device->gobj(),
+ &x, &y, nullptr);
+
+ setup.mouse_loc = sp_canvas_window_to_world(this, Geom::Point(x,y));
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ unsigned tile_multiplier = prefs->getIntLimited("/options/rendering/tile-multiplier", 16, 1, 512);
+
+ if (_rendermode != Inkscape::RENDERMODE_OUTLINE) {
+ // use 256K as a compromise to not slow down gradients
+ // 256K is the cached buffer and we need 4 channels
+ setup.max_pixels = 65536 * tile_multiplier; // 256K/4
+ } else {
+ // paths only, so 1M works faster
+ // 1M is the cached buffer and we need 4 channels
+ setup.max_pixels = 262144;
+ }
+
+ // Start the clock
+ setup.start_time = g_get_monotonic_time();
+ // Go
+ return paintRectInternal(&setup, paint_rect);
+}
+
+void SPCanvas::forceFullRedrawAfterInterruptions(unsigned int count, bool reset)
+{
+ _forced_redraw_limit = count;
+ if (reset) {
+ _forced_redraw_count = 0;
+ }
+}
+
+void SPCanvas::endForcedFullRedraws()
+{
+ _forced_redraw_limit = -1;
+}
+
+gboolean SPCanvas::handle_draw(GtkWidget *widget, cairo_t *cr) {
+
+ SPCanvas *canvas = SP_CANVAS(widget);
+
+ if (canvas->_surface_for_similar == nullptr && canvas->_backing_store != nullptr) {
+
+ // Device scale is copied but since this is only created one time, we'll
+ // need to check/set device scale anytime it is used in case window moved
+ // to monitor with different scale.
+ canvas->_surface_for_similar =
+ cairo_surface_create_similar(cairo_get_target(cr), CAIRO_CONTENT_COLOR_ALPHA, 1, 1);
+
+ // Check we are using correct device scale
+ double x_scale = 0;
+ double y_scale = 0;
+ cairo_surface_get_device_scale(canvas->_backing_store, &x_scale, &y_scale);
+ assert (canvas->_device_scale == (int)x_scale);
+ assert (canvas->_device_scale == (int)y_scale);
+
+ // Reallocate backing store so that cairo can use shared memory
+ // Function does NOT copy device scale! Width and height are in device pixels.
+ cairo_surface_t *new_backing_store = cairo_surface_create_similar_image(
+ canvas->_surface_for_similar, CAIRO_FORMAT_ARGB32,
+ cairo_image_surface_get_width(canvas->_backing_store),
+ cairo_image_surface_get_height(canvas->_backing_store));
+
+ cairo_surface_set_device_scale(new_backing_store, canvas->_device_scale, canvas->_device_scale);
+
+ // Copy the old backing store contents
+ cairo_t *cr = cairo_create(new_backing_store);
+ cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
+ cairo_set_source_surface(cr, canvas->_backing_store, 0, 0);
+ cairo_paint(cr);
+ cairo_destroy(cr);
+ cairo_surface_destroy(canvas->_backing_store);
+ canvas->_backing_store = new_backing_store;
+ }
+
+ // Blit from the backing store, without regard for the clean region.
+ // This is necessary because GTK clears the widget for us, which causes
+ // severe flicker while drawing if we don't blit the old contents.
+ cairo_set_source_surface(cr, canvas->_backing_store, 0, 0);
+ cairo_paint(cr);
+
+ cairo_rectangle_list_t *rects = cairo_copy_clip_rectangle_list(cr);
+ cairo_region_t *dirty_region = cairo_region_create();
+
+ for (int i = 0; i < rects->num_rectangles; i++) {
+ cairo_rectangle_t rectangle = rects->rectangles[i];
+ Geom::Rect dr = Geom::Rect::from_xywh(rectangle.x + canvas->_x0, rectangle.y + canvas->_y0,
+ rectangle.width, rectangle.height);
+
+ Geom::IntRect ir = dr.roundOutwards();
+ cairo_rectangle_int_t irect = { ir.left(), ir.top(), ir.width(), ir.height() };
+ cairo_region_union_rectangle(dirty_region, &irect);
+ }
+ cairo_rectangle_list_destroy(rects);
+ cairo_region_subtract(dirty_region, canvas->_clean_region);
+
+ // Render the dirty portion in the background
+ if (!cairo_region_is_empty(dirty_region)) {
+ canvas->addIdle();
+ }
+ cairo_region_destroy(dirty_region);
+
+ return TRUE;
+}
+
+gint SPCanvas::handle_key_event(GtkWidget *widget, GdkEventKey *event)
+{
+ return SP_CANVAS(widget)->emitEvent(reinterpret_cast<GdkEvent *>(event));
+}
+
+gint SPCanvas::handle_crossing(GtkWidget *widget, GdkEventCrossing *event)
+{
+ SPCanvas *canvas = SP_CANVAS (widget);
+
+ if (event->window != getWindow(canvas)) {
+ return FALSE;
+ }
+ canvas->_state = event->state;
+ return canvas->pickCurrentItem(reinterpret_cast<GdkEvent *>(event));
+}
+
+gint SPCanvas::handle_focus_in(GtkWidget *widget, GdkEventFocus *event)
+{
+ gtk_widget_grab_focus (widget);
+
+ SPCanvas *canvas = SP_CANVAS (widget);
+
+ if (canvas->_focused_item) {
+ return canvas->emitEvent(reinterpret_cast<GdkEvent *>(event));
+ } else {
+ return FALSE;
+ }
+}
+
+gint SPCanvas::handle_focus_out(GtkWidget *widget, GdkEventFocus *event)
+{
+ SPCanvas *canvas = SP_CANVAS(widget);
+
+ if (canvas->_focused_item) {
+ return canvas->emitEvent(reinterpret_cast<GdkEvent *>(event));
+ } else {
+ return FALSE;
+ }
+}
+
+int SPCanvas::paint()
+{
+ if (_need_update) {
+ sp_canvas_item_invoke_update(_root, Geom::identity(), 0);
+ _need_update = FALSE;
+ }
+ SPCanvas *canvas = SP_CANVAS(this);
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(GTK_WIDGET(canvas), &allocation);
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ SPCanvasArena *arena = nullptr;
+ bool split = false;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _xray_radius = prefs->getIntLimited("/options/rendering/xray-radius", 100, 1, 1500);
+
+ double split_x = 1;
+ double split_y = 1;
+ Inkscape::RenderMode rm = Inkscape::RENDERMODE_NORMAL;
+ if (desktop) {
+ split = desktop->splitMode();
+ arena = SP_CANVAS_ARENA(desktop->drawing);
+ rm = arena->drawing.renderMode();
+ if (split) {
+ auto window = desktop->getInkscapeWindow();
+ auto dtw = window->get_desktop_widget();
+ bool hasrullers = prefs->getBool(desktop->is_fullscreen() ? "/fullscreen/rulers/state" : "/window/rulers/state");
+ int hruler_gap = hasrullers ? dtw->get_hruler_thickness() : 1;
+ int vruler_gap = hasrullers ? dtw->get_vruler_thickness() : 1;
+
+ split_x = !_split_vertical ? 0 : _split_value;
+ split_y = _split_vertical ? 0 : _split_value;
+
+ Geom::IntCoord coord1x =
+ allocation.x + (int((allocation.width) * split_x)) - (3 * canvas->_device_scale) - vruler_gap;
+ Geom::IntCoord coord1y =
+ allocation.y + (int((allocation.height) * split_y)) - (3 * canvas->_device_scale) - hruler_gap;
+ split_x = !_split_vertical ? 1 : _split_value;
+ split_y = _split_vertical ? 1 : _split_value;
+ Geom::IntCoord coord2x =
+ allocation.x + (int((allocation.width) * split_x)) + (3 * canvas->_device_scale) - vruler_gap;
+ Geom::IntCoord coord2y =
+ allocation.y + (int((allocation.height) * split_y)) + (3 * canvas->_device_scale) - hruler_gap;
+ _spliter = Geom::OptIntRect(coord1x, coord1y, coord2x, coord2y);
+ split_x = !_split_vertical ? 0 : _split_value;
+ split_y = _split_vertical ? 0 : _split_value;
+ coord1x = allocation.x + (int((allocation.width ) * split_x)) - vruler_gap;
+ coord1y = allocation.y + (int((allocation.height) * split_y)) - hruler_gap;
+ split_x = !_split_vertical ? 1 : _split_value;
+ split_y = _split_vertical ? 1 : _split_value;
+ coord2x = allocation.x + allocation.width;
+ coord2y = allocation.y + allocation.height;
+ _spliter_area = Geom::OptIntRect(coord1x, coord1y, coord2x, coord2y);
+ } else {
+ sp_reset_spliter(canvas);
+ }
+ } else {
+ sp_reset_spliter(canvas);
+ }
+ cairo_rectangle_int_t crect = { _x0, _y0, int(allocation.width * split_x), int(allocation.height * split_y) };
+ split_x = !_split_vertical ? 0 : _split_value;
+ split_y = _split_vertical ? 0 : _split_value;
+ cairo_rectangle_int_t crect_outline = { _x0 + int(allocation.width * split_x),
+ _y0 + int(allocation.height * split_y),
+ int(allocation.width * (1 - split_x)),
+ int(allocation.height * (1 - split_y)) };
+ cairo_region_t *draw = nullptr;
+ cairo_region_t *to_draw = nullptr;
+ cairo_region_t *to_draw_outline = nullptr;
+ if (_split_inverse && split) {
+ to_draw = cairo_region_create_rectangle(&crect_outline);
+ to_draw_outline = cairo_region_create_rectangle(&crect);
+ } else {
+ to_draw = cairo_region_create_rectangle(&crect);
+ to_draw_outline = cairo_region_create_rectangle(&crect_outline);
+ }
+
+ cairo_region_get_extents(_clean_region, &crect);
+ draw = cairo_region_create_rectangle(&crect);
+ cairo_region_subtract(draw, _clean_region);
+ cairo_region_get_extents(draw, &crect);
+ cairo_region_subtract_rectangle(_clean_region, &crect);
+ cairo_region_subtract(to_draw, _clean_region);
+ cairo_region_subtract(to_draw_outline, _clean_region);
+ cairo_region_destroy(draw);
+ int n_rects = cairo_region_num_rectangles(to_draw);
+ for (int i = 0; i < n_rects; ++i) {
+ cairo_rectangle_int_t crect;
+ cairo_region_get_rectangle(to_draw, i, &crect);
+ if (!paintRect(crect.x, crect.y, crect.x + crect.width, crect.y + crect.height)) {
+ // Aborted
+ cairo_region_destroy(to_draw);
+ cairo_region_destroy(to_draw_outline);
+ return FALSE;
+ };
+ }
+
+ if (split) {
+ arena->drawing.setRenderMode(Inkscape::RENDERMODE_OUTLINE);
+ bool exact = arena->drawing.getExact();
+ arena->drawing.setExact(false);
+ int n_rects = cairo_region_num_rectangles(to_draw_outline);
+ for (int i = 0; i < n_rects; ++i) {
+ cairo_rectangle_int_t crect;
+ cairo_region_get_rectangle(to_draw_outline, i, &crect);
+ if (!paintRect(crect.x, crect.y, crect.x + crect.width, crect.y + crect.height)) {
+ // Aborted
+ arena->drawing.setExact(exact);
+ arena->drawing.setRenderMode(rm);
+ cairo_region_destroy(to_draw);
+ cairo_region_destroy(to_draw_outline);
+ return FALSE;
+ };
+ }
+ arena->drawing.setExact(exact);
+ arena->drawing.setRenderMode(rm);
+ canvas->paintSpliter();
+ } else if (desktop && _xray) {
+ if (rm != Inkscape::RENDERMODE_OUTLINE) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/desktop/xrayactive", true);
+ arena->drawing.setRenderMode(Inkscape::RENDERMODE_OUTLINE);
+ bool exact = arena->drawing.getExact();
+ arena->drawing.setExact(false);
+ Geom::IntRect canvas_rect = Geom::IntRect::from_xywh(_x0, _y0, allocation.width, allocation.height);
+ Geom::IntRect _xray_rect = Geom::IntRect::from_xywh(
+ _xray_orig[0] - _xray_radius, _xray_orig[1] - _xray_radius, (_xray_radius * 2), (_xray_radius * 2));
+ paintXRayBuffer(_xray_rect, canvas_rect);
+ arena->drawing.setExact(exact);
+ arena->drawing.setRenderMode(rm);
+ prefs->setBool("/desktop/xrayactive", false);
+ }
+ }
+
+ // we've had a full unaborted redraw, reset the full redraw counter
+ if (_forced_redraw_limit != -1) {
+ _forced_redraw_count = 0;
+ }
+
+ cairo_region_destroy(to_draw);
+ cairo_region_destroy(to_draw_outline);
+
+ return TRUE;
+}
+
+int SPCanvas::doUpdate()
+{
+ if (!_root) { // canvas may have already be destroyed by closing desktop during interrupted display!
+ return TRUE;
+ }
+ if (_drawing_disabled) {
+ return TRUE;
+ }
+
+ // Cause the update if necessary
+ if (_need_update) {
+ sp_canvas_item_invoke_update(_root, Geom::identity(), 0);
+ _need_update = FALSE;
+ }
+
+ // Paint if able to
+ if (gtk_widget_is_drawable(GTK_WIDGET(this))) {
+ return paint();
+ }
+
+ // Pick new current item
+ while (_need_repick) {
+ _need_repick = FALSE;
+ pickCurrentItem(&_pick_event);
+ }
+
+ return TRUE;
+}
+
+gint SPCanvas::idle_handler(gpointer data)
+{
+ SPCanvas *canvas = SP_CANVAS (data);
+#ifdef DEBUG_PERFORMANCE
+ static int totaloops = 1;
+ gint64 now = 0;
+ gint64 elapsed = 0;
+ now = g_get_monotonic_time();
+ elapsed = now - canvas->_idle_time;
+ g_message("[%i] start loop %i in split %i at %f", canvas->_idle_id, totaloops, canvas->_splits,
+ canvas->_totalelapsed / (double)1000000 + elapsed / (double)1000000);
+#endif
+ int ret = canvas->doUpdate();
+ int n_rects = cairo_region_num_rectangles(canvas->_clean_region);
+ if (n_rects > 1) { // not fully painted, maybe clean region is updated in middle of idle, reload again
+ ret = 0;
+ }
+
+#ifdef DEBUG_PERFORMANCE
+ if (ret == 0) {
+ now = g_get_monotonic_time();
+ elapsed = now - canvas->_idle_time;
+ g_message("[%i] loop ended unclean at %f", canvas->_idle_id,
+ canvas->_totalelapsed / (double)1000000 + elapsed / (double)1000000);
+ totaloops += 1;
+ }
+ if (ret) {
+ // Reset idle id
+ now = g_get_monotonic_time();
+ elapsed = now - canvas->_idle_time;
+ canvas->_totalelapsed += elapsed;
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (desktop) {
+ SPCanvasArena *arena = SP_CANVAS_ARENA(desktop->drawing);
+ Inkscape::RenderMode rm = arena->drawing.renderMode();
+ if (rm == Inkscape::RENDERMODE_OUTLINE) {
+ canvas->_totalelapsed = 0;
+ g_message("Outline mode, we reset and stop total counter");
+ }
+ g_message("[%i] finished", canvas->_idle_id);
+ g_message("[%i] loops %i", canvas->_idle_id, totaloops);
+ g_message("[%i] splits %i", canvas->_idle_id, canvas->_splits);
+ g_message("[%i] duration %f", canvas->_idle_id, elapsed / (double)1000000);
+ g_message("[%i] total %f (toggle outline mode to reset)", canvas->_idle_id,
+ canvas->_totalelapsed / (double)1000000);
+ g_message("[%i] :::::::::::::::::::::::::::::::::::::::", canvas->_idle_id);
+ }
+ canvas->_idle_id = 0;
+ totaloops = 1;
+ canvas->_splits = 0;
+ }
+#else
+ if (ret) {
+ // Reset idle id
+ canvas->_idle_id = 0;
+ }
+#endif
+ return !ret;
+}
+
+void SPCanvas::addIdle()
+{
+ if (_idle_id == 0) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ guint redrawPriority = prefs->getIntLimited("/options/redrawpriority/value", G_PRIORITY_HIGH_IDLE, G_PRIORITY_HIGH_IDLE, G_PRIORITY_DEFAULT_IDLE);
+ SPCanvas *canvas = SP_CANVAS(this);
+ if (canvas->_in_full_redraw) {
+ canvas->_in_full_redraw = false;
+ redrawPriority = G_PRIORITY_DEFAULT_IDLE;
+ }
+#ifdef DEBUG_PERFORMANCE
+ _idle_time = g_get_monotonic_time();
+#endif
+ _idle_id = gdk_threads_add_idle_full(redrawPriority, idle_handler, this, nullptr);
+#ifdef DEBUG_PERFORMANCE
+ g_message("[%i] launched %f", _idle_id, _totalelapsed / (double)1000000);
+#endif
+ }
+}
+void SPCanvas::removeIdle()
+{
+ if (_idle_id) {
+ g_source_remove(_idle_id);
+#ifdef DEBUG_PERFORMANCE
+ g_message("[%i] aborted in split %i", _idle_id, _splits);
+ _splits = 0;
+#endif
+ _idle_id = 0;
+ }
+}
+
+SPCanvasGroup *SPCanvas::getRoot()
+{
+ return SP_CANVAS_GROUP(_root);
+}
+
+/**
+ * Scroll screen to point 'c'. 'c' is measured in screen pixels.
+ */
+void SPCanvas::scrollTo( Geom::Point const &c, unsigned int clear, bool is_scrolling)
+{
+ // To do: extract out common code with SPCanvas::handle_size_allocate()
+
+ // For HiDPI monitors
+ int device_scale = gtk_widget_get_scale_factor(GTK_WIDGET(this));
+ assert( device_scale == _device_scale);
+
+ double cx = c[Geom::X];
+ double cy = c[Geom::Y];
+
+ int ix = (int) round(cx); // ix and iy are the new canvas coordinates (integer screen pixels)
+ int iy = (int) round(cy); // cx might be negative, so (int)(cx + 0.5) will not do!
+ int dx = ix - _x0; // dx and dy specify the displacement (scroll) of the
+ int dy = iy - _y0; // canvas w.r.t its previous position
+
+ Geom::IntRect old_area = getViewboxIntegers();
+ Geom::IntRect new_area = old_area + Geom::IntPoint(dx, dy);
+ bool outsidescrool = false;
+ if (!new_area.intersects(old_area)) {
+ outsidescrool = true;
+ }
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(&_widget, &allocation);
+
+ // cairo_surface_write_to_png( _backing_store, "scroll1.png" );
+ bool split = false;
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (desktop && desktop->splitMode()) {
+ split = true;
+ }
+ if (clear || split || _xray || outsidescrool) {
+ _dx0 = cx; // here the 'd' stands for double, not delta!
+ _dy0 = cy;
+ _x0 = ix;
+ _y0 = iy;
+ requestFullRedraw();
+ } else {
+ // Adjust backing store contents
+ assert(_backing_store);
+ // this cairo operation is slow, improvements welcome
+ cairo_surface_t *new_backing_store = nullptr;
+ if (_surface_for_similar != nullptr)
+
+ // Size in device pixels. Does not set device scale.
+ new_backing_store =
+ cairo_surface_create_similar_image(_surface_for_similar,
+ CAIRO_FORMAT_ARGB32,
+ allocation.width * _device_scale,
+ allocation.height * _device_scale);
+ if (new_backing_store == nullptr)
+ // Size in device pixels. Does not set device scale.
+ new_backing_store =
+ cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
+ allocation.width * _device_scale,
+ allocation.height * _device_scale);
+
+ // Set device scale
+ cairo_surface_set_device_scale(new_backing_store, _device_scale, _device_scale);
+
+ cairo_t *cr = cairo_create(new_backing_store);
+ cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
+ // Paint the background
+ cairo_translate(cr, -ix, -iy);
+ cairo_set_source_surface(cr, _backing_store, ix, iy);
+ cairo_paint(cr);
+
+ // cairo_surface_write_to_png( _backing_store, "scroll0.png" );
+
+ // Copy the old backing store contents
+ cairo_set_source_surface(cr, _backing_store, _x0, _y0);
+ cairo_rectangle(cr, _x0, _y0, allocation.width, allocation.height);
+ cairo_clip(cr);
+ cairo_paint(cr);
+ cairo_destroy(cr);
+ cairo_surface_destroy(_backing_store);
+ _backing_store = new_backing_store;
+ _dx0 = cx; // here the 'd' stands for double, not delta!
+ _dy0 = cy;
+ _x0 = ix;
+ _y0 = iy;
+ cairo_rectangle_int_t crect = { _x0, _y0, allocation.width, allocation.height };
+ cairo_region_intersect_rectangle(_clean_region, &crect);
+ }
+
+ SPCanvasArena *arena = SP_CANVAS_ARENA(desktop->drawing);
+ if (arena) {
+ Geom::IntRect expanded = new_area;
+ Geom::IntPoint expansion(new_area.width()/2, new_area.height()/2);
+ expanded.expandBy(expansion);
+ arena->drawing.setCacheLimit(expanded, false);
+ }
+
+ if (!clear) {
+ // scrolling without zoom; redraw only the newly exposed areas
+ if ((dx != 0) || (dy != 0)) {
+ if (gtk_widget_get_realized(GTK_WIDGET(this))) {
+ SPCanvas *canvas = SP_CANVAS(this);
+ if (split) {
+ double scroll_horiz = 1 / (allocation.width / (double)-dx);
+ double scroll_vert = 1 / (allocation.height / (double)-dy);
+ double gap = canvas->_split_vertical ? scroll_horiz : scroll_vert;
+ canvas->_split_value = canvas->_split_value + gap;
+ if (scroll_horiz < 0.03 || scroll_horiz > 0.97 || scroll_vert < 0.03 || scroll_vert > 0.97) {
+ if (canvas->_split_value > 0.97) {
+ canvas->_split_value = 0.97;
+ } else if (canvas->_split_value < 0.03) {
+ canvas->_split_value = 0.03;
+ }
+ }
+ }
+ gdk_window_scroll(getWindow(this), -dx, -dy);
+ }
+ }
+ }
+}
+
+void SPCanvas::updateNow()
+{
+ if (_need_update) {
+#ifdef DEBUG_PERFORMANCE
+ guint64 now = g_get_monotonic_time();
+ gint64 elapsed = now - _idle_time;
+ g_message("[%i] start updateNow(): %f at %f", _idle_id, elapsed / (double)1000000,
+ _totalelapsed / (double)1000000 + elapsed / (double)1000000);
+#endif
+ doUpdate();
+#ifdef DEBUG_PERFORMANCE
+ now = g_get_monotonic_time();
+ elapsed = now - _idle_time;
+ g_message("[%i] end updateNow(): %f at %f", _idle_id, elapsed / (double)1000000,
+ _totalelapsed / (double)1000000 + elapsed / (double)1000000);
+#endif
+ }
+}
+
+void SPCanvas::requestUpdate()
+{
+ _need_update = TRUE;
+ addIdle();
+}
+
+void SPCanvas::requestRedraw(int x0, int y0, int x1, int y1)
+{
+ if (!gtk_widget_is_drawable( GTK_WIDGET(this) )) {
+ return;
+ }
+ if (x0 >= x1 || y0 >= y1) {
+ return;
+ }
+
+ Geom::IntRect bbox(x0, y0, x1, y1);
+ dirtyRect(bbox);
+ addIdle();
+}
+void SPCanvas::requestFullRedraw()
+{
+ SPCanvas *canvas = SP_CANVAS(this);
+ canvas->_in_full_redraw = true;
+ dirtyAll();
+ addIdle();
+}
+
+void SPCanvas::setBackgroundColor(guint32 rgba) {
+ double new_r = SP_RGBA32_R_F(rgba);
+ double new_g = SP_RGBA32_G_F(rgba);
+ double new_b = SP_RGBA32_B_F(rgba);
+ if (!_background_is_checkerboard) {
+ double old_r, old_g, old_b;
+ cairo_pattern_get_rgba(_background, &old_r, &old_g, &old_b, nullptr);
+ if (new_r == old_r && new_g == old_g && new_b == old_b) return;
+ }
+ if (_background) {
+ cairo_pattern_destroy(_background);
+ }
+ _background = cairo_pattern_create_rgb(new_r, new_g, new_b);
+ _background_is_checkerboard = false;
+ requestFullRedraw();
+}
+
+void SPCanvas::setBackgroundCheckerboard(guint32 rgba)
+{
+ if (_background_is_checkerboard) return;
+ if (_background) {
+ cairo_pattern_destroy(_background);
+ }
+ _background = ink_cairo_pattern_create_checkerboard(rgba);
+ _background_is_checkerboard = true;
+ requestFullRedraw();
+}
+
+/**
+ * Sets world coordinates from win and canvas.
+ */
+void sp_canvas_window_to_world(SPCanvas const *canvas, double winx, double winy, double *worldx, double *worldy)
+{
+ g_return_if_fail (canvas != nullptr);
+ g_return_if_fail (SP_IS_CANVAS (canvas));
+
+ if (worldx) *worldx = canvas->_x0 + winx;
+ if (worldy) *worldy = canvas->_y0 + winy;
+}
+
+/**
+ * Sets win coordinates from world and canvas.
+ */
+void sp_canvas_world_to_window(SPCanvas const *canvas, double worldx, double worldy, double *winx, double *winy)
+{
+ g_return_if_fail (canvas != nullptr);
+ g_return_if_fail (SP_IS_CANVAS (canvas));
+
+ if (winx) *winx = worldx - canvas->_x0;
+ if (winy) *winy = worldy - canvas->_y0;
+}
+
+/**
+ * Converts point from win to world coordinates.
+ */
+Geom::Point sp_canvas_window_to_world(SPCanvas const *canvas, Geom::Point const win)
+{
+ g_assert (canvas != nullptr);
+ g_assert (SP_IS_CANVAS (canvas));
+
+ return Geom::Point(canvas->_x0 + win[0], canvas->_y0 + win[1]);
+}
+
+/**
+ * Converts point from world to win coordinates.
+ */
+Geom::Point sp_canvas_world_to_window(SPCanvas const *canvas, Geom::Point const world)
+{
+ g_assert (canvas != nullptr);
+ g_assert (SP_IS_CANVAS (canvas));
+
+ return Geom::Point(world[0] - canvas->_x0, world[1] - canvas->_y0);
+}
+
+/**
+ * Returns true if point given in world coordinates is inside window.
+ */
+bool sp_canvas_world_pt_inside_window(SPCanvas const *canvas, Geom::Point const &world)
+{
+ GtkAllocation allocation;
+
+ g_assert( canvas != nullptr );
+ g_assert(SP_IS_CANVAS(canvas));
+
+ GtkWidget *w = GTK_WIDGET(canvas);
+ gtk_widget_get_allocation (w, &allocation);
+
+ return ( ( canvas->_x0 <= world[Geom::X] ) &&
+ ( canvas->_y0 <= world[Geom::Y] ) &&
+ ( world[Geom::X] < canvas->_x0 + allocation.width ) &&
+ ( world[Geom::Y] < canvas->_y0 + allocation.height ) );
+}
+
+/**
+ * Return canvas window coordinates as Geom::Rect.
+ */
+Geom::Rect SPCanvas::getViewbox() const
+{
+ GtkAllocation allocation;
+
+ gtk_widget_get_allocation (GTK_WIDGET (this), &allocation);
+ return Geom::Rect::from_xywh(_dx0, _dy0, allocation.width, allocation.height);
+}
+
+/**
+ * Return canvas window coordinates as integer rectangle.
+ */
+Geom::IntRect SPCanvas::getViewboxIntegers() const
+{
+ GtkAllocation allocation;
+
+ gtk_widget_get_allocation (GTK_WIDGET(this), &allocation);
+ return Geom::IntRect::from_xywh(_x0, _y0, allocation.width, allocation.height);
+}
+
+inline int sp_canvas_tile_floor(int x)
+{
+ return (x & (~(TILE_SIZE - 1))) / TILE_SIZE;
+}
+
+inline int sp_canvas_tile_ceil(int x)
+{
+ return ((x + (TILE_SIZE - 1)) & (~(TILE_SIZE - 1))) / TILE_SIZE;
+}
+
+void SPCanvas::dirtyRect(Geom::IntRect const &area) {
+ markRect(area, 1);
+}
+
+void SPCanvas::dirtyAll() {
+ if (_clean_region && !cairo_region_is_empty(_clean_region)) {
+ cairo_region_destroy(_clean_region);
+ _clean_region = cairo_region_create();
+ }
+}
+
+void SPCanvas::markRect(Geom::IntRect const &area, uint8_t val)
+{
+ cairo_rectangle_int_t crect = { area.left(), area.top(), area.width(), area.height() };
+ if (val) {
+ cairo_region_subtract_rectangle(_clean_region, &crect);
+ } else {
+ cairo_region_union_rectangle(_clean_region, &crect);
+ }
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :