summaryrefslogtreecommitdiffstats
path: root/src/display/control
diff options
context:
space:
mode:
Diffstat (limited to 'src/display/control')
-rw-r--r--src/display/control/README112
-rw-r--r--src/display/control/canvas-axonomgrid.cpp770
-rw-r--r--src/display/control/canvas-axonomgrid.h99
-rw-r--r--src/display/control/canvas-grid.cpp1120
-rw-r--r--src/display/control/canvas-grid.h198
-rw-r--r--src/display/control/canvas-item-bpath.cpp262
-rw-r--r--src/display/control/canvas-item-bpath.h83
-rw-r--r--src/display/control/canvas-item-buffer.h50
-rw-r--r--src/display/control/canvas-item-catchall.cpp81
-rw-r--r--src/display/control/canvas-item-catchall.h60
-rw-r--r--src/display/control/canvas-item-ctrl.cpp1221
-rw-r--r--src/display/control/canvas-item-ctrl.h107
-rw-r--r--src/display/control/canvas-item-curve.cpp236
-rw-r--r--src/display/control/canvas-item-curve.h87
-rw-r--r--src/display/control/canvas-item-drawing.cpp296
-rw-r--r--src/display/control/canvas-item-drawing.h108
-rw-r--r--src/display/control/canvas-item-enums.h87
-rw-r--r--src/display/control/canvas-item-grid.cpp122
-rw-r--r--src/display/control/canvas-item-grid.h65
-rw-r--r--src/display/control/canvas-item-group.cpp158
-rw-r--r--src/display/control/canvas-item-group.h72
-rw-r--r--src/display/control/canvas-item-guideline.cpp343
-rw-r--r--src/display/control/canvas-item-guideline.h109
-rw-r--r--src/display/control/canvas-item-quad.cpp207
-rw-r--r--src/display/control/canvas-item-quad.h69
-rw-r--r--src/display/control/canvas-item-rect.cpp353
-rw-r--r--src/display/control/canvas-item-rect.h81
-rw-r--r--src/display/control/canvas-item-rotate.cpp212
-rw-r--r--src/display/control/canvas-item-rotate.h81
-rw-r--r--src/display/control/canvas-item-text.cpp317
-rw-r--r--src/display/control/canvas-item-text.h92
-rw-r--r--src/display/control/canvas-item.cpp261
-rw-r--r--src/display/control/canvas-item.h175
-rw-r--r--src/display/control/canvas-page.cpp176
-rw-r--r--src/display/control/canvas-page.h72
-rw-r--r--src/display/control/canvas-temporary-item-list.cpp88
-rw-r--r--src/display/control/canvas-temporary-item-list.h65
-rw-r--r--src/display/control/canvas-temporary-item.cpp81
-rw-r--r--src/display/control/canvas-temporary-item.h60
-rw-r--r--src/display/control/snap-indicator.cpp640
-rw-r--r--src/display/control/snap-indicator.h85
41 files changed, 8961 insertions, 0 deletions
diff --git a/src/display/control/README b/src/display/control/README
new file mode 100644
index 0000000..680f0bf
--- /dev/null
+++ b/src/display/control/README
@@ -0,0 +1,112 @@
+
+This directory contains code for handling on-canvas editing objects as
+well as document display.
+
+Historically, the code originates from GnomeCanvas but at this point it
+has been completely rewritten for Inkscape's needs.
+
+One can think of a CanvasItem as a light-weight widget. There is a
+selection mechanism based on an item's position and a method to handle
+events. The selection mechanism is currently very simplistic,
+selecting the top-most item whose bounding box contains the
+cursor. Probably as a result, many items do not use these capabilities
+with external code replacing this functionality (e.g CanvasItemQuad,
+CanvasItemCurve).
+
+Points are stored as document coordinates. They are converted to
+screen coordinates by multiplying by the document to window affine.
+When an object's geometry changes, it must call request_update() to
+flag that its bounds need to be recalculated (_need_update =
+true). This will also cause all ancestors to also be marked and
+finally the Canvas. Before picking or drawing, all bounds must be
+up-to-date. (Changing the Canvas affine will also require bounds to be
+recalculated.) This mechanism ensures that bounds are correct in the
+most efficient manner. If only the style is changed (without geometric
+ramifications) then call canvas->redraw_area() to trigger a redraw.
+
+CanvasItemGroup keeps a list of child items using a Boost intrusive
+list. The pointers between nodes are kept inside the items. This
+allows for quick deletion by avoiding the need to search through the
+list to find the item to delete. This is important when a path
+contains hundress of nodes. However, a Boost intrusive list cannot be
+used with C++ smart pointers. Deleting an item can be done by either
+calling CanvasItemGroup::remove(CanvasItem*) or by directly deleting
+the item. Deleting a CanvasItemGroup will delete it and all of its
+children.
+
+
+Contents (x == pickable):
+
+* CanvasItem: Abstract base class.
+* CanvasItemBPath: x An item representing a Bezier path.
+* CanvasItemCatchall: x An infinite item, for capturing left-over events.
+* CanvasItemCtrl: x An item representing a control point (knot, node, etc.).
+* CanvasItemCurve: A single Bezier curve item (line or cubic). Not pickable!
+* CanvasItemDrawing: x The SVG drawing.
+* CanvasItemGrid: Base class for snapping grids.
+* CanvasItemGroup: x An item that contains other items.
+* CanvasItemGuideline: x A guideline for snapping.
+* CanvasItemQuad: An object defined by four points (only used by Text tool).
+* CanvasItemRect: An axis aligned rectangle item.
+* CanvasItemRotate: x For previewing the rotation of the canvas.
+* CanvasItemText: A text item.
+
+* CanvasItemEnum: All the enums you'll want to use!
+* CanvasItemBuffer: A class that wraps a Cairo buffer for drawing.
+
+Classes here that use CanvasItem's:
+
+* CanvasGridXY: A Cartesian grid for snapping.
+* CanvasGridAxonom An Axonometric grid for snapping.
+* SnapIndicator: A class for showing a snap possibility on canvas.
+* TemporaryItem: A class to manage the lifetime of a temporary CanvasItem.
+* TemporaryItemList: A class to track TemporaryItem's.
+
+
+Notes:
+
+CanvasItemCtrl (a.k.a. "Node", "Knot", "Handle", "Dragger", "Ctrl", "Mouse Grab", "Control Point")
+
+ Used by several groups of classes:
+
+ * Knot
+ ** KnotHolderEntity
+ *** live_effects/...
+ *** KnotHolder: Contains one or more KnotHolderEntity's.
+ Example: an object's fill and stroke gradients could have
+ overlapping knot-entities and are moved together via the knot holder.
+ **** shape-editor-knotholders.cpp
+ Classes derived from KnotHolder which contain classes derived from KnotHolderEntity for
+ editing shapes.
+ **** ShapeEditor contains two KnotHolders, one for shapes, one for LPE's.
+
+ ** ui/tools/tool-base.h
+ ** seltrans.h,.cpp
+ ** display/snap-indicator.h
+ ** gradient-drag.cpp
+ ** vanishing-point.h
+
+
+ * ControlPoint, SelectorPoint, SelectableControlPoint, Handle, Node, CurveDragPoint, TransformHandle, plus
+ auxiliary classes (manipulator...).
+ * drag-anchor
+ * pen-tool
+ * measure-tool
+ * guide-line
+ * snap-indicator
+
+
+TODO:
+
+Move files that use CanvasItem's to more appropriate places.
+
+All files that use CanvasItem's in src directory:
+* Move rubberband.h/.cpp to src/ui
+* Move gradient-drag to src/ui
+* Move selcue* to src/ui
+* Move seltrans* to src/ui
+* Move vanishing-point to src/ui
+* Move snap code to src/ui/snap display/snap-indicator.h/.cpp snap.h etc.
+
+See also src/ui/tool and src/ui/knot.
+
diff --git a/src/display/control/canvas-axonomgrid.cpp b/src/display/control/canvas-axonomgrid.cpp
new file mode 100644
index 0000000..6ec4626
--- /dev/null
+++ b/src/display/control/canvas-axonomgrid.cpp
@@ -0,0 +1,770 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl>
+ *
+ * Copyright (C) 2006-2012 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/*
+ * Current limits are: one axis (y-axis) is always vertical. The other two
+ * axes are bound to a certain range of angles. The z-axis always has an angle
+ * smaller than 90 degrees (measured from horizontal, 0 degrees being a line extending
+ * to the right). The x-axis will always have an angle between 0 and 90 degrees.
+ */
+
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <gtkmm/grid.h>
+
+#include <glibmm/i18n.h>
+
+#include <2geom/angle.h>
+#include <2geom/line.h>
+
+#include "canvas-axonomgrid.h"
+#include "canvas-grid.h"
+#include "canvas-item-grid.h"
+
+#include "desktop.h"
+#include "document.h"
+#include "inkscape.h"
+#include "preferences.h"
+
+#include "display/cairo-utils.h"
+
+#include "helper/mathfns.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-object.h"
+#include "object/sp-root.h"
+
+#include "svg/svg-color.h"
+
+#include "ui/widget/canvas.h"
+#include "ui/widget/registered-widget.h"
+
+#include "util/units.h"
+
+using Inkscape::Util::unit_table;
+
+enum Dim3 { X=0, Y, Z };
+
+/**
+ * This function calls Cairo to render a line on a particular canvas buffer.
+ * Coordinates are interpreted as SCREENcoordinates
+ */
+static void
+sp_caxonomgrid_drawline (Inkscape::CanvasItemBuffer *buf, gint x0, gint y0, gint x1, gint y1, guint32 rgba)
+{
+ buf->cr->move_to(0.5 + x0, 0.5 + y0);
+ buf->cr->line_to(0.5 + x1, 0.5 + y1);
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba),
+ SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba));
+ buf->cr->stroke();
+}
+
+static void
+sp_grid_vline (Inkscape::CanvasItemBuffer *buf, gint x, gint ys, gint ye, guint32 rgba)
+{
+ if ((x < buf->rect.left()) || (x >= buf->rect.right()))
+ return;
+
+ buf->cr->move_to(0.5 + x, 0.5 + ys);
+ buf->cr->line_to(0.5 + x, 0.5 + ye);
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba),
+ SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba));
+ buf->cr->stroke();
+}
+
+namespace Inkscape {
+
+
+CanvasAxonomGrid::CanvasAxonomGrid (SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument * in_doc)
+ : CanvasGrid(nv, in_repr, in_doc, GRID_AXONOMETRIC)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gridunit = unit_table.getUnit(prefs->getString("/options/grids/axonom/units"));
+ if (!gridunit) {
+ gridunit = unit_table.getUnit("px");
+ }
+ origin[Geom::X] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/axonom/origin_x", 0.0), gridunit, "px");
+ origin[Geom::Y] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/axonom/origin_y", 0.0), gridunit, "px");
+ color = prefs->getInt("/options/grids/axonom/color", GRID_DEFAULT_COLOR);
+ empcolor = prefs->getInt("/options/grids/axonom/empcolor", GRID_DEFAULT_EMPCOLOR);
+ empspacing = prefs->getInt("/options/grids/axonom/empspacing", 5);
+ lengthy = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/axonom/spacing_y", 1.0), gridunit, "px");
+ angle_deg[X] = prefs->getDouble("/options/grids/axonom/angle_x", 30.0);
+ angle_deg[Z] = prefs->getDouble("/options/grids/axonom/angle_z", 30.0);
+ angle_deg[Y] = 0;
+
+ angle_rad[X] = Geom::rad_from_deg(angle_deg[X]);
+ tan_angle[X] = tan(angle_rad[X]);
+ angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]);
+ tan_angle[Z] = tan(angle_rad[Z]);
+
+ snapper = new CanvasAxonomGridSnapper(this, &namedview->snap_manager, 0);
+
+ if (repr) readRepr();
+}
+
+CanvasAxonomGrid::~CanvasAxonomGrid ()
+{
+ if (snapper) delete snapper;
+}
+
+static gboolean sp_nv_read_opacity(gchar const *str, guint32 *color)
+{
+ if (!str) {
+ return FALSE;
+ }
+
+ gchar *u;
+ gdouble v = g_ascii_strtod(str, &u);
+ if (!u) {
+ return FALSE;
+ }
+ v = CLAMP(v, 0.0, 1.0);
+
+ *color = (*color & 0xffffff00) | (guint32) floor(v * 255.9999);
+
+ return TRUE;
+}
+
+
+
+void
+CanvasAxonomGrid::readRepr()
+{
+ SPRoot *root = doc->getRoot();
+ double scale_x = 1.0;
+ double scale_y = 1.0;
+ if( root->viewBox_set ) {
+ scale_x = root->width.computed / root->viewBox.width();
+ scale_y = root->height.computed / root->viewBox.height();
+ if (Geom::are_near(scale_x / scale_y, 1.0, Geom::EPSILON)) {
+ // scaling is uniform, try to reduce numerical error
+ scale_x = (scale_x + scale_y)/2.0;
+ double scale_none = Inkscape::Util::Quantity::convert(1, doc->getDisplayUnit(), "px");
+ if (Geom::are_near(scale_x / scale_none, 1.0, Geom::EPSILON))
+ scale_x = scale_none; // objects are same size, reduce numerical error
+ scale_y = scale_x;
+ }
+ }
+
+ gchar const *value;
+
+ if ( (value = repr->attribute("originx")) ) {
+
+ Inkscape::Util::Quantity q = unit_table.parseQuantity(value);
+
+ if( q.unit->type == UNIT_TYPE_LINEAR ) {
+ // Legacy grid not in 'user units'
+ origin[Geom::X] = q.value("px");
+ } else {
+ // Grid in 'user units'
+ origin[Geom::X] = q.quantity * scale_x;
+ }
+ }
+
+ if ( (value = repr->attribute("originy")) ) {
+
+ Inkscape::Util::Quantity q = unit_table.parseQuantity(value);
+
+ if( q.unit->type == UNIT_TYPE_LINEAR ) {
+ // Legacy grid not in 'user units'
+ origin[Geom::Y] = q.value("px");
+ } else {
+ // Grid in 'user units'
+ origin[Geom::Y] = q.quantity * scale_y;
+ }
+ }
+
+ if ( (value = repr->attribute("spacingy")) ) {
+
+ Inkscape::Util::Quantity q = unit_table.parseQuantity(value);
+
+ if( q.unit->type == UNIT_TYPE_LINEAR ) {
+ // Legacy grid not in 'user units'
+ lengthy = q.value("px");
+ } else {
+ // Grid in 'user units'
+ lengthy = q.quantity * scale_y; // We do not handle scale_x != scale_y
+ }
+ if (lengthy < 0.0500) lengthy = 0.0500;
+ }
+
+ if ( (value = repr->attribute("gridanglex")) ) {
+ angle_deg[X] = g_ascii_strtod(value, nullptr);
+ if (angle_deg[X] < 0.) angle_deg[X] = 0.;
+ if (angle_deg[X] > 89.0) angle_deg[X] = 89.0;
+ angle_rad[X] = Geom::rad_from_deg(angle_deg[X]);
+ tan_angle[X] = tan(angle_rad[X]);
+ }
+
+ if ( (value = repr->attribute("gridanglez")) ) {
+ angle_deg[Z] = g_ascii_strtod(value, nullptr);
+ if (angle_deg[Z] < 0.) angle_deg[Z] = 0.;
+ if (angle_deg[Z] > 89.0) angle_deg[Z] = 89.0;
+ angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]);
+ tan_angle[Z] = tan(angle_rad[Z]);
+ }
+
+ if ( (value = repr->attribute("color")) ) {
+ color = (color & 0xff) | sp_svg_read_color(value, color);
+ }
+
+ if ( (value = repr->attribute("empcolor")) ) {
+ empcolor = (empcolor & 0xff) | sp_svg_read_color(value, empcolor);
+ }
+
+ if ( (value = repr->attribute("opacity")) ) {
+ sp_nv_read_opacity(value, &color);
+ }
+ if ( (value = repr->attribute("empopacity")) ) {
+ sp_nv_read_opacity(value, &empcolor);
+ }
+
+ if ( (value = repr->attribute("empspacing")) ) {
+ empspacing = atoi(value);
+ }
+
+ if ( (value = repr->attribute("visible")) ) {
+ visible = (strcmp(value,"false") != 0 && strcmp(value, "0") != 0);
+ }
+
+ if ( (value = repr->attribute("enabled")) ) {
+ g_assert(snapper != nullptr);
+ snapper->setEnabled(strcmp(value,"false") != 0 && strcmp(value, "0") != 0);
+ }
+
+ if ( (value = repr->attribute("snapvisiblegridlinesonly")) ) {
+ g_assert(snapper != nullptr);
+ snapper->setSnapVisibleOnly(strcmp(value,"false") != 0 && strcmp(value, "0") != 0);
+ }
+
+ if ( (value = repr->attribute("units")) ) {
+ gridunit = unit_table.getUnit(value); // Display unit identifier in grid menu
+ }
+
+ for (auto grid : canvas_item_grids) {
+ grid->request_update();
+ }
+
+ return;
+}
+
+/**
+ * Called when XML node attribute changed; updates dialog widgets if change was not done by widgets themselves.
+ */
+void
+CanvasAxonomGrid::onReprAttrChanged(Inkscape::XML::Node */*repr*/, gchar const */*key*/, gchar const */*oldval*/, gchar const */*newval*/, bool /*is_interactive*/)
+{
+ readRepr();
+
+ if ( ! (_wr.isUpdating()) )
+ updateWidgets();
+}
+
+Gtk::Widget *
+CanvasAxonomGrid::newSpecificWidget()
+{
+ _rumg = Gtk::manage( new Inkscape::UI::Widget::RegisteredUnitMenu(
+ _("Grid _units:"), "units", _wr, repr, doc) );
+ _rsu_ox = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit(
+ _("_Origin X:"), _("X coordinate of grid origin"), "originx",
+ *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_x) );
+ _rsu_oy = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit(
+ _("O_rigin Y:"), _("Y coordinate of grid origin"), "originy",
+ *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_y) );
+ _rsu_sy = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit(
+ _("Spacing _Y:"), _("Base length of z-axis"), "spacingy",
+ *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_y) );
+ _rsu_ax = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalar(
+ _("Angle X:"), _("Angle of x-axis"), "gridanglex", _wr, repr, doc ) );
+ _rsu_az = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalar(
+ _("Angle Z:"), _("Angle of z-axis"), "gridanglez", _wr, repr, doc ) );
+
+ _rcp_gcol = Gtk::manage( new Inkscape::UI::Widget::RegisteredColorPicker(
+ _("Minor grid line _color:"), _("Minor grid line color"), _("Color of the minor grid lines"),
+ "color", "opacity", _wr, repr, doc));
+ _rcp_gmcol = Gtk::manage( new Inkscape::UI::Widget::RegisteredColorPicker(
+ _("Ma_jor grid line color:"), _("Major grid line color"),
+ _("Color of the major (highlighted) grid lines"),
+ "empcolor", "empopacity", _wr, repr, doc));
+
+ _rsi = Gtk::manage( new Inkscape::UI::Widget::RegisteredSuffixedInteger(
+ _("_Major grid line every:"), "", _("lines"), "empspacing", _wr, repr, doc ) );
+
+ _rumg->set_hexpand();
+ _rsu_ox->set_hexpand();
+ _rsu_oy->set_hexpand();
+ _rsu_sy->set_hexpand();
+ _rsu_ax->set_hexpand();
+ _rsu_az->set_hexpand();
+ _rcp_gcol->set_hexpand();
+ _rcp_gmcol->set_hexpand();
+ _rsi->set_hexpand();
+
+ // set widget values
+ _wr.setUpdating (true);
+
+ _rsu_ox->setDigits(5);
+ _rsu_ox->setIncrements(0.1, 1.0);
+
+ _rsu_oy->setDigits(5);
+ _rsu_oy->setIncrements(0.1, 1.0);
+
+ _rsu_sy->setDigits(5);
+ _rsu_sy->setIncrements(0.1, 1.0);
+
+ _rumg->setUnit (gridunit->abbr);
+
+ gdouble val;
+ val = origin[Geom::X];
+ val = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_ox->setValue (val);
+ val = origin[Geom::Y];
+ val = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_oy->setValue (val);
+ val = lengthy;
+ double gridy = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_sy->setValue (gridy);
+
+ _rsu_ax->setValue(angle_deg[X]);
+ _rsu_az->setValue(angle_deg[Z]);
+
+ _rcp_gcol->setRgba32 (color);
+ _rcp_gmcol->setRgba32 (empcolor);
+ _rsi->setValue (empspacing);
+
+ _wr.setUpdating (false);
+
+ _rsu_ox->setProgrammatically = false;
+ _rsu_oy->setProgrammatically = false;
+
+ Gtk::Box *column = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4));
+ column->pack_start(*_rumg, true, false);
+ column->pack_start(*_rsu_ox, true, false);
+ column->pack_start(*_rsu_oy, true, false);
+ column->pack_start(*_rsu_sy, true, false);
+ column->pack_start(*_rsu_ax, true, false);
+ column->pack_start(*_rsu_az, true, false);
+ column->pack_start(*_rcp_gcol, true, false);
+ column->pack_start(*_rcp_gmcol, true, false);
+ column->pack_start(*_rsi, true, false);
+
+ return column;
+}
+
+
+/**
+ * Update dialog widgets from object's values.
+ */
+void
+CanvasAxonomGrid::updateWidgets()
+{
+ if (_wr.isUpdating()) return;
+
+ //no widgets (grid created with the document, not with the dialog)
+ if (!_rcb_visible) return;
+
+ _wr.setUpdating (true);
+
+ _rcb_visible->setActive(visible);
+ if (snapper != nullptr) {
+ _rcb_enabled->setActive(snapper->getEnabled());
+ _rcb_snap_visible_only->setActive(snapper->getSnapVisibleOnly());
+ }
+
+ _rumg->setUnit (gridunit->abbr);
+
+ gdouble val;
+
+ val = origin[Geom::X];
+ val = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_ox->setValue (val);
+
+ val = origin[Geom::Y];
+ val = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_oy->setValue (val);
+
+ val = lengthy;
+ val = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_sy->setValue (val);
+
+ _rsu_ax->setValue(angle_deg[X]);
+ _rsu_az->setValue(angle_deg[Z]);
+
+ _rsu_ox->setProgrammatically = false;
+ _rsu_oy->setProgrammatically = false;
+ _rsu_sy->setProgrammatically = false;
+ _rsu_ax->setProgrammatically = false;
+ _rsu_az->setProgrammatically = false;
+
+ _rcp_gcol->setRgba32 (color);
+ _rcp_gmcol->setRgba32 (empcolor);
+ _rsi->setValue (empspacing);
+
+ _wr.setUpdating (false);
+}
+
+
+
+void
+CanvasAxonomGrid::Update (Geom::Affine const &affine, unsigned int /*flags*/)
+{
+ ow = origin * affine;
+ sw = Geom::Point(fabs(affine[0]),fabs(affine[3]));
+ sw *= lengthy;
+
+ scaled = false;
+
+ for(int dim = 0; dim < 2; dim++) {
+ gint scaling_factor = empspacing;
+
+ if (scaling_factor <= 1)
+ scaling_factor = 5;
+
+ int watchdog = 0;
+ while ( (sw[dim] < 8.0) & (watchdog < 100) ) {
+ scaled = true;
+ sw[dim] *= scaling_factor;
+ // First pass, go up to the major line spacing, then
+ // keep increasing by two.
+ scaling_factor = 2;
+ watchdog++;
+ }
+
+ }
+
+ spacing_ylines = sw[Geom::X] /(tan_angle[X] + tan_angle[Z]);
+ lyw = sw[Geom::Y];
+ lxw_x = Geom::are_near(tan_angle[X],0.) ? Geom::infinity() : sw[Geom::X] / tan_angle[X];
+ lxw_z = Geom::are_near(tan_angle[Z],0.) ? Geom::infinity() : sw[Geom::X] / tan_angle[Z];
+
+ if (empspacing == 0) {
+ scaled = true;
+ }
+}
+
+void
+CanvasAxonomGrid::Render (Inkscape::CanvasItemBuffer *buf)
+{
+ //set correct coloring, depending preference (when zoomed out, always major coloring or minor coloring)
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ guint32 _empcolor;
+ guint32 _color = color;
+ bool preference = prefs->getBool("/options/grids/no_emphasize_when_zoomedout", false);
+ if( scaled && preference ) {
+ _empcolor = color;
+ } else {
+ _empcolor = empcolor;
+ }
+
+ bool xrayactive = prefs->getBool("/desktop/xrayactive", false);
+ if (xrayactive) { //this allow good looking on xray zones
+ guint32 bg = 0xffffffff;
+ _color = SP_RGBA32_F_COMPOSE(
+ CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_R_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_R_F(_color)), 0.0, 1.0),
+ CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_G_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_G_F(_color)), 0.0, 1.0),
+ CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_B_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_B_F(_color)), 0.0, 1.0),
+ 1.0);
+ _empcolor = SP_RGBA32_F_COMPOSE(
+ CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_R_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_R_F(_empcolor)), 0.0, 1.0),
+ CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_G_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_G_F(_empcolor)), 0.0, 1.0),
+ CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_B_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_B_F(_empcolor)), 0.0, 1.0),
+ 1.0);
+ }
+
+ buf->cr->save();
+ buf->cr->translate(-buf->rect.left(), -buf->rect.top());
+ buf->cr->set_line_width(1.0);
+ buf->cr->set_line_cap(Cairo::LINE_CAP_SQUARE);
+
+ // gc = gridcoordinates (the coordinates calculated from the grids origin 'grid->ow'.
+ // sc = screencoordinates ( for example "buf->rect.left()" is in screencoordinates )
+ // bc = buffer patch coordinates (x=0 on left side of page, y=0 on bottom of page)
+
+ // tl = topleft ; br = bottomright
+ Geom::Point buf_tl_gc;
+ Geom::Point buf_br_gc;
+ buf_tl_gc[Geom::X] = buf->rect.left() - ow[Geom::X];
+ buf_tl_gc[Geom::Y] = buf->rect.top() - ow[Geom::Y];
+ buf_br_gc[Geom::X] = buf->rect.right() - ow[Geom::X];
+ buf_br_gc[Geom::Y] = buf->rect.bottom() - ow[Geom::Y];
+
+ // render the three separate line groups representing the main-axes
+
+ // x-axis always goes from topleft to bottomright. (0,0) - (1,1)
+ gdouble const xintercept_y_bc = (buf_tl_gc[Geom::X] * tan_angle[X]) - buf_tl_gc[Geom::Y] ;
+ gdouble const xstart_y_sc = ( xintercept_y_bc - floor(xintercept_y_bc/lyw)*lyw ) + buf->rect.top();
+ gint const xlinestart = round( (xstart_y_sc - buf_tl_gc[Geom::X]*tan_angle[X] - ow[Geom::Y]) / lyw );
+ gint xlinenum = xlinestart;
+
+ // lines starting on left side.
+
+ for (gdouble y = xstart_y_sc; y < buf->rect.bottom(); y += lyw, xlinenum++) {
+ gint const x0 = buf->rect.left();
+ gint const y0 = round(y);
+ gint x1 = x0 + round( (buf->rect.bottom() - y) / tan_angle[X] );
+ gint y1 = buf->rect.bottom();
+ if ( Geom::are_near(tan_angle[X],0.) ) {
+ x1 = buf->rect.right();
+ y1 = y0;
+ }
+
+ if (!scaled && (xlinenum % empspacing) != 0) {
+ sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _color);
+ } else {
+ sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _empcolor);
+ }
+ }
+ // lines starting from top side
+ if (!Geom::are_near(tan_angle[X],0.))
+ {
+ gdouble const xstart_x_sc = buf->rect.left() + (lxw_x - (xstart_y_sc - buf->rect.top()) / tan_angle[X]) ;
+ xlinenum = xlinestart-1;
+ for (gdouble x = xstart_x_sc; x < buf->rect.right(); x += lxw_x, xlinenum--) {
+ gint const y0 = buf->rect.top();
+ gint const y1 = buf->rect.bottom();
+ gint const x0 = round(x);
+ gint const x1 = x0 + round( (y1 - y0) / tan_angle[X] );
+
+ if (!scaled && (xlinenum % empspacing) != 0) {
+ sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _color);
+ } else {
+ sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _empcolor);
+ }
+ }
+ }
+
+ // y-axis lines (vertical)
+ gdouble const ystart_x_sc = floor (buf_tl_gc[Geom::X] / spacing_ylines) * spacing_ylines + ow[Geom::X];
+ gint const ylinestart = round((ystart_x_sc - ow[Geom::X]) / spacing_ylines);
+ gint ylinenum = ylinestart;
+ for (gdouble x = ystart_x_sc; x < buf->rect.right(); x += spacing_ylines, ylinenum++) {
+ gint const x0 = floor(x); // sp_grid_vline will add 0.5 again, so we'll pre-emptively use floor()
+ // instead of round() to avoid biasing the vertical lines to the right by half a pixel; see
+ // CanvasXYGrid::Render() for more details
+ if (!scaled && (ylinenum % empspacing) != 0) {
+ sp_grid_vline (buf, x0, buf->rect.top(), buf->rect.bottom() - 1, _color);
+ } else {
+ sp_grid_vline (buf, x0, buf->rect.top(), buf->rect.bottom() - 1, _empcolor);
+ }
+ }
+
+ // z-axis always goes from bottomleft to topright. (0,1) - (1,0)
+ gdouble const zintercept_y_bc = (buf_tl_gc[Geom::X] * -tan_angle[Z]) - buf_tl_gc[Geom::Y] ;
+ gdouble const zstart_y_sc = ( zintercept_y_bc - floor(zintercept_y_bc/lyw)*lyw ) + buf->rect.top();
+ gint const zlinestart = round( (zstart_y_sc + buf_tl_gc[Geom::X]*tan_angle[Z] - ow[Geom::Y]) / lyw );
+ gint zlinenum = zlinestart;
+ // lines starting from left side
+ gdouble next_y = zstart_y_sc;
+ for (gdouble y = zstart_y_sc; y < buf->rect.bottom(); y += lyw, zlinenum++, next_y = y) {
+ gint const x0 = buf->rect.left();
+ gint const y0 = round(y);
+ gint x1 = x0 + round( (y - buf->rect.top() ) / tan_angle[Z] );
+ gint y1 = buf->rect.top();
+ if ( Geom::are_near(tan_angle[Z],0.) ) {
+ x1 = buf->rect.right();
+ y1 = y0;
+ }
+
+ if (!scaled && (zlinenum % empspacing) != 0) {
+ sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _color);
+ } else {
+ sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _empcolor);
+ }
+ }
+ // draw lines from bottom-up
+ if (!Geom::are_near(tan_angle[Z],0.))
+ {
+ gdouble const zstart_x_sc = buf->rect.left() + (next_y - buf->rect.bottom()) / tan_angle[Z] ;
+ for (gdouble x = zstart_x_sc; x < buf->rect.right(); x += lxw_z, zlinenum++) {
+ gint const y0 = buf->rect.bottom();
+ gint const y1 = buf->rect.top();
+ gint const x0 = round(x);
+ gint const x1 = x0 + round(buf->rect.height() / tan_angle[Z] );
+
+ if (!scaled && (zlinenum % empspacing) != 0) {
+ sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _color);
+ } else {
+ sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _empcolor);
+ }
+ }
+ }
+
+ buf->cr->restore();
+}
+
+CanvasAxonomGridSnapper::CanvasAxonomGridSnapper(CanvasAxonomGrid *grid, SnapManager *sm, Geom::Coord const d) : LineSnapper(sm, d)
+{
+ this->grid = grid;
+}
+
+/**
+ * \return Snap tolerance (desktop coordinates); depends on current zoom so that it's always the same in screen pixels
+ */
+Geom::Coord CanvasAxonomGridSnapper::getSnapperTolerance() const
+{
+ SPDesktop const *dt = _snapmanager->getDesktop();
+ double const zoom = dt ? dt->current_zoom() : 1;
+ return _snapmanager->snapprefs.getGridTolerance() / zoom;
+}
+
+bool CanvasAxonomGridSnapper::getSnapperAlwaysSnap() const
+{
+ return _snapmanager->snapprefs.getGridTolerance() == 10000; //TODO: Replace this threshold of 10000 by a constant; see also tolerance-slider.cpp
+}
+
+LineSnapper::LineList
+CanvasAxonomGridSnapper::_getSnapLines(Geom::Point const &p) const
+{
+ LineList s;
+
+ if ( grid == nullptr ) {
+ return s;
+ }
+
+ SPDesktop const *dt = _snapmanager->getDesktop();
+ double ta_x = grid->tan_angle[X];
+ double ta_z = grid->tan_angle[Z];
+
+ if (dt && dt->is_yaxisdown()) {
+ std::swap(ta_x, ta_z);
+ }
+
+ double spacing_h;
+ double spacing_v;
+
+ if (getSnapVisibleOnly()) {
+ // Only snapping to visible grid lines
+ spacing_h = grid->spacing_ylines; // this is the spacing of the visible grid lines measured in screen pixels
+ spacing_v = grid->lyw; // vertical
+ // convert screen pixels to px
+ // FIXME: after we switch to snapping dist in screen pixels, this will be unnecessary
+ if (dt) {
+ spacing_h /= dt->current_zoom();
+ spacing_v /= dt->current_zoom();
+ }
+ } else {
+ // Snapping to any grid line, whether it's visible or not
+ spacing_h = grid->lengthy /(grid->tan_angle[X] + grid->tan_angle[Z]);
+ spacing_v = grid->lengthy;
+
+ }
+
+ // In an axonometric grid, any point will be surrounded by 6 grid lines:
+ // - 2 vertical grid lines, one left and one right from the point
+ // - 2 angled z grid lines, one above and one below the point
+ // - 2 angled x grid lines, one above and one below the point
+
+ // Calculate the x coordinate of the vertical grid lines
+ Geom::Coord x_max = Inkscape::Util::round_to_upper_multiple_plus(p[Geom::X], spacing_h, grid->origin[Geom::X]);
+ Geom::Coord x_min = Inkscape::Util::round_to_lower_multiple_plus(p[Geom::X], spacing_h, grid->origin[Geom::X]);
+
+ // Calculate the y coordinate of the intersection of the angled grid lines with the y-axis
+ double y_proj_along_z = p[Geom::Y] - ta_z * (p[Geom::X] - grid->origin[Geom::X]);
+ double y_proj_along_x = p[Geom::Y] + ta_x * (p[Geom::X] - grid->origin[Geom::X]);
+ double y_proj_along_z_max = Inkscape::Util::round_to_upper_multiple_plus(y_proj_along_z, spacing_v, grid->origin[Geom::Y]);
+ double y_proj_along_z_min = Inkscape::Util::round_to_lower_multiple_plus(y_proj_along_z, spacing_v, grid->origin[Geom::Y]);
+ double y_proj_along_x_max = Inkscape::Util::round_to_upper_multiple_plus(y_proj_along_x, spacing_v, grid->origin[Geom::Y]);
+ double y_proj_along_x_min = Inkscape::Util::round_to_lower_multiple_plus(y_proj_along_x, spacing_v, grid->origin[Geom::Y]);
+
+ // Calculate the versor for the angled grid lines
+ Geom::Point vers_x = Geom::Point(1, -ta_x);
+ Geom::Point vers_z = Geom::Point(1, ta_z);
+
+ // Calculate the normal for the angled grid lines
+ Geom::Point norm_x = Geom::rot90(vers_x);
+ Geom::Point norm_z = Geom::rot90(vers_z);
+
+ // The four angled grid lines form a parallelogram, enclosing the point
+ // One of the two vertical grid lines divides this parallelogram in two triangles
+ // We will now try to find out in which half (i.e. triangle) our point is, and return
+ // only the three grid lines defining that triangle
+
+ // The vertical grid line is at the intersection of two angled grid lines.
+ // Now go find that intersection!
+ Geom::Point p_x(0, y_proj_along_x_max);
+ Geom::Line line_x(p_x, p_x + vers_x);
+ Geom::Point p_z(0, y_proj_along_z_max);
+ Geom::Line line_z(p_z, p_z + vers_z);
+
+ Geom::OptCrossing inters = Geom::OptCrossing(); // empty by default
+ try
+ {
+ inters = Geom::intersection(line_x, line_z);
+ }
+ catch (Geom::InfiniteSolutions &e)
+ {
+ // We're probably dealing with parallel lines; this is useless!
+ return s;
+ }
+
+ // Determine which half of the parallelogram to use
+ bool use_left_half = true;
+ bool use_right_half = true;
+
+ if (inters) {
+ Geom::Point inters_pt = line_x.pointAt((*inters).ta);
+ use_left_half = (p[Geom::X] - grid->origin[Geom::X]) < inters_pt[Geom::X];
+ use_right_half = !use_left_half;
+ }
+
+ // Return the three grid lines which define the triangle that encloses our point
+ // If we didn't find an intersection above, all 6 grid lines will be returned
+ if (use_left_half) {
+ s.push_back(std::make_pair(norm_z, Geom::Point(grid->origin[Geom::X], y_proj_along_z_max)));
+ s.push_back(std::make_pair(norm_x, Geom::Point(grid->origin[Geom::X], y_proj_along_x_min)));
+ s.push_back(std::make_pair(Geom::Point(1, 0), Geom::Point(x_max, 0)));
+ }
+
+ if (use_right_half) {
+ s.push_back(std::make_pair(norm_z, Geom::Point(grid->origin[Geom::X], y_proj_along_z_min)));
+ s.push_back(std::make_pair(norm_x, Geom::Point(grid->origin[Geom::X], y_proj_along_x_max)));
+ s.push_back(std::make_pair(Geom::Point(1, 0), Geom::Point(x_min, 0)));
+ }
+
+ return s;
+}
+
+void CanvasAxonomGridSnapper::_addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, Geom::Point const &point_on_line) const
+{
+ SnappedLine dummy = SnappedLine(snapped_point, snapped_distance, source, source_num, Inkscape::SNAPTARGET_GRID, getSnapperTolerance(), getSnapperAlwaysSnap(), normal_to_line, point_on_line);
+ isr.grid_lines.push_back(dummy);
+}
+
+void CanvasAxonomGridSnapper::_addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const
+{
+ SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GRID, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true);
+ isr.points.push_back(dummy);
+}
+
+void CanvasAxonomGridSnapper::_addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const
+{
+ SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GRID_PERPENDICULAR, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true);
+ isr.points.push_back(dummy);
+}
+
+bool CanvasAxonomGridSnapper::ThisSnapperMightSnap() const
+{
+ return _snap_enabled && _snapmanager->snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_GRID);
+}
+
+
+}; // namespace Inkscape
+
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-axonomgrid.h b/src/display/control/canvas-axonomgrid.h
new file mode 100644
index 0000000..be23d5b
--- /dev/null
+++ b/src/display/control/canvas-axonomgrid.h
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef CANVAS_AXONOMGRID_H
+#define CANVAS_AXONOMGRID_H
+
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl>
+ *
+ * Copyright (C) 2006-2012 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "line-snapper.h"
+#include "canvas-grid.h"
+
+class SPNamedView;
+
+namespace Inkscape {
+class CanvasItemBuffer;
+namespace XML {
+ class Node;
+};
+
+class CanvasAxonomGrid : public CanvasGrid {
+public:
+ CanvasAxonomGrid(SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument * in_doc);
+ ~CanvasAxonomGrid() override;
+
+ void Update (Geom::Affine const &affine, unsigned int flags) override;
+ void Render (Inkscape::CanvasItemBuffer *buf) override;
+
+ void readRepr() override;
+ void onReprAttrChanged (Inkscape::XML::Node * repr, char const *key, char const *oldval, char const *newval, bool is_interactive) override;
+
+ double lengthy; /**< The lengths of the primary y-axis */
+ double angle_deg[3]; /**< Angle of each axis (note that angle[2] == 0) */
+ double angle_rad[3]; /**< Angle of each axis (note that angle[2] == 0) */
+ double tan_angle[3]; /**< tan(angle[.]) */
+
+ bool scaled; /**< Whether the grid is in scaled mode */
+
+protected:
+ friend class CanvasAxonomGridSnapper;
+
+ Geom::Point ow; /**< Transformed origin by the affine for the zoom */
+ double lyw = 1.0; /**< Transformed length y by the affine for the zoom */
+ double lxw_x = 1.0;
+ double lxw_z = 1.0;
+ double spacing_ylines = 1.0;
+
+ Geom::Point sw; /**< the scaling factors of the affine transform */
+
+ Gtk::Widget * newSpecificWidget() override;
+
+private:
+ CanvasAxonomGrid(const CanvasAxonomGrid&) = delete;
+ CanvasAxonomGrid& operator=(const CanvasAxonomGrid&) = delete;
+
+ void updateWidgets();
+
+ Inkscape::UI::Widget::RegisteredUnitMenu *_rumg;
+ Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_ox;
+ Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_oy;
+ Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_sy;
+ Inkscape::UI::Widget::RegisteredScalar *_rsu_ax;
+ Inkscape::UI::Widget::RegisteredScalar *_rsu_az;
+ Inkscape::UI::Widget::RegisteredColorPicker *_rcp_gcol;
+ Inkscape::UI::Widget::RegisteredColorPicker *_rcp_gmcol;
+ Inkscape::UI::Widget::RegisteredSuffixedInteger *_rsi;
+};
+
+
+
+class CanvasAxonomGridSnapper : public LineSnapper
+{
+public:
+ CanvasAxonomGridSnapper(CanvasAxonomGrid *grid, SnapManager *sm, Geom::Coord const d);
+ bool ThisSnapperMightSnap() const override;
+
+ Geom::Coord getSnapperTolerance() const override; //returns the tolerance of the snapper in screen pixels (i.e. independent of zoom)
+ bool getSnapperAlwaysSnap() const override; //if true, then the snapper will always snap, regardless of its tolerance
+
+private:
+ LineList _getSnapLines(Geom::Point const &p) const override;
+ void _addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, const Geom::Point &point_on_line) const override;
+ void _addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override;
+ void _addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override;
+
+ CanvasAxonomGrid *grid;
+};
+
+
+}; //namespace Inkscape
+
+
+
+#endif
+
+
diff --git a/src/display/control/canvas-grid.cpp b/src/display/control/canvas-grid.cpp
new file mode 100644
index 0000000..8ca17e0
--- /dev/null
+++ b/src/display/control/canvas-grid.cpp
@@ -0,0 +1,1120 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Cartesian grid item for the Inkscape canvas.
+ *//*
+ * Authors:
+ * see git history
+ * Copyright (C) Johan Engelen 2006-2007 <johan@shouraizou.nl>
+ * Copyright (C) Lauris Kaplinski 2000
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ * Copyright (C) Tavmong Bah 2017 <tavmjong@free.fr>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/* As a general comment, I am not exactly proud of how things are done.
+ * (for example the 'enable' widget and readRepr things)
+ * It does seem to work however. I intend to clean up and sort things out later, but that can take forever...
+ * Don't be shy to correct things.
+ */
+
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <gtkmm/grid.h>
+
+#include <glibmm/i18n.h>
+
+#include "canvas-grid.h"
+#include "canvas-axonomgrid.h"
+#include "canvas-item-grid.h"
+
+#include "desktop.h"
+#include "document.h"
+#include "inkscape.h"
+#include "preferences.h"
+
+#include "display/cairo-utils.h"
+
+#include "helper/mathfns.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-object.h"
+#include "object/sp-root.h"
+
+#include "svg/stringstream.h"
+#include "svg/svg-color.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/canvas.h"
+
+#include "util/units.h"
+
+#include "xml/node-event-vector.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+
+static gchar const *const grid_name[] = {
+ N_("Rectangular grid"),
+ N_("Axonometric grid")
+};
+
+static gchar const *const grid_svgname[] = {
+ "xygrid",
+ "axonomgrid"
+};
+
+// ##########################################################
+// CanvasGrid
+
+ static Inkscape::XML::NodeEventVector const _repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ CanvasGrid::on_repr_attr_changed,
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+ };
+
+CanvasGrid::CanvasGrid(SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument *in_doc, GridType type)
+ : visible(true), gridtype(type), legacy(false), pixel(false)
+{
+ repr = in_repr;
+ doc = in_doc;
+ if (repr) {
+ repr->addListener (&_repr_events, this);
+ }
+
+ namedview = nv;
+}
+
+CanvasGrid::~CanvasGrid()
+{
+ if (repr) {
+ repr->removeListenerByData (this);
+ }
+
+ if (_rcb_enabled) {
+ _rcb_enabled->remove_destroy_notify_callback(this);
+ }
+
+ for (auto grid : canvas_item_grids) {
+ delete grid;
+ }
+ canvas_item_grids.clear();
+}
+
+const char *
+CanvasGrid::getName() const
+{
+ return _(grid_name[gridtype]);
+}
+
+const char *
+CanvasGrid::getSVGName() const
+{
+ return grid_svgname[gridtype];
+}
+
+GridType
+CanvasGrid::getGridType() const
+{
+ return gridtype;
+}
+
+
+char const *
+CanvasGrid::getName(GridType type)
+{
+ return _(grid_name[type]);
+}
+
+char const *
+CanvasGrid::getSVGName(GridType type)
+{
+ return grid_svgname[type];
+}
+
+GridType
+CanvasGrid::getGridTypeFromSVGName(char const *typestr)
+{
+ if (!typestr) return GRID_RECTANGULAR;
+
+ gint t = 0;
+ for (t = GRID_MAXTYPENR; t >= 0; t--) { //this automatically defaults to grid0 which is rectangular grid
+ if (!strcmp(typestr, grid_svgname[t])) break;
+ }
+ return (GridType) t;
+}
+
+GridType
+CanvasGrid::getGridTypeFromName(char const *typestr)
+{
+ if (!typestr) return GRID_RECTANGULAR;
+
+ gint t = 0;
+ for (t = GRID_MAXTYPENR; t >= 0; t--) { //this automatically defaults to grid0 which is rectangular grid
+ if (!strcmp(typestr, _(grid_name[t]))) break;
+ }
+ return (GridType) t;
+}
+
+
+/*
+* writes an <inkscape:grid> child to repr.
+*/
+void
+CanvasGrid::writeNewGridToRepr(Inkscape::XML::Node * repr, SPDocument * doc, GridType gridtype)
+{
+ if (!repr) return;
+ if (gridtype > GRID_MAXTYPENR) return;
+
+ // first create the child xml node, then hook it to repr. This order is important, to not set off listeners to repr before the new node is complete.
+
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+ Inkscape::XML::Node *newnode;
+ newnode = xml_doc->createElement("inkscape:grid");
+ newnode->setAttribute("type", getSVGName(gridtype));
+
+ repr->appendChild(newnode);
+ Inkscape::GC::release(newnode);
+
+ DocumentUndo::done(doc, _("Create new grid"), INKSCAPE_ICON("document-properties"));
+}
+
+/*
+* Creates a new CanvasGrid object of type gridtype
+*/
+CanvasGrid*
+CanvasGrid::NewGrid(SPNamedView * nv, Inkscape::XML::Node * repr, SPDocument * doc, GridType gridtype)
+{
+ if (!repr) return nullptr;
+ if (!doc) {
+ g_error("CanvasGrid::NewGrid - doc==NULL");
+ return nullptr;
+ }
+
+ switch (gridtype) {
+ case GRID_RECTANGULAR:
+ return new CanvasXYGrid(nv, repr, doc);
+ case GRID_AXONOMETRIC:
+ return new CanvasAxonomGrid(nv, repr, doc);
+ }
+
+ return nullptr;
+}
+
+
+/**
+* creates a new grid canvasitem for the SPDesktop given as parameter. Keeps a link to this canvasitem in the canvas_item_grids list.
+*/
+Inkscape::CanvasItemGrid *
+CanvasGrid::createCanvasItem(SPDesktop * desktop)
+{
+ if (!desktop) return nullptr;
+// Johan: I think for multiple desktops it is best if each has their own canvasitem,
+// but share the same CanvasGrid object; that is what this function is for.
+
+ // check if there is already a canvasitem on this desktop linking to this grid
+ for (auto grid : canvas_item_grids) {
+ if ( desktop->getCanvasGrids() == grid->get_parent() ) {
+ return nullptr;
+ }
+ }
+
+ Inkscape::CanvasItemGrid *grid = new Inkscape::CanvasItemGrid(desktop->getCanvasGrids(), this);
+ grid->show();
+ canvas_item_grids.push_back(grid);
+ return grid;
+}
+
+/**
+ * Remove a CanvasGridItem from vector. Does NOT delete CanvasGridItem.
+ * This is used by the CanvasGridItem destructor to ensure no dangling pointer is left.
+ */
+void
+CanvasGrid::removeCanvasItem(Inkscape::CanvasItemGrid *item)
+{
+ auto it = std::find(canvas_item_grids.begin(), canvas_item_grids.end(), item);
+ if (it != canvas_item_grids.end()) {
+ canvas_item_grids.erase(it);
+ }
+}
+
+Gtk::Widget *
+CanvasGrid::newWidget()
+{
+ Gtk::Box * vbox = Gtk::manage( new Gtk::Box(Gtk::ORIENTATION_VERTICAL) );
+ Gtk::Label * namelabel = Gtk::manage(new Gtk::Label("", Gtk::ALIGN_CENTER) );
+
+ Glib::ustring str("<b>");
+ str += getName();
+ str += "</b>";
+ namelabel->set_markup(str);
+ vbox->pack_start(*namelabel, false, false);
+
+ _rcb_enabled = Gtk::manage( new Inkscape::UI::Widget::RegisteredCheckButton(
+ _("_Enabled"),
+ _("Makes the grid available for working with on the canvas."),
+ "enabled", _wr, false, repr, doc) );
+ // _rcb_enabled serves as a canary that tells us that the widgets have been destroyed
+ _rcb_enabled->add_destroy_notify_callback(this, &CanvasGrid::notifyWidgetsDestroyed);
+
+ _rcb_snap_visible_only = Gtk::manage( new Inkscape::UI::Widget::RegisteredCheckButton(
+ _("Snap to visible _grid lines only"),
+ _("When zoomed out, not all grid lines will be displayed. Only the visible ones will be snapped to"),
+ "snapvisiblegridlinesonly", _wr, false, repr, doc) );
+
+ _rcb_visible = Gtk::manage( new Inkscape::UI::Widget::RegisteredCheckButton(
+ _("_Visible"),
+ _("Determines whether the grid is displayed or not. Objects are still snapped to invisible grids."),
+ "visible", _wr, false, repr, doc) );
+
+ _as_alignment = Gtk::manage( new Inkscape::UI::Widget::AlignmentSelector() );
+ _as_alignment->on_alignmentClicked().connect(sigc::mem_fun(*this, &CanvasGrid::align_clicked));
+
+ Gtk::Box *left = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4));
+ left->pack_start(*_rcb_enabled, false, false);
+ left->pack_start(*_rcb_visible, false, false);
+ left->pack_start(*_rcb_snap_visible_only, false, false);
+
+ if (getGridType() == GRID_RECTANGULAR) {
+ _rcb_dotted = Gtk::manage( new Inkscape::UI::Widget::RegisteredCheckButton(
+ _("_Show dots instead of lines"), _("If set, displays dots at gridpoints instead of gridlines"),
+ "dotted", _wr, false, repr, doc) );
+ _rcb_dotted->setActive(render_dotted);
+ left->pack_start(*_rcb_dotted, false, false);
+ }
+
+ left->pack_start(*Gtk::manage(new Gtk::Label(_("Align to page:"))), false, false);
+ left->pack_start(*_as_alignment, false, false);
+
+ auto right = newSpecificWidget();
+ right->set_hexpand(false);
+
+ Gtk::Box *inner = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4));
+ inner->pack_start(*left, true, true);
+ inner->pack_start(*right, false, false);
+ vbox->pack_start(*inner, false, false);
+ vbox->set_border_width(4);
+
+ std::list<Gtk::Widget*> slaves;
+ for (auto &item : left->get_children()) {
+ if (item != _rcb_enabled) {
+ slaves.push_back(item);
+ }
+ }
+ slaves.push_back(right);
+ _rcb_enabled->setSlaveWidgets(slaves);
+
+ // set widget values
+ _wr.setUpdating (true);
+ _rcb_visible->setActive(visible);
+ if (snapper != nullptr) {
+ _rcb_enabled->setActive(snapper->getEnabled());
+ _rcb_snap_visible_only->setActive(snapper->getSnapVisibleOnly());
+ }
+ _wr.setUpdating (false);
+ return dynamic_cast<Gtk::Widget *> (vbox);
+}
+
+/**
+ * @brief Zeroes out our pointers to the widgets when the widgets are destroyed
+ * @param data - a pointer to the CanvasGrid object holding the widget pointers
+ * @return always returns nullptr
+ */
+void *CanvasGrid::notifyWidgetsDestroyed(void *data)
+{
+ CanvasGrid *obj = (CanvasGrid *)(data);
+ obj->_rcb_enabled = nullptr;
+ obj->_rcb_snap_visible_only = nullptr;
+ obj->_rcb_visible = nullptr;
+ obj->_rcb_dotted = nullptr;
+ obj->_as_alignment = nullptr;
+ return nullptr;
+}
+
+void
+CanvasGrid::on_repr_attr_changed(Inkscape::XML::Node *repr, gchar const *key, gchar const *oldval, gchar const *newval, bool is_interactive, void *data)
+{
+ if (!data)
+ return;
+
+ (static_cast<CanvasGrid*>(data))->onReprAttrChanged(repr, key, oldval, newval, is_interactive);
+}
+
+bool CanvasGrid::isEnabled() const
+{
+ if (snapper == nullptr) {
+ return false;
+ }
+
+ return snapper->getEnabled();
+}
+
+// Used to shift origin when page size changed to fit drawing.
+void CanvasGrid::setOrigin(Geom::Point const &origin_px)
+{
+ SPRoot *root = doc->getRoot();
+ double scale_x = 1.0;
+ double scale_y = 1.0;
+ if( root->viewBox_set ) {
+ scale_x = root->viewBox.width() / root->width.computed;
+ scale_y = root->viewBox.height() / root->height.computed;
+ }
+
+ // Write out in 'user-units'
+ Inkscape::SVGOStringStream os_x, os_y;
+ os_x << origin_px[Geom::X] * scale_x;
+ os_y << origin_px[Geom::Y] * scale_y;
+ repr->setAttribute("originx", os_x.str());
+ repr->setAttribute("originy", os_y.str());
+}
+
+void CanvasGrid::align_clicked(int align)
+{
+ Geom::Point dimensions = doc->getDimensions();
+ dimensions[Geom::X] *= align % 3 * 0.5;
+ dimensions[Geom::Y] *= align / 3 * 0.5;
+ dimensions *= doc->doc2dt();
+ setOrigin(dimensions);
+}
+
+
+
+// ##########################################################
+// CanvasXYGrid
+
+CanvasXYGrid::CanvasXYGrid (SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument * in_doc)
+ : CanvasGrid(nv, in_repr, in_doc, GRID_RECTANGULAR)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gridunit = unit_table.getUnit(prefs->getString("/options/grids/xy/units"));
+ if (!gridunit) {
+ gridunit = unit_table.getUnit("px");
+ }
+ origin[Geom::X] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/xy/origin_x", 0.0), gridunit, "px");
+ origin[Geom::Y] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/xy/origin_y", 0.0), gridunit, "px");
+ color = prefs->getInt("/options/grids/xy/color", GRID_DEFAULT_COLOR);
+ empcolor = prefs->getInt("/options/grids/xy/empcolor", GRID_DEFAULT_EMPCOLOR);
+ empspacing = prefs->getInt("/options/grids/xy/empspacing", 5);
+ spacing[Geom::X] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/xy/spacing_x", 0.0), gridunit, "px");
+ spacing[Geom::Y] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/xy/spacing_y", 0.0), gridunit, "px");
+ render_dotted = prefs->getBool("/options/grids/xy/dotted", false);
+
+ snapper = new CanvasXYGridSnapper(this, &namedview->snap_manager, 0);
+
+ if (repr) readRepr();
+}
+
+CanvasXYGrid::~CanvasXYGrid ()
+{
+ if (snapper) delete snapper;
+}
+
+static gboolean sp_nv_read_opacity(gchar const *str, guint32 *color)
+{
+ if (!str) {
+ return FALSE;
+ }
+
+ gchar *u;
+ gdouble v = g_ascii_strtod(str, &u);
+ if (!u) {
+ return FALSE;
+ }
+ v = CLAMP(v, 0.0, 1.0);
+
+ *color = (*color & 0xffffff00) | (guint32) floor(v * 255.9999);
+
+ return TRUE;
+}
+
+/** If the passed int is invalid (<=0), then set the widget and the int
+ to use the given old value.
+
+ @param oldVal Old value to use if the new one is invalid.
+ @param pTarget The int to validate.
+ @param widget Widget associated with the int.
+*/
+static void validateInt(gint oldVal,
+ gint* pTarget)
+{
+ // Avoid nullness.
+ if ( pTarget == nullptr )
+ return;
+
+ // Invalid new value?
+ if ( *pTarget <= 0 ) {
+ // If the old value is somehow invalid as well, then default to 1.
+ if ( oldVal <= 0 )
+ oldVal = 1;
+
+ // Reset the int and associated widget to the old value.
+ *pTarget = oldVal;
+ } //if
+
+} //validateInt
+
+void
+CanvasXYGrid::readRepr()
+{
+ SPRoot *root = doc->getRoot();
+ double scale_x = 1.0;
+ double scale_y = 1.0;
+ if( root->viewBox_set ) {
+ scale_x = root->width.computed / root->viewBox.width();
+ scale_y = root->height.computed / root->viewBox.height();
+ if (Geom::are_near(scale_x / scale_y, 1.0, Geom::EPSILON)) {
+ // scaling is uniform, try to reduce numerical error
+ scale_x = (scale_x + scale_y)/2.0;
+ double scale_none = Inkscape::Util::Quantity::convert(1, doc->getDisplayUnit(), "px");
+ if (Geom::are_near(scale_x / scale_none, 1.0, Geom::EPSILON))
+ scale_x = scale_none; // objects are same size, reduce numerical error
+ scale_y = scale_x;
+ }
+ }
+
+ gchar const *value;
+
+ if ( (value = repr->attribute("originx")) ) {
+
+ Inkscape::Util::Quantity q = unit_table.parseQuantity(value);
+
+ if( q.unit->type == UNIT_TYPE_LINEAR ) {
+ // Legacy grid not in 'user units'
+ origin[Geom::X] = q.value("px");
+ legacy = true;
+ if (q.unit->abbr == "px" ) {
+ pixel = true;
+ }
+ } else {
+ // Grid in 'user units'
+ origin[Geom::X] = q.quantity * scale_x;
+ }
+ }
+
+ if ( (value = repr->attribute("originy")) ) {
+
+ Inkscape::Util::Quantity q = unit_table.parseQuantity(value);
+
+ if( q.unit->type == UNIT_TYPE_LINEAR ) {
+ // Legacy grid not in 'user units'
+ origin[Geom::Y] = q.value("px");
+ legacy = true;
+ if (q.unit->abbr == "px" ) {
+ pixel = true;
+ }
+ } else {
+ // Grid in 'user units'
+ origin[Geom::Y] = q.quantity * scale_y;
+ }
+ }
+
+ if ( (value = repr->attribute("spacingx")) ) {
+
+ // Ensure a valid default value
+ if( spacing[Geom::X] <= 0.0 )
+ spacing[Geom::X] = 1.0;
+
+ Inkscape::Util::Quantity q = unit_table.parseQuantity(value);
+ // Ensure a valid new value
+ if( q.quantity > 0 ) {
+ if( q.unit->type == UNIT_TYPE_LINEAR ) {
+ // Legacy grid not in 'user units'
+ spacing[Geom::X] = q.value("px");
+ legacy = true;
+ if (q.unit->abbr == "px" ) {
+ pixel = true;
+ }
+ } else {
+ // Grid in 'user units'
+ spacing[Geom::X] = q.quantity * scale_x;
+ }
+ }
+ }
+
+ if ( (value = repr->attribute("spacingy")) ) {
+
+ // Ensure a valid default value
+ if( spacing[Geom::Y] <= 0.0 )
+ spacing[Geom::Y] = 1.0;
+
+ Inkscape::Util::Quantity q = unit_table.parseQuantity(value);
+ // Ensure a valid new value
+ if( q.quantity > 0 ) {
+ if( q.unit->type == UNIT_TYPE_LINEAR ) {
+ // Legacy grid not in 'user units'
+ spacing[Geom::Y] = q.value("px");
+ legacy = true;
+ if (q.unit->abbr == "px" ) {
+ pixel = true;
+ }
+ } else {
+ // Grid in 'user units'
+ spacing[Geom::Y] = q.quantity * scale_y;
+ }
+ }
+ }
+
+ if ( (value = repr->attribute("color")) ) {
+ color = (color & 0xff) | sp_svg_read_color(value, color);
+ }
+
+ if ( (value = repr->attribute("empcolor")) ) {
+ empcolor = (empcolor & 0xff) | sp_svg_read_color(value, empcolor);
+ }
+
+ if ( (value = repr->attribute("opacity")) ) {
+ sp_nv_read_opacity(value, &color);
+ }
+ if ( (value = repr->attribute("empopacity")) ) {
+ sp_nv_read_opacity(value, &empcolor);
+ }
+
+ if ( (value = repr->attribute("empspacing")) ) {
+ gint oldVal = empspacing;
+ empspacing = atoi(value);
+ validateInt( oldVal, &empspacing);
+ }
+
+ if ( (value = repr->attribute("dotted")) ) {
+ render_dotted = (strcmp(value,"false") != 0 && strcmp(value, "0") != 0);
+ }
+
+ if ( (value = repr->attribute("visible")) ) {
+ visible = (strcmp(value,"false") != 0 && strcmp(value, "0") != 0);
+ }
+
+ if ( (value = repr->attribute("enabled")) ) {
+ g_assert(snapper != nullptr);
+ snapper->setEnabled(strcmp(value,"false") != 0 && strcmp(value, "0") != 0);
+ }
+
+ if ( (value = repr->attribute("snapvisiblegridlinesonly")) ) {
+ g_assert(snapper != nullptr);
+ snapper->setSnapVisibleOnly(strcmp(value,"false") != 0 && strcmp(value, "0") != 0);
+ }
+
+ if ( (value = repr->attribute("units")) ) {
+ gridunit = unit_table.getUnit(value); // Display unit identifier in grid menu
+ }
+
+ for (auto grid : canvas_item_grids) {
+ grid->request_update();
+ }
+
+ return;
+}
+
+/**
+ * Called when XML node attribute changed; updates dialog widgets if change was not done by widgets themselves.
+ */
+void
+CanvasXYGrid::onReprAttrChanged(Inkscape::XML::Node */*repr*/, gchar const */*key*/, gchar const */*oldval*/, gchar const */*newval*/, bool /*is_interactive*/)
+{
+ readRepr();
+ updateWidgets();
+}
+
+
+Gtk::Widget *
+CanvasXYGrid::newSpecificWidget()
+{
+ _rumg = Gtk::manage( new Inkscape::UI::Widget::RegisteredUnitMenu(
+ _("Grid _units:"), "units", _wr, repr, doc) );
+ _rsu_ox = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit(
+ _("_Origin X:"), _("X coordinate of grid origin"), "originx",
+ *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_x) );
+ _rsu_oy = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit(
+ _("O_rigin Y:"), _("Y coordinate of grid origin"), "originy",
+ *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_y) );
+ _rsu_sx = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit(
+ _("Spacing _X:"), _("Distance between vertical grid lines"), "spacingx",
+ *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_x) );
+ _rsu_sy = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit(
+ _("Spacing _Y:"), _("Distance between horizontal grid lines"), "spacingy",
+ *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_y) );
+
+ _rcp_gcol = Gtk::manage( new Inkscape::UI::Widget::RegisteredColorPicker(
+ _("Minor grid line _color:"), _("Minor grid line color"), _("Color of the minor grid lines"),
+ "color", "opacity", _wr, repr, doc) );
+
+ _rcp_gmcol = Gtk::manage( new Inkscape::UI::Widget::RegisteredColorPicker(
+ _("Ma_jor grid line color:"), _("Major grid line color"),
+ _("Color of the major (highlighted) grid lines"), "empcolor", "empopacity",
+ _wr, repr, doc) );
+
+ _rsi = Gtk::manage( new Inkscape::UI::Widget::RegisteredSuffixedInteger(
+ _("_Major grid line every:"), "", _("lines"), "empspacing", _wr, repr, doc) );
+
+ _rumg->set_hexpand();
+ _rsu_ox->set_hexpand();
+ _rsu_oy->set_hexpand();
+ _rsu_sx->set_hexpand();
+ _rsu_sy->set_hexpand();
+ _rcp_gcol->set_hexpand();
+ _rcp_gmcol->set_hexpand();
+ _rsi->set_hexpand();
+
+ // set widget values
+ _wr.setUpdating (true);
+
+ _rsu_ox->setDigits(5);
+ _rsu_ox->setIncrements(0.1, 1.0);
+
+ _rsu_oy->setDigits(5);
+ _rsu_oy->setIncrements(0.1, 1.0);
+
+ _rsu_sx->setDigits(5);
+ _rsu_sx->setIncrements(0.1, 1.0);
+
+ _rsu_sy->setDigits(5);
+ _rsu_sy->setIncrements(0.1, 1.0);
+
+ _rumg->setUnit (gridunit->abbr);
+
+ gdouble val;
+ val = origin[Geom::X];
+ val = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_ox->setValue (val);
+ val = origin[Geom::Y];
+ val = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_oy->setValue (val);
+ val = spacing[Geom::X];
+ double gridx = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_sx->setValue (gridx);
+ val = spacing[Geom::Y];
+ double gridy = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_sy->setValue (gridy);
+
+ _rcp_gcol->setRgba32 (color);
+ _rcp_gmcol->setRgba32 (empcolor);
+ _rsi->setValue (empspacing);
+
+ _wr.setUpdating (false);
+
+ _rsu_ox->setProgrammatically = false;
+ _rsu_oy->setProgrammatically = false;
+ _rsu_sx->setProgrammatically = false;
+ _rsu_sy->setProgrammatically = false;
+
+ Gtk::Box *column = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4));
+ column->pack_start(*_rumg, true, false);
+ column->pack_start(*_rsu_ox, true, false);
+ column->pack_start(*_rsu_oy, true, false);
+ column->pack_start(*_rsu_sx, true, false);
+ column->pack_start(*_rsu_sy, true, false);
+ column->pack_start(*_rcp_gcol, true, false);
+ column->pack_start(*_rcp_gmcol, true, false);
+ column->pack_start(*_rsi, true, false);
+
+ return column;
+}
+
+
+/**
+ * Update dialog widgets from object's values.
+ */
+void
+CanvasXYGrid::updateWidgets()
+{
+ if (_wr.isUpdating()) return;
+
+ //no widgets (grid created with the document, not with the dialog)
+ if (!_rcb_visible) return;
+
+ _wr.setUpdating (true);
+
+ _rcb_visible->setActive(visible);
+ if (snapper != nullptr) {
+ _rcb_enabled->setActive(snapper->getEnabled());
+ _rcb_snap_visible_only->setActive(snapper->getSnapVisibleOnly());
+ }
+
+ _rumg->setUnit (gridunit->abbr);
+
+ gdouble val;
+
+ val = origin[Geom::X];
+ val = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_ox->setValue (val);
+
+ val = origin[Geom::Y];
+ val = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_oy->setValue (val);
+
+ val = spacing[Geom::X];
+ val = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_sx->setValue (val);
+
+ val = spacing[Geom::Y];
+ val = Inkscape::Util::Quantity::convert(val, "px", gridunit);
+ _rsu_sy->setValue (val);
+
+ _rsu_ox->setProgrammatically = false;
+ _rsu_oy->setProgrammatically = false;
+ _rsu_sx->setProgrammatically = false;
+ _rsu_sy->setProgrammatically = false;
+
+ _rcp_gcol->setRgba32 (color);
+ _rcp_gmcol->setRgba32 (empcolor);
+ _rsi->setValue (empspacing);
+ _rcb_dotted->setActive (render_dotted);
+
+ _wr.setUpdating (false);
+}
+
+// For correcting old SVG Inkscape files
+void
+CanvasXYGrid::Scale (Geom::Scale const &scale ) {
+ origin *= scale;
+ spacing *= scale;
+
+ // Write out in 'user-units'
+ Inkscape::SVGOStringStream os_x, os_y, ss_x, ss_y;
+ os_x << origin[Geom::X];
+ os_y << origin[Geom::Y];
+ ss_x << spacing[Geom::X];
+ ss_y << spacing[Geom::Y];
+ repr->setAttribute("originx", os_x.str());
+ repr->setAttribute("originy", os_y.str());
+ repr->setAttribute("spacingx", ss_x.str());
+ repr->setAttribute("spacingy", ss_y.str());
+}
+
+void
+CanvasXYGrid::Update (Geom::Affine const &affine, unsigned int /*flags*/)
+{
+ ow = origin * affine;
+ sw[0] = Geom::Point(spacing[0], 0) * affine.withoutTranslation();
+ sw[1] = Geom::Point(0, spacing[1]) * affine.withoutTranslation();
+
+ // Find suitable grid spacing for display
+ for(int dim = 0; dim < 2; dim++) {
+ gint scaling_factor = empspacing;
+
+ if (scaling_factor <= 1)
+ scaling_factor = 5;
+
+ scaled[dim] = false;
+ while (fabs(sw[dim].length()) < 8.0) {
+ scaled[dim] = true;
+ sw[dim] *= scaling_factor;
+ /* First pass, go up to the major line spacing, then
+ keep increasing by two. */
+ scaling_factor = 2;
+ }
+ }
+}
+
+
+// Find intersections of line with rectangle. There should be zero or two.
+// If line is degenerate with rectangle side, two corner points are returned.
+static std::vector<Geom::Point>
+intersect_line_rectangle( Geom::Line const &line, Geom::Rect const &rect )
+{
+ std::vector<Geom::Point> intersections;
+ for (unsigned i = 0; i < 4; ++i) {
+ Geom::LineSegment side( rect.corner(i), rect.corner((i+1)%4) );
+ try {
+ Geom::OptCrossing oc = Geom::intersection(line, side);
+ if (oc) {
+ intersections.push_back( line.pointAt((*oc).ta));
+ }
+ } catch (Geom::InfiniteSolutions) {
+ intersections.clear();
+ intersections.push_back( side.pointAt(0) );
+ intersections.push_back( side.pointAt(1) );
+ return intersections;
+ }
+ }
+ return intersections;
+}
+
+// Find the signed distance of a point to a line. The distance is negative if
+// the point lies to the left of the line considering the line's versor.
+static double
+signed_distance( Geom::Point const &point, Geom::Line const &line ) {
+ Geom::Coord t = line.nearestTime( point );
+ Geom::Point p = line.pointAt(t);
+ double distance = Geom::distance( p, point );
+ if ( Geom::cross( Geom::Line( p, point ).versor(), line.versor() ) < 0.0 ) {
+ distance = -distance;
+ }
+ return distance;
+}
+
+void
+CanvasXYGrid::Render (Inkscape::CanvasItemBuffer *buf)
+{
+
+ // no_emphasize_when_zoomedout determines color (minor or major) when only major grid lines/dots shown.
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ guint32 _empcolor;
+ guint32 _color = color;
+ bool no_emp_when_zoomed_out = prefs->getBool("/options/grids/no_emphasize_when_zoomedout", false);
+ if( (scaled[Geom::X] || scaled[Geom::Y]) && no_emp_when_zoomed_out ) {
+ _empcolor = color;
+ } else {
+ _empcolor = empcolor;
+ }
+
+ bool xrayactive = prefs->getBool("/desktop/xrayactive", false);
+ if (xrayactive) {
+ guint32 bg = 0xffffffff;
+ _color = SP_RGBA32_F_COMPOSE(
+ CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_R_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_R_F(_color)), 0.0, 1.0),
+ CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_G_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_G_F(_color)), 0.0, 1.0),
+ CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_B_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_B_F(_color)), 0.0, 1.0),
+ 1.0);
+ _empcolor = SP_RGBA32_F_COMPOSE(
+ CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_R_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_R_F(_empcolor)), 0.0, 1.0),
+ CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_G_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_G_F(_empcolor)), 0.0, 1.0),
+ CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_B_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_B_F(_empcolor)), 0.0, 1.0),
+ 1.0);
+ }
+
+ buf->cr->save();
+ buf->cr->translate(-buf->rect.left(), -buf->rect.top());
+ buf->cr->set_line_width(1.0);
+ buf->cr->set_line_cap(Cairo::LINE_CAP_SQUARE);
+
+ // Adding a 2 px margin to the buffer rectangle to avoid missing intersections (in case of rounding errors, and due to adding 0.5 below)
+ Geom::IntRect buf_rect_with_margin = buf->rect;
+ buf_rect_with_margin.expandBy(2);
+
+ for (unsigned dim = 0; dim < 2; ++dim) {
+
+ // std::cout << "\n " << (dim==0?"Horizontal":"Vertical") << " ------------" << std::endl;
+
+ // Construct an axis line through origin with direction normal to grid spacing.
+ Geom::Line axis = Geom::Line::from_origin_and_vector( ow, sw[dim] );
+ Geom::Line orth = Geom::Line::from_origin_and_vector( ow, sw[(dim+1)%2] );
+
+ double spacing = sw[(dim+1)%2].length(); // Spacing between grid lines.
+ double dash = sw[dim].length(); // Total length of dash pattern.
+
+ // std::cout << " axis: " << axis.origin() << ", " << axis.vector() << std::endl;
+ // std::cout << " spacing: " << spacing << std::endl;
+ // std::cout << " dash period: " << dash << std::endl;
+
+ // Find the minimum and maximum distances of the buffer corners from axis.
+ double min = Geom::infinity();
+ double max = -Geom::infinity();
+ for (unsigned c = 0; c < 4; ++c) {
+
+ // We need signed distance... lib2geom offers only positive distance.
+ double distance = signed_distance( buf_rect_with_margin.corner(c), axis );
+
+ // Correct it for coordinate flips (inverts handedness).
+ if (Geom::cross( axis.vector(), orth.vector() ) > 0 ) {
+ distance = -distance;
+ }
+
+ if (distance < min)
+ min = distance;
+ if (distance > max)
+ max = distance;
+ }
+ int start = floor( min/spacing );
+ int stop = floor( max/spacing );
+
+ // std::cout << " rect: " << buf->rect << std::endl;
+ // std::cout << " min: " << min << " max: " << max << std::endl;
+ // std::cout << " start: " << start << " stop: " << stop << std::endl;
+
+ // Loop over grid lines that intersected buf rectangle.
+ for (int j = start+1; j <= stop; ++j) {
+
+ Geom::Line grid_line = Geom::make_parallel_line( ow + j * sw[(dim+1)%2], axis );
+
+ std::vector<Geom::Point> x = intersect_line_rectangle( grid_line, buf_rect_with_margin );
+
+ // If we have two intersections, grid line intersects buffer rectangle.
+ if (x.size() == 2 ) {
+ // Make sure lines are always drawn in the same direction (or dashes misplaced).
+ Geom::Line vector( x[0], x[1]);
+ if (Geom::dot( vector.vector(), axis.vector() ) < 0.0) {
+ std::swap(x[0], x[1]);
+ }
+
+ // Set up line. Need to use floor()+0.5 such that Cairo will draw us lines with a width of a single pixel, without any aliasing.
+ // For this we need to position the lines at exactly half pixels, see https://www.cairographics.org/FAQ/#sharp_lines
+ // Must be consistent with the pixel alignment of the guide lines, see CanvasXYGrid::Render(), and the drawing of the rulers
+ buf->cr->move_to(floor(x[0][Geom::X]) + 0.5, floor(x[0][Geom::Y]) + 0.5);
+ buf->cr->line_to(floor(x[1][Geom::X]) + 0.5, floor(x[1][Geom::Y]) + 0.5);
+
+ // Set dash pattern and color.
+ if (render_dotted) {
+ // alpha needs to be larger than in the line case to maintain a similar
+ // visual impact but setting it to the maximal value makes the dots
+ // dominant in some cases. Solution, increase the alpha by a factor of
+ // 4. This then allows some user adjustment.
+ guint32 _empdot = (_empcolor & 0xff) << 2;
+ if (_empdot > 0xff)
+ _empdot = 0xff;
+ _empdot += (_empcolor & 0xffffff00);
+
+ guint32 _colordot = (_color & 0xff) << 2;
+ if (_colordot > 0xff)
+ _colordot = 0xff;
+ _colordot += (_color & 0xffffff00);
+
+ // Dash pattern must use spacing from orthogonal direction.
+ // Offset is to center dash on orthogonal lines.
+ double offset = fmod( signed_distance( x[0], orth ), sw[dim].length());
+ if (Geom::cross( axis.vector(), orth.vector() ) > 0 ) {
+ offset = -offset;
+ }
+
+ std::vector<double> dashes;
+ if (!scaled[dim] && (j % empspacing) != 0) {
+ // Minor lines
+ dashes.push_back(1.0);
+ dashes.push_back(dash - 1.0);
+ offset -= 0.5;
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(_colordot), SP_RGBA32_G_F(_colordot),
+ SP_RGBA32_B_F(_colordot), SP_RGBA32_A_F(_colordot));
+ } else {
+ // Major lines
+ dashes.push_back(3.0);
+ dashes.push_back(dash - 3.0);
+ offset -= 1.5; // Center dash on intersection.
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(_empdot), SP_RGBA32_G_F(_empdot),
+ SP_RGBA32_B_F(_empdot), SP_RGBA32_A_F(_empdot));
+ }
+
+ buf->cr->set_line_cap(Cairo::LINE_CAP_BUTT);
+ buf->cr->set_dash(dashes, -offset);
+
+ } else {
+
+ // Solid lines
+
+ // Set color
+ if (!scaled[dim] && (j % empspacing) != 0) {
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(_color), SP_RGBA32_G_F(_color),
+ SP_RGBA32_B_F(_color), SP_RGBA32_A_F(_color));
+ } else {
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(_empcolor), SP_RGBA32_G_F(_empcolor),
+ SP_RGBA32_B_F(_empcolor), SP_RGBA32_A_F(_empcolor));
+ }
+ }
+
+ buf->cr->stroke();
+
+ } else {
+ std::cerr << "CanvasXYGrid::render: Grid line doesn't intersect!" << std::endl;
+ }
+ }
+ }
+
+ buf->cr->restore();
+}
+
+CanvasXYGridSnapper::CanvasXYGridSnapper(CanvasXYGrid *grid, SnapManager *sm, Geom::Coord const d) : LineSnapper(sm, d)
+{
+ this->grid = grid;
+}
+
+/**
+ * \return Snap tolerance (desktop coordinates); depends on current zoom so that it's always the same in screen pixels
+ */
+Geom::Coord CanvasXYGridSnapper::getSnapperTolerance() const
+{
+ SPDesktop const *dt = _snapmanager->getDesktop();
+ double const zoom = dt ? dt->current_zoom() : 1;
+ return _snapmanager->snapprefs.getGridTolerance() / zoom;
+}
+
+bool CanvasXYGridSnapper::getSnapperAlwaysSnap() const
+{
+ return _snapmanager->snapprefs.getGridTolerance() == 10000; //TODO: Replace this threshold of 10000 by a constant; see also tolerance-slider.cpp
+}
+
+LineSnapper::LineList
+CanvasXYGridSnapper::_getSnapLines(Geom::Point const &p) const
+{
+ LineList s;
+
+ if ( grid == nullptr ) {
+ return s;
+ }
+
+ for (unsigned int i = 0; i < 2; ++i) {
+
+ double spacing;
+
+ if (getSnapVisibleOnly()) {
+ // Only snapping to visible grid lines
+ spacing = grid->sw[i].length(); // this is the spacing of the visible grid lines measured in screen pixels
+ // convert screen pixels to px
+ // FIXME: after we switch to snapping dist in screen pixels, this will be unnecessary
+ SPDesktop const *dt = _snapmanager->getDesktop();
+ if (dt) {
+ spacing /= dt->current_zoom();
+ }
+ } else {
+ // Snapping to any grid line, whether it's visible or not
+ spacing = grid->spacing[i];
+ }
+
+ Geom::Coord rounded;
+ Geom::Point point_on_line;
+ Geom::Point cvec(0.,0.);
+ cvec[i] = 1.;
+
+ rounded = Inkscape::Util::round_to_upper_multiple_plus(p[i], spacing, grid->origin[i]);
+ point_on_line = i ? Geom::Point(0, rounded) : Geom::Point(rounded, 0);
+ s.push_back(std::make_pair(cvec, point_on_line));
+
+ rounded = Inkscape::Util::round_to_lower_multiple_plus(p[i], spacing, grid->origin[i]);
+ point_on_line = i ? Geom::Point(0, rounded) : Geom::Point(rounded, 0);
+ s.push_back(std::make_pair(cvec, point_on_line));
+ }
+
+ return s;
+}
+
+void CanvasXYGridSnapper::_addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, Geom::Point const &point_on_line) const
+{
+ SnappedLine dummy = SnappedLine(snapped_point, snapped_distance, source, source_num, Inkscape::SNAPTARGET_GRID, getSnapperTolerance(), getSnapperAlwaysSnap(), normal_to_line, point_on_line);
+ isr.grid_lines.push_back(dummy);
+}
+
+void CanvasXYGridSnapper::_addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const
+{
+ SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GRID, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true);
+ isr.points.push_back(dummy);
+}
+
+void CanvasXYGridSnapper::_addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const
+{
+ SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GRID_PERPENDICULAR, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true);
+ isr.points.push_back(dummy);
+}
+
+/**
+ * \return true if this Snapper will snap at least one kind of point.
+ */
+bool CanvasXYGridSnapper::ThisSnapperMightSnap() const
+{
+ return _snap_enabled && _snapmanager->snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_GRID);
+}
+
+} // namespace Inkscape
+
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-grid.h b/src/display/control/canvas-grid.h
new file mode 100644
index 0000000..860a106
--- /dev/null
+++ b/src/display/control/canvas-grid.h
@@ -0,0 +1,198 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Cartesian grid item for the Inkscape canvas.
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Copyright (C) Johan Engelen 2006-2007 <johan@shouraizou.nl>
+ * Copyright (C) Lauris Kaplinski 2000
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_CANVAS_GRID_H
+#define INKSCAPE_CANVAS_GRID_H
+
+#include "ui/widget/alignment-selector.h"
+#include "ui/widget/registered-widget.h"
+#include "ui/widget/registry.h"
+#include "line-snapper.h"
+
+class SPDesktop;
+class SPNamedView;
+class SPDocument;
+
+namespace Gtk {
+ class Widget;
+}
+
+namespace Inkscape {
+
+class Snapper;
+class CanvasItemBuffer;
+class CanvasItemGrid;
+
+namespace XML {
+class Node;
+}
+
+namespace Util {
+class Unit;
+}
+
+enum GridType {
+ GRID_RECTANGULAR = 0,
+ GRID_AXONOMETRIC = 1
+};
+
+#define GRID_MAXTYPENR 1
+
+#define GRID_DEFAULT_COLOR 0x0099e526
+#define GRID_DEFAULT_EMPCOLOR 0x0099e54d
+
+class CanvasGrid {
+public:
+ virtual ~CanvasGrid();
+
+ // TODO: see effect.h and effect.cpp from live_effects how to link enums to SVGname to typename properly. (johan)
+ const char * getName() const;
+ const char * getSVGName() const;
+ GridType getGridType() const;
+ static const char * getName(GridType type);
+ static const char * getSVGName(GridType type);
+ static GridType getGridTypeFromSVGName(const char * typestr);
+ static GridType getGridTypeFromName(const char * typestr);
+
+ static CanvasGrid* NewGrid(SPNamedView * nv, Inkscape::XML::Node * repr, SPDocument *doc, GridType gridtype);
+ static void writeNewGridToRepr(Inkscape::XML::Node * repr, SPDocument * doc, GridType gridtype);
+
+ Inkscape::CanvasItemGrid * createCanvasItem(SPDesktop * desktop);
+ void removeCanvasItem(Inkscape::CanvasItemGrid *item);
+
+ virtual void Update (Geom::Affine const &affine, unsigned int flags) = 0;
+ virtual void Render (Inkscape::CanvasItemBuffer *buf) = 0;
+
+ virtual void readRepr() = 0;
+ virtual void onReprAttrChanged (Inkscape::XML::Node * /*repr*/, char const */*key*/, char const */*oldval*/, char const */*newval*/, bool /*is_interactive*/) = 0;
+
+ Gtk::Widget * newWidget();
+ static void *notifyWidgetsDestroyed(void *data);
+
+ void setOrigin(Geom::Point const &origin_px); /**< writes new origin (specified in px units) to SVG */
+ Geom::Point origin; /**< Origin of the grid */
+
+ guint32 color; /**< Color for normal lines */
+ guint32 empcolor; /**< Color for emphasis lines */
+ gint empspacing; /**< Spacing between emphasis lines */
+
+ Inkscape::Util::Unit const* gridunit; /**< points to Unit object in UnitTable (so don't delete it) */
+
+ Inkscape::XML::Node * repr;
+ SPDocument *doc;
+
+ Inkscape::Snapper* snapper;
+
+ static void on_repr_attr_changed (Inkscape::XML::Node * repr, const gchar *key, const gchar *oldval, const gchar *newval, bool is_interactive, void * data);
+
+ bool isLegacy() const { return legacy; }
+ bool isPixel() const { return pixel; }
+
+ bool isVisible() const { return (isEnabled() &&visible); };
+ bool isEnabled() const;
+
+ void align_clicked(int align);
+
+protected:
+ CanvasGrid(SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument *in_doc, GridType type);
+
+ virtual Gtk::Widget * newSpecificWidget() = 0;
+
+ std::vector<Inkscape::CanvasItemGrid *> canvas_item_grids; // List of created CanvasGridItem's.
+
+ SPNamedView * namedview;
+
+ Inkscape::UI::Widget::Registry _wr;
+ bool visible;
+ bool render_dotted;
+
+ GridType gridtype;
+
+ // For dealing with old Inkscape SVG files (pre 0.92)
+ bool legacy;
+ bool pixel;
+
+ Inkscape::UI::Widget::RegisteredCheckButton *_rcb_enabled = nullptr;
+ Inkscape::UI::Widget::RegisteredCheckButton *_rcb_snap_visible_only = nullptr;
+ Inkscape::UI::Widget::RegisteredCheckButton *_rcb_visible = nullptr;
+ Inkscape::UI::Widget::RegisteredCheckButton *_rcb_dotted = nullptr;
+ Inkscape::UI::Widget::AlignmentSelector *_as_alignment = nullptr;
+
+private:
+ CanvasGrid(const CanvasGrid&) = delete;
+ CanvasGrid& operator=(const CanvasGrid&) = delete;
+};
+
+
+class CanvasXYGrid : public CanvasGrid {
+public:
+ CanvasXYGrid(SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument * in_doc);
+ ~CanvasXYGrid() override;
+
+ virtual void Scale (Geom::Scale const &scale);
+ void Update (Geom::Affine const &affine, unsigned int flags) override;
+ void Render (Inkscape::CanvasItemBuffer *buf) override;
+
+ void readRepr() override;
+ void onReprAttrChanged (Inkscape::XML::Node * repr, char const *key, char const *oldval, char const *newval, bool is_interactive) override;
+
+ Geom::Point spacing; /**< Spacing between elements of the grid */
+ bool scaled[2]; /**< Whether the grid is in scaled mode, which can
+ be different in the X or Y direction, hence two
+ variables */
+ Geom::Point ow; /**< Transformed origin by the affine for the zoom */
+ Geom::Point sw[2]; /**< Transformed spacing by the affine for the zoom */
+
+protected:
+ Gtk::Widget * newSpecificWidget() override;
+
+private:
+ CanvasXYGrid(const CanvasXYGrid&) = delete;
+ CanvasXYGrid& operator=(const CanvasXYGrid&) = delete;
+
+ void updateWidgets();
+
+ Inkscape::UI::Widget::RegisteredUnitMenu *_rumg = nullptr;
+ Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_ox = nullptr;
+ Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_oy = nullptr;
+ Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_sx = nullptr;
+ Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_sy = nullptr;
+ Inkscape::UI::Widget::RegisteredColorPicker *_rcp_gcol = nullptr;
+ Inkscape::UI::Widget::RegisteredColorPicker *_rcp_gmcol = nullptr;
+ Inkscape::UI::Widget::RegisteredSuffixedInteger *_rsi = nullptr;
+};
+
+
+
+class CanvasXYGridSnapper : public LineSnapper
+{
+public:
+ CanvasXYGridSnapper(CanvasXYGrid *grid, SnapManager *sm, Geom::Coord const d);
+ bool ThisSnapperMightSnap() const override;
+
+ Geom::Coord getSnapperTolerance() const override; //returns the tolerance of the snapper in screen pixels (i.e. independent of zoom)
+ bool getSnapperAlwaysSnap() const override; //if true, then the snapper will always snap, regardless of its tolerance
+
+private:
+ LineList _getSnapLines(Geom::Point const &p) const override;
+ void _addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, const Geom::Point &point_on_line) const override;
+ void _addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override;
+ void _addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override;
+ CanvasXYGrid *grid;
+};
+
+}; /* namespace Inkscape */
+
+
+
+
+#endif
diff --git a/src/display/control/canvas-item-bpath.cpp b/src/display/control/canvas-item-bpath.cpp
new file mode 100644
index 0000000..6938ac5
--- /dev/null
+++ b/src/display/control/canvas-item-bpath.cpp
@@ -0,0 +1,262 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a Bezier path.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasBPath
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item-bpath.h"
+
+#include <memory> // unique_ptr
+#include <utility> // std::move
+
+#include "color.h" // SP_RGBA_x_F
+
+#include "display/curve.h"
+#include "display/cairo-utils.h"
+
+#include "helper/geom.h" // bounds_exact_transformed()
+
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+/**
+ * Create an null control bpath.
+ */
+CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemBpath:Null";
+ _pickable = true; // For now, nobody gets events from this class!
+}
+
+/**
+ * Create a control bpath. Curve is in document coordinates.
+ */
+CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group, SPCurve *curve, bool phantom_line)
+ : CanvasItem(group)
+ , _phantom_line(phantom_line)
+{
+ _name = "CanvasItemBpath";
+ _pickable = true; // For now, everyone gets events from this class!
+ if (curve) {
+ _path = curve->get_pathvector();
+ }
+
+ request_update(); // Render immediately or temporary bpaths won't show.
+}
+
+/**
+ * Create a control bpath. Path is in document coordinates.
+ */
+CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group, Geom::PathVector path, bool phantom_line)
+ : CanvasItem(group)
+ , _path(std::move(path))
+ , _phantom_line(phantom_line)
+{
+ _name = "CanvasItemBpath";
+ _pickable = true; // For now, everyone gets events from this class!
+ request_update(); // Render immediately or temporary bpaths won't show.
+}
+
+/**
+ * Set a control bpath. Curve is in document coordinates.
+ */
+void CanvasItemBpath::set_bpath(SPCurve *curve, bool phantom_line)
+{
+ if (curve) { // No test to see if *curve == *_curve so we always do the swap.
+ _path = curve->get_pathvector();
+ } else {
+ _path.clear();
+ }
+
+ _phantom_line = phantom_line;
+
+ request_update();
+}
+
+/**
+ * Set a control bpath. Path is in document coordinates.
+ */
+void CanvasItemBpath::set_bpath(Geom::PathVector const &path, bool phantom_line)
+{
+ _path = path;
+ _phantom_line = phantom_line;
+
+ request_update();
+}
+
+/**
+ * Set the fill color and fill rule.
+ */
+void CanvasItemBpath::set_fill(guint rgba, SPWindRule fill_rule)
+{
+ if (_fill != rgba || _fill_rule != fill_rule) {
+ _fill = rgba;
+ _fill_rule = fill_rule;
+ request_redraw();
+ }
+}
+
+/**
+ * Returns distance between point in canvas units and nearest point on bpath.
+ */
+double CanvasItemBpath::closest_distance_to(Geom::Point const &p)
+{
+ double d = Geom::infinity();
+
+ // Convert p to document coordinates (quicker than converting path to canvas units).
+ Geom::Point p_doc = p * _affine.inverse();
+ _path.nearestTime(p_doc, &d);
+ d *= _affine.descrim(); // Uniform scaling and rotation only.
+
+ return d;
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of bpath.
+ */
+bool CanvasItemBpath::contains(Geom::Point const &p, double tolerance)
+{
+ if (tolerance == 0) {
+ tolerance = 1; // Need a minimum tolerance value or always returns false.
+ }
+
+ return closest_distance_to(p) < tolerance;
+}
+
+/**
+ * Update and redraw control bpath.
+ */
+void CanvasItemBpath::update(Geom::Affine const &affine)
+{
+ if (_affine == affine && !_need_update) {
+ // Nothing to do.
+ return;
+ }
+
+ // Queue redraw of old area (erase previous content).
+ request_redraw();
+
+ // Get new bounds
+ _affine = affine;
+
+ _bounds = Geom::Rect(); // Reset bounds
+
+ if (_path.empty()) return; // No path, no chocolate!
+
+ Geom::OptRect bbox = bounds_exact_transformed(_path, _affine);
+
+ if (bbox) {
+ _bounds = *bbox;
+ _bounds.expandBy(2);
+ } else {
+ _bounds = Geom::Rect();
+ }
+
+ // Queue redraw of new area
+ request_redraw();
+
+ _need_update = false;
+}
+
+/**
+ * Render bpath to screen via Cairo.
+ */
+void CanvasItemBpath::render(Inkscape::CanvasItemBuffer *buf)
+{
+ if (!buf) {
+ std::cerr << "CanvasItemBpath::Render: No buffer!" << std::endl;
+ return;
+ }
+
+ if (!_visible) {
+ // Hidden
+ return;
+ }
+
+ if (_path.empty()) {
+ return;
+ }
+
+ bool do_fill = (_fill & 0xff) != 0; // Not invisible.
+ bool do_stroke = (_stroke & 0xff) != 0; // Not invisible.
+
+ if (!do_fill && !do_stroke) {
+ // Both fill and stroke invisible.
+ return;
+ }
+
+ buf->cr->save();
+
+ // Setup path
+ buf->cr->set_tolerance(0.5);
+ buf->cr->begin_new_path();
+
+ feed_pathvector_to_cairo (buf->cr->cobj(), _path, _affine, buf->rect,
+ /* optimized_stroke = */ !do_fill, 1);
+
+ // Do fill
+ if (do_fill) {
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill),
+ SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill));
+ buf->cr->set_fill_rule(_fill_rule == SP_WIND_RULE_EVENODD ?
+ Cairo::FILL_RULE_EVEN_ODD : Cairo::FILL_RULE_WINDING);
+ buf->cr->fill_preserve();
+ }
+
+ // Do stroke
+ if (do_stroke) {
+
+ if (!_dashes.empty()) {
+ buf->cr->set_dash(_dashes, 0.0); // 0.0 is offset
+ }
+
+ if (_phantom_line) {
+ buf->cr->set_source_rgba(1.0, 1.0, 1.0, 0.25);
+ buf->cr->set_line_width(2.0);
+ buf->cr->stroke_preserve();
+ }
+
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke),
+ SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke));
+ buf->cr->set_line_width(1.0);
+ buf->cr->stroke();
+
+ } else {
+ buf->cr->begin_new_path(); // Clears path
+ }
+
+ // Uncomment to show bounds
+ // Geom::Rect bounds = _bounds;
+ // bounds.expandBy(-1);
+ // bounds -= buf->rect.min();
+ // buf->cr->set_source_rgba(1.0, 0.0, 0.0, 1.0);
+ // buf->cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height());
+ // buf->cr->stroke();
+
+ buf->cr->restore();
+}
+
+} // namespace Inkscape
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-bpath.h b/src/display/control/canvas-item-bpath.h
new file mode 100644
index 0000000..e430cc1
--- /dev/null
+++ b/src/display/control/canvas-item-bpath.h
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_BPATH_H
+#define SEEN_CANVAS_ITEM_BPATH_H
+
+/**
+ * A class to represent a Bezier path.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasBPath
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/transforms.h>
+#include <2geom/pathvector.h>
+
+#include "canvas-item.h"
+
+#include "style-enums.h" // Fill rule
+
+class SPCurve;
+
+namespace Inkscape {
+
+class CanvasItemGroup; // A canvas control that contains other canvas controls.
+
+class CanvasItemBpath : public CanvasItem {
+
+public:
+ CanvasItemBpath(CanvasItemGroup *group);
+ CanvasItemBpath(CanvasItemGroup *group, SPCurve *curve, bool phantom_line = false);
+ CanvasItemBpath(CanvasItemGroup *group, Geom::PathVector path, bool phantom_line = false);
+
+ // Geometry
+ void set_bpath (SPCurve *curve, bool phantom_line = false);
+ void set_bpath (Geom::PathVector const &path, bool phantom_line = false);
+ void set_affine_absolute(Geom::Affine const &affine);
+
+ void update(Geom::Affine const &affine) override;
+ double closest_distance_to(Geom::Point const &p); // Maybe not needed
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Display
+ void render(Inkscape::CanvasItemBuffer *buf) override;
+
+ // Properties
+ void set_fill (guint32 rgba, SPWindRule fill_rule);
+ void set_dashes (std::vector<double> & dashes) { _dashes = dashes; }
+
+protected:
+
+ // Geometry
+ Geom::PathVector _path;
+
+ // Properties
+ SPWindRule _fill_rule = SP_WIND_RULE_EVENODD;
+ std::vector<double> _dashes;
+ bool _phantom_line = false;
+};
+
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_BPATH_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-buffer.h b/src/display/control/canvas-item-buffer.h
new file mode 100644
index 0000000..3f829b0
--- /dev/null
+++ b/src/display/control/canvas-item-buffer.h
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_BUFFER_H
+#define SEEN_CANVAS_ITEM_BUFFER_H
+
+/**
+ * Buffer for rendering canvas items.
+ */
+
+/*
+ * Author:
+ * See git history.
+ *
+ * Copyright (C) 2020 Authors
+ *
+ * Rewrite of SPCanvasBuf.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ */
+
+#include <2geom/rect.h>
+#include <cairomm/context.h>
+#include "display/rendermode.h"
+
+namespace Inkscape {
+
+/**
+ * Class used when rendering canvas items.
+ */
+struct CanvasItemBuffer {
+ Geom::IntRect rect;
+ int device_scale; // For high DPI monitors.
+ bool outline_overlay_pass; // Hack for not painting page colour in outline overlay mode
+ Cairo::RefPtr<Cairo::Context> cr;
+};
+
+} // Namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_BUFFER_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-catchall.cpp b/src/display/control/canvas-item-catchall.cpp
new file mode 100644
index 0000000..91bd323
--- /dev/null
+++ b/src/display/control/canvas-item-catchall.cpp
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to catch events after everyone else has had a go.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasAcetate.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item-catchall.h"
+
+#include "color.h" // SP_RGBA_x_F
+
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+/**
+ * Create an null control catchall.
+ */
+CanvasItemCatchall::CanvasItemCatchall(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemCatchall";
+ _pickable = true; // Duh! That's the purpose of this class!
+ _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity());
+}
+
+/**
+ * Returns distance between point in canvas units and nearest point on catchall.
+ */
+double CanvasItemCatchall::closest_distance_to(Geom::Point const &p)
+{
+ return 0.0; // We're everywhere!
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of catchall.
+ */
+bool CanvasItemCatchall::contains(Geom::Point const &p, double tolerance)
+{
+ return true; // We contain every place!
+}
+
+/**
+ * Update and redraw control catchall.
+ */
+void CanvasItemCatchall::update(Geom::Affine const &affine)
+{
+ // No geometry to update.
+ _affine = affine;
+}
+
+/**
+ * Render catchall to screen via Cairo.
+ */
+void CanvasItemCatchall::render(Inkscape::CanvasItemBuffer *buf)
+{
+ // Do nothing! (Needed as CanvasItem is abstract.)
+}
+
+
+} // namespace Inkscape
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-catchall.h b/src/display/control/canvas-item-catchall.h
new file mode 100644
index 0000000..d40ab45
--- /dev/null
+++ b/src/display/control/canvas-item-catchall.h
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_CATCHALL_H
+#define SEEN_CANVAS_ITEM_CATCHALL_H
+
+/**
+ * A class to catch events after everyone else has had a go.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasAcetate.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/point.h>
+#include <2geom/transforms.h>
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+class CanvasItemGroup; // A canvas control that contains other canvas controls.
+
+class CanvasItemCatchall : public CanvasItem {
+
+public:
+ CanvasItemCatchall(CanvasItemGroup *group);
+
+ // Geometry
+ void update(Geom::Affine const &affine) override;
+ double closest_distance_to(Geom::Point const &p);
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Display
+ void render(Inkscape::CanvasItemBuffer *buf) override;
+
+};
+
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_CATCHALL_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-ctrl.cpp b/src/display/control/canvas-item-ctrl.cpp
new file mode 100644
index 0000000..f2696c8
--- /dev/null
+++ b/src/display/control/canvas-item-ctrl.cpp
@@ -0,0 +1,1221 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a control node.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrl
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/transforms.h>
+
+#include "canvas-item-ctrl.h"
+
+#include "preferences.h" // Default size.
+#include "display/cairo-utils.h" // argb32_from_rgba()
+
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+CanvasItemCtrl::~CanvasItemCtrl()
+{
+ delete[] _cache;
+}
+
+/**
+ * Create an null control node.
+ */
+CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemCtrl:Null";
+ _pickable = true; // Everybody gets events from this class!
+}
+
+/**
+ * Create a control ctrl. Shape auto-set by type.
+ */
+CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, Inkscape::CanvasItemCtrlType type)
+ : CanvasItem(group)
+ , _type(type)
+{
+ _name = "CanvasItemCtrl:Type_" + std::to_string(_type);
+ _pickable = true; // Everybody gets events from this class!
+
+ // Use _type to set default values:
+ set_shape_default();
+ set_size_default();
+}
+
+/**
+ * Create a control ctrl. Point is in document coordinates.
+ */
+CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, Inkscape::CanvasItemCtrlType type, Geom::Point const &p)
+ : CanvasItemCtrl(group, type)
+{
+ _position = p;
+}
+
+
+/**
+ * Create a control ctrl.
+ */
+CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, Inkscape::CanvasItemCtrlShape shape)
+ : CanvasItem(group)
+ , _shape(shape)
+ , _type(CANVAS_ITEM_CTRL_TYPE_DEFAULT)
+{
+ _name = "CanvasItemCtrl:Shape_" + std::to_string(_shape);
+ _pickable = true; // Everybody gets events from this class!
+}
+
+
+/**
+ * Create a control ctrl. Point is in document coordinates.
+ */
+CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, Inkscape::CanvasItemCtrlShape shape, Geom::Point const &p)
+ : CanvasItemCtrl(group, shape)
+{
+ _position = p;
+ request_update();
+}
+
+/**
+ * Set the position. Point is in document coordinates.
+ */
+void CanvasItemCtrl::set_position(Geom::Point const &position)
+{
+ // std::cout << "CanvasItemCtrl::set_ctrl: " << _name << ": " << position << std::endl;
+ if (_position != position) {
+ _position = position;
+ request_update();
+ }
+}
+
+/**
+ * Returns distance between point in canvas units and position of ctrl.
+ */
+double CanvasItemCtrl::closest_distance_to(Geom::Point const &p)
+{
+ // TODO: Different criteria for different shapes.
+ Geom::Point position = _position * _affine;
+ return Geom::distance(p, position);
+}
+
+/**
+ * If tolerance is zero, returns true if point p (in canvas units) is inside bounding box,
+ * else returns true if p (in canvas units) is within tolerance (canvas units) distance of ctrl.
+ * The latter assumes ctrl center anchored.
+ */
+bool CanvasItemCtrl::contains(Geom::Point const &p, double tolerance)
+{
+ // TODO: Different criteria for different shapes.
+ if (tolerance == 0) {
+ return _bounds.interiorContains(p);
+ } else {
+ return closest_distance_to(p) <= tolerance;
+ }
+}
+
+/**
+ * Update and redraw control ctrl.
+ */
+void CanvasItemCtrl::update(Geom::Affine const &affine)
+{
+ if (_affine == affine && !_need_update) {
+ // Nothing to do.
+ return;
+ }
+
+ // Queue redraw of old area (erase previous content).
+ request_redraw();
+
+ // Get new bounds
+ _affine = affine;
+
+ // We must be pixel aligned, width and height are always odd.
+ _bounds = Geom::Rect::from_xywh(-(_width/2.0 - 0.5), -(_height/2.0 - 0.5), _width, _height);
+
+ // Adjust for anchor
+ int w_half = _width/2;
+ int h_half = _height/2;
+ int dx = 0;
+ int dy = 0;
+
+ switch (_shape) {
+ case CANVAS_ITEM_CTRL_SHAPE_DARROW:
+ case CANVAS_ITEM_CTRL_SHAPE_SARROW:
+ case CANVAS_ITEM_CTRL_SHAPE_CARROW:
+ case CANVAS_ITEM_CTRL_SHAPE_SALIGN:
+ case CANVAS_ITEM_CTRL_SHAPE_CALIGN:
+ {
+
+ _angle = _anchor * M_PI/4.0 + std::atan2(_affine[1], _affine[0]);
+
+ double half = _width/2.0;
+
+ dx = - (half + 2) * cos(_angle); // Add a bit to prevent tip from overlapping due to rounding errors.
+ dy = - (half + 2) * sin(_angle);
+
+ switch (_shape) {
+
+ case CANVAS_ITEM_CTRL_SHAPE_CARROW:
+ _angle += 5 * M_PI/4.0;
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_SARROW:
+ _angle += M_PI/2.0;
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_SALIGN:
+ dx = - (half/2 + 2) * cos(_angle);
+ dy = - (half/2 + 2) * sin(_angle);
+ _angle -= M_PI/2.0;
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_CALIGN:
+ _angle -= M_PI/4.0;
+ dx = (half/2 + 2) * ( sin(_angle) - cos(_angle));
+ dy = (half/2 + 2) * (-sin(_angle) - cos(_angle));
+ break;
+
+ default:
+ break;
+ }
+
+ _built = false; // Angle may have change, must rebuild!
+
+ break;
+ }
+
+ case CANVAS_ITEM_CTRL_SHAPE_PIVOT:
+ case CANVAS_ITEM_CTRL_SHAPE_MALIGN:
+
+ _angle = std::atan2(_affine[1], _affine[0]);
+
+ _built = false; // Angle may have change, must rebuild!
+
+ break;
+
+ default:
+
+ switch (_anchor) {
+ case SP_ANCHOR_N:
+ case SP_ANCHOR_CENTER:
+ case SP_ANCHOR_S:
+ break;
+
+ case SP_ANCHOR_NW:
+ case SP_ANCHOR_W:
+ case SP_ANCHOR_SW:
+ dx = w_half;
+ break;
+
+ case SP_ANCHOR_NE:
+ case SP_ANCHOR_E:
+ case SP_ANCHOR_SE:
+ dx = -w_half;
+ break;
+ }
+
+ switch (_anchor) {
+ case SP_ANCHOR_W:
+ case SP_ANCHOR_CENTER:
+ case SP_ANCHOR_E:
+ break;
+
+ case SP_ANCHOR_NW:
+ case SP_ANCHOR_N:
+ case SP_ANCHOR_NE:
+ dy = h_half;
+ break;
+
+ case SP_ANCHOR_SW:
+ case SP_ANCHOR_S:
+ case SP_ANCHOR_SE:
+ dy = -h_half;
+ break;
+ }
+ break;
+ }
+
+ _bounds *= Geom::Translate(Geom::IntPoint(dx, dy));
+
+ // Position must also be integer.
+ Geom::Point position = _position * _affine;
+ Geom::IntPoint iposition = position.floor();
+
+ _bounds *= Geom::Translate(iposition);
+
+ // Queue redraw of new area
+ request_redraw();
+
+ _need_update = false;
+}
+
+
+static inline guint32 compose_xor(guint32 bg, guint32 fg, guint32 a)
+{
+ guint32 c = bg * (255-a) + (((bg ^ ~fg) + (bg >> 2) - (bg > 127 ? 63 : 0)) & 255) * a;
+ return (c + 127) / 255;
+}
+
+/**
+ * Render ctrl to screen via Cairo.
+ */
+void CanvasItemCtrl::render(Inkscape::CanvasItemBuffer *buf)
+{
+ if (!buf) {
+ std::cerr << "CanvasItemCtrl::Render: No buffer!" << std::endl;
+ return;
+ }
+
+ if (!_bounds.intersects(buf->rect)) {
+ return; // Control not inside buffer rectangle.
+ }
+
+ if (!_visible) {
+ return; // Hidden.
+ }
+
+ if (!_built) {
+ build_cache(buf->device_scale);
+ }
+
+ Geom::Point c = _bounds.min() - buf->rect.min();
+ int x = c.x(); // Must be pixel aligned.
+ int y = c.y();
+
+ buf->cr->save();
+
+ // This code works regardless of source type.
+
+ // 1. Copy the affected part of output to a temporary surface
+
+ // Size in device pixels. Does not set device scale.
+ int width = _width * buf->device_scale;
+ int height = _height * buf->device_scale;
+ auto work = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, width, height);
+ cairo_surface_set_device_scale(work->cobj(), buf->device_scale, buf->device_scale); // No C++ API!
+
+ auto cr = Cairo::Context::create(work);
+ cr->translate(-_bounds.left(), -_bounds.top());
+ cr->set_source(buf->cr->get_target(), buf->rect.left(), buf->rect.top());
+ cr->paint();
+ // static int a = 0;
+ // std::string name0 = "ctrl0_" + _name + "_" + std::to_string(a++) + ".png";
+ // work->write_to_png(name0);
+
+ // 2. Composite the control on a temporary surface
+ work->flush();
+ int strideb = work->get_stride();
+ unsigned char *pxb = work->get_data();
+
+ // this code allow background become isolated from rendering so we can do things like outline overlay
+ cairo_pattern_t *pattern = _canvas->get_background_pattern()->cobj();
+ guint32 backcolor = ink_cairo_pattern_get_argb32(pattern);
+ guint32 *p = _cache;
+ for (int i = 0; i < height; ++i) {
+ guint32 *pb = reinterpret_cast<guint32*>(pxb + i*strideb);
+ for (int j = 0; j < width; ++j) {
+ guint32 base = *pb;
+ guint32 cc = *p++;
+ guint32 ac = cc & 0xff;
+ if (*pb == 0 && cc != 0) {
+ base = backcolor;
+ }
+ if (ac == 0 && cc != 0) {
+ *pb++ = argb32_from_rgba(cc | 0x000000ff);
+ } else if (ac == 0) {
+ *pb++ = base;
+ } else if (
+ _mode == CANVAS_ITEM_CTRL_MODE_XOR ||
+ _mode == CANVAS_ITEM_CTRL_MODE_GRAYSCALED_XOR ||
+ _mode == CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR)
+ {
+ EXTRACT_ARGB32(base, ab,rb,gb,bb)
+ // here we get canvas color and if color to draw
+ // has opacity, we override base colors
+ // flattenig canvas color
+ EXTRACT_ARGB32(backcolor, abb,rbb,gbb,bbb)
+ if (abb != ab) {
+ rb = (ab/255.0) * rb + (1-(ab/255.0)) * rbb;
+ gb = (ab/255.0) * gb + (1-(ab/255.0)) * gbb;
+ bb = (ab/255.0) * bb + (1-(ab/255.0)) * bbb;
+ ab = 255;
+ }
+ guint32 ro = compose_xor(rb, (cc & 0xff000000) >> 24, ac);
+ guint32 go = compose_xor(gb, (cc & 0x00ff0000) >> 16, ac);
+ guint32 bo = compose_xor(bb, (cc & 0x0000ff00) >> 8, ac);
+ if (_mode == CANVAS_ITEM_CTRL_MODE_GRAYSCALED_XOR ||
+ _mode == CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR) {
+ guint32 gray = ro * 0.299 + go * 0.587 + bo * 0.114;
+ if (_mode == CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR) {
+ double f = 0.85; // desaturate by 15%
+ double p = sqrt(ro * ro * 0.299 + go * go * 0.587 + bo * bo * 0.114);
+ ro = p + (ro - p) * f;
+ go = p + (go - p) * f;
+ bo = p + (bo - p) * f;
+ } else {
+ ro = gray;
+ go = gray;
+ bo = gray;
+ }
+ }
+ ASSEMBLE_ARGB32(px, ab,ro,go,bo)
+ *pb++ = px;
+ } else {
+ *pb++ = argb32_from_rgba(cc | 0x000000ff);
+ }
+ }
+ }
+ work->mark_dirty();
+ // std::string name1 = "ctrl1_" + _name + "_" + std::to_string(a) + ".png";
+ // work->write_to_png(name1);
+
+ // 3. Replace the affected part of output with contents of temporary surface
+ buf->cr->set_source(work, x, y);
+
+
+
+ buf->cr->rectangle(x, y, _width, _height);
+ buf->cr->clip();
+ buf->cr->set_operator(Cairo::OPERATOR_SOURCE);
+ buf->cr->paint();
+ buf->cr->restore();
+}
+
+void CanvasItemCtrl::set_fill(guint32 rgba)
+{
+ if (_fill != rgba) {
+ _fill = rgba;
+ _built = false;
+ request_redraw();
+ }
+}
+
+void CanvasItemCtrl::set_stroke(guint32 rgba)
+{
+ if (_stroke != rgba) {
+ _stroke = rgba;
+ _built = false;
+ request_redraw();
+ }
+}
+
+void CanvasItemCtrl::set_shape(int shape)
+{
+ if (_shape != shape) {
+ _shape = Inkscape::CanvasItemCtrlShape(shape); // Fixme
+ _built = false;
+ request_update(); // Geometry could change
+ }
+}
+
+void CanvasItemCtrl::set_shape_default()
+{
+ switch (_type) {
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_DARROW;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_SARROW;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_CARROW;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_PIVOT;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_SALIGN:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_SALIGN;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_CALIGN:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_CALIGN;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_MALIGN:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_MALIGN;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_NODE_AUTO:
+ case CANVAS_ITEM_CTRL_TYPE_ROTATE:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_CIRCLE;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_CENTER:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_PLUS;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_SHAPER:
+ case CANVAS_ITEM_CTRL_TYPE_LPE:
+ case CANVAS_ITEM_CTRL_TYPE_NODE_CUSP:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_DIAMOND;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_POINT:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_CROSS;
+ break;
+
+ default:
+ _shape = CANVAS_ITEM_CTRL_SHAPE_SQUARE;
+ }
+}
+
+void CanvasItemCtrl::set_mode(int mode)
+{
+ if (_mode != mode) {
+ _mode = Inkscape::CanvasItemCtrlMode(mode); // Fixme
+ _built = false;
+ request_update();
+ }
+}
+
+void CanvasItemCtrl::set_pixbuf(GdkPixbuf *pixbuf)
+{
+ if (_pixbuf != pixbuf) {
+ _pixbuf = pixbuf;
+ _width = gdk_pixbuf_get_width(pixbuf);
+ _height = gdk_pixbuf_get_height(pixbuf);
+ _built = false;
+ request_update();
+ }
+}
+
+// Nominally width == height == size except possibly for pixmaps.
+void CanvasItemCtrl::set_size(int size)
+{
+ if (_pixbuf) {
+ // std::cerr << "CanvasItemCtrl::set_size: Attempting to set size on pixbuf control!" << std::endl;
+ return;
+ }
+ if (_width != size + _extra || _height != size + _extra) {
+ _width = size + _extra;
+ _height = size + _extra;
+ _built = false;
+ request_update(); // Geometry change
+ }
+}
+
+void CanvasItemCtrl::set_size_via_index(int size_index)
+{
+ // Size must always be an odd number to center on pixel.
+
+ if (size_index < 1 || size_index > 15) {
+ std::cerr << "CanvasItemCtrl::set_size_via_index: size_index out of range!" << std::endl;
+ size_index = 3;
+ }
+
+ int size = 0;
+ switch (_type) {
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE:
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW:
+ size = size_index * 2 + 7;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE:
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER:
+ size = size_index * 2 + 9; // 2 larger than HANDLE/SKEW
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_SALIGN:
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_CALIGN:
+ case CANVAS_ITEM_CTRL_TYPE_ADJ_MALIGN:
+ size = size_index * 4 + 5; // Needs to be larger to allow for rotating.
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_POINT:
+ case CANVAS_ITEM_CTRL_TYPE_ROTATE:
+ case CANVAS_ITEM_CTRL_TYPE_CENTER:
+ case CANVAS_ITEM_CTRL_TYPE_SIZER:
+ case CANVAS_ITEM_CTRL_TYPE_SHAPER:
+ case CANVAS_ITEM_CTRL_TYPE_LPE:
+ case CANVAS_ITEM_CTRL_TYPE_NODE_AUTO:
+ case CANVAS_ITEM_CTRL_TYPE_NODE_CUSP:
+ size = size_index * 2 + 5;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH:
+ case CANVAS_ITEM_CTRL_TYPE_NODE_SYMETRICAL:
+ size = size_index * 2 + 3;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_INVISIPOINT:
+ size = 1;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_ANCHOR: // vanishing point for 3D box and anchor for pencil
+ size = size_index * 2 + 1;
+ break;
+
+ case CANVAS_ITEM_CTRL_TYPE_DEFAULT:
+ size = size_index * 2 + 1;
+ break;
+
+ default:
+ g_warning("set_size_via_index: missing case for handle type: %d", static_cast<int>(_type));
+ size = size_index * 2 + 1;
+ break;
+ }
+
+ set_size(size);
+}
+
+void CanvasItemCtrl::set_size_default()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int size = prefs->getIntLimited("/options/grabsize/value", 3, 1, 15);
+ set_size_via_index(size);
+}
+
+void CanvasItemCtrl::set_size_extra(int extra)
+{
+ if (_extra != extra && _pixbuf == nullptr) { // Don't enlarge pixbuf!
+ _width += (extra - _extra);
+ _height += (extra - _extra);
+ _extra = extra;
+ _built = false;
+ request_update(); // Geometry change
+ }
+}
+
+void CanvasItemCtrl::set_type(Inkscape::CanvasItemCtrlType type)
+{
+ if (_type != type) {
+ _type = type;
+
+ // Use _type to set default values:
+ set_shape_default();
+ set_size_default();
+ _built = false;
+ request_update(); // Possible Geometry change
+ }
+}
+
+void CanvasItemCtrl::set_angle(double angle)
+{
+ if (_angle != angle) {
+ _angle = angle;
+ request_update(); // Geometry change
+ }
+}
+
+void CanvasItemCtrl::set_anchor(SPAnchorType anchor)
+{
+ if (_anchor != anchor) {
+ _anchor = anchor;
+ request_update(); // Geometry change
+ }
+}
+
+// ---------- Protected ----------
+
+// Helper function for build_cache():
+bool point_inside_triangle(Geom::Point p1,Geom::Point p2,Geom::Point p3, Geom::Point point){
+ using Geom::X;
+ using Geom::Y;
+ double denominator = (p1[X]*(p2[Y] - p3[Y]) + p1[Y]*(p3[X] - p2[X]) + p2[X]*p3[Y] - p2[Y]*p3[X]);
+ double t1 = (point[X]*(p3[Y] - p1[Y]) + point[Y]*(p1[X] - p3[X]) - p1[X]*p3[Y] + p1[Y]*p3[X]) / denominator;
+ double t2 = (point[X]*(p2[Y] - p1[Y]) + point[Y]*(p1[X] - p2[X]) - p1[X]*p2[Y] + p1[Y]*p2[X]) / -denominator;
+ double see = t1 + t2;
+ return 0 <= t1 && t1 <= 1 && 0 <= t2 && t2 <= 1 && see <= 1;
+}
+
+
+void draw_darrow(Cairo::RefPtr<Cairo::Context>cr, double size) {
+
+ // Find points, starting from tip of one arrowhead, working clockwise.
+ /* 1 4
+ ╱│ │╲
+ ╱ └────────┘ ╲
+ 0╱ 2 3 ╲5
+ ╲ 8 7 ╱
+ ╲ ┌────────┐ ╱
+ ╲│9 6│╱
+ */
+
+ // Length of arrowhead (not including stroke).
+ double delta = (size-1)/4.0; // Use unscaled width.
+
+ // Tip of arrow (0)
+ double tip_x = 0.5; // At edge, allow room for stroke.
+ double tip_y = size/2.0; // Center, assuming width == height.
+
+ // Outer corner (1)
+ double out_x = tip_x + delta;
+ double out_y = tip_y - delta;
+
+ // Inner corner (2)
+ double in_x = out_x;
+ double in_y = out_y + (delta/2.0);
+
+ double x0 = tip_x; double y0 = tip_y;
+ double x1 = out_x; double y1 = out_y;
+ double x2 = in_x; double y2 = in_y;
+ double x3 = size - in_x; double y3 = in_y;
+ double x4 = size - out_x; double y4 = out_y;
+ double x5 = size - tip_x; double y5 = tip_y;
+ double x6 = size - out_x; double y6 = size - out_y;
+ double x7 = size - in_x; double y7 = size - in_y;
+ double x8 = in_x; double y8 = size - in_y;
+ double x9 = out_x; double y9 = size - out_y;
+
+ // Draw arrow
+ cr->move_to(x0, y0);
+ cr->line_to(x1, y1);
+ cr->line_to(x2, y2);
+ cr->line_to(x3, y3);
+ cr->line_to(x4, y4);
+ cr->line_to(x5, y5);
+ cr->line_to(x6, y6);
+ cr->line_to(x7, y7);
+ cr->line_to(x8, y8);
+ cr->line_to(x9, y9);
+ cr->close_path();
+}
+
+void draw_carrow(Cairo::RefPtr<Cairo::Context>cr, double size) {
+
+ // Length of arrowhead (not including stroke).
+ double delta = (size-3)/4.0; // Use unscaled width.
+
+ // Tip of arrow
+ double tip_x = 1.5; // Edge, allow room for stroke when rotated.
+ double tip_y = delta + 1.5;
+
+ // Outer corner (1)
+ double out_x = tip_x + delta;
+ double out_y = tip_y - delta;
+
+ // Inner corner (2)
+ double in_x = out_x;
+ double in_y = out_y + (delta/2.0);
+
+ double x0 = tip_x; double y0 = tip_y;
+ double x1 = out_x; double y1 = out_y;
+ double x2 = in_x; double y2 = in_y;
+ double x3 = size - in_y; //double y3 = size - in_x;
+ double x4 = size - out_y; double y4 = size - out_x;
+ double x5 = size - tip_y; double y5 = size - tip_x;
+ double x6 = x5 - delta; double y6 = y4;
+ double x7 = x5 - delta/2.0; double y7 = y4;
+ double x8 = x1; //double y8 = y0 + delta/2.0;
+ double x9 = x1; double y9 = y0 + delta;
+
+ // Draw arrow
+ cr->move_to(x0, y0);
+ cr->line_to(x1, y1);
+ cr->line_to(x2, y2);
+ cr->arc(x1, y4, x3-x2, 3.0*M_PI/2.0, 0);
+ cr->line_to(x4, y4);
+ cr->line_to(x5, y5);
+ cr->line_to(x6, y6);
+ cr->line_to(x7, y7);
+ cr->arc_negative(x1, y4, x7-x8, 0, 3.0*M_PI/2.0);
+ cr->line_to(x9, y9);
+ cr->close_path();
+}
+
+void draw_pivot(Cairo::RefPtr<Cairo::Context>cr, double size) {
+
+ double delta4 = (size-5)/4.0; // Keep away from edge or will clip when rotating.
+ double delta8 = delta4/2;
+
+ // Line start
+ double center = size/2.0;
+
+ cr->move_to (center - delta8, center - 2*delta4 - delta8);
+ cr->rel_line_to ( delta4, 0 );
+ cr->rel_line_to ( 0, delta4);
+
+ cr->rel_line_to ( delta4, delta4);
+
+ cr->rel_line_to ( delta4, 0 );
+ cr->rel_line_to ( 0, delta4);
+ cr->rel_line_to (-delta4, 0 );
+
+ cr->rel_line_to (-delta4, delta4);
+
+ cr->rel_line_to ( 0, delta4);
+ cr->rel_line_to (-delta4, 0 );
+ cr->rel_line_to ( 0, -delta4);
+
+ cr->rel_line_to (-delta4, -delta4);
+
+ cr->rel_line_to (-delta4, 0 );
+ cr->rel_line_to ( 0, -delta4);
+ cr->rel_line_to ( delta4, 0 );
+
+ cr->rel_line_to ( delta4, -delta4);
+ cr->close_path();
+
+ cr->begin_new_sub_path();
+ cr->arc_negative(center, center, delta4, 0, -2 * M_PI);
+}
+
+void draw_salign(Cairo::RefPtr<Cairo::Context>cr, double size) {
+
+ // Triangle pointing at line.
+
+ // Basic units.
+ double delta4 = (size-1)/4.0; // Use unscaled width.
+ double delta8 = delta4/2;
+ if (delta8 < 2) {
+ // Keep a minimum gap of at least one pixel (after stroking).
+ delta8 = 2;
+ }
+
+ // Tip of triangle
+ double tip_x = size/2.0; // Center (also rotation point).
+ double tip_y = size/2.0;
+
+ // Corner triangle position.
+ double outer = size/2.0 - delta4;
+
+ // Outer line position
+ double oline = size/2.0 + (int)delta4;
+
+ // Inner line position
+ double iline = size/2.0 + (int)delta8;
+
+ // Draw triangle
+ cr->move_to(tip_x, tip_y);
+ cr->line_to(outer, outer);
+ cr->line_to(size - outer, outer);
+ cr->close_path();
+
+ // Draw line
+ cr->move_to(outer, iline);
+ cr->line_to(size - outer, iline);
+ cr->line_to(size - outer, oline);
+ cr->line_to(outer, oline);
+ cr->close_path();
+}
+
+void draw_calign(Cairo::RefPtr<Cairo::Context>cr, double size) {
+
+ // Basic units.
+ double delta4 = (size-1)/4.0; // Use unscaled width.
+ double delta8 = delta4/2;
+ if (delta8 < 2) {
+ // Keep a minimum gap of at least one pixel (after stroking).
+ delta8 = 2;
+ }
+
+ // Tip of triangle
+ double tip_x = size/2.0; // Center (also rotation point).
+ double tip_y = size/2.0;
+
+ // Corner triangle position.
+ double outer = size/2.0 - delta8 - delta4;
+
+ // End of line positin
+ double eline = size/2.0 - delta8;
+
+ // Outer line position
+ double oline = size/2.0 + (int)delta4;
+
+ // Inner line position
+ double iline = size/2.0 + (int)delta8;
+
+ // Draw triangle
+ cr->move_to(tip_x, tip_y);
+ cr->line_to(outer, tip_y);
+ cr->line_to(tip_x, outer);
+ cr->close_path();
+
+ // Draw line
+ cr->move_to(iline, iline);
+ cr->line_to(iline, eline);
+ cr->line_to(oline, eline);
+ cr->line_to(oline, oline);
+ cr->line_to(eline, oline);
+ cr->line_to(eline, iline);
+ cr->close_path();
+}
+
+void draw_malign(Cairo::RefPtr<Cairo::Context>cr, double size) {
+
+ // Basic units.
+ double delta4 = (size-1)/4.0; // Use unscaled width.
+ double delta8 = delta4/2;
+ if (delta8 < 2) {
+ // Keep a minimum gap of at least one pixel (after stroking).
+ delta8 = 2;
+ }
+
+ // Tip of triangle
+ double tip_0 = size/2.0;
+ double tip_1 = size/2.0 - delta8;
+
+ // Draw triangles
+ cr->move_to(tip_0, tip_1);
+ cr->line_to(tip_0 - delta4, tip_1 - delta4);
+ cr->line_to(tip_0 + delta4, tip_1 - delta4);
+ cr->close_path();
+
+ cr->move_to(size - tip_1, tip_0);
+ cr->line_to(size - tip_1 + delta4, tip_0 - delta4);
+ cr->line_to(size - tip_1 + delta4, tip_0 + delta4);
+ cr->close_path();
+
+ cr->move_to(size - tip_0, size - tip_1);
+ cr->line_to(size - tip_0 + delta4, size - tip_1 + delta4);
+ cr->line_to(size - tip_0 - delta4, size - tip_1 + delta4);
+ cr->close_path();
+
+ cr->move_to(tip_1, tip_0);
+ cr->line_to(tip_1 - delta4, tip_0 + delta4);
+ cr->line_to(tip_1 - delta4, tip_0 - delta4);
+ cr->close_path();
+
+}
+
+void CanvasItemCtrl::build_cache(int device_scale)
+{
+ if (_width < 2 || _height < 2) {
+ return; // Nothing to render
+ }
+
+ // Get colors
+ guint32 fill = 0x0;
+ guint32 stroke = 0x0;
+ fill = _fill;
+ stroke = _stroke;
+
+ if (_shape != CANVAS_ITEM_CTRL_SHAPE_BITMAP) {
+ if (_width % 2 == 0 || _height % 2 == 0) {
+ std::cerr << "CanvasItemCtrl::build_cache: Width and/or height not odd integer! "
+ << _name << ": width: " << _width << " height: " << _height << std::endl;
+ }
+ }
+
+ // Get memory for cache.
+ int width = _width * device_scale; // Not unsigned or math errors occur!
+ int height = _height * device_scale;
+ int size = width * height;
+
+ if (_cache) delete[] _cache;
+ _cache = new guint32[size];
+ guint32 *p = _cache;
+
+ switch (_shape) {
+
+ case CANVAS_ITEM_CTRL_SHAPE_SQUARE:
+ // Actually any rectanglular shape.
+ for (int i = 0; i < width; ++i) {
+ for (int j = 0; j < width; ++j) {
+ if (i + 1 > device_scale && device_scale < width - i &&
+ j + 1 > device_scale && device_scale < height - j) {
+ *p++ = fill;
+ } else {
+ *p++ = stroke;
+ }
+ }
+ }
+ _built = true;
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_DIAMOND:
+ { // Assume width == height.
+ int m = (width+1)/2;
+
+ for (int i = 0; i < width; ++i) {
+ for (int j = 0; j < height; ++j) {
+ if ( i + j > m-1+device_scale &&
+ (width-1-i) + j > m-1+device_scale &&
+ (width-1-i) + (height-1-j) > m-1+device_scale &&
+ i + (height-1-j) > m-1+device_scale ) {
+ *p++ = fill;
+ } else
+ if ( i + j > m-2 &&
+ (width-1-i) + j > m-2 &&
+ (width-1-i) + (height-1-j) > m-2 &&
+ i + (height-1-j) > m-2 ) {
+ *p++ = stroke;
+ } else {
+ *p++ = 0;
+ }
+ }
+ }
+ _built = true;
+ break;
+ }
+
+ case CANVAS_ITEM_CTRL_SHAPE_CIRCLE:
+ { // Assume width == height.
+ double rs = width/2.0;
+ double rs2 = rs*rs;
+ double rf = rs-device_scale;
+ double rf2 = rf*rf;
+
+ for (int i = 0; i < width; ++i) {
+ for (int j = 0; j < height; ++j) {
+
+ double rx = i - (width /2.0) + 0.5;
+ double ry = j - (height/2.0) + 0.5;
+ double r2 = rx*rx + ry*ry;
+
+ if (r2 < rf2) {
+ *p++ = fill;
+ } else if (r2 < rs2) {
+ *p++ = stroke;
+ } else {
+ *p++ = 0;
+ }
+ }
+ }
+ _built = true;
+ break;
+ }
+
+ case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE:
+ {
+ Geom::Affine m = Geom::Translate(Geom::Point(-width/2.0,-height/2.0));
+ m *= Geom::Rotate(-_angle);
+ m *= Geom::Translate(Geom::Point(width/2.0, height/2.0));
+
+ // Construct an arrowhead (triangle) of maximum size that won't leak out of rectangle
+ // defined by width and height, assuming width == height.
+ double w2 = width/2.0;
+ double h2 = height/2.0;
+ double w2cos = w2 * cos( M_PI/6 );
+ double h2sin = h2 * sin( M_PI/6 );
+ Geom::Point p1s(0, h2);
+ Geom::Point p2s(w2 + w2cos, h2 + h2sin);
+ Geom::Point p3s(w2 + w2cos, h2 - h2sin);
+
+ // Needed for constructing smaller arrowhead below.
+ double theta = atan2( Geom::Point( p2s - p1s ) );
+
+ p1s *= m;
+ p2s *= m;
+ p3s *= m;
+
+ // Construct a smaller arrow head for fill.
+ Geom::Point p1f(device_scale/sin(theta), h2);
+ Geom::Point p2f(w2 + w2cos, h2 - h2sin + device_scale/cos(theta));
+ Geom::Point p3f(w2 + w2cos, h2 + h2sin - device_scale/cos(theta));
+ p1f *= m;
+ p2f *= m;
+ p3f *= m;
+
+ for(int y = 0; y < height; y++) {
+ for(int x = 0; x < width; x++) {
+ Geom::Point point = Geom::Point(x+0.5, y+0.5);
+
+ if (point_inside_triangle(p1f, p2f, p3f, point)) {
+ p[(y*width)+x] = fill;
+
+ } else if (point_inside_triangle(p1s, p2s, p3s, point)) {
+ p[(y*width)+x] = stroke;
+
+ } else {
+ p[(y*width)+x] = 0;
+ }
+ }
+ }
+ break;
+ }
+
+ case CANVAS_ITEM_CTRL_SHAPE_CROSS:
+ // Actually an 'X'.
+ for(int y = 0; y < height; y++) {
+ for(int x = 0; x < width; x++) {
+ if ( abs(x - y) < device_scale ||
+ abs(width - 1 - x - y) < device_scale ) {
+ *p++ = stroke;
+ } else {
+ *p++ = 0;
+ }
+ }
+ }
+ _built = true;
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_PLUS:
+ // Actually an '+'.
+ for(int y = 0; y < height; y++) {
+ for(int x = 0; x < width; x++) {
+ if ( std::abs(x-width/2) < device_scale ||
+ std::abs(y-height/2) < device_scale ) {
+ *p++ = stroke;
+ } else {
+ *p++ = 0;
+ }
+ }
+ }
+ _built = true;
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_DARROW: // Double arrow
+ case CANVAS_ITEM_CTRL_SHAPE_SARROW: // Same shape as darrow but rendered rotated 90 degrees.
+ case CANVAS_ITEM_CTRL_SHAPE_CARROW: // Double corner arrow
+ case CANVAS_ITEM_CTRL_SHAPE_PIVOT: // Fancy "plus"
+ case CANVAS_ITEM_CTRL_SHAPE_SALIGN: // Side align (triangle pointing toward line)
+ case CANVAS_ITEM_CTRL_SHAPE_CALIGN: // Corner align (triangle pointing into "L")
+ case CANVAS_ITEM_CTRL_SHAPE_MALIGN: // Middle align (four triangles poining inward)
+ {
+ double size = _width; // Use unscaled width.
+
+ auto work = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, device_scale * size, device_scale * size);
+ cairo_surface_set_device_scale(work->cobj(), device_scale, device_scale); // No C++ API!
+ auto cr = Cairo::Context::create(work);
+
+ // Rotate around center
+ cr->translate( size/2.0, size/2.0);
+ cr->rotate(_angle);
+ cr->translate(-size/2.0, -size/2.0);
+
+ // Construct path
+ switch (_shape) {
+ case CANVAS_ITEM_CTRL_SHAPE_DARROW:
+ case CANVAS_ITEM_CTRL_SHAPE_SARROW:
+ draw_darrow(cr, size);
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_CARROW:
+ draw_carrow(cr, size);
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_PIVOT:
+ draw_pivot(cr, size);
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_SALIGN:
+ draw_salign(cr, size);
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_CALIGN:
+ draw_calign(cr, size);
+ break;
+
+ case CANVAS_ITEM_CTRL_SHAPE_MALIGN:
+ draw_malign(cr, size);
+ break;
+
+ default:
+ // Shouldn't happen
+ break;
+ }
+
+ // Fill and stroke.
+ cr->set_source_rgba(SP_RGBA32_R_F(_fill),
+ SP_RGBA32_G_F(_fill),
+ SP_RGBA32_B_F(_fill),
+ SP_RGBA32_A_F(_fill));
+ cr->fill_preserve();
+ cr->set_source_rgba(SP_RGBA32_R_F(_stroke),
+ SP_RGBA32_G_F(_stroke),
+ SP_RGBA32_B_F(_stroke),
+ SP_RGBA32_A_F(_stroke));
+ cr->set_line_width(1);
+ cr->stroke();
+
+ // Copy to buffer.
+ work->flush();
+ int strideb = work->get_stride();
+ unsigned char* pxb = work->get_data();
+ guint32 *p = _cache;
+ for (int i = 0; i < device_scale * size; ++i) {
+ guint32 *pb = reinterpret_cast<guint32*>(pxb + i*strideb);
+ for (int j = 0; j < width; ++j) {
+
+ guint32 color = 0x0;
+
+ // Need to un-premultiply alpha and change order argb -> rgba.
+ guint32 alpha = (*pb & 0xff000000) >> 24;
+ if (alpha == 0x0) {
+ color = 0x0;
+ } else {
+ guint32 rgb = unpremul_alpha(*pb & 0xffffff, alpha);
+ color = (rgb << 8) + alpha;
+ }
+
+ *p++ = color;
+ pb++;
+ }
+ }
+ _built = true;
+ break;
+ }
+
+ case CANVAS_ITEM_CTRL_SHAPE_BITMAP:
+ {
+ if (_pixbuf) {
+ unsigned char* px = gdk_pixbuf_get_pixels (_pixbuf);
+ unsigned int rs = gdk_pixbuf_get_rowstride (_pixbuf);
+ for (int y = 0; y < height/device_scale; y++){
+ for (int x = 0; x < width/device_scale; x++) {
+ unsigned char *s = px + rs*y + 4*x;
+ guint32 color;
+ if (s[3] < 0x80) {
+ color = 0;
+ } else if (s[0] < 0x80) {
+ color = _stroke;
+ } else {
+ color = _fill;
+ }
+
+ // Fill in device_scale x device_scale block
+ for (int i = 0; i < device_scale; ++i) {
+ for (int j = 0; j < device_scale; ++j) {
+ guint* p = _cache +
+ (x * device_scale + i) + // Column
+ (y * device_scale + j) * width; // Row
+ *p = color;
+ }
+ }
+ }
+ }
+ } else {
+ std::cerr << "CanvasItemCtrl::build_cache: No bitmap!" << std::endl;
+ guint *p = _cache;
+ for (int y = 0; y < height/device_scale; y++){
+ for (int x = 0; x < width/device_scale; x++) {
+ if (x == y) {
+ *p++ = 0xffff0000;
+ } else {
+ *p++ = 0;
+ }
+ }
+ }
+ }
+ _built = true;
+ break;
+ }
+
+ case CANVAS_ITEM_CTRL_SHAPE_IMAGE:
+ std::cerr << "CanvasItemCtrl::build_cache: image: UNIMPLEMENTED" << std::endl;
+ break;
+
+ default:
+ std::cerr << "CanvasItemCtrl::build_cache: unhandled shape!" << std::endl;
+ }
+}
+
+} // namespace Inkscape
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-ctrl.h b/src/display/control/canvas-item-ctrl.h
new file mode 100644
index 0000000..4cccc88
--- /dev/null
+++ b/src/display/control/canvas-item-ctrl.h
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_CTRL_H
+#define SEEN_CANVAS_ITEM_CTRL_H
+
+/**
+ * A class to represent a control node.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrl
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <memory>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <2geom/point.h>
+
+#include "canvas-item.h"
+#include "canvas-item-enums.h"
+
+#include "enums.h" // SP_ANCHOR_X
+
+namespace Inkscape {
+
+class CanvasItemGroup; // A canvas control that contains other canvas controls.
+
+class CanvasItemCtrl : public CanvasItem {
+
+public:
+ ~CanvasItemCtrl() override;
+ CanvasItemCtrl(CanvasItemGroup *group);
+ CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type);
+ CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type, Geom::Point const &p);
+ CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape);
+ CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape, Geom::Point const &p);
+
+ // Geometry
+ void set_position(Geom::Point const &position);
+
+ void update(Geom::Affine const &affine) override;
+ double closest_distance_to(Geom::Point const &p);
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Display
+ void render(Inkscape::CanvasItemBuffer *buf) override;
+
+ // Properties
+ void set_fill(guint32 rgba) override;
+ void set_stroke(guint32 rgba) override;
+ void set_shape(int shape);
+ void set_shape_default(); // Use type to determine shape.
+ void set_mode(int mode);
+ void set_mode_default();
+ void set_size(int size);
+ virtual void set_size_via_index(int size_index);
+ void set_size_default(); // Use preference and type to set size.
+ void set_size_extra(int extra); // Used to temporary increase size of ctrl.
+ void set_anchor(SPAnchorType anchor);
+ void set_angle(double angle);
+ void set_type(CanvasItemCtrlType type);
+ void set_pixbuf(GdkPixbuf *pixbuf);
+
+protected:
+ void build_cache(int device_scale);
+
+ // Geometry
+ Geom::Point _position;
+
+ // Display
+ guint32 *_cache = nullptr;
+ bool _built = false;
+
+ // Properties
+ CanvasItemCtrlType _type = CANVAS_ITEM_CTRL_TYPE_DEFAULT;
+ CanvasItemCtrlShape _shape = CANVAS_ITEM_CTRL_SHAPE_SQUARE;
+ CanvasItemCtrlMode _mode = CANVAS_ITEM_CTRL_MODE_XOR;
+ unsigned int _width = 5; // Nominally width == height == size... unless we use a pixmap.
+ unsigned int _height = 5;
+ unsigned int _extra = 0; // Used to temporarily increase size.
+ double _angle = 0.0; // Used for triangles, could be used for arrows.
+ SPAnchorType _anchor = SP_ANCHOR_CENTER;
+ GdkPixbuf *_pixbuf = nullptr;
+};
+
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_CTRL_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-curve.cpp b/src/display/control/canvas-item-curve.cpp
new file mode 100644
index 0000000..a99a154
--- /dev/null
+++ b/src/display/control/canvas-item-curve.cpp
@@ -0,0 +1,236 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a single Bezier control curve, either a line or a cubic Bezier.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrlLine and SPCtrlCurve
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/bezier-curve.h>
+
+#include "canvas-item-curve.h"
+
+#include "color.h" // SP_RGBA_x_F
+
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+/**
+ * Create an null control curve.
+ */
+CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemCurve:Null";
+ _pickable = false; // For now, nobody gets events from this class!
+}
+
+/**
+ * Create a linear control curve. Points are in document coordinates.
+ */
+CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1)
+ : CanvasItem(group)
+ , _curve(std::make_unique<Geom::LineSegment>(p0, p1))
+{
+ _name = "CanvasItemCurve:Line";
+ _pickable = false; // For now, nobody gets events from this class!
+
+ request_update();
+}
+
+/**
+ * Create a cubic Bezier control curve. Points are in document coordinates.
+ */
+CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group,
+ Geom::Point const &p0, Geom::Point const &p1,
+ Geom::Point const &p2, Geom::Point const &p3)
+ : CanvasItem(group)
+ , _curve(std::make_unique<Geom::CubicBezier>(p0, p1, p2, p3))
+{
+ _name = "CanvasItemCurve:CubicBezier";
+ _pickable = false; // For now, nobody gets events from this class!
+
+ request_update();
+}
+
+/**
+ * Set a linear control curve. Points are in document coordinates.
+ */
+void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1)
+{
+ _name = "CanvasItemCurve:Line";
+ _curve = std::make_unique<Geom::LineSegment>(p0, p1);
+
+ request_update();
+}
+
+/**
+ * Set a cubic Bezier control curve. Points are in document coordinates.
+ */
+void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3)
+{
+ _name = "CanvasItemCurve:CubicBezier";
+ _curve = std::make_unique<Geom::CubicBezier>(p0, p1, p2, p3);
+
+ request_update();
+}
+
+/**
+ * Set stroke width.
+ */
+void CanvasItemCurve::set_width(int w)
+{
+ width = w;
+
+ request_update();
+}
+
+/**
+ * Set background stroke alpha.
+ */
+void CanvasItemCurve::set_bg_alpha(float alpha)
+{
+ bg_alpha = alpha;
+
+ request_update();
+}
+
+/**
+ * Returns distance between point in canvas units and nearest point on curve.
+ */
+double CanvasItemCurve::closest_distance_to(Geom::Point const &p)
+{
+ double d = Geom::infinity();
+ if (_curve) {
+ Geom::BezierCurve curve = *_curve;
+ curve *= _affine; // Document to canvas.
+ Geom::Point n = curve.pointAt(curve.nearestTime(p));
+ d = Geom::distance(p, n);
+ }
+ return d;
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of curve.
+ */
+bool CanvasItemCurve::contains(Geom::Point const &p, double tolerance)
+{
+ return closest_distance_to(p) <= tolerance;
+}
+
+/**
+ * Update and redraw control curve.
+ */
+void CanvasItemCurve::update(Geom::Affine const &affine)
+{
+ if (_affine == affine && !_need_update) {
+ // Nothing to do.
+ return;
+ }
+
+ if (!_curve) {
+ return; // No curve! See node.h.
+ }
+
+ // Queue redraw of old area (erase previous content).
+ request_redraw(); // This is actually never useful as curves are always deleted
+ // and recreated when a node is moved! But keep it in case we
+ // change that. CHECK
+ // Get new bounds
+ _affine = affine;
+
+ // Trade off between updating a larger area (typically twice for Beziers?) vs computation time for bounds.
+ _bounds = _curve->boundsExact(); // Document units.
+ _bounds *= _affine; // Document to canvas.
+ _bounds.expandBy(2); // Room for stroke.
+
+ // Queue redraw of new area
+ request_redraw();
+
+ _need_update = false;
+}
+
+/**
+ * Render curve to screen via Cairo.
+ */
+void CanvasItemCurve::render(Inkscape::CanvasItemBuffer *buf)
+{
+ if (!buf) {
+ std::cerr << "CanvasItemCurve::Render: No buffer!" << std::endl;
+ return;
+ }
+
+ if (!_curve) {
+ // Curve not defined (see node.h).
+ return;
+ }
+
+ if (!_visible) {
+ // Hidden
+ return;
+ }
+
+ if (_curve->isDegenerate()) {
+ // Nothing to render!
+ return;
+ }
+
+ Geom::BezierCurve curve = *_curve;
+ curve *= _affine; // Document to canvas.
+ curve *= Geom::Translate(-buf->rect.min()); // Canvas to screen.
+
+ buf->cr->save();
+
+ buf->cr->begin_new_path();
+
+ if (curve.size() == 2) {
+ // Line
+ buf->cr->move_to(curve[0].x(), curve[0].y());
+ buf->cr->line_to(curve[1].x(), curve[1].y());
+ } else {
+ // Curve
+ buf->cr->move_to(curve[0].x(), curve[0].y());
+ buf->cr->curve_to(curve[1].x(), curve[1].y(), curve[2].x(), curve[2].y(), curve[3].x(), curve[3].y());
+ }
+
+ buf->cr->set_source_rgba(1.0, 1.0, 1.0, bg_alpha);
+ buf->cr->set_line_width(background_width);
+ buf->cr->stroke_preserve();
+
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke),
+ SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke));
+ buf->cr->set_line_width(width);
+ buf->cr->stroke();
+
+ // Uncomment to show bounds
+ // Geom::Rect bounds = _bounds;
+ // bounds.expandBy(-1);
+ // bounds -= buf->rect.min();
+ // buf->cr->set_source_rgba(1.0, 0.0, 0.0, 1.0);
+ // buf->cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height());
+ // buf->cr->stroke();
+
+ buf->cr->restore();
+}
+
+} // namespace Inkscape
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-curve.h b/src/display/control/canvas-item-curve.h
new file mode 100644
index 0000000..84d6511
--- /dev/null
+++ b/src/display/control/canvas-item-curve.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_CURVE_H
+#define SEEN_CANVAS_ITEM_CURVE_H
+
+/**
+ * A class to represent a single Bezier control curve.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrlLine and SPCtrlCurve
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <memory>
+#include <2geom/path.h>
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+class CanvasItemGroup; // A canvas control that contains other canvas controls.
+
+class CanvasItemCurve : public CanvasItem {
+
+public:
+ CanvasItemCurve(CanvasItemGroup *group);
+ CanvasItemCurve(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1);
+ CanvasItemCurve(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1,
+ Geom::Point const &p2, Geom::Point const &p3);
+
+ // Geometry
+ void set_coords(Geom::Point const &p0, Geom::Point const &p1);
+ void set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3);
+ void set(Geom::BezierCurve &curve);
+ void set_width(int w);
+ void set_bg_alpha(float alpha);
+ bool is_line() { return _curve->size() == 2; }
+
+ void update(Geom::Affine const &affine) override;
+ double closest_distance_to(Geom::Point const &p); // Maybe not needed
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Display
+ void render(Inkscape::CanvasItemBuffer *buf) override;
+
+ // Properties
+ void set_is_fill(bool is_fill) { _is_fill = is_fill; }
+ bool get_is_fill() { return _is_fill; }
+ void set_corner0(int corner0) { _corner0 = corner0; } // Used for meshes
+ int get_corner0() { return _corner0; }
+ void set_corner1(int corner1) { _corner1 = corner1; }
+ int get_corner1() { return _corner1; }
+
+protected:
+ std::unique_ptr<Geom::BezierCurve> _curve;
+ bool _is_fill = true; // Fill or stroke, used by meshes.
+
+ int width = 1;
+ int background_width = 3; // this should be an odd number so that the background appears on both the sides of the curve.
+ float bg_alpha = 0.5f;
+ int _corner0 = -1; // For meshes
+ int _corner1 = -1; // For meshes
+};
+
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_CURVE_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-drawing.cpp b/src/display/control/canvas-item-drawing.cpp
new file mode 100644
index 0000000..17750b7
--- /dev/null
+++ b/src/display/control/canvas-item-drawing.cpp
@@ -0,0 +1,296 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to render the SVG drawing.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of _SPCavasArena.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item-drawing.h"
+
+#include "desktop.h"
+
+#include "preferences.h"
+
+#include "display/cairo-utils.h"
+#include "display/drawing.h"
+#include "display/drawing-context.h"
+#include "display/drawing-item.h"
+#include "display/drawing-group.h"
+#include "display/drawing-surface.h"
+
+#include "ui/widget/canvas.h"
+#include "ui/modifiers.h"
+
+namespace Inkscape {
+
+struct CachePref2Observer : public Inkscape::Preferences::Observer {
+ CachePref2Observer(Inkscape::CanvasItemDrawing *item)
+ : Inkscape::Preferences::Observer("/options/renderingcache")
+ , _canvas_item_drawing(item)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ std::vector<Inkscape::Preferences::Entry> v = prefs->getAllEntries(observed_path);
+ for (const auto & i : v) {
+ notify(i);
+ }
+ prefs->addObserver(*this);
+ }
+ void notify(Inkscape::Preferences::Entry const &v) override {
+ Glib::ustring name = v.getEntryName();
+ if (name == "size") {
+ _canvas_item_drawing->get_drawing()->setCacheBudget((1 << 20) * v.getIntLimited(64, 0, 4096));
+ }
+ }
+ Inkscape::CanvasItemDrawing *_canvas_item_drawing;
+};
+
+/**
+ * Create the drawing. One per window!
+ */
+CanvasItemDrawing::CanvasItemDrawing(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemDrawing";
+ _pickable = true;
+
+ _drawing = new Inkscape::Drawing(this);
+ _drawing->delta = 1.0; // Default
+ auto root = new Inkscape::DrawingGroup(*_drawing);
+ root->setPickChildren(true);
+ _drawing->setRoot(root);
+
+ // _drawing->signal_request_update.connect(...); Not needed now.
+ // _drawing->signal_request_render.connect(...); Not needed now.
+ // _drawing->signal_item_deleted.connect(...); Not needed now.
+
+ _observer = new CachePref2Observer(this);
+}
+
+CanvasItemDrawing::~CanvasItemDrawing()
+{
+ delete _observer;
+ delete _drawing;
+}
+
+/**
+ * Returns distance between point in canvas units and nearest point on drawing.
+ */
+double CanvasItemDrawing::closest_distance_to(Geom::Point const &p)
+{
+ double d = Geom::infinity();
+ std::cerr << "CanvasItemDrawing::closest_distance_to: Not implemented!" << std::endl;
+ return d;
+}
+
+/**
+ * Returns true if point p (in canvas units) is inside some object in drawing.
+ */
+bool CanvasItemDrawing::contains(Geom::Point const &p, double tolerance)
+{
+ if (tolerance != 0) {
+ std::cerr << "CanvasItemDrawing::contains: Non-zero tolerance not implemented!" << std::endl;
+ }
+
+ _drawing->update(Geom::IntRect::infinite(), DrawingItem::STATE_PICK | DrawingItem::STATE_BBOX);
+ _picked_item = _drawing->pick(p, _drawing->delta, _sticky);
+
+ if (_picked_item) {
+ // This will trigger a signal that is handled by our event handler. Seems a bit of a
+ // round-a-bout way of doing things but it matches what other pickable canvas-item classes
+ // do.
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Update and redraw drawing.
+ */
+void CanvasItemDrawing::update(Geom::Affine const &affine)
+{
+ auto new_affine = affine;
+
+ // Correct for y-axis. This should not be here!!!!
+ if (auto *desktop = _canvas->get_desktop()) {
+ new_affine = desktop->doc2dt() * new_affine;
+ }
+
+ // if (_affine == new_affine && !_need_update) {
+ // // Nothing to do.
+ // return;
+ // }
+
+ _ctx.ctm = new_affine; // TODO Remove _ctx.ctm... it's exactly the same as _affine!
+
+ unsigned reset = (_affine != new_affine ? DrawingItem::STATE_ALL : 0);
+
+ _affine = new_affine;
+
+ _drawing->update(Geom::IntRect::infinite(), DrawingItem::STATE_ALL, reset);
+
+ Geom::OptIntRect bbox = _drawing->root()->visualBounds();
+ if (bbox) {
+ _bounds = *bbox;
+ _bounds.expandBy(1); // Avoid aliasing artifacts.
+ }
+
+ if (_cursor) {
+ /* Mess with enter/leave notifiers */
+ DrawingItem *new_drawing_item = _drawing->pick(_c, _delta, _sticky);
+ if (_active_item != new_drawing_item) {
+
+ GdkEventCrossing ec;
+ ec.window = _canvas->get_window()->gobj();
+ ec.send_event = true;
+ ec.subwindow = ec.window;
+ ec.time = GDK_CURRENT_TIME;
+ ec.x = _c.x();
+ ec.y = _c.y();
+
+ /* fixme: Why? */
+ if (_active_item) {
+ ec.type = GDK_LEAVE_NOTIFY;
+ _drawing_event_signal.emit((GdkEvent *) &ec, _active_item);
+ }
+
+ _active_item = new_drawing_item;
+
+ if (_active_item) {
+ ec.type = GDK_ENTER_NOTIFY;
+ _drawing_event_signal.emit((GdkEvent *) &ec, _active_item);
+ }
+ }
+ }
+
+ _need_update = false;
+}
+
+/**
+ * Render drawing to screen via Cairo.
+ */
+void CanvasItemDrawing::render(Inkscape::CanvasItemBuffer *buf)
+{
+ if (!buf) {
+ std::cerr << "CanvasItemDrawing::Render: No buffer!" << std::endl;
+ return;
+ }
+
+ if (buf->rect.hasZeroArea()) {
+ return;
+ }
+
+ Inkscape::DrawingContext dc(buf->cr->cobj(), buf->rect.min());
+ _drawing->update();
+ _drawing->render(dc, buf->rect);
+}
+
+/**
+ * Handle events directed at the drawing. We first attempt to handle them here.
+ */
+bool CanvasItemDrawing::handle_event(GdkEvent *event)
+{
+ bool retval = false;
+
+ switch (event->type) {
+ case GDK_ENTER_NOTIFY:
+ if (!_cursor) {
+ if (_active_item) {
+ std::cerr << "CanvasItemDrawing::event_handler: cursor entered drawing with an active item!" << std::endl;
+ }
+ _cursor = true;
+
+ /* TODO ... event -> arena transform? */
+ _c = Geom::Point(event->crossing.x, event->crossing.y);
+
+ /* fixme: Not sure abut this, but seems the right thing (Lauris) */
+ //_drawing->update(Geom::IntRect::infinite(), _ctx, DrawingItem::STATE_PICK | DrawingItem::STATE_BBOX, 0);
+ _active_item = _drawing->pick(_c, _drawing->delta, _sticky);
+ retval = _drawing_event_signal.emit(event, _active_item);
+ }
+ break;
+
+ case GDK_LEAVE_NOTIFY:
+ if (_cursor) {
+ retval = _drawing_event_signal.emit(event, _active_item);
+ _active_item = nullptr;
+ _cursor = false;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY:
+ {
+ /* TODO ... event -> arena transform? */
+ _c = Geom::Point(event->motion.x, event->motion.y);
+
+ /* fixme: Not sure abut this, but seems the right thing (Lauris) */
+ //_drawing->update(Geom::IntRect::infinite(), _ctx, DrawingItem::STATE_PICK | DrawingItem::STATE_BBOX);
+
+ auto new_drawing_item = _drawing->pick(_c, _drawing->delta, _sticky);
+ if (_active_item != new_drawing_item) {
+
+ GdkEventCrossing ec;
+ ec.window = event->motion.window;
+ ec.send_event = event->motion.send_event;
+ ec.subwindow = event->motion.window;
+ ec.time = event->motion.time;
+ ec.x = event->motion.x;
+ ec.y = event->motion.y;
+
+ /* fixme: What is wrong? */
+ if (_active_item) {
+ ec.type = GDK_LEAVE_NOTIFY;
+ retval = _drawing_event_signal.emit((GdkEvent *) &ec, _active_item);
+ }
+
+ _active_item = new_drawing_item;
+
+ if (_active_item) {
+ ec.type = GDK_ENTER_NOTIFY;
+ retval = _drawing_event_signal.emit((GdkEvent *) &ec, _active_item);
+ }
+ }
+ retval = retval || _drawing_event_signal.emit(event, _active_item);
+ break;
+ }
+
+ case GDK_SCROLL:
+ {
+ if (Modifiers::Modifier::get(Modifiers::Type::CANVAS_ZOOM)->active(event->scroll.state)) {
+ /* Zoom is emitted by the canvas as well, ignore here */
+ return false;
+ }
+ retval = _drawing_event_signal.emit(event, _active_item);
+ break;
+ }
+
+ default:
+ /* Just send event */
+ retval = _drawing_event_signal.emit(event, _active_item);
+ break;
+ }
+
+ return retval;
+}
+
+} // namespace Inkscape
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-drawing.h b/src/display/control/canvas-item-drawing.h
new file mode 100644
index 0000000..6e8a206
--- /dev/null
+++ b/src/display/control/canvas-item-drawing.h
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_DRAWING_H
+#define SEEN_CANVAS_ITEM_DRAWING_H
+
+/**
+ * A class to render the SVG drawing.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of _SPCanvasArena.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <sigc++/sigc++.h>
+
+
+#include "canvas-item.h"
+#include "display/drawing-item.h" // Only for ctx... which should be remove (it's the same as _affine!).
+
+namespace Inkscape {
+
+class CachePref2Observer;
+
+class Drawing;
+class DrawingItem;
+class Updatecontext;
+
+class CanvasItemGroup; // A canvas control that contains other canvas controls.
+
+class CanvasItemDrawing : public CanvasItem {
+
+public:
+ CanvasItemDrawing(CanvasItemGroup *group);
+ ~CanvasItemDrawing() override;
+
+
+ // Geometry
+ UpdateContext get_context() { return _ctx; } // TODO Remove this as ctx only contains affine.
+
+ void update(Geom::Affine const &affine) override;
+ double closest_distance_to(Geom::Point const &p);
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Display
+ void render(Inkscape::CanvasItemBuffer *buf) override;
+ Inkscape::Drawing * get_drawing() { return _drawing; }
+
+ // Drawing items
+ void set_active(Inkscape::DrawingItem *active) { _active_item = active; }
+ Inkscape::DrawingItem *get_active() { return _active_item; }
+
+ // Events
+ bool handle_event(GdkEvent* event) override;
+ void set_sticky(bool sticky) { _sticky = sticky; }
+
+ // Signals
+ sigc::connection connect_drawing_event(sigc::slot<bool, GdkEvent*, Inkscape::DrawingItem *> slot) {
+ return _drawing_event_signal.connect(slot);
+ }
+
+protected:
+
+ // Geometry
+
+ // Selection
+ Geom::Point _c;
+ double _delta = Geom::infinity();
+ Inkscape::DrawingItem *_active_item = nullptr;
+ Inkscape::DrawingItem *_picked_item = nullptr;
+
+ // Display
+ Inkscape::Drawing *_drawing;
+ Inkscape::UpdateContext _ctx; // TODO Remove this... it's the same as _affine!
+
+ // Events
+ bool _cursor = false;
+ bool _sticky = false; // Pick anything, even if hidden.
+
+ // Properties
+ CachePref2Observer *_observer = nullptr;
+
+ // Signals
+ sigc::signal<bool, GdkEvent*, Inkscape::DrawingItem *> _drawing_event_signal;
+};
+
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_DRAWING_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-enums.h b/src/display/control/canvas-item-enums.h
new file mode 100644
index 0000000..37561b5
--- /dev/null
+++ b/src/display/control/canvas-item-enums.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_ENUMS_H
+#define SEEN_CANVAS_ITEM_ENUMS_H
+
+/**
+ * Enums for CanvasControlItem's.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ */
+
+namespace Inkscape {
+
+enum CanvasItemColor {
+ CANVAS_ITEM_PRIMARY,
+ CANVAS_ITEM_SECONDARY,
+ CANVAS_ITEM_TERTIARY
+};
+
+enum CanvasItemCtrlShape {
+ CANVAS_ITEM_CTRL_SHAPE_SQUARE,
+ CANVAS_ITEM_CTRL_SHAPE_DIAMOND,
+ CANVAS_ITEM_CTRL_SHAPE_CIRCLE,
+ CANVAS_ITEM_CTRL_SHAPE_TRIANGLE,
+ CANVAS_ITEM_CTRL_SHAPE_CROSS,
+ CANVAS_ITEM_CTRL_SHAPE_PLUS,
+ CANVAS_ITEM_CTRL_SHAPE_PIVOT, // Fancy "plus"
+ CANVAS_ITEM_CTRL_SHAPE_DARROW, // Double headed arrow.
+ CANVAS_ITEM_CTRL_SHAPE_SARROW, // Double headed arrow, rotated (skew).
+ CANVAS_ITEM_CTRL_SHAPE_CARROW, // Double headed curved arrow.
+ CANVAS_ITEM_CTRL_SHAPE_SALIGN, // Side alignment.
+ CANVAS_ITEM_CTRL_SHAPE_CALIGN, // Corner alignment.
+ CANVAS_ITEM_CTRL_SHAPE_MALIGN, // Center (middle) alignment.
+ CANVAS_ITEM_CTRL_SHAPE_BITMAP,
+ CANVAS_ITEM_CTRL_SHAPE_IMAGE,
+ CANVAS_ITEM_CTRL_SHAPE_LINE
+};
+
+// Applies to control points.
+enum CanvasItemCtrlType {
+ CANVAS_ITEM_CTRL_TYPE_DEFAULT,
+ CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE, // Stretch & Scale
+ CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW,
+ CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE,
+ CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER,
+ CANVAS_ITEM_CTRL_TYPE_ADJ_SALIGN,
+ CANVAS_ITEM_CTRL_TYPE_ADJ_CALIGN,
+ CANVAS_ITEM_CTRL_TYPE_ADJ_MALIGN,
+ CANVAS_ITEM_CTRL_TYPE_ANCHOR,
+ CANVAS_ITEM_CTRL_TYPE_POINT,
+ CANVAS_ITEM_CTRL_TYPE_ROTATE,
+ CANVAS_ITEM_CTRL_TYPE_CENTER,
+ CANVAS_ITEM_CTRL_TYPE_SIZER,
+ CANVAS_ITEM_CTRL_TYPE_SHAPER,
+ CANVAS_ITEM_CTRL_TYPE_LPE,
+ CANVAS_ITEM_CTRL_TYPE_NODE_AUTO,
+ CANVAS_ITEM_CTRL_TYPE_NODE_CUSP,
+ CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH,
+ CANVAS_ITEM_CTRL_TYPE_NODE_SYMETRICAL,
+ CANVAS_ITEM_CTRL_TYPE_INVISIPOINT
+};
+
+enum CanvasItemCtrlMode {
+ CANVAS_ITEM_CTRL_MODE_COLOR,
+ CANVAS_ITEM_CTRL_MODE_XOR,
+ CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR,
+ CANVAS_ITEM_CTRL_MODE_GRAYSCALED_XOR
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_ENUMS_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-grid.cpp b/src/display/control/canvas-item-grid.cpp
new file mode 100644
index 0000000..2965b86
--- /dev/null
+++ b/src/display/control/canvas-item-grid.cpp
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a grid. All the magic happens elsewhere.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of GridCanvasItem.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item-grid.h"
+
+#include "canvas-grid.h"
+
+#include "color.h" // SP_RGBA_x_F
+
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+/**
+ * Create an null control grid.
+ */
+CanvasItemGrid::CanvasItemGrid(CanvasItemGroup *group, CanvasGrid *grid)
+ : CanvasItem(group)
+ , _grid(grid)
+{
+ _name = "CanvasItemGrid:";
+ _name += grid->getName(grid->getGridType());
+ _pickable = false; // For now, nobody gets events from this class!
+ _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity());
+
+ request_update(); // Update affine
+}
+
+/**
+ * Destructor: must remove ourself from the CanvasGrid's vector of CanvasItemGrids.
+ */
+CanvasItemGrid::~CanvasItemGrid()
+{
+ if (_grid) {
+ _grid->removeCanvasItem(this);
+ }
+}
+
+/**
+ * Returns distance between point in canvas units and nearest point on grid.
+ */
+double CanvasItemGrid::closest_distance_to(Geom::Point const &p)
+{
+ double d = Geom::infinity();
+ std::cerr << "CanvasItemGrid::closest_distance_to: Not implemented!" << std::endl;
+ return d;
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of grid.
+ */
+bool CanvasItemGrid::contains(Geom::Point const &p, double tolerance)
+{
+ return false; // We're not pickable!
+}
+
+/**
+ * Update and redraw control grid.
+ */
+void CanvasItemGrid::update(Geom::Affine const &affine)
+{
+ if (_affine == affine && !_need_update) {
+ // Nothing to do.
+ return;
+ }
+
+ _affine = affine;
+ _grid->Update(affine, 0); // TODO: Remove flag (not used).
+ _need_update = false;
+
+ // Queue redraw of grid area
+ request_redraw();
+}
+
+/**
+ * Render grid to screen via Cairo.
+ */
+void CanvasItemGrid::render(Inkscape::CanvasItemBuffer *buf)
+{
+ if (!buf) {
+ std::cerr << "CanvasItemGrid::Render: No buffer!" << std::endl;
+ return;
+ }
+
+ if (!_visible) {
+ // Hidden
+ return;
+ }
+
+ if (!_grid->isVisible()) {
+ // Hidden: Grid code doesn't set CanvasItem::visible!
+ return;
+ }
+
+ _grid->Render(buf);
+}
+
+} // namespace Inkscape
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-grid.h b/src/display/control/canvas-item-grid.h
new file mode 100644
index 0000000..f858c57
--- /dev/null
+++ b/src/display/control/canvas-item-grid.h
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_GRID_H
+#define SEEN_CANVAS_ITEM_GRID_H
+
+/**
+ * A class to represent a grid.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of GridCanvasItem.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/point.h>
+#include <2geom/transforms.h>
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+class CanvasGrid;
+
+class CanvasItemGroup; // A canvas control that contains other canvas controls.
+
+class CanvasItemGrid : public CanvasItem {
+
+public:
+ CanvasItemGrid(CanvasItemGroup *group, CanvasGrid *grid);
+ ~CanvasItemGrid() override;
+
+ // Geometry
+ void update(Geom::Affine const &affine) override;
+ double closest_distance_to(Geom::Point const &p); // Maybe not needed
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Display
+ void render(Inkscape::CanvasItemBuffer *buf) override;
+
+protected:
+ CanvasGrid *_grid = nullptr;
+};
+
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_GRID_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-group.cpp b/src/display/control/canvas-item-group.cpp
new file mode 100644
index 0000000..8e53857
--- /dev/null
+++ b/src/display/control/canvas-item-group.cpp
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A CanvasItem that contains other CanvasItem's.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasGroup
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item-group.h"
+#include "canvas-item-ctrl.h" // Update sizes
+
+namespace Inkscape {
+
+CanvasItemGroup::CanvasItemGroup(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemGroup";
+ _pickable = true; // For now all groups are pickable... look into turning this off for some groups (e.g. temp).
+}
+
+CanvasItemGroup::~CanvasItemGroup()
+{
+ while (!items.empty()) {
+ CanvasItem & item = items.front();
+ remove(&item);
+ }
+
+ if (_parent) {
+ _parent->remove(this, false); // remove() should not delete this or we'll double delete!
+ }
+}
+
+void CanvasItemGroup::add(CanvasItem *item)
+{
+#ifdef CANVAS_ITEM_DEBUG
+ std::cout << "CanvasItemGroup::add: " << item->get_name() << " to " << _name << " " << items.size() << std::endl;
+#endif
+ items.push_back(*item);
+ // canvas request update
+}
+
+void CanvasItemGroup::remove(CanvasItem *item, bool Delete)
+{
+#ifdef CANVAS_ITEM_DEBUG
+ std::cout << "CanvasItemGroup::remove: " << item->get_name() << " from " << _name << " " << items.size() << std::endl;
+#endif
+ auto position = items.iterator_to(*item);
+ if (position != items.end()) {
+ position->set_parent(nullptr);
+ items.erase(position);
+ if (Delete) {
+ delete (&*position); // An item directly deleted should not be deleted here.
+ }
+ }
+}
+
+void CanvasItemGroup::update(Geom::Affine const &affine)
+{
+ if (_affine == affine && !_need_update) {
+ // Nothing to do.
+ return;
+ }
+
+ _affine = affine;
+ _need_update = false;
+
+ _bounds = Geom::Rect(); // Zero
+
+ // Update all children and calculate new bounds.
+ for (auto & item : items) {
+ // We don't need to update what is not visible
+ if (!item.is_visible()) continue;
+ item.update(_affine);
+ _bounds.unionWith(item.get_bounds());
+ }
+}
+
+void CanvasItemGroup::render(Inkscape::CanvasItemBuffer *buf)
+{
+ if (_visible) {
+ if (_bounds.interiorIntersects(buf->rect)) {
+ for (auto & item : items) {
+ item.render(buf);
+ }
+ }
+ }
+}
+
+// Return last visible and pickable item that contains point.
+// SPCanvasGroup returned distance but it was not used.
+CanvasItem* CanvasItemGroup::pick_item(Geom::Point& p)
+{
+#ifdef CANVAS_ITEM_DEBUG
+ std::cout << "CanvasItemGroup::pick_item:" << std::endl;
+ std::cout << " PICKING: In group: " << _name << " bounds: " << _bounds << std::endl;
+#endif
+ for (auto item = items.rbegin(); item != items.rend(); ++item) { // C++20 will allow us to loop in reverse.
+#ifdef CANVAS_ITEM_DEBUG
+ std::cout << " PICKING: Checking: " << item->get_name() << " bounds: " << item->get_bounds() << std::endl;
+#endif
+ CanvasItem* picked_item = nullptr;
+ if (item->is_visible() &&
+ item->is_pickable() &&
+ item->contains(p) ) {
+
+ auto group = dynamic_cast<CanvasItemGroup *>(&*item);
+ if (group) {
+ picked_item = group->pick_item(p);
+ } else {
+ picked_item = &*item;
+ }
+ }
+
+ if (picked_item != nullptr) {
+#ifdef CANVAS_ITEM_DEBUG
+ std::cout << " PICKING: pick_item: " << picked_item->get_name() << std::endl;
+#endif
+ return picked_item;
+ }
+ }
+
+ return nullptr;
+}
+
+void CanvasItemGroup::update_canvas_item_ctrl_sizes(int size_index)
+{
+ for (auto & item : items) {
+ auto ctrl = dynamic_cast<CanvasItemCtrl *>(&item);
+ if (ctrl) {
+ // We can't use set_size_default as the preference file is updated ->after<- the signal is emitted!
+ ctrl->set_size_via_index(size_index);
+ }
+ auto group = dynamic_cast<CanvasItemGroup *>(&item);
+ if (group) {
+ group->update_canvas_item_ctrl_sizes(size_index);
+ }
+ }
+}
+
+} // Namespace Inkscape
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-group.h b/src/display/control/canvas-item-group.h
new file mode 100644
index 0000000..27cb0da
--- /dev/null
+++ b/src/display/control/canvas-item-group.h
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_GROUP_H
+#define SEEN_CANVAS_ITEM_GROUP_H
+
+/**
+ * A CanvasItem that contains other CanvasItem's.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasGroup
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+//#include <2geom/rect.h>
+#include <boost/intrusive/list.hpp>
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+class CanvasItemGroup : public CanvasItem {
+
+public:
+ CanvasItemGroup(CanvasItemGroup* group = nullptr);
+ ~CanvasItemGroup() override;
+
+ // Structure
+ void add(CanvasItem *item);
+ void remove(CanvasItem *item, bool Delete = true);
+
+ void update(Geom::Affine const &affine) override;
+
+ // Display
+ void render(Inkscape::CanvasItemBuffer *buf) override;
+
+ // Selection
+ CanvasItem* pick_item(Geom::Point &p);
+
+ CanvasItemList & get_items() { return items; }
+
+ // Properties
+ void update_canvas_item_ctrl_sizes(int size_index);
+
+protected:
+
+private:
+public:
+ // TODO: Make private (used in canvas-item.cpp).
+ CanvasItemList items; // Used to speed deletion.
+};
+
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-guideline.cpp b/src/display/control/canvas-item-guideline.cpp
new file mode 100644
index 0000000..6855701
--- /dev/null
+++ b/src/display/control/canvas-item-guideline.cpp
@@ -0,0 +1,343 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a control guide line.
+ */
+
+/*
+ * Authors:
+ * Tavmjong Bah - Rewrite of SPGuideLine
+ * Rafael Siejakowski - Tweaks to handle appearance
+ *
+ * Copyright (C) 2020-2022 the Authors.
+ *
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item-guideline.h"
+
+#include <2geom/line.h>
+
+#include "canvas-item-ctrl.h"
+
+#include "desktop.h" // Canvas orientation so label is orientated correctly.
+
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+/**
+ * Create a control guide line. Points are in document units.
+ */
+CanvasItemGuideLine::CanvasItemGuideLine(CanvasItemGroup *group, Glib::ustring label,
+ Geom::Point const &origin, Geom::Point const &normal)
+ : CanvasItem(group)
+ , _origin(origin)
+ , _normal(normal)
+ , _label(std::move(label))
+{
+ _name = "CanvasItemGuideLine:" + _label;
+ _pickable = true; // For now, everybody gets events from this class!
+
+ // Required when rotating canvas:
+ _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity());
+
+ // Control to move guide line.
+ _origin_ctrl = std::make_unique<CanvasItemGuideHandle>(group, _origin, this);
+ _origin_ctrl->set_name("CanvasItemGuideLine:Ctrl:" + _label);
+ _origin_ctrl->set_size_default();
+ _origin_ctrl->set_pickable(true); // The handle will also react to dragging
+ set_locked(false); // Init _origin_ctrl shape and stroke.
+}
+
+CanvasItemGuideLine::~CanvasItemGuideLine() = default;
+
+/**
+ * Sets origin of guide line (place where handle is located).
+ */
+void CanvasItemGuideLine::set_origin(Geom::Point const &origin)
+{
+ if (_origin != origin) {
+ _origin = origin;
+ _origin_ctrl->set_position(_origin);
+ request_update();
+ }
+}
+
+/**
+ * Sets orientation of guide line.
+ */
+void CanvasItemGuideLine::set_normal(Geom::Point const &normal)
+{
+ if (_normal != normal) {
+ _normal = normal;
+ request_update();
+ }
+}
+
+/**
+ * Sets the inverted nature of the line
+ */
+void CanvasItemGuideLine::set_inverted(bool inverted)
+{
+ if (_inverted != inverted) {
+ _inverted = inverted;
+ request_update();
+ }
+}
+
+
+/**
+ * Returns distance between point in canvas units and nearest point on guideLine.
+ */
+double CanvasItemGuideLine::closest_distance_to(Geom::Point const &p)
+{
+ // Maybe store guide as a Geom::Line?
+ Geom::Line guide =
+ Geom::Line::from_origin_and_vector(_origin, Geom::rot90(_normal));
+ guide *= _affine;
+ return Geom::distance(p, guide);
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of guideLine (or 1 if tolerance is zero).
+ */
+bool CanvasItemGuideLine::contains(Geom::Point const &p, double tolerance)
+{
+ if (tolerance == 0) {
+ tolerance = 1; // Can't pick of zero!
+ }
+
+ return closest_distance_to(p) < tolerance;
+}
+
+/**
+ * Returns the pointer to the origin control (the "dot")
+ */
+CanvasItemGuideHandle* CanvasItemGuideLine::dot() const
+{
+ return _origin_ctrl.get();
+}
+
+/**
+ * Update and redraw control guideLine.
+ */
+void CanvasItemGuideLine::update(Geom::Affine const &affine)
+{
+ if (_affine == affine && !_need_update) {
+ // Nothing to do.
+ return;
+ }
+
+ _affine = affine;
+
+ // Queue redraw of new area (and old too).
+ request_redraw();
+
+ _need_update = false;
+}
+
+/**
+ * Render guideLine to screen via Cairo.
+ */
+void CanvasItemGuideLine::render(Inkscape::CanvasItemBuffer *buf)
+{
+ if (!buf) {
+ std::cerr << "CanvasItemGuideLine::Render: No buffer!" << std::endl;
+ return;
+ }
+
+ if (!_visible) {
+ // Hidden
+ return;
+ }
+
+ // Document to canvas
+ Geom::Point const normal = _normal * _affine.withoutTranslation(); // Direction only
+ Geom::Point const origin = _origin * _affine;
+
+ /* Need to use floor()+0.5 such that Cairo will draw us lines with a width of a single pixel,
+ * without any aliasing. For this we need to position the lines at exactly half pixels, see
+ * https://www.cairographics.org/FAQ/#sharp_lines
+ * Must be consistent with the pixel alignment of the grid lines, see CanvasXYGrid::Render(),
+ * and the drawing of the rulers.
+ * Lastly, the origin control is also pixel-aligned and we want to visually cut through its
+ * exact center.
+ */
+ Geom::Point const aligned_origin = origin.floor() + Geom::Point(0.5, 0.5);
+
+ // Set up the Cairo rendering context
+ Cairo::RefPtr<Cairo::Context> ctx = buf->cr;
+ ctx->save();
+ ctx->translate(-buf->rect.left(), -buf->rect.top()); // Canvas to screen
+ ctx->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke),
+ SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke));
+ ctx->set_line_width(1);
+
+ if (_inverted) {
+ // operator not available in cairo C++ bindings
+ cairo_set_operator(ctx->cobj(), CAIRO_OPERATOR_DIFFERENCE);
+ }
+
+ if (!_label.empty()) { // Render text label
+ ctx->save();
+ ctx->translate(aligned_origin.x(), aligned_origin.y());
+
+ SPDesktop *desktop = nullptr;
+ if (_canvas) {
+ desktop = _canvas->get_desktop();
+ }
+ ctx->rotate(atan2(normal.cw()) + M_PI * (desktop && desktop->is_yaxisdown() ? 1 : 0));
+ ctx->translate(0, -(_origin_ctrl->radius() + LABEL_SEP)); // Offset by dot radius + 2
+ ctx->move_to(0, 0);
+ ctx->show_text(_label);
+ ctx->restore();
+ }
+
+ // Draw guide.
+ // Special case: horizontal and vertical lines (easier calculations)
+
+ // Don't use isHorizontal()/isVertical() as they test only exact matches.
+ if (Geom::are_near(normal.y(), 0.0)) {
+ // Vertical
+ double const position = aligned_origin.x();
+ ctx->move_to(position, buf->rect.top() + 0.5);
+ ctx->line_to(position, buf->rect.bottom() - 0.5);
+ } else if (Geom::are_near(normal.x(), 0.0)) {
+ // Horizontal
+ double position = aligned_origin.y();
+ ctx->move_to(buf->rect.left() + 0.5, position);
+ ctx->line_to(buf->rect.right() - 0.5, position);
+ } else {
+ // Angled
+ Geom::Line line = Geom::Line::from_origin_and_vector(aligned_origin, Geom::rot90(normal));
+
+ // Find intersections of the line with buf rectangle. There should be zero or two.
+ std::vector<Geom::Point> intersections;
+ for (unsigned i = 0; i < 4; ++i) {
+ Geom::LineSegment side(buf->rect.corner(i), buf->rect.corner((i+1)%4));
+ try {
+ Geom::OptCrossing oc = Geom::intersection(line, side);
+ if (oc) {
+ intersections.push_back(line.pointAt((*oc).ta));
+ }
+ } catch (Geom::InfiniteSolutions) {
+ // Shouldn't happen as we have already taken care of horizontal/vertical guides.
+ std::cerr << "CanvasItemGuideLine::render: Error: Infinite intersections." << std::endl;
+ }
+ }
+
+ if (intersections.size() == 2) {
+ double const x0 = intersections[0].x();
+ double const x1 = intersections[1].x();
+ double const y0 = intersections[0].y();
+ double const y1 = intersections[1].y();
+ ctx->move_to(x0, y0);
+ ctx->line_to(x1, y1);
+ }
+ }
+ ctx->stroke();
+
+ ctx->restore();
+}
+
+void CanvasItemGuideLine::hide()
+{
+ CanvasItem::hide();
+ _origin_ctrl->hide();
+}
+
+void CanvasItemGuideLine::show()
+{
+ CanvasItem::show();
+ _origin_ctrl->show();
+}
+
+void CanvasItemGuideLine::set_stroke(guint32 color)
+{
+ // Make sure the fill of the control is the same as the stroke
+ // of the guide-line:
+ _origin_ctrl->set_fill(color);
+ CanvasItem::set_stroke(color);
+}
+
+void CanvasItemGuideLine::set_label(Glib::ustring const & label)
+{
+ if (_label != label) {
+ _label = label;
+ request_update();
+ }
+}
+
+void CanvasItemGuideLine::set_locked(bool locked)
+{
+ if (_locked != locked) {
+ _locked = locked;
+ if (_locked) {
+ _origin_ctrl->set_shape(CANVAS_ITEM_CTRL_SHAPE_CROSS);
+ _origin_ctrl->set_stroke(CONTROL_LOCKED_COLOR);
+ _origin_ctrl->set_fill(0x00000000); // no fill
+ } else {
+ _origin_ctrl->set_shape(CANVAS_ITEM_CTRL_SHAPE_CIRCLE);
+ _origin_ctrl->set_stroke(0x00000000); // no stroke
+ _origin_ctrl->set_fill(_stroke); // fill the control with this guide's color
+ }
+ }
+}
+
+//===============================================================================================
+
+/**
+ * @brief Create a handle ("dot") along a guide line
+ * @param group - the associated canvas item group
+ * @param pos - position
+ * @param line - pointer to the corresponding guide line
+ */
+CanvasItemGuideHandle::CanvasItemGuideHandle(CanvasItemGroup *group,
+ Geom::Point const &pos,
+ CanvasItemGuideLine* line)
+ : CanvasItemCtrl(group, CANVAS_ITEM_CTRL_SHAPE_CIRCLE, pos)
+ , _my_line(line) // Save a pointer to our guide line
+{
+}
+
+/**
+ * Return the radius of the handle dot
+ */
+double CanvasItemGuideHandle::radius() const
+{
+ return 0.5 * static_cast<double>(_width); // radius is half the width
+}
+
+/**
+ * Update the size of the handle based on the index from Preferences
+ */
+void CanvasItemGuideHandle::set_size_via_index(int index)
+{
+ double const r = static_cast<double>(index) * SCALE;
+ unsigned long const rounded_diameter = std::lround(r * 2.0); // diameter is twice the radius
+ unsigned long size = rounded_diameter | 0x1; // make sure the size is always odd
+ if (size < MINIMUM_SIZE) {
+ size = MINIMUM_SIZE;
+ }
+ if (_width != size) {
+ _width = size;
+ _height = size;
+ _built = false;
+ request_update();
+ _my_line->request_update();
+ }
+}
+
+} // namespace Inkscape
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-guideline.h b/src/display/control/canvas-item-guideline.h
new file mode 100644
index 0000000..af22e4e
--- /dev/null
+++ b/src/display/control/canvas-item-guideline.h
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_GUIDELINE_H
+#define SEEN_CANVAS_ITEM_GUIDELINE_H
+
+/**
+ * A class to represent a control guide line.
+ */
+
+/*
+ * Authors:
+ * Tavmjong Bah - Rewrite of SPGuideLine
+ * Rafael Siejakowski - Tweaks to handle appearance
+ *
+ * Copyright (C) 2020-2022 the Authors.
+ *
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/ustring.h>
+
+#include <2geom/point.h>
+#include <2geom/transforms.h>
+
+#include "canvas-item.h"
+#include "canvas-item-ctrl.h"
+
+namespace Inkscape {
+
+class CanvasItemGroup; // A canvas control that contains other canvas controls.
+class CanvasItemGuideHandle; // A handle ("dot") serving as draggable origin control
+
+class CanvasItemGuideLine : public CanvasItem {
+
+public:
+ CanvasItemGuideLine(CanvasItemGroup *group, Glib::ustring label,
+ Geom::Point const &origin, Geom::Point const &normal);
+ ~CanvasItemGuideLine() override;
+
+ // Geometry
+ void set_origin(Geom::Point const &origin);
+ void set_normal(Geom::Point const &normal);
+ bool is_horizontal() const { return (_normal.x() == 0.0); }
+ bool is_vertical() const { return (_normal.y() == 0.0); }
+ void update(Geom::Affine const &affine) override;
+ double closest_distance_to(Geom::Point const &p);
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Display
+ void render(Inkscape::CanvasItemBuffer *buf) override;
+
+ // Properties
+ void hide() override;
+ void show() override;
+ void set_stroke(guint32 color) final;
+ void set_label(Glib::ustring const & label);
+ void set_locked(bool locked);
+ void set_inverted(bool inverted);
+ void set_sensitive(bool sensitive) { _sensitive = sensitive; }
+
+ // Getters
+ CanvasItemGuideHandle* dot() const;
+
+protected:
+ Geom::Point _origin;
+ Geom::Point _normal = Geom::Point(0,1);
+ Glib::ustring _label;
+ bool _locked = true; // Flipped in constructor to trigger init of _origin_ctrl.
+ bool _inverted = false;
+ bool _sensitive = false;
+ std::unique_ptr<CanvasItemGuideHandle> _origin_ctrl;
+
+private:
+ inline static guint32 const CONTROL_LOCKED_COLOR = 0x00000080; // RGBA black semitranslucent
+ inline static double const LABEL_SEP = 2.0; // Distance between the label and the origin control
+};
+
+
+class CanvasItemGuideHandle : public CanvasItemCtrl {
+
+public:
+ CanvasItemGuideHandle(CanvasItemGroup *group, Geom::Point const &pos, CanvasItemGuideLine *line);
+ double radius() const;
+ void set_size_via_index(int index) final;
+
+private:
+ CanvasItemGuideLine *_my_line; // The guide line we belong to
+
+ // static data
+ inline static double const SCALE = 0.55; // handle size relative to an auto-smooth node
+ inline static unsigned const MINIMUM_SIZE = 7; // smallest handle size, must be an odd int
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_GUIDELINE_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-quad.cpp b/src/display/control/canvas-item-quad.cpp
new file mode 100644
index 0000000..ccc7b2b
--- /dev/null
+++ b/src/display/control/canvas-item-quad.cpp
@@ -0,0 +1,207 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a control quadrilateral. Used to highlight selected text.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrlQuadr
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item-quad.h"
+
+#include "color.h" // SP_RGBA_x_F
+
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+/**
+ * Create an null control quad.
+ */
+CanvasItemQuad::CanvasItemQuad(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemQuad:Null";
+ _pickable = false; // For now, nobody gets events from this class!
+}
+
+/**
+ * Create a control quad. Point are in document coordinates.
+ */
+CanvasItemQuad::CanvasItemQuad(CanvasItemGroup *group,
+ Geom::Point const &p0, Geom::Point const &p1,
+ Geom::Point const &p2, Geom::Point const &p3)
+ : CanvasItem(group)
+ , _p0(p0)
+ , _p1(p1)
+ , _p2(p2)
+ , _p3(p3)
+{
+ _name = "CanvasItemQuad";
+ _pickable = false; // For now, nobody gets events from this class!
+
+ request_update();
+}
+
+/**
+ * Set a control quad. Points are in document coordinates.
+ */
+void CanvasItemQuad::set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3)
+{
+ std::cout << "Canvas_ItemQuad::set_cords: " << p0 << ", " << p1 << ", " << p2 << ", " << p3 << std::endl;
+ _p0 = p0;
+ _p1 = p1;
+ _p2 = p2;
+ _p3 = p3;
+
+ request_update();
+}
+
+/**
+ * Returns distance between point in canvas units and nearest point on quad.
+ */
+double CanvasItemQuad::closest_distance_to(Geom::Point const &p)
+{
+ double d = Geom::infinity();
+ std::cerr << "CanvasItemQuad::closest_distance_to: Not implemented!" << std::endl;
+ return d;
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of quad.
+ */
+bool CanvasItemQuad::contains(Geom::Point const &p, double tolerance)
+{
+ if (tolerance != 0) {
+ std::cerr << "CanvasItemQuad::contains: Non-zero tolerance not implemented!" << std::endl;
+ }
+
+ Geom::Point p0 = _p0 * _affine;
+ Geom::Point p1 = _p1 * _affine;
+ Geom::Point p2 = _p2 * _affine;
+ Geom::Point p3 = _p3 * _affine;
+
+ // From 2geom rotated-rect.cpp
+ return
+ Geom::cross(p1 - p0, p - p0) >= 0 &&
+ Geom::cross(p2 - p1, p - p1) >= 0 &&
+ Geom::cross(p3 - p2, p - p2) >= 0 &&
+ Geom::cross(p0 - p3, p - p3) >= 0;
+}
+
+/**
+ * Update and redraw control quad.
+ */
+void CanvasItemQuad::update(Geom::Affine const &affine)
+{
+ if (_affine == affine && !_need_update) {
+ // Nothing to do.
+ return;
+ }
+
+ if (_p0 == _p1 ||
+ _p1 == _p2 ||
+ _p2 == _p3 ||
+ _p3 == _p0) {
+ return; // Not quad or not initialized.
+ }
+
+ // Queue redraw of old area (erase previous content).
+ request_redraw(); // This is actually never useful as quads are always deleted
+ // and recreated when a node is moved! But keep it in case we
+ // change that. CHECK
+ // Get new bounds
+ _affine = affine;
+
+ Geom::Rect bounds;
+ bounds.expandTo(_p0);
+ bounds.expandTo(_p1);
+ bounds.expandTo(_p2);
+ bounds.expandTo(_p3);
+ bounds *= _affine; // Document to canvas.
+ bounds.expandBy(2); // Room for anti-aliasing effects.
+ _bounds = bounds;
+
+ // Queue redraw of new area
+ request_redraw();
+
+ _need_update = false;
+}
+
+/**
+ * Render quad to screen via Cairo.
+ */
+void CanvasItemQuad::render(Inkscape::CanvasItemBuffer *buf)
+{
+ if (!buf) {
+ std::cerr << "CanvasItemQuad::Render: No buffer!" << std::endl;
+ return;
+ }
+
+ if (_p0 == _p1 ||
+ _p1 == _p2 ||
+ _p2 == _p3 ||
+ _p3 == _p0) {
+ return; // Not quad or not initialized.
+ }
+
+ if (!_visible) {
+ // Hidden
+ return;
+ }
+
+ // Document to canvas
+ Geom::Point p0 = _p0 * _affine;
+ Geom::Point p1 = _p1 * _affine;
+ Geom::Point p2 = _p2 * _affine;
+ Geom::Point p3 = _p3 * _affine;
+
+ // Canvas to screen
+ p0 *= Geom::Translate(-buf->rect.min());
+ p1 *= Geom::Translate(-buf->rect.min());
+ p2 *= Geom::Translate(-buf->rect.min());
+ p3 *= Geom::Translate(-buf->rect.min());
+
+ buf->cr->save();
+
+ buf->cr->begin_new_path();
+
+ buf->cr->move_to(p0.x(), p0.y());
+ buf->cr->line_to(p1.x(), p1.y());
+ buf->cr->line_to(p2.x(), p2.y());
+ buf->cr->line_to(p3.x(), p3.y());
+ buf->cr->close_path();
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill),
+ SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill));
+ buf->cr->fill();
+
+ // Uncomment to show bounds
+ // Geom::Rect bounds = _bounds;
+ // bounds.expandBy(-1);
+ // bounds -= buf->rect.min();
+ // buf->cr->set_source_rgba(1.0, 0.0, 0.0, 1.0);
+ // buf->cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height());
+ // buf->cr->stroke();
+
+ buf->cr->restore();
+}
+
+} // namespace Inkscape
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-quad.h b/src/display/control/canvas-item-quad.h
new file mode 100644
index 0000000..d7945de
--- /dev/null
+++ b/src/display/control/canvas-item-quad.h
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_QUAD_H
+#define SEEN_CANVAS_ITEM_QUAD_H
+
+/**
+ * A class to represent a control quadrilateral. Used to highlight selected text.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrlQuadr
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/point.h>
+#include <2geom/transforms.h>
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+class CanvasItemGroup; // A canvas control that contains other canvas controls.
+
+class CanvasItemQuad : public CanvasItem {
+
+public:
+ CanvasItemQuad(CanvasItemGroup *group);
+ CanvasItemQuad(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1,
+ Geom::Point const &p2, Geom::Point const &p3);
+
+ // Geometry
+ void set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3);
+
+ void update(Geom::Affine const &affine) override;
+ double closest_distance_to(Geom::Point const &p); // Maybe not needed
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Display
+ void render(Inkscape::CanvasItemBuffer *buf) override;
+
+protected:
+ Geom::Point _p0;
+ Geom::Point _p1;
+ Geom::Point _p2;
+ Geom::Point _p3;
+};
+
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_QUAD_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-rect.cpp b/src/display/control/canvas-item-rect.cpp
new file mode 100644
index 0000000..137cbf1
--- /dev/null
+++ b/src/display/control/canvas-item-rect.cpp
@@ -0,0 +1,353 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a control rectangle. Used for rubberband selector, page outline, etc.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of CtrlRect
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cairo/cairo.h>
+
+#include "canvas-item-rect.h"
+#include "display/cairo-utils.h"
+
+#include "color.h" // SP_RGBA_x_F
+#include "inkscape.h" //
+#include "ui/widget/canvas.h"
+#include "display/cairo-utils.h" // Checkerboard background.
+
+namespace Inkscape {
+
+/**
+ * Create an null control rect.
+ */
+CanvasItemRect::CanvasItemRect(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemRect:Null";
+ _pickable = false; // For now, nobody gets events from this class!
+}
+
+/**
+ * Create a control rect. Point are in document coordinates.
+ */
+CanvasItemRect::CanvasItemRect(CanvasItemGroup *group, Geom::Rect const &rect)
+ : CanvasItem(group)
+ , _rect(rect)
+{
+ _name = "CanvasItemRect";
+ _pickable = false; // For now, nobody gets events from this class!
+ request_update();
+}
+
+/**
+ * Set a control rect. Points are in document coordinates.
+ */
+void CanvasItemRect::set_rect(Geom::Rect const &rect)
+{
+ _rect = rect;
+ request_update();
+}
+
+/**
+ * Returns distance between point in canvas units and nearest point in rect (zero if inside rect).
+ * Only valid if canvas is not rotated. (A rotated Geom::Rect yields a new axis-aligned Geom::Rect
+ * that contains the original rectangle; not a rotated rectangle.)
+ */
+double CanvasItemRect::closest_distance_to(Geom::Point const &p)
+{
+ if (_affine.isNonzeroRotation()) {
+ std::cerr << "CanvasItemRect::closest_distance_to: Affine includes rotation!" << std::endl;
+ }
+
+ Geom::Rect rect = _rect;
+ rect *= _affine; // Convert from document to canvas coordinates. (TODO Cache this.)
+ return Geom::distance(p, rect);
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of rect.
+ * Non-zero tolerance not implemented! Is valid for a rotated canvas.
+ */
+bool CanvasItemRect::contains(Geom::Point const &p, double tolerance)
+{
+ if (tolerance != 0) {
+ std::cerr << "CanvasItemRect::contains: Non-zero tolerance not implemented!" << std::endl;
+ }
+
+ Geom::Point p0 = _rect.corner(0) * _affine;
+ Geom::Point p1 = _rect.corner(1) * _affine;
+ Geom::Point p2 = _rect.corner(2) * _affine;
+ Geom::Point p3 = _rect.corner(3) * _affine;
+
+ // From 2geom rotated-rect.cpp
+ return
+ Geom::cross(p1 - p0, p - p0) >= 0 &&
+ Geom::cross(p2 - p1, p - p1) >= 0 &&
+ Geom::cross(p3 - p2, p - p2) >= 0 &&
+ Geom::cross(p0 - p3, p - p3) >= 0;
+}
+
+/**
+ * Update and redraw control rect.
+ */
+void CanvasItemRect::update(Geom::Affine const &affine)
+{
+ if (_affine == affine && !_need_update) {
+ // Nothing to do.
+ return;
+ }
+
+ if (_rect.area() == 0) {
+ return; // Nothing to show
+ }
+
+ // Queue redraw of old area (erase previous content).
+ request_redraw();
+
+ // Get new bounds
+ _affine = affine;
+
+ // Enlarge bbox by twice shadow size (to allow for shadow on any side with a 45deg rotation).
+ _bounds = _rect;
+ // note: add shadow size before applying transformation, since get_shadow_size accounts for scale
+ _bounds.expandBy(2 * get_shadow_size());
+ _bounds *= _affine;
+ _bounds.expandBy(2); // Room for stroke.
+
+ // Queue redraw of new area
+ request_redraw();
+
+ _need_update = false;
+}
+
+/**
+ * Render rect to screen via Cairo.
+ */
+void CanvasItemRect::render(Inkscape::CanvasItemBuffer *buf)
+{
+ if (!buf) {
+ std::cerr << "CanvasItemRect::Render: No buffer!" << std::endl;
+ return;
+ }
+
+ if (!_bounds.intersects(buf->rect)) {
+ return; // Rectangle not inside buffer rectangle.
+ }
+
+ if (!_visible) {
+ // Hidden
+ return;
+ }
+
+ // Geom::Rect is an axis-aligned rectangle. We need to rotate it if the canvas is rotated!
+
+ // Get canvas rotation (scale is isotropic).
+ double rotation = atan2(_affine[1], _affine[0]);
+
+ // Are we axis aligned?
+ double mod_rot = fmod(rotation * M_2_PI, 1);
+ bool axis_aligned = Geom::are_near(mod_rot, 0) || Geom::are_near(mod_rot, 1.0);
+
+ // Get the points we need transformed into window coordinates.
+ Geom::Point rect_transformed[4];
+ for (unsigned int i = 0; i < 4; ++i) {
+ rect_transformed[i] = _rect.corner(i) * _affine;
+ }
+
+ auto rect = _rect;
+
+ using Geom::X;
+ using Geom::Y;
+
+ if (axis_aligned) {
+ auto temp = _rect * _affine;
+ auto min = temp.min();
+ auto max = temp.max();
+ auto pixgrid = Geom::Rect(
+ Geom::Point(floor(min[X]) + 0.5, floor(min[Y]) + 0.5),
+ Geom::Point(floor(max[X]) + 0.5, floor(max[Y]) + 0.5));
+ rect = pixgrid * _affine.inverse();
+ }
+
+ buf->cr->save();
+ buf->cr->translate(-buf->rect.left(), -buf->rect.top());
+
+ if (_inverted) {
+ // buf->cr->set_operator(Cairo::OPERATOR_XOR); // Blend mode operators do not have C++ bindings!
+ cairo_set_operator(buf->cr->cobj(), CAIRO_OPERATOR_DIFFERENCE);
+ }
+
+ // fill background?
+ if (_background && !buf->outline_overlay_pass) {
+ buf->cr->save();
+ Cairo::Matrix m(_affine[0], _affine[1], _affine[2], _affine[3], _affine[4], _affine[5]);
+ buf->cr->transform(m);
+ buf->cr->rectangle(rect.corner(0)[X], rect.corner(0)[Y], rect.width(), rect.height());
+ // counter fill scaling (necessary for checkerboard pattern)
+ _background->set_matrix(m);
+ buf->cr->set_source(_background);
+ buf->cr->fill();
+ buf->cr->restore();
+ }
+
+ cairo_pattern_t *pattern = _canvas->get_background_pattern()->cobj();
+ guint32 backcolor = ink_cairo_pattern_get_argb32(pattern);
+ EXTRACT_ARGB32(backcolor, ab,rb,gb,bb)
+
+ // Draw shadow first. Shadow extends under rectangle to reduce aliasing effects.
+ if (_shadow_width > 0 && !_dashed) {
+ // there's only one UI knob to adjust border and shadow color, so instead of using border color
+ // transparency as is, it is boosted by this function, since shadow attenuates it
+ const auto a = (exp(-3 * SP_RGBA32_A_F(_shadow_color)) - 1) / (exp(-3) - 1);
+ buf->cr->save();
+
+ auto affine = _affine;
+ if (auto desktop = _canvas->get_desktop()) {
+ rect *= desktop->doc2dt();
+ affine = desktop->doc2dt() * affine;
+ }
+ Cairo::Matrix m(affine[0], affine[1], affine[2], affine[3], affine[4], affine[5]);
+ buf->cr->transform(m);
+ ink_cairo_draw_drop_shadow(buf->cr, rect, get_shadow_size(), _shadow_color, a);
+ buf->cr->restore();
+ }
+
+ // Setup rectangle path
+ if (axis_aligned) {
+
+ // Snap to pixel grid
+ Geom::Rect outline( _rect.min() * _affine, _rect.max() * _affine);
+ buf->cr->rectangle(floor(outline.min()[X])+0.5,
+ floor(outline.min()[Y])+0.5,
+ floor(outline.max()[X]) - floor(outline.min()[X]),
+ floor(outline.max()[Y]) - floor(outline.min()[Y]));
+ } else {
+
+ // Rotated
+ buf->cr->move_to(rect_transformed[0][X], rect_transformed[0][Y] );
+ buf->cr->line_to(rect_transformed[1][X], rect_transformed[1][Y] );
+ buf->cr->line_to(rect_transformed[2][X], rect_transformed[2][Y] );
+ buf->cr->line_to(rect_transformed[3][X], rect_transformed[3][Y] );
+ buf->cr->close_path();
+ }
+ static std::valarray<double> dashes = {4.0, 4.0};
+ if (_dashed) {
+ buf->cr->set_dash(dashes, -0.5);
+ }
+ // Draw border (stroke).
+ buf->cr->set_line_width(1);
+ // we maybe have painted the background, back to "normal" compositing
+
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke),
+ SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke));
+ buf->cr->stroke_preserve();
+
+ // Highlight the border by drawing it in _shadow_color.
+ if (_shadow_width == 1 && _dashed) {
+ buf->cr->set_dash(dashes, 3.5); // Dash offset by dash length.
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(_shadow_color), SP_RGBA32_G_F(_shadow_color),
+ SP_RGBA32_B_F(_shadow_color), SP_RGBA32_A_F(_shadow_color));
+ buf->cr->stroke_preserve();
+ }
+
+ buf->cr->begin_new_path(); // Clear path or get weird artifacts.
+
+ // Uncomment to show bounds
+ // Geom::Rect bounds = _bounds;
+ // bounds.expandBy(-1);
+ // bounds -= buf->rect.min();
+ // buf->cr->set_source_rgba(1.0, 0.0, _shadow_width / 3.0, 1.0);
+ // buf->cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height());
+ // buf->cr->stroke();
+
+ buf->cr->restore();
+}
+
+void CanvasItemRect::set_dashed(bool dashed)
+{
+ if (_dashed != dashed) {
+ _dashed = dashed;
+ request_redraw();
+ }
+}
+
+void CanvasItemRect::set_inverted(bool inverted)
+{
+ if (_inverted != inverted) {
+ _inverted = inverted;
+ request_redraw();
+ }
+}
+
+void CanvasItemRect::set_shadow(guint32 color, int width)
+{
+ if (_shadow_color != color || _shadow_width != width) {
+ _shadow_color = color;
+ _shadow_width = width;
+ request_redraw();
+ }
+}
+
+void CanvasItemRect::set_background(guint32 background) {
+ _set_background(Cairo::SolidPattern::create_rgba(SP_RGBA32_R_F(background), SP_RGBA32_G_F(background), SP_RGBA32_B_F(background), SP_RGBA32_A_F(background)));
+}
+
+void CanvasItemRect::_set_background(Cairo::RefPtr<Cairo::Pattern> background) {
+ if (_background != background) {
+ _background = background;
+ request_redraw();
+ }
+}
+
+double CanvasItemRect::get_scale() const {
+ return sqrt(_affine[0] * _affine[0] + _affine[1] * _affine[1]);
+}
+
+double CanvasItemRect::get_shadow_size() const {
+ // gradient drop shadow needs much more room than solid one, so inflating the size;
+ // fudge factor of 6 used to make sizes baked in svg documents work as steps:
+ // typical value of 2 will work out to 12 pixels which is a narrow shadow (b/c of exponential fall of)
+ auto size = _shadow_width * 6;
+ if (size < 0) {
+ size = 0;
+ }
+ else if (size > 120) {
+ // arbitrarily selected max size, so Cairo gradient doesn't blow up if document has bogus shadow values
+ size = 120;
+ }
+ auto scale = get_scale();
+
+ // calculate space for gradient shadow; if divided by 'scale' it would be zoom independent (fixed in size);
+ // if 'scale' is not used, drop shadow will be getting smaller with document zoom;
+ // here hybrid approach is used: "unscaling" with square root of scale allows shadows to diminish
+ // more slowly at small zoom levels (so it's still perceptible) and grow more slowly at high mag (where it doesn't matter, b/c it's typically off-screen)
+ return size / (scale > 0 ? sqrt(scale) : 1);
+}
+
+void CanvasItemRect::set_background_checkerboard(guint32 rgba, bool use_alpha) {
+ auto pattern = ink_cairo_pattern_create_checkerboard(rgba, use_alpha);
+ auto background = Cairo::RefPtr<Cairo::Pattern>(new Cairo::Pattern(pattern));
+ _set_background(background);
+}
+
+} // namespace Inkscape
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-rect.h b/src/display/control/canvas-item-rect.h
new file mode 100644
index 0000000..0c69d43
--- /dev/null
+++ b/src/display/control/canvas-item-rect.h
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_RECT_H
+#define SEEN_CANVAS_ITEM_RECT_H
+
+/**
+ * A class to represent a control rectangle. Used for rubberband selector, page outline, etc.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of CtrlRect
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <memory>
+#include <2geom/path.h>
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+class CanvasItemGroup; // A canvas control that contains other canvas controls.
+
+class CanvasItemRect : public CanvasItem {
+
+public:
+ CanvasItemRect(CanvasItemGroup *group);
+ CanvasItemRect(CanvasItemGroup *group, Geom::Rect const &rect);
+
+ // Geometry
+ void set_rect(Geom::Rect const &rect);
+
+ void update(Geom::Affine const &affine) override;
+ double closest_distance_to(Geom::Point const &p); // Maybe not needed
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Display
+ void render(Inkscape::CanvasItemBuffer *buf) override;
+
+ // Properties
+ void set_dashed(bool dash = true);
+ void set_inverted(bool inverted = false);
+ void set_shadow(guint32 color, int width);
+ void set_background(guint32 background);
+ void set_background_checkerboard(guint32 rgba, bool use_alpha);
+
+protected:
+ void _set_background(Cairo::RefPtr<Cairo::Pattern> background);
+ double get_shadow_size() const;
+ double get_scale() const;
+
+ Geom::Rect _rect;
+ bool _dashed = false;
+ bool _inverted = false;
+ int _shadow_width = 0;
+ guint32 _shadow_color = 0x00000000;
+ Cairo::RefPtr<Cairo::Pattern> _background; // optional background
+};
+
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_RECT_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-rotate.cpp b/src/display/control/canvas-item-rotate.cpp
new file mode 100644
index 0000000..a013b65
--- /dev/null
+++ b/src/display/control/canvas-item-rotate.cpp
@@ -0,0 +1,212 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class for previewing a canvas rotation.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasRotate.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cairomm/cairomm.h>
+#include <gdkmm/event.h>
+
+#include "canvas-item-rotate.h"
+
+#include "color.h" // SP_RGBA_x_F
+#include "desktop.h"
+#include "inkscape.h"
+
+#include "display/cairo-utils.h" // Copy Cairo surface.
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+/**
+ * Create an null control rotate.
+ */
+CanvasItemRotate::CanvasItemRotate(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemRotate";
+ _pickable = true; // We need the events!
+ _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity());
+}
+
+
+/**
+ * Returns distance between point in canvas units and nearest point on rotate.
+ */
+double CanvasItemRotate::closest_distance_to(Geom::Point const &p)
+{
+ double d = Geom::infinity();
+ std::cerr << "CanvasItemRotate::closest_distance_to: Not implemented!" << std::endl;
+ return d;
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of rotate.
+ */
+bool CanvasItemRotate::contains(Geom::Point const &p, double tolerance)
+{
+ return true; // We're always picked!
+}
+
+/**
+ * Update and redraw control rotate.
+ */
+void CanvasItemRotate::update(Geom::Affine const &affine)
+{
+ _affine = affine;
+ request_redraw();
+}
+
+/**
+ * Render rotate to screen via Cairo.
+ */
+void CanvasItemRotate::render(Inkscape::CanvasItemBuffer *buf)
+{
+ return; // We do no proper rendering!
+}
+
+/**
+ * Start
+ */
+void CanvasItemRotate::start(SPDesktop *desktop)
+{
+ _desktop = desktop;
+
+ _current_angle = 0.0;
+
+ _surface_copy = ink_cairo_surface_copy(_canvas->get_backing_store());
+}
+
+/**
+ * Render to widget.
+ */
+void CanvasItemRotate::paint()
+{
+ auto background = _canvas->get_backing_store();
+
+ if (!background) {
+ std::cerr << "CanvasItemRotate::paint(): No background!" << std::endl;
+ return;
+ }
+
+ double width = background->get_width();
+ double height = background->get_height();
+
+ // Draw rotated canvas.
+ auto context = Cairo::Context::create(background);
+ context->set_operator(Cairo::OPERATOR_SOURCE);
+ context->translate(width/2.0, height/2.0);
+ context->rotate(Geom::rad_from_deg(-_current_angle));
+ context->translate(-width/2.0, -height/2.0);
+ context->set_source(_surface_copy, 0, 0);
+ context->paint();
+
+ _canvas->queue_draw();
+}
+
+/**
+ * Handle events.
+ */
+bool CanvasItemRotate::handle_event(GdkEvent *event)
+{
+ // Get geometry
+ Geom::Rect viewbox = _canvas->get_area_world();
+ _center = viewbox.midpoint();
+
+ switch (event->type) {
+ case GDK_MOTION_NOTIFY:
+ {
+ Geom::Point cursor( event->motion.x, event->motion.y );
+
+ // Both cursor and center are in window coordinates
+ Geom::Point rcursor( cursor - _center );
+ double angle = Geom::deg_from_rad( Geom::atan2(rcursor) );
+
+
+ // Set start angle
+ if (_start_angle < -360) {
+ _start_angle = angle;
+ }
+
+ const double rotation_snap = 15;
+
+ double delta_angle = _start_angle - angle;
+
+ if (event->motion.state & GDK_SHIFT_MASK &&
+ event->motion.state & GDK_CONTROL_MASK) {
+ delta_angle = 0;
+ } else if (event->motion.state & GDK_SHIFT_MASK) {
+ delta_angle = round(delta_angle/rotation_snap) * rotation_snap;
+ } else if (event->motion.state & GDK_CONTROL_MASK) {
+ // ?
+ } else if (event->motion.state & GDK_MOD1_MASK) {
+ // Decimal raw angle
+ } else {
+ delta_angle = floor(delta_angle);
+ }
+
+ _current_angle = delta_angle;
+
+ // Correct line for snapping of angle
+ double distance = rcursor.length();
+ _cursor = Geom::Point::polar( Geom::rad_from_deg(angle), distance );
+
+ // Update screen
+ paint();
+ break;
+ }
+ case GDK_BUTTON_RELEASE:
+ {
+ // Rotate the actual canvas
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP; // FIXME..
+ desktop->rotate_relative_center_point (desktop->w2d(_center),
+ (desktop->w2d().det() > 0 ? -1 : 1) *
+ Geom::rad_from_deg(_current_angle) );
+
+ // We're done
+ ungrab();
+ hide();
+
+ _start_angle = -1000;
+ break;
+ }
+ case GDK_KEY_PRESS:
+ // std::cout << " Key press: " << std::endl;
+ break;
+ case GDK_KEY_RELEASE:
+ // std::cout << " Key release: " << std::endl;
+ break;
+ default:
+ // ui_dump_event (event, "sp_canvas_rotate_event: unwanted event: ");
+ break;
+ }
+
+ if (event->type == GDK_KEY_PRESS) return false; // Why?
+
+ // Don't emit event signal!
+
+ return true;
+}
+
+} // namespace Inkscape
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-rotate.h b/src/display/control/canvas-item-rotate.h
new file mode 100644
index 0000000..a2aebd5
--- /dev/null
+++ b/src/display/control/canvas-item-rotate.h
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_ROTATE_H
+#define SEEN_CANVAS_ITEM_ROTATE_H
+
+/**
+ * A class for previewing a canvas rotation.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of RotateCanvasItem.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/point.h>
+#include <2geom/transforms.h>
+
+#include "canvas-item.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+
+class CanvasRotate;
+
+namespace UI::Widget {
+class Canvas;
+}
+
+class CanvasItemGroup; // A canvas control that contains other canvas controls.
+
+class CanvasItemRotate : public CanvasItem {
+
+public:
+ CanvasItemRotate(CanvasItemGroup *group);
+
+ // Geometry
+ void update(Geom::Affine const &affine) override;
+ double closest_distance_to(Geom::Point const &p); // Maybe not needed
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Display
+ void render(Inkscape::CanvasItemBuffer *buf) override;
+
+ // Events
+ bool handle_event(GdkEvent *event) override;
+ void start(SPDesktop *desktop);
+ void paint();
+
+
+protected:
+ SPDesktop *_desktop = nullptr;
+ Geom::Point _center; // Center of screen.
+ Geom::Point _cursor; // Position of cursor relative to center (after angle snapping).
+ double _current_angle = 0.0; // Rotation in degrees.
+ double _start_angle = -1000; // Initial angle, determined by cursor position.
+ Cairo::RefPtr<Cairo::ImageSurface> _surface_copy; // Copy of original surface.
+};
+
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_ROTATE_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-text.cpp b/src/display/control/canvas-item-text.cpp
new file mode 100644
index 0000000..f4a6801
--- /dev/null
+++ b/src/display/control/canvas-item-text.cpp
@@ -0,0 +1,317 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A class to represent a control textrilateral. Used to highlight selected text.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCtrlTextr
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item-text.h"
+
+#include <cmath>
+#include <utility> // std::move
+#include <glibmm/i18n.h>
+
+#include "color.h" // SP_RGBA_x_F
+
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+/**
+ * Create an null control text.
+ */
+CanvasItemText::CanvasItemText(CanvasItemGroup *group)
+ : CanvasItem(group)
+{
+ _name = "CanvasItemText";
+ _pickable = false; // Text is never pickable.
+ _fill = 0x33337fff; // Override CanvasItem default.
+}
+
+/**
+ * Create a control text. Point are in document coordinates.
+ */
+CanvasItemText::CanvasItemText(CanvasItemGroup *group, Geom::Point const &p, Glib::ustring text)
+ : CanvasItem(group)
+ , _p(p)
+ , _text(std::move(text))
+{
+ _name = "CanvasItemText";
+ _pickable = false; // Text is never pickable.
+ _fill = 0x33337fff; // Override CanvasItem default.
+
+ request_update();
+}
+
+/**
+ * Set a text position. Position is in document coordinates.
+ */
+void CanvasItemText::set_coord(Geom::Point const &p)
+{
+ _p = p;
+
+ request_update();
+}
+
+/**
+ * Set a text position. Position is in document coordinates.
+ */
+void CanvasItemText::set_bg_radius(double const &rad)
+{
+ _bg_rad = rad;
+
+ request_update();
+}
+
+/**
+ * Returns distance between point in canvas units and nearest point on text.
+ */
+double CanvasItemText::closest_distance_to(Geom::Point const &p)
+{
+ double d = Geom::infinity();
+ std::cerr << "CanvasItemText::closest_distance_to: Not implemented!" << std::endl;
+ return d;
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of text.
+ */
+bool CanvasItemText::contains(Geom::Point const &p, double tolerance)
+{
+ return false; // We never select text.
+}
+
+/**
+ * Update and redraw control text.
+ */
+void CanvasItemText::update(Geom::Affine const &affine)
+{
+ if (_affine == affine && !_need_update) {
+ // Nothing to do.
+ return;
+ }
+
+ // Queue redraw of old area (erase previous content).
+ request_redraw();
+
+ // Get new bounds
+ _affine = affine;
+ Geom::Point p = _p * _affine;
+
+ // Measure text size
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, 1, 1);
+ auto context = Cairo::Context::create(surface);
+ context->select_font_face(_fontname, Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_NORMAL);
+ context->set_font_size(_fontsize);
+ context->get_text_extents(_text, _text_size);
+
+ if (_fixed_line) {
+ // TRANSLATORS: This is a set of letters to test for font accender and decenders.
+ context->get_text_extents(_("lg1p$"), _text_extent);
+ } else {
+ _text_extent = _text_size;
+ }
+
+ // See note at bottom.
+ _bounds = Geom::Rect::from_xywh(0, 0,
+ _text_size.x_advance + (_border * 2),
+ _text_extent.height + (_border * 2));
+
+ // Offset relative to requested point
+ double offset_x = -(_anchor_position.x() * _bounds.width());
+ double offset_y = -(_anchor_position.y() * _bounds.height());
+ offset_x += p.x() + _adjust_offset.x();
+ offset_y += p.y() + _adjust_offset.y();
+ _bounds *= Geom::Translate(Geom::Point(int(offset_x), int(offset_y)));
+
+ // Pixel alignment of background. Avoid aliasing artifacts on redraw.
+ _bounds = _bounds.roundOutwards();
+
+ // Queue redraw of new area
+ request_redraw();
+
+ _need_update = false;
+}
+
+/**
+ * Render text to screen via Cairo.
+ */
+void CanvasItemText::render(Inkscape::CanvasItemBuffer *buf)
+{
+ if (!buf) {
+ std::cerr << "CanvasItemText::Render: No buffer!" << std::endl;
+ return;
+ }
+
+ if (!_visible) {
+ // Hidden
+ return;
+ }
+
+ buf->cr->save();
+
+ double x = _bounds.min().x() - buf->rect.min().x();
+ double y = _bounds.min().y() - buf->rect.min().y();
+
+ // Background
+ if (_use_background) {
+ if (_bg_rad == 0) {
+ buf->cr->rectangle(x, y, _bounds.width(), _bounds.height());
+ } else {
+ double smallest = std::min(_bounds.width(), _bounds.height());
+ double radius = _bg_rad * (smallest / 2);
+ buf->cr->arc(x + _bounds.width() - radius,
+ y + radius,
+ radius,
+ -M_PI_2,
+ 0);
+
+ buf->cr->arc(x + _bounds.width() - radius,
+ y + _bounds.height() - radius,
+ radius,
+ 0,
+ M_PI_2);
+
+ buf->cr->arc(x + radius, y + _bounds.height() - radius,
+ radius,
+ M_PI_2,
+ M_PI);
+
+ buf->cr->arc(x + radius,
+ y + radius,
+ radius,
+ M_PI,
+ 3*M_PI_2);
+ }
+ buf->cr->set_line_width(2);
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(_background), SP_RGBA32_G_F(_background),
+ SP_RGBA32_B_F(_background), SP_RGBA32_A_F(_background));
+ buf->cr->fill();
+ }
+
+ // Center the text inside the draw background box
+ auto bx = x + _bounds.width()/2.0;
+ auto by = y + _bounds.height()/2.0;
+ buf->cr->move_to(int(bx - _text_size.x_bearing - _text_size.width/2.0),
+ int(by - _text_size.y_bearing - _text_extent.height/2.0));
+
+ buf->cr->select_font_face(_fontname, Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_NORMAL);
+ buf->cr->set_font_size(_fontsize);
+ buf->cr->text_path(_text);
+ buf->cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill),
+ SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill));
+ buf->cr->fill();
+
+#ifdef CANVAS_ITEM_DEBUG
+ Geom::Rect bounds = _bounds;
+ bounds.expandBy(-1);
+ bounds -= buf->rect.min();
+ buf->cr->set_source_rgba(1.0, 0.0, 0.0, 1.0);
+ buf->cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height());
+ buf->cr->stroke();
+#endif
+
+ buf->cr->restore();
+}
+
+void CanvasItemText::set_text(Glib::ustring const &text)
+{
+ if (_text != text) {
+ _text = text;
+ request_update(); // Might be larger than before!
+ }
+}
+
+void CanvasItemText::set_fontsize(double fontsize)
+{
+ if (_fontsize != fontsize) {
+ _fontsize = fontsize;
+ request_update(); // Might be larger than before!
+ }
+}
+
+void CanvasItemText::set_background(guint32 background)
+{
+ if (_background != background) {
+ _background = background;
+ request_redraw();
+ }
+ _use_background = true;
+}
+
+/**
+ * Set the anchor point, x and y between 0.0 and 1.0.
+ */
+void CanvasItemText::set_anchor(Geom::Point const &anchor_pt)
+{
+ if (_anchor_position != anchor_pt) {
+ _anchor_position = anchor_pt;
+ _canvas->request_update();
+ }
+}
+
+void CanvasItemText::set_adjust(Geom::Point const &adjust_pt)
+{
+ if (_adjust_offset != adjust_pt) {
+ _adjust_offset = adjust_pt;
+ _canvas->request_update();
+ }
+}
+
+void CanvasItemText::set_fixed_line(bool fixed_line)
+{
+ if (_fixed_line != fixed_line) {
+ _fixed_line = fixed_line;
+ _canvas->request_update();
+ }
+}
+
+} // namespace Inkscape
+
+/* FROM: http://lists.cairographics.org/archives/cairo-bugs/2009-March/003014.html
+ - Glyph surfaces: In most font rendering systems, glyph surfaces
+ have an origin at (0,0) and a bounding box that is typically
+ represented as (x_bearing,y_bearing,width,height). Depending on
+ which way y progresses in the system, y_bearing may typically be
+ negative (for systems similar to cairo, with origin at top left),
+ or be positive (in systems like PDF with origin at bottom left).
+ No matter which is the case, it is important to note that
+ (x_bearing,y_bearing) is the coordinates of top-left of the glyph
+ relative to the glyph origin. That is, for example:
+
+ Scaled-glyph space:
+
+ (x_bearing,y_bearing) <-- negative numbers
+ +----------------+
+ | . |
+ | . |
+ |......(0,0) <---|-- glyph origin
+ | |
+ | |
+ +----------------+
+ (width+x_bearing,height+y_bearing)
+
+ Note the similarity of the origin to the device space. That is
+ exactly how we use the device_offset to represent scaled glyphs:
+ to use the device-space origin as the glyph origin.
+*/
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item-text.h b/src/display/control/canvas-item-text.h
new file mode 100644
index 0000000..c5d5cff
--- /dev/null
+++ b/src/display/control/canvas-item-text.h
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_TEXT_H
+#define SEEN_CANVAS_ITEM_TEXT_H
+
+/**
+ * A class to represent on-screen text.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasText.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/ustring.h>
+
+#include <2geom/point.h>
+#include <2geom/transforms.h>
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+
+namespace UI::Widget {
+class Canvas;
+}
+
+class CanvasItemGroup; // A canvas control that contains other canvas controls.
+
+class CanvasItemText : public CanvasItem {
+
+public:
+ CanvasItemText(CanvasItemGroup *group);
+ CanvasItemText(CanvasItemGroup *group, Geom::Point const &p, Glib::ustring text);
+
+ // Geometry
+ void set_coord(Geom::Point const &p);
+ void set_bg_radius(double const &rad);
+
+ void update(Geom::Affine const &affine) override;
+ double closest_distance_to(Geom::Point const &p); // Maybe not needed
+
+ // Selection
+ bool contains(Geom::Point const &p, double tolerance = 0) override;
+
+ // Display
+ void render(Inkscape::CanvasItemBuffer *buf) override;
+
+ // Properties
+ void set_text(Glib::ustring const &text);
+ void set_fontsize(double fontsize);
+ void set_background(guint32 background);
+ void set_anchor(Geom::Point const &anchor_pt);
+ void set_adjust(Geom::Point const &adjust_pt);
+ void set_fixed_line(bool fixed_line);
+
+protected:
+ Geom::Point _p; // Position of text (not box around text).
+ Cairo::TextExtents _text_extent;
+ Cairo::TextExtents _text_size;
+ Geom::Point _anchor_position;
+ Geom::Point _adjust_offset;
+ Glib::ustring _text;
+ std::string _fontname = "sans-serif";
+ double _fontsize = 10;
+ double _bg_rad = 0;
+ guint32 _background = 0x0000007f;
+ bool _use_background = false;
+ bool _fixed_line = false; // Correction for font heights
+ const double _border = 3; // Must be a const to allow alignment with other text boxes.
+};
+
+
+} // namespace Inkscape
+
+#endif // SEEN_CANVAS_ITEM_TEXT_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item.cpp b/src/display/control/canvas-item.cpp
new file mode 100644
index 0000000..6670b19
--- /dev/null
+++ b/src/display/control/canvas-item.cpp
@@ -0,0 +1,261 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * Abstract base class for on-canvas control items.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasItem
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-item.h"
+
+#include "canvas-item-group.h"
+
+#include "ui/widget/canvas.h"
+
+namespace Inkscape {
+
+CanvasItem::CanvasItem(CanvasItemGroup *group)
+ : _name("CanvasItem")
+{
+ if (group) {
+ group->add(this);
+ _parent = group;
+ _canvas = group->get_canvas();
+ _affine = group->get_affine();
+ }
+}
+
+CanvasItem::~CanvasItem()
+{
+ if (_parent) {
+ _parent->remove(this, false); // remove() should not delete this or we'll double delete!
+ }
+
+ // Clear canvas of item.
+ request_redraw();
+
+ // Clear any pointers to this object in canvas.
+ _canvas->canvas_item_destructed(this);
+}
+
+bool CanvasItem::is_descendant_of(CanvasItem *ancestor)
+{
+ auto item = this;
+ while (item) {
+ if (item == ancestor) {
+ return true;
+ }
+ item = item->get_parent();
+ }
+ return false;
+}
+
+int CanvasItem::get_z_position()
+{
+ if (!_parent) {
+ std::cerr << "CanvasItem::get_z_position: No parent!" << std::endl;
+ return -1;
+ }
+
+ size_t position = 0;
+ for (auto it = _parent->items.begin(); it != _parent->items.end(); ++it, ++position) {
+ if (&*it == this) {
+ return position;
+ }
+ }
+
+ std::cerr << "CanvasItem::get_z_position: item not found!" << std::endl;
+ return -1;
+}
+
+void CanvasItem::set_z_position(unsigned int n)
+{
+ if (!_parent) {
+ std::cerr << "CanvasItem::set_z_position: No parent!" << std::endl;
+ }
+
+ if (n == 0) {
+ this->lower_to_bottom(); // Low cost operation
+ return;
+ }
+
+ if (n > _parent->items.size() - 2) {
+ this->raise_to_top(); // Low cost operation
+ return;
+ }
+
+ _parent->items.erase(_parent->items.iterator_to(*this));
+
+ size_t position = 0;
+ for (auto it = _parent->items.begin(); it != _parent->items.end(); ++it, ++position) {
+ if (position == n) {
+ _parent->items.insert(it, *this);
+ break;
+ }
+ }
+}
+
+void CanvasItem::raise_to_top()
+{
+ if (!_parent) {
+ std::cerr << "CanvasItem::raise_to_top: No parent!" << std::endl;
+ }
+
+ _parent->items.erase(_parent->items.iterator_to(*this));
+ _parent->items.push_back(*this);
+}
+
+void CanvasItem::lower_to_bottom()
+{
+ if (!_parent) {
+ std::cerr << "CanvasItem::lower_to_bottom: No parent!" << std::endl;
+ }
+
+ _parent->items.erase(_parent->items.iterator_to(*this));
+ _parent->items.push_front(*this);
+}
+
+// Indicate geometry changed and bounds needs recalculating.
+void CanvasItem::request_update()
+{
+ _need_update = true;
+ if (_parent) {
+ _parent->request_update();
+ } else {
+ _canvas->request_update();
+ }
+}
+
+void CanvasItem::show()
+{
+ if (_visible) {
+ return; // Already visible.
+ }
+
+ _visible = true;
+ // update bounds when visibility changes
+ request_update();
+ request_redraw();
+}
+
+int CanvasItem::grab(Gdk::EventMask event_mask, GdkCursor *cursor)
+{
+ return grab(event_mask, Glib::wrap(cursor));
+}
+
+// Grab all events! TODO: Return boolean
+int CanvasItem::grab(Gdk::EventMask event_mask, Glib::RefPtr<Gdk::Cursor> cursor)
+{
+#ifdef CANVAS_ITEM_DEBUG
+ std::cout << "CanvasItem::grab: " << _name << std::endl;
+#endif
+ // Don't grab if we already have a grabbed item!
+ if (_canvas->get_grabbed_canvas_item()) {
+ return -1;
+ }
+
+ auto const display = Gdk::Display::get_default();
+ auto const seat = display->get_default_seat();
+ auto const window = _canvas->get_window();
+ seat->grab(window, Gdk::SEAT_CAPABILITY_ALL_POINTING, false, cursor, nullptr);
+
+ _canvas->set_grabbed_canvas_item(this, event_mask);
+ _canvas->set_current_canvas_item(this); // So that all events go to grabbed item.
+ return 0;
+}
+
+void CanvasItem::ungrab()
+{
+#ifdef CANVAS_ITEM_DEBUG
+ std::cout << "CanvasItem::ungrab: " << _name << std::endl;
+#endif
+ if (_canvas->get_grabbed_canvas_item() != this) {
+ return; // Sanity check
+ }
+
+ _canvas->set_grabbed_canvas_item(nullptr, (Gdk::EventMask)0); // Zero mask
+
+ auto const display = Gdk::Display::get_default();
+ auto const seat = display->get_default_seat();
+ seat->ungrab();
+}
+
+void CanvasItem::hide()
+{
+ if (!_visible) {
+ return; // Already hidden
+ }
+
+ _visible = false;
+ // update bounds when visibility changes
+ request_update();
+ request_redraw();
+}
+
+void CanvasItem::set_fill(guint32 rgba)
+{
+ if (_fill != rgba) {
+ _fill = rgba;
+ request_redraw();
+ }
+}
+
+void CanvasItem::set_stroke(guint32 rgba)
+{
+ if (_stroke != rgba) {
+ _stroke = rgba;
+ request_redraw();
+ }
+}
+
+void CanvasItem::request_redraw() {
+ if (_canvas) {
+ // Queue redraw request
+ _canvas->redraw_area(_bounds);
+ }
+}
+
+} // Namespace Inkscape
+
+void canvas_item_print_tree(Inkscape::CanvasItem *item)
+{
+ static int level = 0;
+ if (level == 0) {
+ std::cout << "Canvas Item Tree" << std::endl;
+ }
+
+ std::cout << "CC: ";
+ for (unsigned i = 0; i < level; ++i) {
+ std::cout << " ";
+ }
+
+ std::cout << item->get_z_position() << ": " << item->get_name() << std::endl;
+
+ auto group = dynamic_cast<Inkscape::CanvasItemGroup *>(item);
+ if (group) {
+ ++level;
+ for (auto & item : group->items) {
+ canvas_item_print_tree(&item);
+ }
+ --level;
+ }
+}
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-item.h b/src/display/control/canvas-item.h
new file mode 100644
index 0000000..37d5ec7
--- /dev/null
+++ b/src/display/control/canvas-item.h
@@ -0,0 +1,175 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_CANVAS_ITEM_H
+#define SEEN_CANVAS_ITEM_H
+
+/**
+ * Abstract base class for on-canvas control items.
+ */
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of SPCanvasItem
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ * A note about coordinates:
+ *
+ * 1. Canvas items are constructed using document (SVG) coordinates.
+ * 2. Calculations are made in canvas units, which is equivalent of SVG units multiplied by zoom factor.
+ * This is true for bounds and closest distance calculations.
+ * 3 Drawing is done in screen units which is the same as canvas units but translated.
+ * The document and canvas origins overlap.
+ * The affine contains only scaling and rotating components.
+ */
+
+//#define CANVAS_ITEM_DEBUG
+
+#include <gdk/gdk.h> // GdkEvent
+#include <gdkmm/device.h> // Gdk::EventMask
+#include <glib.h> // guint32
+#include <sigc++/sigc++.h>
+
+#include <2geom/rect.h>
+
+#include <boost/intrusive/list.hpp>
+
+#include "canvas-item-buffer.h"
+#include "canvas-item-enums.h"
+
+class SPItem;
+
+namespace Inkscape {
+
+static guint32 CANVAS_ITEM_COLORS[] = { 0x0000ff7f, 0xff00007f, 0xffff007f };
+
+namespace UI::Widget {
+class Canvas;
+}
+
+class CanvasItemGroup; // A canvas control that contains other canvas controls.
+
+class CanvasItem {
+
+public:
+ CanvasItem(CanvasItemGroup* group);
+ virtual ~CanvasItem();
+
+ // Structure
+ void set_canvas(UI::Widget::Canvas *canvas) { _canvas = canvas; }
+ UI::Widget::Canvas* get_canvas() { return _canvas; }
+
+ void set_parent(CanvasItemGroup *parent) { _parent = parent; }
+ CanvasItemGroup* get_parent() { return _parent; }
+
+ void set_item(SPItem *item) { _item = item; }
+ SPItem* get_item() { return _item; }
+
+ // Z Position
+ bool is_descendant_of(CanvasItem *ancestor);
+ void set_z_position(unsigned int n);
+ int get_z_position(); // z position in group.
+ // void raise_by(unsigned int n);
+ void raise_to_top(); // Move to top of group (last entry).
+ // void lower_by(unsigned int n);
+ void lower_to_bottom(); // Move to bottom of group (first entry).
+
+ // Geometry
+ void request_update();
+ virtual void update(Geom::Affine const &affine) = 0;
+ Geom::Affine get_affine() { return _affine; }
+ Geom::Rect get_bounds() { return _bounds; }
+
+ // Selection
+ virtual bool contains(Geom::Point const &p, double tolerance = 0) { return _bounds.interiorContains(p); }
+ int grab(Gdk::EventMask event_mask, GdkCursor *cursor = nullptr);
+ int grab(Gdk::EventMask event_mask, Glib::RefPtr<Gdk::Cursor> cursor);
+ void ungrab();
+
+ // Display
+ virtual void render(Inkscape::CanvasItemBuffer *buf) = 0;
+ bool is_visible() { return _visible; }
+ virtual void hide();
+ virtual void show();
+ void request_redraw(); // queue redraw request
+
+ // Properties
+ virtual void set_fill(guint32 rgba);
+ void set_fill(CanvasItemColor color) { set_fill(CANVAS_ITEM_COLORS[color]); }
+ virtual void set_stroke(guint32 rgba);
+ void set_stroke(CanvasItemColor color) { set_stroke(CANVAS_ITEM_COLORS[color]); }
+ void set_name(std::string const &name) { _name = name; }
+ std::string get_name() { return _name; }
+
+ // Events
+ void set_pickable(bool pickable) { _pickable = pickable; }
+ bool is_pickable() { return _pickable; }
+ sigc::connection connect_event(sigc::slot<bool, GdkEvent*> slot) {
+ return _event_signal.connect(slot);
+ }
+ virtual bool handle_event(GdkEvent *event) {
+ return _event_signal.emit(event); // Default just emit event.
+ }
+
+ // Boost linked list member hook, speeds deletion.
+ boost::intrusive::list_member_hook<> member_hook;
+
+protected:
+
+ // Structure
+ CanvasItemGroup *_parent = nullptr;
+ Inkscape::UI::Widget::Canvas *_canvas = nullptr;
+ SPItem *_item; // The object this canvas item is linked to in some sense. Can be nullptr.
+
+ // Geometry
+ Geom::Rect _bounds;
+ Geom::Affine _affine;
+ bool _need_update = true; // Need update after creation!
+
+ // Display
+ bool _visible = true;
+ bool _align_to_drawing = false; // Rotate if drawing is rotated. TODO: Implement!
+
+ // Selection
+ bool _pickable = false; // Most items are just for display and are not pickable!
+
+ // Properties
+ guint32 _fill = CANVAS_ITEM_COLORS[CANVAS_ITEM_SECONDARY];
+ guint32 _stroke = CANVAS_ITEM_COLORS[CANVAS_ITEM_PRIMARY];
+ std::string _name; // For debugging
+
+ // Events
+ sigc::signal<bool, GdkEvent*> _event_signal;
+};
+
+
+} // namespace Inkscape
+
+/** Type for linked list storing CanvasItem's.
+ *
+ * Used to speed deletion when a group contains a large number of item's (as in nodes for a
+ * complex path).
+ */
+typedef boost::intrusive::list<
+ Inkscape::CanvasItem,
+ boost::intrusive::member_hook<Inkscape::CanvasItem, boost::intrusive::list_member_hook<>,
+ &Inkscape::CanvasItem::member_hook> > CanvasItemList;
+
+// Recursively print CanvasItem tree.
+void canvas_item_print_tree(Inkscape::CanvasItem *item);
+
+#endif // SEEN_CANVAS_ITEM_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-page.cpp b/src/display/control/canvas-page.cpp
new file mode 100644
index 0000000..d6b363f
--- /dev/null
+++ b/src/display/control/canvas-page.cpp
@@ -0,0 +1,176 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape pages implementation
+ *
+ * Authors:
+ * Martin Owens <doctormo@geek-2.com>
+ *
+ * Copyright (C) 2021 Martin Owens
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-page.h"
+#include "canvas-item-rect.h"
+#include "canvas-item-text.h"
+
+namespace Inkscape {
+
+CanvasPage::~CanvasPage()
+{
+ for (auto item : canvas_items) {
+ delete item;
+ }
+ canvas_items.clear();
+}
+
+/**
+ * Add the page canvas to the given canvas item groups (canvas view is implicit)
+ */
+void CanvasPage::add(Geom::Rect size, CanvasItemGroup *background_group, CanvasItemGroup *border_group)
+{
+ // Foreground 'border'
+ if (auto item = new CanvasItemRect(border_group, size)) {
+ item->set_name("foreground");
+ canvas_items.push_back(item);
+ }
+
+ // Background rectangle 'fill'
+ if (auto item = new CanvasItemRect(background_group, size)) {
+ item->set_name("background");
+ item->set_dashed(false);
+ item->set_inverted(false);
+ item->set_stroke(0x00000000);
+ canvas_items.push_back(item);
+ }
+
+ if (auto label = new CanvasItemText(border_group, Geom::Point(0, 0), "{Page Label}")) {
+ label->set_fontsize(10.0);
+ label->set_fill(0xffffffff);
+ label->set_background(0x00000099);
+ label->set_bg_radius(0.2);
+ label->set_anchor(Geom::Point(0.0, 1.0));
+ label->set_fixed_line(true);
+ canvas_items.push_back(label);
+ }
+}
+/**
+ * Hide the page in the given canvas widget.
+ */
+void CanvasPage::remove(UI::Widget::Canvas *canvas)
+{
+ g_assert(canvas != nullptr);
+ for (auto it = canvas_items.begin(); it != canvas_items.end();) {
+ if (canvas == (*it)->get_canvas()) {
+ delete (*it);
+ it = canvas_items.erase(it);
+ } else {
+ ++it;
+ }
+ }
+}
+
+void CanvasPage::show()
+{
+ for (auto item : canvas_items) {
+ item->show();
+ }
+}
+
+void CanvasPage::hide()
+{
+ for (auto item : canvas_items) {
+ item->hide();
+ }
+}
+
+/**
+ * Update the visual representation of a page on screen.
+ *
+ * @param size - The size of the page in desktop units
+ * @param txt - An optional label for the page
+ * @param outline - Disable normal rendering and show as an outline.
+ */
+void CanvasPage::update(Geom::Rect size, const char *txt, bool outline)
+{
+ // Put these in the preferences?
+ bool border_on_top = _border_on_top;
+ guint32 shadow_color = _border_color; // there's no separate shadow color in the UI, border color is used
+ guint32 select_color = 0xff0000cc;
+ guint32 border_color = _border_color;
+
+ // This is used when showing the viewport as *not a page* it's mostly
+ // never used as the first page is normally the viewport too.
+ if (outline) {
+ border_on_top = false;
+ _shadow_size = 0;
+ border_color = select_color;
+ }
+
+ for (auto item : canvas_items) {
+ if (auto rect = dynamic_cast<CanvasItemRect *>(item)) {
+ rect->set_rect(size);
+
+ bool is_foreground = (rect->get_name() == "foreground");
+ // This will put the border on the background OR foreground layer as needed.
+ if (is_foreground == border_on_top) {
+ rect->show();
+ rect->set_shadow(shadow_color, _shadow_size);
+ rect->set_stroke(is_selected ? select_color : border_color);
+ } else {
+ rect->hide();
+ rect->set_shadow(0x0, 0);
+ rect->set_stroke(0x0);
+ }
+ // This undoes the hide for the background rect, but that's ok
+ if (!is_foreground) {
+ rect->show();
+ if (_checkerboard) {
+ rect->set_background_checkerboard(_background_color, true);
+ }
+ else {
+ // TODO: This ignores the requested transparency to paint the background.
+ // there is disagreement between developers about this feature.
+ rect->set_background(_background_color | 0xff);
+ }
+ }
+ }
+ if (auto label = dynamic_cast<CanvasItemText *>(item)) {
+ if (txt) {
+ auto corner = size.corner(0);
+ label->set_coord(corner);
+ label->set_text(txt);
+ label->show();
+ } else {
+ label->set_text("");
+ label->hide();
+ }
+ }
+ }
+}
+
+bool CanvasPage::setAttributes(bool on_top, guint32 border, guint32 bg, int shadow, bool checkerboard)
+{
+ if (on_top != _border_on_top || border != _border_color || bg != _background_color || shadow != _shadow_size || checkerboard != _checkerboard) {
+ this->_border_on_top = on_top;
+ this->_border_color = border;
+ this->_background_color = bg;
+ _shadow_size = shadow;
+ _checkerboard = checkerboard;
+ return true;
+ }
+ return false;
+}
+
+};
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-page.h b/src/display/control/canvas-page.h
new file mode 100644
index 0000000..db66b4f
--- /dev/null
+++ b/src/display/control/canvas-page.h
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ *
+ *//*
+ * Authors:
+ * Martin Owens 2021
+ *
+ * Copyright (C) 2021 Martin Owens
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_CANVAS_PAGE_H
+#define SEEN_CANVAS_PAGE_H
+
+#include <2geom/rect.h>
+#include <glib.h>
+#include <vector>
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+ namespace UI {
+ namespace Widget {
+ class Canvas;
+ };
+ };
+
+ class CanvasItemGroup;
+
+class CanvasPage
+{
+public:
+ CanvasPage() = default;
+ ~CanvasPage();
+
+ void update(Geom::Rect size, const char *txt, bool outline = false);
+ void add(Geom::Rect size, CanvasItemGroup *background_group, CanvasItemGroup *foreground_group);
+ void remove(UI::Widget::Canvas *canvas);
+ void show();
+ void hide();
+
+ bool setAttributes(bool on_top, guint32 border, guint32 bg, int shadow, bool checkerboard);
+ void setOutline(bool outline);
+
+ bool is_selected = false;
+private:
+ // This may make this look like a CanvasItemGroup, but it's not one. This
+ // isn't a collection of items, but a set of items in multiple Canvases.
+ // Each item can belong in either a foreground or background group.
+ std::vector<Inkscape::CanvasItem *> canvas_items;
+
+ int _shadow_size = 0;
+ bool _border_on_top = true;
+ guint32 _background_color = 0xffffff00;
+ guint32 _border_color = 0x00000040;
+ bool _checkerboard = false;
+};
+
+};
+
+#endif // SEEN_CANVAS_PAGE_H
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-temporary-item-list.cpp b/src/display/control/canvas-temporary-item-list.cpp
new file mode 100644
index 0000000..4be97f9
--- /dev/null
+++ b/src/display/control/canvas-temporary-item-list.cpp
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Provides a class that can contain active TemporaryItem's on a desktop
+ * Code inspired by message-stack.cpp
+ *
+ * Authors:
+ * Johan Engelen
+ *
+ * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "canvas-temporary-item.h"
+#include "canvas-temporary-item-list.h"
+
+namespace Inkscape {
+namespace Display {
+
+TemporaryItemList::TemporaryItemList(SPDesktop *desktop)
+ : desktop(desktop)
+{
+
+}
+
+TemporaryItemList::~TemporaryItemList()
+{
+ // delete all items in list so the timeouts are removed
+ for (auto tempitem : itemlist) {
+ delete tempitem;
+ }
+ itemlist.clear();
+}
+
+/* Note that TemporaryItem or TemporaryItemList is responsible for deletion and such, so this return pointer can safely be ignored. */
+TemporaryItem *
+TemporaryItemList::add_item(CanvasItem *item, unsigned int lifetime)
+{
+ // beware of strange things happening due to very short timeouts
+ TemporaryItem * tempitem;
+ if (lifetime == 0)
+ tempitem = new TemporaryItem(item, 0, true);
+ else {
+ tempitem = new TemporaryItem(item, lifetime);
+ tempitem->signal_timeout.connect( sigc::mem_fun(*this, &TemporaryItemList::_item_timeout) );
+ }
+
+ itemlist.push_back(tempitem);
+ return tempitem;
+}
+
+void
+TemporaryItemList::delete_item( TemporaryItem * tempitem )
+{
+ // check if the item is in the list, if so, delete it. (in other words, don't wait for the item to delete itself)
+ bool in_list = false;
+ for (auto & it : itemlist) {
+ if ( it == tempitem ) {
+ in_list = true;
+ break;
+ }
+ }
+ if (in_list) {
+ itemlist.remove(tempitem);
+ delete tempitem;
+ }
+}
+
+void
+TemporaryItemList::_item_timeout(TemporaryItem * tempitem)
+{
+ itemlist.remove(tempitem);
+ // no need to delete the item, it does that itself after signal_timeout.emit() completes
+}
+
+} //namespace Display
+} /* namespace Inkscape */
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-temporary-item-list.h b/src/display/control/canvas-temporary-item-list.h
new file mode 100644
index 0000000..29b270a
--- /dev/null
+++ b/src/display/control/canvas-temporary-item-list.h
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_CANVAS_TEMPORARY_ITEM_LIST_H
+#define INKSCAPE_CANVAS_TEMPORARY_ITEM_LIST_H
+
+/*
+ * Authors:
+ * Johan Engelen
+ *
+ * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <list>
+
+struct SPCanvasItem;
+class SPDesktop;
+
+namespace Inkscape {
+
+class CanvasItem;
+
+namespace Display {
+
+class TemporaryItem;
+
+/**
+ * Provides a class that can contain active TemporaryItem[s] on a desktop.
+ */
+class TemporaryItemList {
+public:
+ TemporaryItemList(SPDesktop *desktop);
+ virtual ~TemporaryItemList();
+
+ TemporaryItem* add_item (SPCanvasItem *item, unsigned int lifetime);
+ TemporaryItem* add_item (CanvasItem *item, unsigned int lifetime);
+ void delete_item (TemporaryItem * tempitem);
+
+protected:
+ SPDesktop *desktop; /** Desktop we are on. */
+
+ std::list<TemporaryItem *> itemlist; /** list of temp items */
+
+ void _item_timeout (TemporaryItem * tempitem);
+
+private:
+ TemporaryItemList(const TemporaryItemList&) = delete;
+ TemporaryItemList& operator=(const TemporaryItemList&) = delete;
+};
+
+} //namespace Display
+} //namespace Inkscape
+
+#endif
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-temporary-item.cpp b/src/display/control/canvas-temporary-item.cpp
new file mode 100644
index 0000000..08a8607
--- /dev/null
+++ b/src/display/control/canvas-temporary-item.cpp
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Provides a class that can contain active TemporaryItem's on a desktop
+ * When the object is deleted, it also deletes the canvasitem it contains!
+ * This object should be created/managed by a TemporaryItemList.
+ * After its lifetime, it fires the timeout signal, afterwards *it deletes itself*.
+ *
+ * (part of code inspired by message-stack.cpp)
+ *
+ * Authors:
+ * Johan Engelen
+ *
+ * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glib.h>
+
+#include "canvas-temporary-item.h"
+
+#include "canvas-item.h"
+
+namespace Inkscape {
+namespace Display {
+
+/** lifetime is measured in milliseconds
+ */
+TemporaryItem::TemporaryItem(CanvasItem *item, guint lifetime, bool deselect_destroy)
+ : canvasitem(item),
+ timeout_id(0),
+ destroy_on_deselect(deselect_destroy)
+{
+ if (lifetime > 0 && destroy_on_deselect) {
+ g_print ("Warning: lifetime should be 0 when destroy_on_deselect is true\n");
+ lifetime = 0;
+ }
+ // zero lifetime means stay forever, so do not add timeout event.
+ if (lifetime > 0) {
+ timeout_id = g_timeout_add(lifetime, &TemporaryItem::_timeout, this);
+ }
+}
+
+TemporaryItem::~TemporaryItem()
+{
+ // when it has not expired yet...
+ if (timeout_id) {
+ g_source_remove(timeout_id);
+ timeout_id = 0;
+ }
+
+ if (canvasitem) {
+ // destroying the item automatically hides it
+ delete canvasitem;
+ canvasitem = nullptr;
+ }
+}
+
+/* static method */
+int TemporaryItem::_timeout(void* data) {
+ TemporaryItem *tempitem = static_cast<TemporaryItem *>(data);
+ tempitem->timeout_id = 0;
+ tempitem->signal_timeout.emit(tempitem);
+ delete tempitem;
+ return FALSE;
+}
+
+
+} //namespace Display
+} /* namespace Inkscape */
+
+/*
+ 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 :
diff --git a/src/display/control/canvas-temporary-item.h b/src/display/control/canvas-temporary-item.h
new file mode 100644
index 0000000..b2457c7
--- /dev/null
+++ b/src/display/control/canvas-temporary-item.h
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_CANVAS_TEMPORARY_ITEM_H
+#define INKSCAPE_CANVAS_TEMPORARY_ITEM_H
+
+/*
+ * Authors:
+ * Johan Engelen
+ *
+ * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include <sigc++/signal.h>
+
+namespace Inkscape {
+
+class CanvasItem;
+
+namespace Display {
+
+/**
+ * Provides a class to put a canvasitem temporarily on-canvas.
+ */
+class TemporaryItem {
+public:
+ TemporaryItem(Inkscape::CanvasItem *item, unsigned int lifetime, bool destroy_on_deselect = false);
+ virtual ~TemporaryItem();
+
+ TemporaryItem(const TemporaryItem&) = delete;
+ TemporaryItem& operator=(const TemporaryItem&) = delete;
+
+ sigc::signal<void, TemporaryItem *> signal_timeout;
+
+protected:
+ friend class TemporaryItemList;
+
+ Inkscape::CanvasItem * canvasitem = nullptr; /** The item we are holding on to */
+ unsigned int timeout_id; /** ID by which glib knows the timeout event */
+ bool destroy_on_deselect; // only destroy when parent item is deselected, not when mouse leaves
+
+ static int _timeout(void* data); ///< callback for when lifetime expired
+};
+
+} //namespace Display
+} //namespace Inkscape
+
+#endif
+
+/*
+ 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 :
diff --git a/src/display/control/snap-indicator.cpp b/src/display/control/snap-indicator.cpp
new file mode 100644
index 0000000..4f42820
--- /dev/null
+++ b/src/display/control/snap-indicator.cpp
@@ -0,0 +1,640 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Provides a class that shows a temporary indicator on the canvas of where the snap was, and what kind of snap
+ *
+ * Authors:
+ * Johan Engelen
+ * Diederik van Lierop
+ *
+ * Copyright (C) Johan Engelen 2009 <j.b.c.engelen@utwente.nl>
+ * Copyright (C) Diederik van Lierop 2010 - 2012 <mail@diedenrezi.nl>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <string>
+#include <iomanip>
+#include <unordered_map>
+
+#include "snap-indicator.h"
+
+#include "desktop.h"
+#include "enums.h"
+#include "preferences.h"
+#include "util/units.h"
+#include "document.h"
+
+
+#include "canvas-item-ctrl.h"
+#include "canvas-item-rect.h"
+#include "canvas-item-text.h"
+#include "canvas-item-curve.h"
+
+#include "ui/tools/measure-tool.h"
+
+#define DISTANCE_BG_RADIUS 0.3
+
+namespace Inkscape {
+namespace Display {
+
+static std::map<SnapSourceType, Glib::ustring> source2string = {
+ {SNAPSOURCE_UNDEFINED, _("UNDEFINED")},
+ {SNAPSOURCE_BBOX_CORNER, _("Bounding box corner")},
+ {SNAPSOURCE_BBOX_MIDPOINT, _("Bounding box midpoint")},
+ {SNAPSOURCE_BBOX_EDGE_MIDPOINT, _("Bounding box side midpoint")},
+ {SNAPSOURCE_NODE_SMOOTH, _("Smooth node")},
+ {SNAPSOURCE_NODE_CUSP, _("Cusp node")},
+ {SNAPSOURCE_LINE_MIDPOINT, _("Line midpoint")},
+ {SNAPSOURCE_PATH_INTERSECTION, _("Path intersection")},
+ {SNAPSOURCE_RECT_CORNER, _("Corner")},
+ {SNAPSOURCE_CONVEX_HULL_CORNER, _("Convex hull corner")},
+ {SNAPSOURCE_ELLIPSE_QUADRANT_POINT, _("Quadrant point")},
+ {SNAPSOURCE_NODE_HANDLE, _("Handle")},
+ {SNAPSOURCE_GUIDE, _("Guide")},
+ {SNAPSOURCE_GUIDE_ORIGIN, _("Guide origin")},
+ {SNAPSOURCE_ROTATION_CENTER, _("Object rotation center")},
+ {SNAPSOURCE_OBJECT_MIDPOINT, _("Object midpoint")},
+ {SNAPSOURCE_IMG_CORNER, _("Corner")},
+ {SNAPSOURCE_TEXT_ANCHOR, _("Text anchor")},
+ {SNAPSOURCE_OTHER_HANDLE, _("Handle")},
+ {SNAPSOURCE_GRID_PITCH, _("Multiple of grid spacing")},
+ {SNAPSOURCE_PAGE_CORNER, _("Page corner")},
+ {SNAPSOURCE_PAGE_CENTER, _("Page center")},
+};
+
+static std::map<SnapTargetType, Glib::ustring> target2string = {
+ {SNAPTARGET_UNDEFINED, _("UNDEFINED")},
+ {SNAPTARGET_BBOX_CORNER, _("bounding box corner")},
+ {SNAPTARGET_BBOX_EDGE, _("bounding box side")},
+ {SNAPTARGET_BBOX_EDGE_MIDPOINT, _("bounding box side midpoint")},
+ {SNAPTARGET_BBOX_MIDPOINT, _("bounding box midpoint")},
+ {SNAPTARGET_NODE_SMOOTH, _("smooth node")},
+ {SNAPTARGET_NODE_CUSP, _("cusp node")},
+ {SNAPTARGET_LINE_MIDPOINT, _("line midpoint")},
+ {SNAPTARGET_PATH, _("path")},
+ {SNAPTARGET_PATH_PERPENDICULAR, _("path (perpendicular)")},
+ {SNAPTARGET_PATH_TANGENTIAL, _("path (tangential)")},
+ {SNAPTARGET_PATH_INTERSECTION, _("path intersection")},
+ {SNAPTARGET_PATH_GUIDE_INTERSECTION, _("guide-path intersection")},
+ {SNAPTARGET_PATH_CLIP, _("clip-path")},
+ {SNAPTARGET_PATH_MASK, _("mask-path")},
+ {SNAPTARGET_ELLIPSE_QUADRANT_POINT, _("quadrant point")},
+ {SNAPTARGET_RECT_CORNER, _("corner")},
+ {SNAPTARGET_GRID, _("grid line")},
+ {SNAPTARGET_GRID_INTERSECTION, _("grid intersection")},
+ {SNAPTARGET_GRID_PERPENDICULAR, _("grid line (perpendicular)")},
+ {SNAPTARGET_GUIDE, _("guide")},
+ {SNAPTARGET_GUIDE_INTERSECTION, _("guide intersection")},
+ {SNAPTARGET_GUIDE_ORIGIN, _("guide origin")},
+ {SNAPTARGET_GUIDE_PERPENDICULAR, _("guide (perpendicular)")},
+ {SNAPTARGET_GRID_GUIDE_INTERSECTION, _("grid-guide intersection")},
+ {SNAPTARGET_PAGE_BORDER, _("page border")},
+ {SNAPTARGET_PAGE_CORNER, _("page corner")},
+ {SNAPTARGET_PAGE_CENTER, _("page center")},
+ {SNAPTARGET_OBJECT_MIDPOINT, _("object midpoint")},
+ {SNAPTARGET_IMG_CORNER, _("corner")},
+ {SNAPTARGET_ROTATION_CENTER, _("object rotation center")},
+ {SNAPTARGET_TEXT_ANCHOR, _("text anchor")},
+ {SNAPTARGET_TEXT_BASELINE, _("text baseline")},
+ {SNAPTARGET_CONSTRAINED_ANGLE, _("constrained angle")},
+ {SNAPTARGET_CONSTRAINT, _("constraint")},
+};
+
+SnapIndicator::SnapIndicator(SPDesktop * desktop)
+ : _snaptarget(nullptr),
+ _snaptarget_tooltip(nullptr),
+ _snaptarget_bbox(nullptr),
+ _snapsource(nullptr),
+ _snaptarget_is_presnap(false),
+ _desktop(desktop)
+{
+}
+
+SnapIndicator::~SnapIndicator()
+{
+ // remove item that might be present
+ remove_snaptarget();
+ remove_snapsource();
+}
+
+void
+SnapIndicator::set_new_snaptarget(Inkscape::SnappedPoint const &p, bool pre_snap)
+{
+ remove_snaptarget(); //only display one snaptarget at a time
+
+ g_assert(_desktop != nullptr);
+
+ if (!p.getSnapped()) {
+ return; // If we haven't snapped, then it is of no use to draw a snapindicator
+ }
+
+ if (p.getTarget() == SNAPTARGET_CONSTRAINT) {
+ // This is not a real snap, although moving along the constraint did affect the mouse pointer's position.
+ // Maybe we should only show a snap indicator when the user explicitly asked for a constraint by pressing ctrl?
+ // We should not show a snap indicator when stretching a selection box, which is also constrained. That would be
+ // too much information.
+ return;
+ }
+
+ bool is_alignment = p.getAlignmentTarget().has_value();
+ bool is_distribution = p.getTarget() & SNAPTARGET_DISTRIBUTION_CATEGORY;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0;
+
+ bool value = prefs->getBool("/options/snapindicator/value", true);
+
+ if (value) {
+ Glib::ustring target_name = _("UNDEFINED");
+ Glib::ustring source_name = _("UNDEFINED");
+
+ if (!is_alignment && !is_distribution) {
+ if (target2string.find(p.getTarget()) == target2string.end())
+ g_warning("Target type %i not present in target2string", p.getTarget());
+
+ if (source2string.find(p.getSource()) == source2string.end())
+ g_warning("Source type %i not present in target2string", p.getSource());
+
+ target_name = _(target2string[p.getTarget()].c_str());
+ source_name = _(source2string[p.getSource()].c_str());
+ }
+ //std::cout << "Snapped " << source_name << " to " << target_name << std::endl;
+
+ remove_snapsource(); // Don't set both the source and target indicators, as these will overlap
+
+ double timeout_val = prefs->getDouble("/options/snapindicatorpersistence/value", 2.0);
+ if (timeout_val < 0.1) {
+ timeout_val = 0.1; // a zero value would mean infinite persistence (i.e. until new snap occurs)
+ // Besides, negatives values would ....?
+ }
+
+ // TODO: should this be a constant or a separate prefrence
+ // we are using the preference of measure tool here.
+ double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0);
+
+ if (is_distribution) {
+ make_distribution_indicators(p, fontsize, scale);
+ }
+
+ if (is_alignment) {
+ auto color = pre_snap ? 0x7f7f7fff : get_guide_color(p.getAlignmentTargetType());
+ make_alignment_indicator(p.getPoint(), *p.getAlignmentTarget(), color, fontsize, scale);
+ if (p.getAlignmentTargetType() == SNAPTARGET_ALIGNMENT_INTERSECTION) {
+ make_alignment_indicator(p.getPoint(), *p.getAlignmentTarget2(), color, fontsize, scale);
+ }
+ }
+
+ _snaptarget_is_presnap = pre_snap;
+
+ // Display the snap indicator (i.e. the cross)
+ Inkscape::CanvasItemCtrl *ctrl;
+
+ if (!is_alignment && !is_distribution) {
+ // Display snap indicator at snap target
+ ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CROSS);
+ ctrl->set_size(11);
+ ctrl->set_stroke( pre_snap ? 0x7f7f7fff : 0xff0000ff);
+ ctrl->set_position(p.getPoint());
+
+ _snaptarget = _desktop->add_temporary_canvasitem(ctrl, timeout_val*1000.0);
+ // The snap indicator will be deleted after some time-out, and sp_canvas_item_dispose
+ // will be called. This will set canvas->current_item to NULL if the snap indicator was
+ // the current item, after which any events will go to the root handler instead of any
+ // item handler. Dragging an object which has just snapped might therefore not be possible
+ // without selecting / repicking it again. To avoid this, we make sure here that the
+ // snap indicator will never be picked, and will therefore never be the current item.
+ // Reported bugs:
+ // - scrolling when hovering above a pre-snap indicator won't work (for example)
+ // (https://bugs.launchpad.net/inkscape/+bug/522335/comments/8)
+ // - dragging doesn't work without repicking
+ // (https://bugs.launchpad.net/inkscape/+bug/1420301/comments/15)
+ ctrl->set_pickable(false);
+
+ // Display the tooltip, which reveals the type of snap source and the type of snap target
+ Glib::ustring tooltip_str;
+ if ( (p.getSource() != SNAPSOURCE_GRID_PITCH) && (p.getTarget() != SNAPTARGET_UNDEFINED) ) {
+ tooltip_str = source_name + _(" to ") + target_name;
+ } else if (p.getSource() != SNAPSOURCE_UNDEFINED) {
+ tooltip_str = source_name;
+ }
+
+
+ if (!tooltip_str.empty()) {
+ Geom::Point tooltip_pos = p.getPoint();
+ if (dynamic_cast<Inkscape::UI::Tools::MeasureTool *>(_desktop->event_context)) {
+ // Make sure that the snap tooltips do not overlap the ones from the measure tool
+ tooltip_pos += _desktop->w2d(Geom::Point(0, -3*fontsize));
+ } else {
+ tooltip_pos += _desktop->w2d(Geom::Point(0, -2*fontsize));
+ }
+
+ auto canvas_tooltip = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), tooltip_pos, tooltip_str);
+ canvas_tooltip->set_fontsize(fontsize);
+ canvas_tooltip->set_fill(0xffffffff);
+ canvas_tooltip->set_background(pre_snap ? 0x33337f40 : 0x33337f7f);
+
+ _snaptarget_tooltip = _desktop->add_temporary_canvasitem(canvas_tooltip, timeout_val*1000.0);
+ }
+
+ // Display the bounding box, if we snapped to one
+ Geom::OptRect const bbox = p.getTargetBBox();
+ if (bbox) {
+ auto box = new Inkscape::CanvasItemRect(_desktop->getCanvasTemp(), *bbox);
+ box->set_stroke(pre_snap ? 0x7f7f7fff : 0xff0000ff);
+ box->set_dashed(true);
+ box->set_pickable(false); // Is false by default.
+ box->set_z_position(0);
+ _snaptarget_bbox = _desktop->add_temporary_canvasitem(box, timeout_val*1000.0);
+ }
+ }
+ }
+}
+
+void
+SnapIndicator::remove_snaptarget(bool only_if_presnap)
+{
+ if (only_if_presnap && !_snaptarget_is_presnap) {
+ return;
+ }
+
+ if (_snaptarget) {
+ _desktop->remove_temporary_canvasitem(_snaptarget);
+ _snaptarget = nullptr;
+ _snaptarget_is_presnap = false;
+ }
+
+ if (_snaptarget_tooltip) {
+ _desktop->remove_temporary_canvasitem(_snaptarget_tooltip);
+ _snaptarget_tooltip = nullptr;
+ }
+
+ if (_snaptarget_bbox) {
+ _desktop->remove_temporary_canvasitem(_snaptarget_bbox);
+ _snaptarget_bbox = nullptr;
+ }
+
+ for (auto *item : _alignment_snap_indicators) {
+ _desktop->remove_temporary_canvasitem(item);
+ }
+ _alignment_snap_indicators.clear();
+
+ for (auto *item : _distribution_snap_indicators) {
+ _desktop->remove_temporary_canvasitem(item);
+ }
+ _distribution_snap_indicators.clear();
+}
+
+void
+SnapIndicator::set_new_snapsource(Inkscape::SnapCandidatePoint const &p)
+{
+ remove_snapsource();
+
+ g_assert(_desktop != nullptr); // If this fails, then likely setup() has not been called on the snap manager (see snap.cpp -> setup())
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool value = prefs->getBool("/options/snapindicator/value", true);
+
+ if (value) {
+ auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE);
+ ctrl->set_size(7);
+ ctrl->set_stroke(0xff0000ff);
+ ctrl->set_position(p.getPoint());
+ _snapsource = _desktop->add_temporary_canvasitem(ctrl, 1000);
+ }
+}
+
+void
+SnapIndicator::set_new_debugging_point(Geom::Point const &p)
+{
+ g_assert(_desktop != nullptr);
+ auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_DIAMOND);
+ ctrl->set_size(11);
+ ctrl->set_stroke(0x00ff00ff);
+ ctrl->set_position(p);
+ _debugging_points.push_back(_desktop->add_temporary_canvasitem(ctrl, 5000));
+}
+
+void
+SnapIndicator::remove_snapsource()
+{
+ if (_snapsource) {
+ _desktop->remove_temporary_canvasitem(_snapsource);
+ _snapsource = nullptr;
+ }
+}
+
+void
+SnapIndicator::remove_debugging_points()
+{
+ for (std::list<TemporaryItem *>::const_iterator i = _debugging_points.begin(); i != _debugging_points.end(); ++i) {
+ _desktop->remove_temporary_canvasitem(*i);
+ }
+ _debugging_points.clear();
+}
+
+guint32 SnapIndicator::get_guide_color(SnapTargetType t)
+{
+ switch(t) {
+ case SNAPTARGET_ALIGNMENT_BBOX_CORNER:
+ case SNAPTARGET_ALIGNMENT_BBOX_MIDPOINT:
+ case SNAPTARGET_ALIGNMENT_BBOX_EDGE_MIDPOINT:
+ return 0xff0000ff;
+ case SNAPTARGET_ALIGNMENT_PAGE_CENTER:
+ case SNAPTARGET_ALIGNMENT_PAGE_CORNER:
+ return 0x00ff00ff;
+ case SNAPTARGET_ALIGNMENT_HANDLE:
+ return 0x0000ffff;
+ case SNAPTARGET_ALIGNMENT_INTERSECTION:
+ return 0xd13bd1ff;
+ default:
+ g_warning("Alignment guide color not handled %i", t);
+ return 0x000000ff;
+ }
+}
+
+std::pair<Geom::Coord, int> get_y_and_sign(Geom::Rect const &source, Geom::Rect const &target, double const offset)
+{
+ Geom::Coord y;
+ int sign;
+
+ // We add a margin of 5px here to make sure that very small movements of mouse
+ // pointer do not cause the position of distribution indicator to change.
+ if (source.midpoint().y() < target.midpoint().y() + 5) {
+ y = source.max().y() + offset;
+ sign = 1;
+ } else {
+ y = source.min().y() - offset;
+ sign = -1;
+ }
+
+ return {y, sign};
+}
+
+std::pair<Geom::Coord, int> get_x_and_sign(Geom::Rect const &source, Geom::Rect const &target, double const offset)
+{
+ Geom::Coord x;
+ int sign;
+
+ // We add a margin of 5px here to make sure that very small movements of mouse
+ // pointer do not cause the position of distribution indicator to change.
+ if (source.midpoint().x() < target.midpoint().x() + 5) {
+ x = source.max().x() + offset;
+ sign = 1;
+ } else {
+ x = source.min().x() - offset;
+ sign = -1;
+ }
+
+ return {x, sign};
+}
+
+void SnapIndicator::make_alignment_indicator(Geom::Point const &p1, Geom::Point const &p2, guint32 color, double fontsize, double scale)
+{
+ //make sure the line is straight
+ g_assert(p1.x() == p2.x() || p1.y() == p2.y());
+
+ Preferences *prefs = Preferences::get();
+ bool show_distance = prefs->getBool("/options/snapindicatordistance/value", false);
+
+ Inkscape::CanvasItemCurve *line;
+
+ auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE);
+ ctrl->set_size(7);
+ ctrl->set_mode(Inkscape::CanvasItemCtrlMode::CANVAS_ITEM_CTRL_MODE_COLOR);
+ ctrl->set_stroke(0xffffffff);
+ ctrl->set_fill(color);
+ ctrl->set_position(p1);
+ ctrl->set_pickable(false);
+ _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(ctrl, 0));
+
+ ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE);
+ ctrl->set_size(7);
+ ctrl->set_mode(Inkscape::CanvasItemCtrlMode::CANVAS_ITEM_CTRL_MODE_COLOR);
+ ctrl->set_stroke(0xffffffff);
+ ctrl->set_fill(color);
+ ctrl->set_position(p2);
+ ctrl->set_pickable(false);
+ _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(ctrl, 0));
+
+ if (show_distance) {
+ auto dist = Geom::L2(p2 - p1);
+ double offset = (fontsize + 5)/_desktop->current_zoom();
+ auto direction = Geom::unit_vector(p1 - p2);
+ auto text_pos = (p1 + p2)/2;
+
+ Glib::ustring unit_name = _desktop->doc()->getDisplayUnit()->abbr.c_str();
+ if (!unit_name.compare("")) {
+ unit_name = DEFAULT_UNIT_NAME;
+ }
+
+ dist = Inkscape::Util::Quantity::convert(dist, "px", unit_name);
+
+ Glib::ustring distance = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*dist);
+
+ auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance);
+ text->set_fontsize(fontsize);
+ text->set_fill(color);
+ text->set_background(0xffffffc8);
+ text->set_bg_radius(DISTANCE_BG_RADIUS);
+ text->set_anchor({0.5, 0.5});
+ _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0));
+
+ auto temp_point = text_pos + offset*direction;
+ line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, temp_point);
+ line->set_stroke(color);
+ line->set_bg_alpha(1.0f);
+ _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0));
+
+ temp_point = text_pos - offset*direction;
+ line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), temp_point, p2);
+ line->set_stroke(color);
+ line->set_bg_alpha(1.0f);
+ _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0));
+ } else {
+ line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2);
+ line->set_stroke(color);
+ line->set_bg_alpha(1.0f);
+ _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0));
+ }
+}
+
+Inkscape::CanvasItemCurve* SnapIndicator::make_stub_line_v(Geom::Point const & p)
+{
+ Geom::Coord length = 10/_desktop->current_zoom();
+ auto line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p + Geom::Point(0, length/2), p - Geom::Point(0, length/2));
+ line->set_stroke(0xff5f1fff);
+ return line;
+}
+
+Inkscape::CanvasItemCurve* SnapIndicator::make_stub_line_h(Geom::Point const & p)
+{
+ Geom::Coord length = 10/_desktop->current_zoom();
+ auto line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p + Geom::Point(length/2, 0), p - Geom::Point(length/2, 0));
+ line->set_stroke(0xff5f1fff);
+ return line;
+}
+
+void SnapIndicator::make_distribution_indicators(SnappedPoint const &p,
+ double fontsize,
+ double scale)
+{
+ Preferences *prefs = Preferences::get();
+ bool show_distance = prefs->getBool("/options/snapindicatordistance/value", false);
+
+ guint32 color = 0xff5f1fff;
+ guint32 text_fill = 0xffffffff;
+ guint32 text_bg = 0xff5f1fff; //0x33337f7f
+ Geom::Point text_pos;
+ double text_offset = (fontsize * 2);
+ // double line_offset = 5/_desktop->current_zoom();
+ double line_offset = 0;
+
+ Glib::ustring unit_name = _desktop->doc()->getDisplayUnit()->abbr.c_str();
+ if (!unit_name.compare("")) {
+ unit_name = DEFAULT_UNIT_NAME;
+ }
+ auto equal_dist = Inkscape::Util::Quantity::convert(p.getDistributionDistance(), "px", unit_name);
+ Glib::ustring distance = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*equal_dist);
+
+ switch (p.getTarget()) {
+ case SNAPTARGET_DISTRIBUTION_Y:
+ case SNAPTARGET_DISTRIBUTION_X:
+ case SNAPTARGET_DISTRIBUTION_RIGHT:
+ case SNAPTARGET_DISTRIBUTION_LEFT:
+ case SNAPTARGET_DISTRIBUTION_UP:
+ case SNAPTARGET_DISTRIBUTION_DOWN: {
+ Geom::Point p1, p2;
+ Inkscape::CanvasItemCurve *point1, *point2;
+
+ for (auto it = p.getBBoxes().begin(); it + 1 != p.getBBoxes().end(); it++) {
+ switch (p.getTarget()) {
+ case SNAPTARGET_DISTRIBUTION_RIGHT:
+ case SNAPTARGET_DISTRIBUTION_LEFT:
+ case SNAPTARGET_DISTRIBUTION_X: {
+ auto [y, sign] = get_y_and_sign(*it, *std::next(it), 5/_desktop->current_zoom());
+ p1 = Geom::Point(it->max().x() + line_offset, y);
+ p2 = Geom::Point(std::next(it)->min().x() - line_offset, y);
+ text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(0, sign*text_offset));
+
+ point1 = make_stub_line_v(p1);
+ point2 = make_stub_line_v(p2);
+ break;
+ }
+
+ case SNAPTARGET_DISTRIBUTION_DOWN:
+ case SNAPTARGET_DISTRIBUTION_UP:
+ case SNAPTARGET_DISTRIBUTION_Y: {
+ auto [x, sign] = get_x_and_sign(*it, *std::next(it), 5/_desktop->current_zoom());
+ p1 = Geom::Point(x, it->max().y() + line_offset);
+ p2 = Geom::Point(x, std::next(it)->min().y() - line_offset);
+ text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(sign*text_offset, 0));
+
+ point1 = make_stub_line_h(p1);
+ point2 = make_stub_line_h(p2);
+ break;
+ }
+ }
+
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0));
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0));
+
+ auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2);
+ line1->set_stroke(color);
+ line1->set_width(2);
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0));
+
+ if (show_distance) {
+ auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance);
+ text->set_fontsize(fontsize);
+ text->set_fill(text_fill);
+ text->set_background(text_bg);
+ text->set_bg_radius(DISTANCE_BG_RADIUS);
+ text->set_anchor({0.5, 0.5});
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0));
+ }
+ }
+ break;
+ }
+ case SNAPTARGET_DISTRIBUTION_XY: {
+ Geom::Point p1, p2;
+ Inkscape::CanvasItemCurve *point1, *point2;
+
+ auto equal_dist2 = Inkscape::Util::Quantity::convert(p.getDistributionDistance2(), "px", unit_name);
+ Glib::ustring distance2 = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*equal_dist2);
+
+ for (auto it = p.getBBoxes().begin(); it + 1 != p.getBBoxes().end(); it++) {
+ auto [y, sign] = get_y_and_sign(*it, *std::next(it), 5/_desktop->current_zoom());
+ p1 = Geom::Point(it->max().x() + line_offset, y);
+ p2 = Geom::Point(std::next(it)->min().x() - line_offset, y);
+ text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(0, sign*text_offset));
+
+ point1 = make_stub_line_v(p1);
+ point2 = make_stub_line_v(p2);
+
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0));
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0));
+
+ auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2);
+ line1->set_stroke(color);
+ line1->set_width(2);
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0));
+
+ if (show_distance) {
+ auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance);
+ text->set_fontsize(fontsize);
+ text->set_fill(text_fill);
+ text->set_background(text_bg);
+ text->set_bg_radius(DISTANCE_BG_RADIUS);
+ text->set_anchor({0.5, 0.5});
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0));
+ }
+ }
+
+ for (auto it = p.getBBoxes2().begin(); it + 1 != p.getBBoxes2().end(); it++) {
+ auto [x, sign] = get_x_and_sign(*it, *std::next(it), 5/_desktop->current_zoom());
+ p1 = Geom::Point(x, it->max().y() + line_offset);
+ p2 = Geom::Point(x, std::next(it)->min().y() - line_offset);
+ text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(sign*text_offset, 0));
+
+ point1 = make_stub_line_h(p1);
+ point2 = make_stub_line_h(p2);
+
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0));
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0));
+
+ auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2);
+ line1->set_stroke(color);
+ line1->set_width(2);
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0));
+
+ if (show_distance) {
+ auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance2);
+ text->set_fontsize(fontsize);
+ text->set_fill(text_fill);
+ text->set_background(text_bg);
+ text->set_bg_radius(DISTANCE_BG_RADIUS);
+ text->set_anchor({0.5, 0.5});
+ _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0));
+ }
+ }
+
+ break;
+ }
+ }
+}
+
+} //namespace Display
+} /* namespace Inkscape */
+
+
+/*
+ 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=4:softtabstop=4 :
diff --git a/src/display/control/snap-indicator.h b/src/display/control/snap-indicator.h
new file mode 100644
index 0000000..ca4721b
--- /dev/null
+++ b/src/display/control/snap-indicator.h
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_DISPLAY_SNAP_INDICATOR_H
+#define INKSCAPE_DISPLAY_SNAP_INDICATOR_H
+
+/**
+ * @file
+ * Provides a class that shows a temporary indicator on the canvas of where the snap was, and what kind of snap
+ */
+/*
+ * Authors:
+ * Johan Engelen
+ * Diederik van Lierop
+ *
+ * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl>
+ * Copyright (C) Diederik van Lierop 2010 <mail@diedenrezi.nl>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "snap-enums.h"
+#include "snapped-point.h"
+#include "display/control/canvas-item-curve.h"
+
+#include <glib.h>
+#include <glibmm/i18n.h>
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace Display {
+
+class TemporaryItem;
+
+class SnapIndicator {
+public:
+ SnapIndicator(SPDesktop *desktop);
+ virtual ~SnapIndicator();
+
+ void set_new_snaptarget(Inkscape::SnappedPoint const &p, bool pre_snap = false);
+ void remove_snaptarget(bool only_if_presnap = false);
+
+ void set_new_snapsource(Inkscape::SnapCandidatePoint const &p);
+ void remove_snapsource();
+
+ void set_new_debugging_point(Geom::Point const &p);
+ void remove_debugging_points();
+
+protected:
+ TemporaryItem *_snaptarget;
+ TemporaryItem *_snaptarget_tooltip;
+ TemporaryItem *_snaptarget_bbox;
+ TemporaryItem *_snapsource;
+
+ std::list<TemporaryItem *> _alignment_snap_indicators;
+ std::list<TemporaryItem *> _distribution_snap_indicators;
+ std::list<TemporaryItem *> _debugging_points;
+ bool _snaptarget_is_presnap;
+ SPDesktop *_desktop;
+
+private:
+ SnapIndicator(const SnapIndicator&) = delete;
+ SnapIndicator& operator=(const SnapIndicator&) = delete;
+
+ void make_distribution_indicators(SnappedPoint const &p, double fontsize, double scale);
+ void make_alignment_indicator(Geom::Point const &p1, Geom::Point const &p2, guint32 color, double fontsize, double scale);
+ guint32 get_guide_color(SnapTargetType t);
+ Inkscape::CanvasItemCurve* make_stub_line_h(Geom::Point const &p);
+ Inkscape::CanvasItemCurve* make_stub_line_v(Geom::Point const &p);
+};
+
+} //namespace Display
+} //namespace Inkscape
+
+#endif
+
+/*
+ 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 :