// 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 #include #include #include #include #include #include #include #include #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 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 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 W& get_widget(Glib::RefPtr& 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 create_builder() { auto glade = Resource::get_filename(Resource::UIS, "gradient-edit.glade"); Glib::RefPtr 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(_builder, "repeatIco")), _popover(get_widget(_builder, "libraryPopover")), _stop_tree(get_widget(_builder, "stopList")), _offset_btn(get_widget(_builder, "offsetSpin")), _show_stops_list(get_widget(_builder, "stopsBtn")), _add_stop(get_widget(_builder, "stopAdd")), _delete_stop(get_widget(_builder, "stopDelete")), _stops_gallery(get_widget(_builder, "stopsGallery")), _colors_box(get_widget(_builder, "colorsBox")), _linear_btn(get_widget(_builder, "linearBtn")), _radial_btn(get_widget(_builder, "radialBtn")), _main_grid(get_widget(_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(_builder, "reverseBtn"); set_icon(reverse, INKSCAPE_ICON("object-flip-horizontal")); reverse.signal_clicked().connect([=](){ reverse_gradient(); }); auto& gradBox = get_widget(_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(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(index)); } }); // connect gradient repeat modes menu std::tuple 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(_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 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 (is(&child)) { auto stop = cast(&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