diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /src/ui/widget/gradient-editor.cpp | |
parent | Initial commit. (diff) | |
download | inkscape-upstream.tar.xz inkscape-upstream.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/ui/widget/gradient-editor.cpp | 653 |
1 files changed, 653 insertions, 0 deletions
diff --git a/src/ui/widget/gradient-editor.cpp b/src/ui/widget/gradient-editor.cpp new file mode 100644 index 0000000..df1b51b --- /dev/null +++ b/src/ui/widget/gradient-editor.cpp @@ -0,0 +1,653 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Gradient editor widget for "Fill and Stroke" dialog + * + * Author: + * Michael Kowalski + * + * Copyright (C) 2020-2021 Michael Kowalski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "gradient-editor.h" + +#include <gtkmm/builder.h> +#include <gtkmm/grid.h> +#include <gtkmm/togglebutton.h> +#include <gtkmm/button.h> +#include <gtkmm/menubutton.h> +#include <gtkmm/treeview.h> +#include <gtkmm/treemodelcolumn.h> +#include <glibmm/i18n.h> +#include <cairo.h> + +#include "document-undo.h" +#include "gradient-chemistry.h" +#include "gradient-selector.h" +#include "preferences.h" + +#include "display/cairo-utils.h" + +#include "io/resource.h" + +#include "object/sp-gradient-vector.h" +#include "object/sp-linear-gradient.h" + +#include "svg/css-ostringstream.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/color-preview.h" + + +namespace Inkscape { +namespace UI { +namespace Widget { + +using namespace Inkscape::IO; +using Inkscape::UI::Widget::ColorNotebook; + +class scope { +public: + scope(bool& flag): _flag(flag) { + flag = true; + } + + ~scope() { + _flag = false; + } + +private: + bool& _flag; +}; + +void set_icon(Gtk::Button& btn, gchar const* pixmap) { + if (Gtk::Image* img = sp_get_icon_image(pixmap, Gtk::ICON_SIZE_BUTTON)) { + btn.set_image(*img); + } +} + +// draw solid color circle with black outline; right side is to show checkerboard if color's alpha is > 0 +Glib::RefPtr<Gdk::Pixbuf> draw_circle(int size, guint32 rgba) { + int width = size; + int height = size; + gint w2 = width / 2; + + cairo_surface_t* s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t* cr = cairo_create(s); + + int x = 0, y = 0; + double radius = size / 2; + double degrees = M_PI / 180.0; + cairo_new_sub_path(cr); + cairo_arc(cr, x + radius, y + radius, radius, 0, 2 * M_PI); + cairo_close_path(cr); + // semi-transparent black outline + cairo_set_source_rgba(cr, 0, 0, 0, 0.2); + cairo_fill(cr); + + radius--; + + cairo_new_sub_path(cr); + cairo_line_to(cr, x + w2, 0); + cairo_line_to(cr, x + w2, height); + cairo_arc(cr, x + w2, y + w2, radius, 90 * degrees, 270 * degrees); + cairo_close_path(cr); + + // solid part + ink_cairo_set_source_rgba32(cr, rgba | 0xff); + cairo_fill(cr); + + x = w2; + + cairo_new_sub_path(cr); + cairo_arc(cr, x, y + w2, radius, -90 * degrees, 90 * degrees); + cairo_line_to(cr, x, y); + cairo_close_path(cr); + + // (semi)transparent part + if ((rgba & 0xff) != 0xff) { + cairo_pattern_t* checkers = ink_cairo_pattern_create_checkerboard(); + cairo_set_source(cr, checkers); + cairo_fill_preserve(cr); + cairo_pattern_destroy(checkers); + } + ink_cairo_set_source_rgba32(cr, rgba); + cairo_fill(cr); + + cairo_destroy(cr); + cairo_surface_flush(s); + + GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s); + return Glib::wrap(pixbuf); +} + + +Glib::RefPtr<Gdk::Pixbuf> get_stop_pixmap(SPStop* stop) { + const int size = 30; + return draw_circle(size, stop->getColor().toRGBA32(stop->getOpacity())); +} + +// get widget from builder or throw +template<class W> W& get_widget(Glib::RefPtr<Gtk::Builder>& builder, const char* id) { + W* widget; + builder->get_widget(id, widget); + if (!widget) { + throw std::runtime_error("Missing widget in a glade resource file"); + } + return *widget; +} + +Glib::RefPtr<Gtk::Builder> create_builder() { + auto glade = Resource::get_filename(Resource::UIS, "gradient-edit.glade"); + Glib::RefPtr<Gtk::Builder> builder; + try { + return Gtk::Builder::create_from_file(glade); + } + catch (Glib::Error& ex) { + g_error("Cannot load glade file for gradient editor: %s", + ex.what().c_str()); + throw; + } +} + +Glib::ustring get_repeat_icon(SPGradientSpread mode) { + const char* ico = ""; + switch (mode) { + case SP_GRADIENT_SPREAD_PAD: + ico = "gradient-spread-pad"; + break; + case SP_GRADIENT_SPREAD_REPEAT: + ico = "gradient-spread-repeat"; + break; + case SP_GRADIENT_SPREAD_REFLECT: + ico = "gradient-spread-reflect"; + break; + default: + g_warning("Missing case in %s\n", __func__); + break; + } + return ico; +} + +GradientEditor::GradientEditor(const char* prefs) : + _builder(create_builder()), + _selector(Gtk::manage(new GradientSelector())), + _repeat_icon(get_widget<Gtk::Image>(_builder, "repeatIco")), + _popover(get_widget<Gtk::Popover>(_builder, "libraryPopover")), + _stop_tree(get_widget<Gtk::TreeView>(_builder, "stopList")), + _offset_btn(get_widget<Gtk::SpinButton>(_builder, "offsetSpin")), + _show_stops_list(get_widget<Gtk::Expander>(_builder, "stopsBtn")), + _add_stop(get_widget<Gtk::Button>(_builder, "stopAdd")), + _delete_stop(get_widget<Gtk::Button>(_builder, "stopDelete")), + _stops_gallery(get_widget<Gtk::Box>(_builder, "stopsGallery")), + _colors_box(get_widget<Gtk::Box>(_builder, "colorsBox")), + _linear_btn(get_widget<Gtk::ToggleButton>(_builder, "linearBtn")), + _radial_btn(get_widget<Gtk::ToggleButton>(_builder, "radialBtn")), + _main_grid(get_widget<Gtk::Grid>(_builder, "mainGrid")), + _prefs(prefs) +{ + // gradient type buttons; not currently used, hidden, WIP + set_icon(_linear_btn, INKSCAPE_ICON("paint-gradient-linear")); + set_icon(_radial_btn, INKSCAPE_ICON("paint-gradient-radial")); + + auto& reverse = get_widget<Gtk::Button>(_builder, "reverseBtn"); + set_icon(reverse, INKSCAPE_ICON("object-flip-horizontal")); + reverse.signal_clicked().connect([=](){ reverse_gradient(); }); + + auto& gradBox = get_widget<Gtk::Box>(_builder, "gradientBox"); + const int dot_size = 8; + _gradient_image.show(); + _gradient_image.set_margin_start(dot_size / 2); + _gradient_image.set_margin_end(dot_size / 2); + // gradient stop selected in a gradient widget; sync list selection + _gradient_image.signal_stop_selected().connect([=](size_t index) { + select_stop(index); + fire_stop_selected(get_current_stop()); + }); + _gradient_image.signal_stop_offset_changed().connect([=](size_t index, double offset) { + set_stop_offset(index, offset); + }); + _gradient_image.signal_add_stop_at().connect([=](double offset) { + insert_stop_at(offset); + }); + _gradient_image.signal_delete_stop().connect([=](size_t index) { + delete_stop(index); + }); + + gradBox.pack_start(_gradient_image, true, true, 0); + + // add color selector + auto color_selector = Gtk::manage(new ColorNotebook(_selected_color)); + color_selector->set_label(_("Stop color")); + color_selector->show(); + _colors_box.pack_start(*color_selector, true, true, 0); + + // gradient library in a popup + _popover.add(*_selector); + const int h = 5; + const int v = 3; + _selector->set_margin_start(h); + _selector->set_margin_end(h); + _selector->set_margin_top(v); + _selector->set_margin_bottom(v); + _selector->show(); + _selector->show_edit_button(false); + _selector->set_gradient_size(160, 20); + _selector->set_name_col_size(120); + // gradient changed is currently the only signal that GradientSelector can emit: + _selector->signal_changed().connect([=](SPGradient* gradient) { + // new gradient selected from the library + _signal_changed.emit(gradient); + }); + + // construct store for a list of stops + _stop_columns.add(_stopObj); + _stop_columns.add(_stopIdx); + _stop_columns.add(_stopID); + _stop_columns.add(_stop_color); + _stop_list_store = Gtk::ListStore::create(_stop_columns); + _stop_tree.set_model(_stop_list_store); + // indices in the stop list view; currently hidden + // _stop_tree.append_column("n", _stopID); // 1-based stop index + _stop_tree.append_column("c", _stop_color); // and its color + + auto selection = _stop_tree.get_selection(); + selection->signal_changed().connect([=]() { + if (!_update.pending()) { + stop_selected(); + fire_stop_selected(get_current_stop()); + } + }); + + _show_stops_list.property_expanded().signal_changed().connect( + [&](){ show_stops(_show_stops_list.get_expanded()); } + ); + + set_icon(_add_stop, "list-add"); + _add_stop.signal_clicked().connect([=](){ + if (auto row = current_stop()) { + auto index = row->get_value(_stopIdx); + add_stop(static_cast<int>(index)); + } + }); + + set_icon(_delete_stop, "list-remove"); + _delete_stop.signal_clicked().connect([=]() { + if (auto row = current_stop()) { + auto index = row->get_value(_stopIdx); + delete_stop(static_cast<int>(index)); + } + }); + + // connect gradient repeat modes menu + std::tuple<const char*, SPGradientSpread> repeats[3] = { + {"repeatNone", SP_GRADIENT_SPREAD_PAD}, + {"repeatDirect", SP_GRADIENT_SPREAD_REPEAT}, + {"repeatReflected", SP_GRADIENT_SPREAD_REFLECT} + }; + for (auto& el : repeats) { + auto& item = get_widget<Gtk::MenuItem>(_builder, std::get<0>(el)); + auto mode = std::get<1>(el); + item.signal_activate().connect([=](){ set_repeat_mode(mode); }); + // pack icon and text into MenuItem, since MenuImageItem is deprecated + auto text = item.get_label(); + auto hbox = Gtk::manage(new Gtk::Box); + Gtk::Image* img = sp_get_icon_image(get_repeat_icon(mode), Gtk::ICON_SIZE_BUTTON); + hbox->pack_start(*img, false, true, 8); + auto label = Gtk::manage(new Gtk::Label); + label->set_label(text); + hbox->pack_start(*label, false, true, 8); + hbox->show_all(); + item.remove(); + item.add(*hbox); + } + + set_repeat_icon(SP_GRADIENT_SPREAD_PAD); + + _selected_color.signal_changed.connect([=]() { + set_stop_color(_selected_color.color(), _selected_color.alpha()); + }); + _selected_color.signal_dragged.connect([=]() { + set_stop_color(_selected_color.color(), _selected_color.alpha()); + }); + + _offset_btn.signal_changed().connect([=]() { + if (auto row = current_stop()) { + auto index = row->get_value(_stopIdx); + double offset = _offset_btn.get_value(); + set_stop_offset(index, offset); + } + }); + + pack_start(_main_grid); + + // restore visibility of the stop list view + _stops_list_visible = Inkscape::Preferences::get()->getBool(_prefs + "/stoplist", true); + _show_stops_list.set_expanded(_stops_list_visible); + update_stops_layout(); +} + +GradientEditor::~GradientEditor() noexcept { +} + +void GradientEditor::set_stop_color(SPColor color, float opacity) { + if (_update.pending()) return; + + SPGradient* vector = get_gradient_vector(); + if (!vector) return; + + if (auto row = current_stop()) { + auto index = row->get_value(_stopIdx); + SPStop* stop = sp_get_nth_stop(vector, index); + if (stop && _document) { + auto scoped(_update.block()); + + // update list view too + row->set_value(_stop_color, get_stop_pixmap(stop)); + + sp_set_gradient_stop_color(_document, stop, color, opacity); + } + } +} + +std::optional<Gtk::TreeRow> GradientEditor::current_stop() { + auto sel = _stop_tree.get_selection(); + auto it = sel->get_selected(); + if (!it) { + return std::nullopt; + } + else { + return *it; + } +} + +SPStop* GradientEditor::get_nth_stop(size_t index) { + if (SPGradient* vector = get_gradient_vector()) { + return sp_get_nth_stop(vector, index); + } + return nullptr; +} + +// stop has been selected in a list view +void GradientEditor::stop_selected() { + if (auto row = current_stop()) { + SPStop* stop = row->get_value(_stopObj); + if (stop) { + auto scoped(_update.block()); + + _selected_color.setColor(stop->getColor()); + _selected_color.setAlpha(stop->getOpacity()); + + auto stops = sp_get_before_after_stops(stop); + if (stops.first && stops.second) { + _offset_btn.set_range(stops.first->offset, stops.second->offset); + } + else { + _offset_btn.set_range(stops.first ? stops.first->offset : 0, stops.second ? stops.second->offset : 1); + } + _offset_btn.set_sensitive(); + _offset_btn.set_value(stop->offset); + + int index = row->get_value(_stopIdx); + _gradient_image.set_focused_stop(index); + } + } + else { + // no selection + auto scoped(_update.block()); + + _selected_color.setColor(SPColor()); + + _offset_btn.set_range(0, 0); + _offset_btn.set_value(0); + _offset_btn.set_sensitive(false); + } +} + +void GradientEditor::insert_stop_at(double offset) { + if (SPGradient* vector = get_gradient_vector()) { + // only insert new stop if there are some stops present + if (vector->hasStops()) { + SPStop* stop = sp_gradient_add_stop_at(vector, offset); + // just select next stop; newly added stop will be in a list view after selection refresh (on idle) + auto pos = sp_number_of_stops_before_stop(vector, stop); + auto selected = select_stop(pos); + fire_stop_selected(stop); + if (!selected) { + select_stop(pos); + } + } + } +} + +void GradientEditor::add_stop(int index) { + if (SPGradient* vector = get_gradient_vector()) { + if (SPStop* current = sp_get_nth_stop(vector, index)) { + SPStop* stop = sp_gradient_add_stop(vector, current); + // just select next stop; newly added stop will be in a list view after selection refresh (on idle) + select_stop(sp_number_of_stops_before_stop(vector, stop)); + fire_stop_selected(stop); + } + } +} + +void GradientEditor::delete_stop(int index) { + if (SPGradient* vector = get_gradient_vector()) { + if (SPStop* stop = sp_get_nth_stop(vector, index)) { + // try deleting a stop, if it can be + sp_gradient_delete_stop(vector, stop); + } + } +} + +// collapse/expand list of stops in the UI +void GradientEditor::show_stops(bool visible) { + _stops_list_visible = visible; + update_stops_layout(); + Inkscape::Preferences::get()->setBool(_prefs + "/stoplist", _stops_list_visible); +} + +void GradientEditor::update_stops_layout() { + if (_stops_list_visible) { + _stops_gallery.show(); + } + else { + _stops_gallery.hide(); + } +} + +void GradientEditor::reverse_gradient() { + if (_document && _gradient) { + // reverse works on a gradient definition, the one with stops: + SPGradient* vector = get_gradient_vector(); + + if (vector) { + sp_gradient_reverse_vector(vector); + DocumentUndo::done(_document, _("Reverse gradient"), INKSCAPE_ICON("color-gradient")); + } + } +} + +void GradientEditor::set_repeat_mode(SPGradientSpread mode) { + if (_update.pending()) return; + + if (_document && _gradient) { + auto scoped(_update.block()); + + // spread is set on a gradient reference, which is _gradient object + _gradient->setSpread(mode); + _gradient->updateRepr(); + + DocumentUndo::done(_document, _("Set gradient repeat"), INKSCAPE_ICON("color-gradient")); + + set_repeat_icon(mode); + } +} + +void GradientEditor::set_repeat_icon(SPGradientSpread mode) { + auto ico = get_repeat_icon(mode); + if (!ico.empty()) { + _repeat_icon.set_from_icon_name(ico, Gtk::ICON_SIZE_BUTTON); + } +} + +void GradientEditor::setGradient(SPGradient* gradient) { + auto scoped(_update.block()); + auto scoped2(_notification.block()); + _gradient = gradient; + _document = gradient ? gradient->document : nullptr; + set_gradient(gradient); +} + +SPGradient* GradientEditor::getVector() { + return _selector->getVector(); +} + +void GradientEditor::setVector(SPDocument* doc, SPGradient* vector) { + auto scoped(_update.block()); + _selector->setVector(doc, vector); +} + +void GradientEditor::setMode(SelectorMode mode) { + _selector->setMode(mode); +} + +void GradientEditor::setUnits(SPGradientUnits units) { + _selector->setUnits(units); +} + +SPGradientUnits GradientEditor::getUnits() { + return _selector->getUnits(); +} + +void GradientEditor::setSpread(SPGradientSpread spread) { + _selector->setSpread(spread); +} + +SPGradientSpread GradientEditor::getSpread() { + return _selector->getSpread(); +} + +void GradientEditor::selectStop(SPStop* selected) { + if (_notification.pending()) return; + + auto scoped(_notification.block()); + // request from the outside to sync stop selection + const auto& items = _stop_tree.get_model()->children(); + auto it = std::find_if(items.begin(), items.end(), [=](const auto& row) { + SPStop* stop = row->get_value(_stopObj); + return stop == selected; + }); + if (it != items.end()) { + select_stop(std::distance(items.begin(), it)); + } +} + +SPGradient* GradientEditor::get_gradient_vector() { + if (!_gradient) return nullptr; + return sp_gradient_get_forked_vector_if_necessary(_gradient, false); +} + +void GradientEditor::set_gradient(SPGradient* gradient) { + auto scoped(_update.block()); + + // remember which stop is selected, so we can restore it + size_t selected_stop_index = 0; + if (auto it = _stop_tree.get_selection()->get_selected()) { + selected_stop_index = it->get_value(_stopIdx); + } + + _stop_list_store->clear(); + + SPGradient* vector = gradient ? gradient->getVector() : nullptr; + + if (vector) { + vector->ensureVector(); + } + + _gradient_image.set_gradient(vector); + + if (!vector || !vector->hasStops()) return; + + size_t index = 0; + for (auto& child : vector->children) { + if (SP_IS_STOP(&child)) { + auto stop = SP_STOP(&child); + auto it = _stop_list_store->append(); + it->set_value(_stopObj, stop); + it->set_value(_stopIdx, index); + it->set_value(_stopID, Glib::ustring::compose("%1.", index + 1)); + it->set_value(_stop_color, get_stop_pixmap(stop)); + + ++index; + } + } + + auto mode = gradient->isSpreadSet() ? gradient->getSpread() : SP_GRADIENT_SPREAD_PAD; + set_repeat_icon(mode); + + // list not empty? + if (index > 0) { + select_stop(std::min(selected_stop_index, index - 1)); + // update related widgets + stop_selected(); + // + // emit_stop_selected(get_current_stop()); + } +} + +void GradientEditor::set_stop_offset(size_t index, double offset) { + if (_update.pending()) return; + + // adjust stop's offset after user edits it in offset spin button or drags stop handle + SPStop* stop = get_nth_stop(index); + if (stop) { + auto scoped(_update.block()); + + stop->offset = offset; + if (auto repr = stop->getRepr()) { + repr->setAttributeCssDouble("offset", stop->offset); + } + + DocumentUndo::maybeDone(stop->document, "gradient:stop:offset", _("Change gradient stop offset"), INKSCAPE_ICON("color-gradient")); + } +} + +// select requested stop in a list view +bool GradientEditor::select_stop(size_t index) { + if (!_gradient) return false; + + bool selected = false; + const auto& items = _stop_tree.get_model()->children(); + if (index < items.size()) { + auto it = items.begin(); + std::advance(it, index); + auto path = _stop_tree.get_model()->get_path(it); + _stop_tree.get_selection()->select(it); + _stop_tree.scroll_to_cell(path, *_stop_tree.get_column(0)); + selected = true; + } + + return selected; +} + +SPStop* GradientEditor::get_current_stop() { + if (auto row = current_stop()) { + SPStop* stop = row->get_value(_stopObj); + return stop; + } + return nullptr; +} + +void GradientEditor::fire_stop_selected(SPStop* stop) { + if (!_notification.pending()) { + auto scoped(_notification.block()); + emit_stop_selected(stop); + } +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape |