summaryrefslogtreecommitdiffstats
path: root/src/ui/widget/ink-ruler.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/widget/ink-ruler.cpp')
-rw-r--r--src/ui/widget/ink-ruler.cpp645
1 files changed, 645 insertions, 0 deletions
diff --git a/src/ui/widget/ink-ruler.cpp b/src/ui/widget/ink-ruler.cpp
new file mode 100644
index 0000000..1705978
--- /dev/null
+++ b/src/ui/widget/ink-ruler.cpp
@@ -0,0 +1,645 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Ruler widget. Indicates horizontal or vertical position of a cursor in a specified widget.
+ *
+ * Copyright (C) 2019 Tavmjong Bah
+ * 2022 Martin Owens
+ *
+ * Rewrite of the 'C' ruler code which came originally from Gimp.
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#include "ink-ruler.h"
+
+#include <gdkmm/rgba.h>
+#include <glibmm/ustring.h>
+#include <iostream>
+#include <cmath>
+
+#include "inkscape.h"
+#include "ui/themes.h"
+#include "ui/util.h"
+#include "util/units.h"
+
+using Inkscape::Util::unit_table;
+
+struct SPRulerMetric
+{
+ gdouble ruler_scale[16];
+ gint subdivide[5];
+};
+
+// Ruler metric for general use.
+static SPRulerMetric const ruler_metric_general = {
+ { 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000 },
+ { 1, 5, 10, 50, 100 }
+};
+
+// Ruler metric for inch scales.
+static SPRulerMetric const ruler_metric_inches = {
+ { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768 },
+ { 1, 2, 4, 8, 16 }
+};
+
+// Half width of pointer triangle.
+static double half_width = 5.0;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Ruler::Ruler(Gtk::Orientation orientation)
+ : _orientation(orientation)
+ , _backing_store(nullptr)
+ , _lower(0)
+ , _upper(1000)
+ , _max_size(1000)
+ , _unit(nullptr)
+ , _backing_store_valid(false)
+ , _rect()
+ , _position(0)
+{
+ set_name("InkRuler");
+
+ set_events(Gdk::POINTER_MOTION_MASK |
+ Gdk::BUTTON_PRESS_MASK | // For guide creation
+ Gdk::BUTTON_RELEASE_MASK );
+
+ set_no_show_all();
+
+ auto prefs = Inkscape::Preferences::get();
+ _watch_prefs = prefs->createObserver("/options/ruler/show_bbox", sigc::mem_fun(*this, &Ruler::on_prefs_changed));
+ on_prefs_changed();
+
+ INKSCAPE.themecontext->getChangeThemeSignal().connect(sigc::mem_fun(*this, &Ruler::on_style_updated));
+}
+
+void Ruler::on_prefs_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ _sel_visible = prefs->getBool("/options/ruler/show_bbox", true);
+
+ _backing_store_valid = false;
+ queue_draw();
+}
+
+// Set display unit for ruler.
+void
+Ruler::set_unit(Inkscape::Util::Unit const *unit)
+{
+ if (_unit != unit) {
+ _unit = unit;
+
+ _backing_store_valid = false;
+ queue_draw();
+ }
+}
+
+// Set range for ruler, update ticks.
+void
+Ruler::set_range(double lower, double upper)
+{
+ if (_lower != lower || _upper != upper) {
+
+ _lower = lower;
+ _upper = upper;
+ _max_size = _upper - _lower;
+ if (_max_size == 0) {
+ _max_size = 1;
+ }
+
+ _backing_store_valid = false;
+ queue_draw();
+ }
+}
+
+/**
+ * Set the location of the currently selected page.
+ */
+void Ruler::set_page(double lower, double upper)
+{
+ if (_page_lower != lower || _page_upper != upper) {
+ _page_lower = lower;
+ _page_upper = upper;
+
+ _backing_store_valid = false;
+ queue_draw();
+ }
+}
+
+/**
+ * Set the location of the currently selected page.
+ */
+void Ruler::set_selection(double lower, double upper)
+{
+ if (_sel_lower != lower || _sel_upper != upper) {
+ _sel_lower = lower;
+ _sel_upper = upper;
+
+ _backing_store_valid = false;
+ queue_draw();
+ }
+}
+
+// Add a widget (i.e. canvas) to monitor. Note, we don't worry about removing this signal as
+// our ruler is tied tightly to the canvas, if one is destroyed, so is the other.
+void
+Ruler::add_track_widget(Gtk::Widget& widget)
+{
+ widget.signal_motion_notify_event().connect(sigc::mem_fun(*this, &Ruler::on_motion_notify_event), false); // false => connect first
+}
+
+
+// Draws marker in response to motion events from canvas. Position is defined in ruler pixel
+// coordinates. The routine assumes that the ruler is the same width (height) as the canvas. If
+// not, one could use Gtk::Widget::translate_coordinates() to convert the coordinates.
+bool
+Ruler::on_motion_notify_event(GdkEventMotion *motion_event)
+{
+ double position = 0;
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ position = motion_event->x;
+ } else {
+ position = motion_event->y;
+ }
+
+ if (position != _position) {
+
+ _position = position;
+
+ // Find region to repaint (old and new marker positions).
+ Cairo::RectangleInt new_rect = marker_rect();
+ Cairo::RefPtr<Cairo::Region> region = Cairo::Region::create(new_rect);
+ region->do_union(_rect);
+
+ // Queue repaint
+ queue_draw_region(region);
+
+ _rect = new_rect;
+ }
+
+ return false;
+}
+
+bool Ruler::on_button_press_event(GdkEventButton *event)
+{
+ if (event->button == 3) {
+ auto menu = getContextMenu();
+ menu->popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ // Question to Reviewer: Does this leak?
+ return true;
+ }
+ return false;
+}
+
+// Find smallest dimension of ruler based on font size.
+void
+Ruler::size_request (Gtk::Requisition& requisition) const
+{
+ // Get border size
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border border = style_context->get_border(get_state_flags());
+
+ // get ruler's size from CSS style
+ GValue minimum_height = G_VALUE_INIT;
+ gtk_style_context_get_property(style_context->gobj(), "min-height", GTK_STATE_FLAG_NORMAL, &minimum_height);
+ auto size = g_value_get_int(&minimum_height);
+ g_value_unset(&minimum_height);
+
+ int width = border.get_left() + border.get_right();
+ int height = border.get_top() + border.get_bottom();
+
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ width += 1;
+ height += size;
+ } else {
+ width += size;
+ height += 1;
+ }
+
+ // Only valid for orientation in question (smallest dimension)!
+ requisition.width = width;
+ requisition.height = height;
+}
+
+void
+Ruler::get_preferred_width_vfunc (int& minimum_width, int& natural_width) const
+{
+ Gtk::Requisition requisition;
+ size_request(requisition);
+ minimum_width = natural_width = requisition.width;
+}
+
+void
+Ruler::get_preferred_height_vfunc (int& minimum_height, int& natural_height) const
+{
+ Gtk::Requisition requisition;
+ size_request(requisition);
+ minimum_height = natural_height = requisition.height;
+}
+
+// Update backing store when scale changes.
+// Note: in principle, there should not be a border (ruler ends should match canvas ends). If there
+// is a border, we calculate tick position ignoring border width at ends of ruler but move the
+// ticks and position marker inside the border.
+bool
+Ruler::draw_scale(const::Cairo::RefPtr<::Cairo::Context>& cr_in)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int awidth = allocation.get_width();
+ int aheight = allocation.get_height();
+
+ // Create backing store (need surface_in to get scale factor correct).
+ Cairo::RefPtr<Cairo::Surface> surface_in = cr_in->get_target();
+ _backing_store = Cairo::Surface::create(surface_in, Cairo::CONTENT_COLOR_ALPHA, awidth, aheight);
+
+ // Get context
+ Cairo::RefPtr<::Cairo::Context> cr = ::Cairo::Context::create(_backing_store);
+
+ // background
+ auto context = get_style_context();
+ context->render_background(cr, 0, 0, awidth, aheight);
+
+ // Color in page indication box
+ if (double psize = std::abs(_page_upper - _page_lower)) {
+ Gdk::Cairo::set_source_rgba(cr, _page_fill);
+ cr->begin_new_path();
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->rectangle(_page_lower, 0, psize, aheight);
+ } else {
+ cr->rectangle(0, _page_lower, awidth, psize);
+ }
+ cr->fill();
+ } else {
+ g_warning("No size?");
+ }
+ cr->set_line_width(1.0);
+
+ // Ruler size (only smallest dimension used later).
+ int rwidth = awidth - (_border.get_left() + _border.get_right());
+ int rheight = aheight - (_border.get_top() + _border.get_bottom());
+
+ auto paint_line = [=](Gdk::RGBA color, int offset) {
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->move_to(0, offset - 0.5);
+ cr->line_to(allocation.get_width(), offset - 0.5);
+ } else {
+ cr->move_to(offset - 0.5, 0);
+ cr->line_to(offset - 0.5, allocation.get_height());
+ }
+ Gdk::Cairo::set_source_rgba(cr, color);
+ cr->stroke();
+ };
+
+ if (_orientation != Gtk::ORIENTATION_HORIZONTAL) {
+ // From here on, awidth is the longest dimension of the ruler, rheight is the shortest.
+ std::swap(awidth, aheight);
+ std::swap(rwidth, rheight);
+ }
+ // Draw bottom/right line of ruler
+ paint_line(_foreground, aheight);
+
+ // Draw a shadow which overlaps any previously painted object.
+ auto paint_shadow = [=](double size_x, double size_y, double width, double height) {
+ auto trans = change_alpha(_shadow, 0.0);
+ auto gr = create_cubic_gradient(Geom::Rect(0, 0, size_x, size_y), _shadow, trans, Geom::Point(0, 0.5), Geom::Point(0.5, 1));
+ cr->rectangle(0, 0, width, height);
+ cr->set_source(gr);
+ cr->fill();
+ };
+ int gradient_size = 4;
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ paint_shadow(0, gradient_size, allocation.get_width(), gradient_size);
+ } else {
+ paint_shadow(gradient_size, 0, gradient_size, allocation.get_height());
+ }
+
+ // Figure out scale. Largest ticks must be far enough apart to fit largest text in vertical ruler.
+ // We actually require twice the distance.
+ unsigned int scale = std::ceil (_max_size); // Largest number
+ Glib::ustring scale_text = std::to_string(scale);
+ unsigned int digits = scale_text.length() + 1; // Add one for negative sign.
+ unsigned int minimum = digits * _font_size * 2;
+
+ double pixels_per_unit = awidth/_max_size; // pixel per distance
+
+ SPRulerMetric ruler_metric = ruler_metric_general;
+ if (_unit == Inkscape::Util::unit_table.getUnit("in")) {
+ ruler_metric = ruler_metric_inches;
+ }
+
+ unsigned scale_index;
+ for (scale_index = 0; scale_index < G_N_ELEMENTS (ruler_metric.ruler_scale)-1; ++scale_index) {
+ if (ruler_metric.ruler_scale[scale_index] * std::abs (pixels_per_unit) > minimum) break;
+ }
+
+ // Now we find out what is the subdivide index for the closest ticks we can draw
+ unsigned divide_index;
+ for (divide_index = 0; divide_index < G_N_ELEMENTS (ruler_metric.subdivide)-1; ++divide_index) {
+ if (ruler_metric.ruler_scale[scale_index] * std::abs (pixels_per_unit) < 5 * ruler_metric.subdivide[divide_index+1]) break;
+ }
+
+ // We'll loop over all ticks.
+ double pixels_per_tick = pixels_per_unit *
+ ruler_metric.ruler_scale[scale_index] / ruler_metric.subdivide[divide_index];
+
+ double units_per_tick = pixels_per_tick/pixels_per_unit;
+ double ticks_per_unit = 1.0/units_per_tick;
+
+ // Find first and last ticks
+ int start = 0;
+ int end = 0;
+ if (_lower < _upper) {
+ start = std::floor (_lower * ticks_per_unit);
+ end = std::ceil (_upper * ticks_per_unit);
+ } else {
+ start = std::floor (_upper * ticks_per_unit);
+ end = std::ceil (_lower * ticks_per_unit);
+ }
+
+ // Loop over all ticks
+ Gdk::Cairo::set_source_rgba(cr, _foreground);
+ for (int i = start; i < end+1; ++i) {
+
+ // Position of tick (add 0.5 to center tick on pixel).
+ double position = std::floor(i*pixels_per_tick - _lower*pixels_per_unit) + 0.5;
+
+ // Height of tick
+ int height = rheight - 7;
+ for (int j = divide_index; j > 0; --j) {
+ if (i%ruler_metric.subdivide[j] == 0) break;
+ height = height/2 + 1;
+ }
+
+ // Draw text for major ticks.
+ if (i%ruler_metric.subdivide[divide_index] == 0) {
+ cr->save();
+
+ int label_value = std::round(i * units_per_tick);
+
+ auto &label = _label_cache[label_value];
+ if (!label) {
+ label = draw_label(surface_in, label_value);
+ }
+
+ // Align text to pixel
+ int x = _border.get_left() + 3;
+ int y = position + 2.5;
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ x = position + 2.5;
+ y = _border.get_top() + 3;
+ }
+
+ // We don't know the surface height/width, damn you cairo.
+ cr->rectangle(x, y, 100, 100);
+ cr->clip();
+ cr->set_source(label, x, y);
+ cr->paint();
+ cr->restore();
+ }
+
+ // Draw ticks
+ Gdk::Cairo::set_source_rgba(cr, _foreground);
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->move_to(position, rheight + _border.get_top() - height);
+ cr->line_to(position, rheight + _border.get_top());
+ } else {
+ cr->move_to(rheight + _border.get_left() - height, position);
+ cr->line_to(rheight + _border.get_left(), position);
+ }
+ cr->stroke();
+ }
+
+ // Draw a selection bar
+ if (_sel_lower != _sel_upper && _sel_visible) {
+
+ const auto radius = 3.0;
+ const auto delta = _sel_upper - _sel_lower;
+ const auto dxy = delta > 0 ? radius : -radius;
+ double sy0 = _sel_lower;
+ double sy1 = _sel_upper;
+ double sx0 = floor(aheight * 0.7);
+ double sx1 = sx0;
+
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ std::swap(sy0, sx0);
+ std::swap(sy1, sx1);
+ }
+
+ cr->set_line_width(2.0);
+
+ if (fabs(delta) > 2 * radius) {
+ Gdk::Cairo::set_source_rgba(cr, _select_stroke);
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->move_to(sx0 + dxy, sy0);
+ cr->line_to(sx1 - dxy, sy1);
+ }
+ else {
+ cr->move_to(sx0, sy0 + dxy);
+ cr->line_to(sx1, sy1 - dxy);
+ }
+ cr->stroke();
+ }
+
+ // Markers
+ Gdk::Cairo::set_source_rgba(cr, _select_fill);
+ cr->begin_new_path();
+ cr->arc(sx0, sy0, radius, 0, 2 * M_PI);
+ cr->arc(sx1, sy1, radius, 0, 2 * M_PI);
+ cr->fill();
+
+ Gdk::Cairo::set_source_rgba(cr, _select_stroke);
+ cr->begin_new_path();
+ cr->arc(sx0, sy0, radius, 0, 2 * M_PI);
+ cr->stroke();
+ cr->begin_new_path();
+ cr->arc(sx1, sy1, radius, 0, 2 * M_PI);
+ cr->stroke();
+ }
+
+ _backing_store_valid = true;
+ return true;
+}
+
+/**
+ * Generate the label as it's only small surface for caching.
+ */
+Cairo::RefPtr<Cairo::Surface> Ruler::draw_label(Cairo::RefPtr<Cairo::Surface> const &surface_in, int label_value)
+{
+ bool rotate = _orientation != Gtk::ORIENTATION_HORIZONTAL;
+
+ Glib::RefPtr<Pango::Layout> layout = create_pango_layout(std::to_string(label_value));
+ layout->set_font_description(_font);
+
+ int text_width;
+ int text_height;
+ layout->get_pixel_size(text_width, text_height);
+ if (rotate) {
+ std::swap(text_width, text_height);
+ }
+
+ auto surface = Cairo::Surface::create(surface_in, Cairo::CONTENT_COLOR_ALPHA, text_width, text_height);
+ Cairo::RefPtr<::Cairo::Context> cr = ::Cairo::Context::create(surface);
+
+ cr->save();
+ Gdk::Cairo::set_source_rgba(cr, _foreground);
+ if (rotate) {
+ cr->translate(text_width / 2, text_height / 2);
+ cr->rotate(-M_PI_2);
+ cr->translate(-text_height / 2, -text_width / 2);
+ }
+ layout->show_in_cairo_context(cr);
+ cr->restore();
+
+ return surface;
+}
+
+// Draw position marker, we use doubles here.
+void
+Ruler::draw_marker(const Cairo::RefPtr<::Cairo::Context>& cr)
+{
+ Gtk::Allocation allocation = get_allocation();
+ const int awidth = allocation.get_width();
+ const int aheight = allocation.get_height();
+
+ Gdk::Cairo::set_source_rgba(cr, _foreground);
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ double offset = aheight - _border.get_bottom();
+ cr->move_to(_position, offset);
+ cr->line_to(_position - half_width, offset - half_width);
+ cr->line_to(_position + half_width, offset - half_width);
+ cr->close_path();
+ } else {
+ double offset = awidth - _border.get_right();
+ cr->move_to(offset, _position);
+ cr->line_to(offset - half_width, _position - half_width);
+ cr->line_to(offset - half_width, _position + half_width);
+ cr->close_path();
+ }
+ cr->fill();
+}
+
+// This is a pixel aligned integer rectangle that encloses the position marker. Used to define the
+// redraw area.
+Cairo::RectangleInt
+Ruler::marker_rect()
+{
+ Gtk::Allocation allocation = get_allocation();
+ const int awidth = allocation.get_width();
+ const int aheight = allocation.get_height();
+
+ int rwidth = awidth - _border.get_left() - _border.get_right();
+ int rheight = aheight - _border.get_top() - _border.get_bottom();
+
+ Cairo::RectangleInt rect;
+ rect.x = 0;
+ rect.y = 0;
+ rect.width = 0;
+ rect.height = 0;
+
+ // Find size of rectangle to enclose triangle.
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ rect.x = std::floor(_position - half_width);
+ rect.y = std::floor(_border.get_top() + rheight - half_width);
+ rect.width = std::ceil(half_width * 2.0 + 1);
+ rect.height = std::ceil(half_width);
+ } else {
+ rect.x = std::floor(_border.get_left() + rwidth - half_width);
+ rect.y = std::floor(_position - half_width);
+ rect.width = std::ceil(half_width);
+ rect.height = std::ceil(half_width * 2.0 + 1);
+ }
+
+ return rect;
+}
+
+// Draw the ruler using the tick backing store.
+bool
+Ruler::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) {
+
+ if (!_backing_store_valid) {
+ draw_scale (cr);
+ }
+
+ cr->set_source (_backing_store, 0, 0);
+ cr->paint();
+
+ draw_marker (cr);
+
+ return true;
+}
+
+// Update ruler on style change (font-size, etc.)
+void
+Ruler::on_style_updated() {
+
+ Gtk::DrawingArea::on_style_updated();
+
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ style_context->add_class(_orientation == Gtk::ORIENTATION_HORIZONTAL ? "horz" : "vert");
+
+ // Cache all our colors to speed up rendering.
+ _border = style_context->get_border();
+ _foreground = get_context_color(style_context, "color");
+ _font = style_context->get_font();
+ _font_size = _font.get_size();
+ if (!_font.get_size_is_absolute())
+ _font_size /= Pango::SCALE;
+
+ style_context->add_class("shadow");
+ _shadow = get_context_color(style_context, "border-color");
+ style_context->remove_class("shadow");
+
+ style_context->add_class("page");
+ _page_fill = get_background_color(style_context);
+ style_context->remove_class("page");
+
+ style_context->add_class("selection");
+ _select_fill = get_background_color(style_context);
+ _select_stroke = get_context_color(style_context, "border-color");
+ style_context->remove_class("selection");
+ _label_cache.clear();
+ _backing_store_valid = false;
+ queue_resize();
+ queue_draw();
+}
+
+/**
+ * Return a contextmenu for the ruler
+ */
+Gtk::Menu *Ruler::getContextMenu()
+{
+ auto gtk_menu = new Gtk::Menu();
+ auto gio_menu = Gio::Menu::create();
+ auto unit_menu = Gio::Menu::create();
+
+ for (auto &pair : unit_table.units(Inkscape::Util::UNIT_TYPE_LINEAR)) {
+ auto unit = pair.second.abbr;
+ Glib::ustring action_name = "doc.set-display-unit('" + unit + "')";
+ auto item = Gio::MenuItem::create(unit, action_name);
+ unit_menu->append_item(item);
+ }
+
+ gio_menu->append_section(unit_menu);
+ gtk_menu->bind_model(gio_menu, true);
+ gtk_menu->attach_to_widget(*this); // Might need canvas here
+ gtk_menu->show();
+ return gtk_menu;
+}
+
+} // 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 :