From cca66b9ec4e494c1d919bff0f71a820d8afab1fa Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 20:24:48 +0200 Subject: Adding upstream version 1.2.2. Signed-off-by: Daniel Baumann --- src/ui/widget/gradient-with-stops.cpp | 552 ++++++++++++++++++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100644 src/ui/widget/gradient-with-stops.cpp (limited to 'src/ui/widget/gradient-with-stops.cpp') diff --git a/src/ui/widget/gradient-with-stops.cpp b/src/ui/widget/gradient-with-stops.cpp new file mode 100644 index 0000000..1d413dd --- /dev/null +++ b/src/ui/widget/gradient-with-stops.cpp @@ -0,0 +1,552 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Gradient image widget with stop handles + * + * Author: + * Michael Kowalski + * + * Copyright (C) 2020-2021 Michael Kowalski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "gradient-with-stops.h" +#include "object/sp-gradient.h" +#include "object/sp-stop.h" +#include "display/cairo-utils.h" +#include "io/resource.h" +#include "ui/cursor-utils.h" +#include "ui/util.h" + +// widget's height; it should take stop template's height into account +// current value is fine-tuned to make stop handles overlap gradient image just the right amount +const int GRADIENT_WIDGET_HEIGHT = 33; +// gradient's image height (multiple of checkerboard tiles, they are 6x6) +const int GRADIENT_IMAGE_HEIGHT = 3 * 6; + +namespace Inkscape { +namespace UI { +namespace Widget { + +using namespace Inkscape::IO; + +std::string get_stop_template_path(const char* filename) { + // "stop handle" template files path + return Resource::get_filename(Resource::UIS, filename); +} + +GradientWithStops::GradientWithStops() : + _template(get_stop_template_path("gradient-stop.svg").c_str()), + _tip_template(get_stop_template_path("gradient-tip.svg").c_str()) + { + // default color, it will be updated + _background_color.set_grey(0.5); + // for theming, but not used + set_name("GradientEdit"); + // we need some events + add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::BUTTON_MOTION_MASK | + Gdk::POINTER_MOTION_MASK | Gdk::KEY_PRESS_MASK); + set_can_focus(); +} + +void GradientWithStops::set_gradient(SPGradient* gradient) { + _gradient = gradient; + + // listen to release & changes + _release = gradient ? gradient->connectRelease([=](SPObject*){ set_gradient(nullptr); }) : sigc::connection(); + _modified = gradient ? gradient->connectModified([=](SPObject*, guint){ modified(); }) : sigc::connection(); + + // TODO: check selected/focused stop index + + modified(); + + set_sensitive(gradient != nullptr); +} + +void GradientWithStops::modified() { + // gradient has been modified + + // read all stops + _stops.clear(); + + if (_gradient) { + SPStop* stop = _gradient->getFirstStop(); + while (stop) { + _stops.push_back(stop_t { + .offset = stop->offset, .color = stop->getColor(), .opacity = stop->getOpacity() + }); + stop = stop->getNextStop(); + } + } + + update(); +} + +void GradientWithStops::size_request(GtkRequisition* requisition) const { + requisition->width = 60; + requisition->height = GRADIENT_WIDGET_HEIGHT; +} + +void GradientWithStops::get_preferred_width_vfunc(int& minimal_width, int& natural_width) const { + GtkRequisition requisition; + size_request(&requisition); + minimal_width = natural_width = requisition.width; +} + +void GradientWithStops::get_preferred_height_vfunc(int& minimal_height, int& natural_height) const { + GtkRequisition requisition; + size_request(&requisition); + minimal_height = natural_height = requisition.height; +} + +void GradientWithStops::update() { + if (get_is_drawable()) { + queue_draw(); + } +} + +// capture background color when styles change +void GradientWithStops::on_style_updated() { + if (auto wnd = dynamic_cast(this->get_toplevel())) { + auto sc = wnd->get_style_context(); + _background_color = get_background_color(sc); + } + + // load and cache cursors + auto wnd = get_window(); + if (wnd && !_cursor_mouseover) { + // use standard cursors: + _cursor_mouseover = Gdk::Cursor::create(get_display(), "grab"); + _cursor_dragging = Gdk::Cursor::create(get_display(), "grabbing"); + _cursor_insert = Gdk::Cursor::create(get_display(), "crosshair"); + // or custom cursors: + // _cursor_mouseover = load_svg_cursor(get_display(), wnd, "gradient-over-stop.svg"); + // _cursor_dragging = load_svg_cursor(get_display(), wnd, "gradient-drag-stop.svg"); + // _cursor_insert = load_svg_cursor(get_display(), wnd, "gradient-add-stop.svg"); + wnd->set_cursor(); + } +} + +void draw_gradient(const Cairo::RefPtr& cr, SPGradient* gradient, int x, int width) { + cairo_pattern_t* check = ink_cairo_pattern_create_checkerboard(); + + cairo_set_source(cr->cobj(), check); + cr->fill_preserve(); + cairo_pattern_destroy(check); + + if (gradient) { + auto p = gradient->create_preview_pattern(width); + cairo_matrix_t m; + cairo_matrix_init_translate(&m, -x, 0); + cairo_pattern_set_matrix(p, &m); + cairo_set_source(cr->cobj(), p); + cr->fill(); + cairo_pattern_destroy(p); + } +} + +// return on-screen position of the UI stop corresponding to the gradient's color stop at 'index' +GradientWithStops::stop_pos_t GradientWithStops::get_stop_position(size_t index, const layout_t& layout) const { + if (!_gradient || index >= _stops.size()) { + return stop_pos_t {}; + } + + // half of the stop template width; round it to avoid half-pixel coordinates + const auto dx = round((_template.get_width_px() + 1) / 2); + + auto pos = [&](double offset) { return round(layout.x + layout.width * CLAMP(offset, 0, 1)); }; + const auto& v = _stops; + + auto offset = pos(v[index].offset); + auto left = offset - dx; + if (index > 0) { + // check previous stop; it may overlap + auto prev = pos(v[index - 1].offset) + dx; + if (prev > left) { + // overlap + left = round((left + prev) / 2); + } + } + + auto right = offset + dx; + if (index + 1 < v.size()) { + // check next stop for overlap + auto next = pos(v[index + 1].offset) - dx; + if (right > next) { + // overlap + right = round((right + next) / 2); + } + } + + return stop_pos_t { + .left = left, + .tip = offset, + .right = right, + .top = layout.height - _template.get_height_px(), + .bottom = layout.height + }; +} + +// widget's layout; mainly location of the gradient's image and stop handles +GradientWithStops::layout_t GradientWithStops::get_layout() const { + auto allocation = get_allocation(); + + const auto stop_width = _template.get_width_px(); + const auto half_stop = round((stop_width + 1) / 2); + const auto x = half_stop; + const double width = allocation.get_width() - stop_width; + const double height = allocation.get_height(); + + return layout_t { + .x = x, + .y = 0, + .width = width, + .height = height + }; +} + +// check if stop handle is under (x, y) location, return its index or -1 if not hit +int GradientWithStops::find_stop_at(double x, double y) const { + if (!_gradient) return -1; + + const auto& v = _stops; + const auto& layout = get_layout(); + + // find stop handle at (x, y) position; note: stops may not be ordered by offsets + for (size_t i = 0; i < v.size(); ++i) { + auto pos = get_stop_position(i, layout); + if (x >= pos.left && x <= pos.right && y >= pos.top && y <= pos.bottom) { + return static_cast(i); + } + } + + return -1; +} + +// this is range of offset adjustment for a given stop +GradientWithStops::limits_t GradientWithStops::get_stop_limits(int maybe_index) const { + if (!_gradient) return limits_t {}; + + // let negative index turn into a large out-of-range number + auto index = static_cast(maybe_index); + + const auto& v = _stops; + + if (index < v.size()) { + double min = 0; + double max = 1; + + if (v.size() > 1) { + std::vector offsets; + offsets.reserve(v.size()); + for (auto& s : _stops) { + offsets.push_back(s.offset); + } + std::sort(offsets.begin(), offsets.end()); + + // special cases: + if (index == 0) { // first stop + max = offsets[index + 1]; + } + else if (index + 1 == v.size()) { // last stop + min = offsets[index - 1]; + } + else { + // stops "inside" gradient + min = offsets[index - 1]; + max = offsets[index + 1]; + } + } + return limits_t { .min_offset = min, .max_offset = max, .offset = v[index].offset }; + } + else { + return limits_t {}; + } +} + +bool GradientWithStops::on_focus_out_event(GdkEventFocus* event) { + update(); + return false; +} + +bool GradientWithStops::on_focus_in_event(GdkEventFocus* event) { + update(); + return false; +} + +bool GradientWithStops::on_focus(Gtk::DirectionType direction) { + if (has_focus()) { + return false; // let focus go + } + + grab_focus(); + // TODO - add focus indicator frame or some focus indicator + return true; +} + +bool GradientWithStops::on_key_press_event(GdkEventKey* key_event) { + bool consumed = false; + // currently all keyboard activity involves acting on focused stop handle; bail if nothing's selected + if (_focused_stop < 0) return consumed; + + unsigned int key = 0; + auto modifier = static_cast(key_event->state); + gdk_keymap_translate_keyboard_state(Gdk::Display::get_default()->get_keymap(), + key_event->hardware_keycode, modifier, 0, &key, nullptr, nullptr, nullptr); + + auto delta = _stop_move_increment; + if (modifier & GDK_SHIFT_MASK) { + delta *= 10; + } + + consumed = true; + + switch (key) { + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + move_stop(_focused_stop, -delta); + break; + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + move_stop(_focused_stop, delta); + break; + case GDK_KEY_BackSpace: + case GDK_KEY_Delete: + _signal_delete_stop.emit(_focused_stop); + break; + default: + consumed = false; + break; + } + + return consumed; +} + +bool GradientWithStops::on_button_press_event(GdkEventButton* event) { + // single button press selects stop and can start dragging it + constexpr auto LEFT_BTN = 1; + if (event->button == LEFT_BTN && _gradient && event->type == GDK_BUTTON_PRESS) { + _focused_stop = -1; + + if (!has_focus()) { + // grab focus, so we can show selection indicator and move selected stop with left/right keys + grab_focus(); + } + update(); + + // find stop handle + auto index = find_stop_at(event->x, event->y); + + if (index >= 0) { + _focused_stop = index; + // fire stop selection, whether stop can be moved or not + _signal_stop_selected.emit(index); + + auto limits = get_stop_limits(index); + + // check if clicked stop can be moved + if (limits.min_offset < limits.max_offset) { + // TODO: to facilitate selecting stops without accidentally moving them, + // delay dragging mode until mouse cursor moves certain distance... + _dragging = true; + _pointer_x = event->x; + _stop_offset = _stops.at(index).offset; + + if (_cursor_dragging) { + gdk_window_set_cursor(event->window, _cursor_dragging->gobj()); + } + } + } + } + else if (event->button == LEFT_BTN && _gradient && event->type == GDK_2BUTTON_PRESS) { + // double-click may insert a new stop + auto index = find_stop_at(event->x, event->y); + if (index < 0) { + auto layout = get_layout(); + if (layout.width > 0 && event->x > layout.x && event->x < layout.x + layout.width) { + double position = (event->x - layout.x) / layout.width; + // request new stop + _signal_add_stop_at.emit(position); + } + } + } + + return false; +} + +bool GradientWithStops::on_button_release_event(GdkEventButton* event) { + GdkCursor* cursor = get_cursor(event->x, event->y); + gdk_window_set_cursor(event->window, cursor); + + _dragging = false; + return false; +} + +// move stop by a given amount (delta) +void GradientWithStops::move_stop(int stop_index, double offset_shift) { + auto layout = get_layout(); + if (layout.width > 0) { + auto limits = get_stop_limits(stop_index); + if (limits.min_offset < limits.max_offset) { + auto new_offset = CLAMP(limits.offset + offset_shift, limits.min_offset, limits.max_offset); + if (new_offset != limits.offset) { + _signal_stop_offset_changed.emit(stop_index, new_offset); + } + } + } +} + +bool GradientWithStops::on_motion_notify_event(GdkEventMotion* event) { + if (_dragging && _gradient) { + // move stop to a new position (adjust offset) + auto dx = event->x - _pointer_x; + auto layout = get_layout(); + if (layout.width > 0) { + auto delta = dx / layout.width; + auto limits = get_stop_limits(_focused_stop); + if (limits.min_offset < limits.max_offset) { + auto new_offset = CLAMP(_stop_offset + delta, limits.min_offset, limits.max_offset); + _signal_stop_offset_changed.emit(_focused_stop, new_offset); + } + } + } + else if (!_dragging && _gradient) { + GdkCursor* cursor = get_cursor(event->x, event->y); + gdk_window_set_cursor(event->window, cursor); + } + + return false; +} + +GdkCursor* GradientWithStops::get_cursor(double x, double y) const { + GdkCursor* cursor = nullptr; + if (_gradient) { + // check if mouse if over stop handle that we can adjust + auto index = find_stop_at(x, y); + if (index >= 0) { + auto limits = get_stop_limits(index); + if (limits.min_offset < limits.max_offset && _cursor_mouseover) { + cursor = _cursor_mouseover->gobj(); + } + } + else { + if (_cursor_insert) { + cursor = _cursor_insert->gobj(); + } + } + } + return cursor; +} + +bool GradientWithStops::on_draw(const Cairo::RefPtr& cr) { + auto allocation = get_allocation(); + auto context = get_style_context(); + const double scale = get_scale_factor(); + const auto layout = get_layout(); + + if (layout.width <= 0) return true; + + context->render_background(cr, 0, 0, allocation.get_width(), allocation.get_height()); + + // empty gradient checkboard or gradient itself + cr->rectangle(layout.x, layout.y, layout.width, GRADIENT_IMAGE_HEIGHT); + draw_gradient(cr, _gradient, layout.x, layout.width); + + if (!_gradient) return true; + + // draw stop handles + + cr->begin_new_path(); + + Gdk::RGBA fg = context->get_color(get_state_flags()); + Gdk::RGBA bg = _background_color; + + // stop handle outlines and selection indicator use theme colors: + _template.set_style(".outer", "fill", rgba_to_css_color(fg)); + _template.set_style(".inner", "stroke", rgba_to_css_color(bg)); + _template.set_style(".hole", "fill", rgba_to_css_color(bg)); + + auto tip = _tip_template.render(scale); + + for (size_t i = 0; i < _stops.size(); ++i) { + const auto& stop = _stops[i]; + + // stop handle shows stop color and opacity: + _template.set_style(".color", "fill", rgba_to_css_color(stop.color)); + _template.set_style(".opacity", "opacity", double_to_css_value(stop.opacity)); + + // show/hide selection indicator + const auto is_selected = _focused_stop == static_cast(i); + _template.set_style(".selected", "opacity", double_to_css_value(is_selected ? 1 : 0)); + + // render stop handle + auto pix = _template.render(scale); + + if (!pix) { + g_warning("Rendering gradient stop failed."); + break; + } + + auto pos = get_stop_position(i, layout); + + // selected handle sports a 'tip' to make it easily noticeable + if (is_selected && tip) { + if (auto surface = Gdk::Cairo::create_surface_from_pixbuf(tip, 1)) { + cr->save(); + // scale back to physical pixels + cr->scale(1 / scale, 1 / scale); + // paint tip bitmap + cr->set_source(surface, round(pos.tip * scale - tip->get_width() / 2), layout.y * scale); + cr->paint(); + cr->restore(); + } + } + + // surface from pixbuf *without* scaling (scale = 1) + auto surface = Gdk::Cairo::create_surface_from_pixbuf(pix, 1); + if (!surface) continue; + + // calc space available for stop marker + cr->save(); + cr->rectangle(pos.left, layout.y, pos.right - pos.left, layout.height); + cr->clip(); + // scale back to physical pixels + cr->scale(1 / scale, 1 / scale); + // paint bitmap + cr->set_source(surface, round(pos.tip * scale - pix->get_width() / 2), pos.top * scale); + cr->paint(); + cr->restore(); + cr->reset_clip(); + } + + return true; +} + +// focused/selected stop indicator +void GradientWithStops::set_focused_stop(int index) { + if (_focused_stop != index) { + _focused_stop = index; + + if (has_focus()) { + update(); + } + } +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : -- cgit v1.2.3