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/marker-combo-box.cpp | |
parent | Initial commit. (diff) | |
download | inkscape-12fc8abae6d434cac7670a59ed3a67301cc2eb10.tar.xz inkscape-12fc8abae6d434cac7670a59ed3a67301cc2eb10.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/ui/widget/marker-combo-box.cpp')
-rw-r--r-- | src/ui/widget/marker-combo-box.cpp | 1033 |
1 files changed, 1033 insertions, 0 deletions
diff --git a/src/ui/widget/marker-combo-box.cpp b/src/ui/widget/marker-combo-box.cpp new file mode 100644 index 0000000..a8ea110 --- /dev/null +++ b/src/ui/widget/marker-combo-box.cpp @@ -0,0 +1,1033 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Combobox for selecting dash patterns - implementation. + */ +/* Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Maximilian Albert <maximilian.albert@gmail.com> + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "marker-combo-box.h" + +#include <glibmm/fileutils.h> +#include <glibmm/i18n.h> +#include <gtkmm/icontheme.h> +#include <gtkmm/menubutton.h> + +#include "desktop-style.h" +#include "helper/stock-items.h" +#include "io/resource.h" +#include "io/sys.h" +#include "object/sp-defs.h" +#include "object/sp-marker.h" +#include "object/sp-root.h" +#include "path-prefix.h" +#include "style.h" +#include "ui/builder-utils.h" +#include "ui/cache/svg_preview_cache.h" +#include "ui/dialog-events.h" +#include "ui/icon-loader.h" +#include "ui/svg-renderer.h" +#include "ui/util.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/stroke-style.h" + +#define noTIMING_INFO 1; + +using Inkscape::UI::get_widget; +using Inkscape::UI::create_builder; + +// size of marker image in a list +static const int ITEM_WIDTH = 40; +static const int ITEM_HEIGHT = 32; + +namespace Inkscape { +namespace UI { +namespace Widget { + +// separator for FlowBox widget +static cairo_surface_t* create_separator(double alpha, int width, int height, int device_scale) { + width *= device_scale; + height *= device_scale; + cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t* ctx = cairo_create(surface); + cairo_set_source_rgba(ctx, 0.5, 0.5, 0.5, alpha); + cairo_move_to(ctx, 0.5, height / 2 + 0.5); + cairo_line_to(ctx, width + 0.5, height / 2 + 0.5); + cairo_set_line_width(ctx, 1.0 * device_scale); + cairo_stroke(ctx); + cairo_surface_flush(surface); + cairo_surface_set_device_scale(surface, device_scale, device_scale); + return surface; +} + +// empty image; "no marker" +static Cairo::RefPtr<Cairo::Surface> g_image_none; +// error extracting/rendering marker; "bad marker" +static Cairo::RefPtr<Cairo::Surface> g_bad_marker; + +Glib::ustring get_attrib(SPMarker* marker, const char* attrib) { + auto value = marker->getAttribute(attrib); + return value ? value : ""; +} + +double get_attrib_num(SPMarker* marker, const char* attrib) { + auto val = get_attrib(marker, attrib); + return strtod(val.c_str(), nullptr); +} + +MarkerComboBox::MarkerComboBox(Glib::ustring id, int l) : + _combo_id(std::move(id)), + _loc(l), + _builder(create_builder("marker-popup.glade")), + _marker_list(get_widget<Gtk::FlowBox>(_builder, "flowbox")), + _preview(get_widget<Gtk::Image>(_builder, "preview")), + _marker_name(get_widget<Gtk::Label>(_builder, "marker-id")), + _link_scale(get_widget<Gtk::Button>(_builder, "link-scale")), + _scale_x(get_widget<Gtk::SpinButton>(_builder, "scale-x")), + _scale_y(get_widget<Gtk::SpinButton>(_builder, "scale-y")), + _scale_with_stroke(get_widget<Gtk::CheckButton>(_builder, "scale-with-stroke")), + _menu_btn(get_widget<Gtk::MenuButton>(_builder, "menu-btn")), + _angle_btn(get_widget<Gtk::SpinButton>(_builder, "angle")), + _offset_x(get_widget<Gtk::SpinButton>(_builder, "offset-x")), + _offset_y(get_widget<Gtk::SpinButton>(_builder, "offset-y")), + _input_grid(get_widget<Gtk::Grid>(_builder, "input-grid")), + _orient_auto_rev(get_widget<Gtk::RadioButton>(_builder, "orient-auto-rev")), + _orient_auto(get_widget<Gtk::RadioButton>(_builder, "orient-auto")), + _orient_angle(get_widget<Gtk::RadioButton>(_builder, "orient-angle")), + _orient_flip_horz(get_widget<Gtk::Button>(_builder, "btn-horz-flip")), + _current_img(get_widget<Gtk::Image>(_builder, "current-img")), + _edit_marker(get_widget<Gtk::Button>(_builder, "edit-marker")) +{ + _background_color = 0x808080ff; + _foreground_color = 0x808080ff; + + if (!g_image_none) { + auto device_scale = get_scale_factor(); + g_image_none = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(create_separator(1, ITEM_WIDTH, ITEM_HEIGHT, device_scale))); + } + + if (!g_bad_marker) { + auto path = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::UIS, "bad-marker.svg"); + Inkscape::svg_renderer renderer(path.c_str()); + g_bad_marker = renderer.render_surface(1.0); + } + + add(_menu_btn); + + _preview.signal_size_allocate().connect([=](Gtk::Allocation& a){ + // refresh after preview widget has been finally resized/expanded + if (_preview_no_alloc) update_preview(find_marker_item(get_current())); + }); + + _marker_store = Gio::ListStore<MarkerItem>::create(); + _marker_list.bind_list_store(_marker_store, [=](const Glib::RefPtr<MarkerItem>& item){ + auto image = Gtk::make_managed<Gtk::Image>(item->pix); + image->show(); + auto box = Gtk::make_managed<Gtk::FlowBoxChild>(); + box->add(*image); + if (item->separator) { + image->set_sensitive(false); + image->set_can_focus(false); + image->set_size_request(-1, 10); + box->set_sensitive(false); + box->set_can_focus(false); + box->get_style_context()->add_class("marker-separator"); + } + else { + box->get_style_context()->add_class("marker-item-box"); + } + _widgets_to_markers[image] = item; + box->set_size_request(item->width, item->height); + return box; + }); + + _sandbox = ink_markers_preview_doc(_combo_id); + + set_sensitive(true); + + _marker_list.signal_selected_children_changed().connect([=](){ + auto item = get_active(); + if (!item && !_marker_list.get_selected_children().empty()) { + _marker_list.unselect_all(); + } + }); + + _marker_list.signal_child_activated().connect([=](Gtk::FlowBoxChild* box){ + if (box->get_sensitive()) _signal_changed.emit(); + }); + + auto set_orient = [=](bool enable_angle, const char* value) { + if (_update.pending()) return; + _angle_btn.set_sensitive(enable_angle); + sp_marker_set_orient(get_current(), value); + }; + _orient_auto_rev.signal_toggled().connect([=](){ set_orient(false, "auto-start-reverse"); }); + _orient_auto.signal_toggled().connect([=]() { set_orient(false, "auto"); }); + _orient_angle.signal_toggled().connect([=]() { set_orient(true, _angle_btn.get_text().c_str()); }); + _orient_flip_horz.signal_clicked().connect([=]() { sp_marker_flip_horizontally(get_current()); }); + + _angle_btn.signal_value_changed().connect([=]() { + if (_update.pending() || !_angle_btn.is_sensitive()) return; + sp_marker_set_orient(get_current(), _angle_btn.get_text().c_str()); + }); + + auto set_scale = [=](bool changeWidth) { + if (_update.pending()) return; + if (auto marker = get_current()) { + auto sx = _scale_x.get_value(); + auto sy = _scale_y.get_value(); + auto width = get_attrib_num(marker, "markerWidth"); + auto height = get_attrib_num(marker, "markerHeight"); + if (_scale_linked && width > 0.0 && height > 0.0) { + auto scoped(_update.block()); + if (changeWidth) { + // scale height proportionally + sy = height * (sx / width); + _scale_y.set_value(sy); + } + else { + // scale width proportionally + sx = width * (sy / height); + _scale_x.set_value(sx); + } + } + sp_marker_set_size(marker, sx, sy); + } + }; + + _link_scale.signal_clicked().connect([=](){ + if (_update.pending()) return; + _scale_linked = !_scale_linked; + sp_marker_set_uniform_scale(get_current(), _scale_linked); + update_scale_link(); + }); + + _scale_x.signal_value_changed().connect([=]() { set_scale(true); }); + _scale_y.signal_value_changed().connect([=]() { set_scale(false); }); + + _scale_with_stroke.signal_toggled().connect([=](){ + if (_update.pending()) return; + sp_marker_scale_with_stroke(get_current(), _scale_with_stroke.get_active()); + }); + + auto set_offset = [=](){ + if (_update.pending()) return; + sp_marker_set_offset(get_current(), _offset_x.get_value(), _offset_y.get_value()); + }; + _offset_x.signal_value_changed().connect([=]() { set_offset(); }); + _offset_y.signal_value_changed().connect([=]() { set_offset(); }); + + // request to edit marker on canvas; close popup to get it out of the way and call marker edit tool + _edit_marker.signal_clicked().connect([=]() { _menu_btn.get_popover()->popdown(); edit_signal(); }); + + // before showing popover refresh marker attributes + _menu_btn.get_popover()->signal_show().connect([=](){ update_ui(get_current(), false); }, false); + + update_scale_link(); + _current_img.set(g_image_none); + show(); +} + +MarkerComboBox::~MarkerComboBox() { + if (_document) { + modified_connection.disconnect(); + } +} + +void MarkerComboBox::update_widgets_from_marker(SPMarker* marker) { + _input_grid.set_sensitive(marker != nullptr); + + if (marker) { + marker->updateRepr(); + + _scale_x.set_value(get_attrib_num(marker, "markerWidth")); + _scale_y.set_value(get_attrib_num(marker, "markerHeight")); + auto units = get_attrib(marker, "markerUnits"); + _scale_with_stroke.set_active(units == "strokeWidth" || units == ""); + auto aspect = get_attrib(marker, "preserveAspectRatio"); + _scale_linked = aspect != "none"; + update_scale_link(); + // marker->setAttribute("markerUnits", scale_with_stroke ? "strokeWidth" : "userSpaceOnUse"); + _offset_x.set_value(get_attrib_num(marker, "refX")); + _offset_y.set_value(get_attrib_num(marker, "refY")); + auto orient = get_attrib(marker, "orient"); + + // try parsing as number + _angle_btn.set_value(strtod(orient.c_str(), nullptr)); + if (orient == "auto-start-reverse") { + _orient_auto_rev.set_active(); + _angle_btn.set_sensitive(false); + } + else if (orient == "auto") { + _orient_auto.set_active(); + _angle_btn.set_sensitive(false); + } + else { + _orient_angle.set_active(); + _angle_btn.set_sensitive(true); + } + } +} + +void MarkerComboBox::update_scale_link() { + _link_scale.remove(); + _link_scale.add(get_widget<Gtk::Image>(_builder, _scale_linked ? "image-linked" : "image-unlinked")); +} + +// update marker image inside the menu button +void MarkerComboBox::update_menu_btn(Glib::RefPtr<MarkerItem> marker) { + _current_img.set(marker ? marker->pix : g_image_none); +} + +// update marker preview image in the popover panel +void MarkerComboBox::update_preview(Glib::RefPtr<MarkerItem> item) { + Cairo::RefPtr<Cairo::Surface> surface; + Glib::ustring label; + + if (!item) { + // TRANSLATORS: None - no marker selected for a path + label = _("None"); + } + + if (item && item->source && !item->id.empty()) { + Inkscape::Drawing drawing; + unsigned const visionkey = SPItem::display_key_new(1); + drawing.setRoot(_sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY)); + // generate preview + auto alloc = _preview.get_allocation(); + auto size = Geom::IntPoint(alloc.get_width() - 10, alloc.get_height() - 10); + if (size.x() > 0 && size.y() > 0) { + surface = create_marker_image(size, item->id.c_str(), item->source, drawing, visionkey, true, true, 2.60); + } + else { + // too early, preview hasn't been expanded/resized yet + _preview_no_alloc = true; + } + _sandbox->getRoot()->invoke_hide(visionkey); + label = item->label; + } + + _preview.set(surface); + std::ostringstream ost; + ost << "<small>" << label.raw() << "</small>"; + _marker_name.set_markup(ost.str().c_str()); +} + +bool MarkerComboBox::MarkerItem::operator == (const MarkerItem& item) const { + return + id == item.id && + label == item.label && + separator == item.separator && + stock == item.stock && + history == item.history && + source == item.source && + width == item.width && + height == item.height; +} + +// find marker object by ID in a document +SPMarker* find_marker(SPDocument* document, const Glib::ustring& marker_id) { + if (!document) return nullptr; + + SPDefs* defs = document->getDefs(); + if (!defs) return nullptr; + + for (auto& child : defs->children) { + if (SP_IS_MARKER(&child)) { + auto marker = SP_MARKER(&child); + auto id = marker->getId(); + if (id && marker_id == id) { + // found it + return marker; + } + } + } + + // not found + return nullptr; +} + +SPMarker* MarkerComboBox::get_current() const { + // find current marker + return find_marker(_document, _current_marker_id); +} + +void MarkerComboBox::set_active(Glib::RefPtr<MarkerItem> item) { + bool selected = false; + if (item) { + _marker_list.foreach([=,&selected](Gtk::Widget& widget){ + if (auto box = dynamic_cast<Gtk::FlowBoxChild*>(&widget)) { + if (auto marker = _widgets_to_markers[box->get_child()]) { + if (*marker.get() == *item.get()) { + _marker_list.select_child(*box); + selected = true; + } + } + } + }); + } + + if (!selected) { + _marker_list.unselect_all(); + } +} + +Glib::RefPtr<MarkerComboBox::MarkerItem> MarkerComboBox::find_marker_item(SPMarker* marker) { + std::string id; + if (marker != nullptr) { + if (auto markname = marker->getRepr()->attribute("id")) { + id = markname; + } + } + + Glib::RefPtr<MarkerItem> marker_item; + if (!id.empty()) { + for (auto&& item : _history_items) { + if (item->id == id) { + marker_item = item; + break; + } + } + } + + return marker_item; +} + +Glib::RefPtr<MarkerComboBox::MarkerItem> MarkerComboBox::get_active() { + auto empty = Glib::RefPtr<MarkerItem>(); + auto sel = _marker_list.get_selected_children(); + if (sel.size() == 1) { + auto item = _widgets_to_markers[sel.front()->get_child()]; + if (item && item->separator) { + return empty; + } + return item; + } + else { + return empty; + } +} + +void MarkerComboBox::setDocument(SPDocument *document) +{ + if (_document != document) { + + if (_document) { + modified_connection.disconnect(); + } + + _document = document; + + if (_document) { + modified_connection = _document->getDefs()->connectModified([=](SPObject*, unsigned int){ + refresh_after_markers_modified(); + }); + } + + _current_marker_id = ""; + + refresh_after_markers_modified(); + } +} + +/** + * This function is invoked after document "defs" section changes. + * It will change when current marker's attributes are modified in this popup + * and this function will refresh the recent list and a preview to reflect the changes. + * It would be more efficient if there was a way to determine what has changed + * and perform only more targeted update. + */ +void MarkerComboBox::refresh_after_markers_modified() { + if (_update.pending()) return; + + auto scoped(_update.block()); + + // collect orphaned markers, so they are not listed; if they are listed then + // they disappear upon selection leaving dangling URLs + if (_document) _document->collectOrphans(); + + /* + * Seems to be no way to get notified of changes just to markers, + * so listen to changes in all defs and check if the number of markers has changed here + * to avoid unnecessary refreshes when things like gradients change + */ + // TODO: detect changes to markers; ignore changes to everything else; + // simple count check doesn't cut it, so just do it unconditionally for now + marker_list_from_doc(_document, true); + + auto marker = find_marker_item(get_current()); + update_menu_btn(marker); + update_preview(marker); +} + +Glib::RefPtr<MarkerComboBox::MarkerItem> MarkerComboBox::add_separator(bool filler) { + auto item = Glib::RefPtr<MarkerItem>(new MarkerItem); + item->history = false; + item->separator = true; + item->id = "None"; + item->label = filler ? "filler" : "Separator"; + item->stock = false; + if (!filler) { + auto device_scale = get_scale_factor(); + static Cairo::RefPtr<Cairo::Surface> separator(new Cairo::Surface(create_separator(0.7, ITEM_WIDTH, 10, device_scale))); + item->pix = separator; + } + item->height = 10; + item->width = -1; + return item; +} + +/** + * Init the combobox widget to display markers from markers.svg + */ +void +MarkerComboBox::init_combo() +{ + if (_update.pending()) return; + + static SPDocument *markers_doc = nullptr; + + // find and load markers.svg + if (markers_doc == nullptr) { + using namespace Inkscape::IO::Resource; + auto markers_source = get_path_string(SYSTEM, MARKERS, "markers.svg"); + if (Glib::file_test(markers_source, Glib::FILE_TEST_IS_REGULAR)) { + markers_doc = SPDocument::createNewDoc(markers_source.c_str(), false); + } + } + + // load markers from markers.svg + if (markers_doc) { + marker_list_from_doc(markers_doc, false); + } + + refresh_after_markers_modified(); +} + +/** + * Sets the current marker in the marker combobox. + */ +void MarkerComboBox::set_current(SPObject *marker) +{ + auto sp_marker = dynamic_cast<SPMarker*>(marker); + + bool reselect = sp_marker != get_current(); + + update_ui(sp_marker, reselect); +} + +void MarkerComboBox::update_ui(SPMarker* marker, bool select) { + auto scoped(_update.block()); + + auto id = marker ? marker->getId() : nullptr; + _current_marker_id = id ? id : ""; + + auto marker_item = find_marker_item(marker); + + if (select) { + set_active(marker_item); + } + + update_widgets_from_marker(marker); + update_menu_btn(marker_item); + update_preview(marker_item); +} + +/** + * Return a uri string representing the current selected marker used for setting the marker style in the document + */ +std::string MarkerComboBox::get_active_marker_uri() +{ + /* Get Marker */ + auto item = get_active(); + if (!item) { + return std::string(); + } + + std::string marker; + + if (item->id != "none") { + bool stockid = item->stock; + + std::string markurn = stockid ? "urn:inkscape:marker:" + item->id : item->id; + auto mark = dynamic_cast<SPMarker*>(get_stock_item(markurn.c_str(), stockid)); + + if (mark) { + Inkscape::XML::Node* repr = mark->getRepr(); + auto id = repr->attribute("id"); + if (id) { + std::ostringstream ost; + ost << "url(#" << id << ")"; + marker = ost.str(); + } + if (stockid) { + mark->getRepr()->setAttribute("inkscape:collect", "always"); + } + // adjust marker's attributes (or add missing ones) to stay in sync with marker tool + sp_validate_marker(mark, _document); + } + } else { + marker = item->id; + } + + return marker; +} + +/** + * Pick up all markers from source and add items to the list/store. + * If 'history' is true, then update recently used in-document portion of the list; + * otherwise update list of stock markers, which is displayed after recent ones + */ +void MarkerComboBox::marker_list_from_doc(SPDocument* source, bool history) { + std::vector<SPMarker*> markers = get_marker_list(source); + remove_markers(history); + add_markers(markers, source, history); + update_store(); +} + +void MarkerComboBox::update_store() { + _marker_store->freeze_notify(); + + auto selected = get_active(); + + _marker_store->remove_all(); + _widgets_to_markers.clear(); + + // recent and user-defined markers come first + for (auto&& item : _history_items) { + _marker_store->append(item); + } + + // separator + if (!_history_items.empty()) { + // add empty boxes to fill up the row to 'max' elements and then + // extra ones to create entire new empty row (a separator of sorts) + auto max = _marker_list.get_max_children_per_line(); + auto fillup = max - _history_items.size() % max; + + for (int i = 0; i < fillup; ++i) { + _marker_store->append(add_separator(true)); + } + for (int i = 0; i < max; ++i) { + _marker_store->append(add_separator(false)); + } + } + + // stock markers + for (auto&& item : _stock_items) { + _marker_store->append(item); + } + + _marker_store->thaw_notify(); + + // reselect current + set_active(selected); +} +/** + * Returns a vector of markers in the defs of the given source document as a vector. + * Returns empty vector if there are no markers in the document. + * If validate is true then it runs each marker through the validation routine that alters some attributes. + */ +std::vector<SPMarker*> MarkerComboBox::get_marker_list(SPDocument* source) +{ + std::vector<SPMarker *> ml; + if (source == nullptr) return ml; + + SPDefs *defs = source->getDefs(); + if (!defs) { + return ml; + } + + for (auto& child: defs->children) { + if (SP_IS_MARKER(&child)) { + auto marker = SP_MARKER(&child); + ml.push_back(marker); + } + } + return ml; +} + +/** + * Remove history or non-history markers from the combo + */ +void MarkerComboBox::remove_markers (gboolean history) +{ + if (history) { + _history_items.clear(); + } + else { + _stock_items.clear(); + } +} + +/** + * Adds markers in marker_list to the combo + */ +void MarkerComboBox::add_markers (std::vector<SPMarker *> const& marker_list, SPDocument *source, gboolean history) +{ + // Do this here, outside of loop, to speed up preview generation: + Inkscape::Drawing drawing; + unsigned const visionkey = SPItem::display_key_new(1); + drawing.setRoot(_sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY)); + + if (history) { + // add "None" + auto item = Glib::RefPtr<MarkerItem>(new MarkerItem); + item->pix = g_image_none; + item->history = true; + item->separator = false; + item->id = "None"; + item->label = "None"; + item->stock = false; + item->width = ITEM_WIDTH; + item->height = ITEM_HEIGHT; + _history_items.push_back(item); + } + +#if TIMING_INFO +auto old_time = std::chrono::high_resolution_clock::now(); +#endif + + for (auto i:marker_list) { + + Inkscape::XML::Node *repr = i->getRepr(); + gchar const *markid = repr->attribute("inkscape:stockid") ? repr->attribute("inkscape:stockid") : repr->attribute("id"); + + // generate preview + auto pixbuf = create_marker_image(Geom::IntPoint(ITEM_WIDTH, ITEM_HEIGHT), repr->attribute("id"), source, drawing, visionkey, false, true, 1.50); + + auto item = Glib::RefPtr<MarkerItem>(new MarkerItem); + item->source = source; + item->pix = pixbuf; + if (auto id = repr->attribute("id")) { + item->id = id; + } + item->label = markid ? markid : ""; + item->stock = !history; + item->history = history; + item->width = ITEM_WIDTH; + item->height = ITEM_HEIGHT; + + if (history) { + _history_items.push_back(item); + } + else { + _stock_items.push_back(item); + } + } + + _sandbox->getRoot()->invoke_hide(visionkey); + +#if TIMING_INFO +auto current_time = std::chrono::high_resolution_clock::now(); +auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - old_time); +g_warning("%s render time for %d markers: %d ms", combo_id, (int)marker_list.size(), static_cast<int>(elapsed.count())); +#endif +} + +/** + * Creates a copy of the marker named mname, determines its visible and renderable + * area in the bounding box, and then renders it. This allows us to fill in + * preview images of each marker in the marker combobox. + */ +Cairo::RefPtr<Cairo::Surface> +MarkerComboBox::create_marker_image(Geom::IntPoint pixel_size, gchar const *mname, + SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/, bool checkerboard, bool no_clip, double scale) +{ + // Retrieve the marker named 'mname' from the source SVG document + SPObject const *marker = source->getObjectById(mname); + if (marker == nullptr) { + g_warning("bad mname: %s", mname); + return g_bad_marker; + } + + SPObject *oldmarker = _sandbox->getObjectById("sample"); + if (oldmarker) { + oldmarker->deleteObject(false); + } + + // Create a copy repr of the marker with id="sample" + Inkscape::XML::Document *xml_doc = _sandbox->getReprDoc(); + Inkscape::XML::Node *mrepr = marker->getRepr()->duplicate(xml_doc); + mrepr->setAttribute("id", "sample"); + + // Replace the old sample in the sandbox by the new one + Inkscape::XML::Node *defsrepr = _sandbox->getObjectById("defs")->getRepr(); + + // TODO - This causes a SIGTRAP on windows + defsrepr->appendChild(mrepr); + + Inkscape::GC::release(mrepr); + + // If the marker color is a url link to a pattern or gradient copy that too + SPObject *mk = source->getObjectById(mname); + SPCSSAttr *css_marker = sp_css_attr_from_object(mk->firstChild(), SP_STYLE_FLAG_ALWAYS); + //const char *mfill = sp_repr_css_property(css_marker, "fill", "none"); + const char *mstroke = sp_repr_css_property(css_marker, "fill", "none"); + + if (!strncmp (mstroke, "url(", 4)) { + SPObject *linkObj = getMarkerObj(mstroke, source); + if (linkObj) { + Inkscape::XML::Node *grepr = linkObj->getRepr()->duplicate(xml_doc); + SPObject *oldmarker = _sandbox->getObjectById(linkObj->getId()); + if (oldmarker) { + oldmarker->deleteObject(false); + } + defsrepr->appendChild(grepr); + Inkscape::GC::release(grepr); + + if (SP_IS_GRADIENT(linkObj)) { + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (SP_GRADIENT(linkObj), false); + if (vector) { + Inkscape::XML::Node *grepr = vector->getRepr()->duplicate(xml_doc); + SPObject *oldmarker = _sandbox->getObjectById(vector->getId()); + if (oldmarker) { + oldmarker->deleteObject(false); + } + defsrepr->appendChild(grepr); + Inkscape::GC::release(grepr); + } + } + } + } + +// Uncomment this to get the sandbox documents saved (useful for debugging) + // FILE *fp = fopen (g_strconcat(combo_id, mname, ".svg", nullptr), "w"); + // sp_repr_save_stream(_sandbox->getReprDoc(), fp); + // fclose (fp); + + // object to render; note that the id is the same as that of the combo we're building + SPObject *object = _sandbox->getObjectById(_combo_id); + + if (object == nullptr || !SP_IS_ITEM(object)) { + g_warning("no obj: %s", _combo_id.c_str()); + return g_bad_marker; + } + + auto context = get_style_context(); + Gdk::RGBA fg = context->get_color(get_state_flags()); + auto fgcolor = rgba_to_css_color(fg); + fg.set_red(1 - fg.get_red()); + fg.set_green(1 - fg.get_green()); + fg.set_blue(1 - fg.get_blue()); + auto bgcolor = rgba_to_css_color(fg); + auto objects = _sandbox->getObjectsBySelector(".colors"); + for (auto el : objects) { + if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) { + sp_repr_css_set_property(css, "fill", bgcolor.c_str()); + sp_repr_css_set_property(css, "stroke", fgcolor.c_str()); + el->changeCSS(css, "style"); + sp_repr_css_attr_unref(css); + } + } + + auto cross = _sandbox->getObjectsBySelector(".cross"); + double stroke = 0.5; + for (auto el : cross) { + if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) { + sp_repr_css_set_property(css, "display", checkerboard ? "block" : "none"); + sp_repr_css_set_property_double(css, "stroke-width", stroke); + el->changeCSS(css, "style"); + sp_repr_css_attr_unref(css); + } + } + + SPDocument::install_reference_document scoped(_sandbox.get(), marker->document); + + _sandbox->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + _sandbox->ensureUpToDate(); + + SPItem *item = SP_ITEM(object); + // Find object's bbox in document + Geom::OptRect dbox = item->documentVisualBounds(); + + if (!dbox) { + g_warning("no dbox"); + return g_bad_marker; + } + + if (auto measure = dynamic_cast<SPItem*>(_sandbox->getObjectById("measure-marker"))) { + if (auto box = measure->documentVisualBounds()) { + // check size of the marker applied to a path with stroke of 1px + auto size = std::max(box->width(), box->height()); + const double small = 5.0; + // if too small, then scale up; clip needs to be enabled for scale to work + if (size > 0 && size < small) { + auto factor = 1 + small - size; + scale *= factor; + no_clip = false; + + // adjust cross stroke + stroke /= factor; + for (auto el : cross) { + if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) { + sp_repr_css_set_property_double(css, "stroke-width", stroke); + el->changeCSS(css, "style"); + sp_repr_css_attr_unref(css); + } + } + + _sandbox->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + _sandbox->ensureUpToDate(); + } + } + } + + /* Update to renderable state */ + const double device_scale = get_scale_factor(); + auto surface = render_surface(drawing, scale, *dbox, pixel_size, device_scale, checkerboard ? &_background_color : nullptr, no_clip); + cairo_surface_set_device_scale(surface, device_scale, device_scale); + return Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(surface, false)); +} + +// capture background color when styles change +void MarkerComboBox::on_style_updated() { + auto background = _background_color; + if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_toplevel())) { + auto sc = wnd->get_style_context(); + auto color = get_background_color(sc); + background = + gint32(0xff * color.get_red()) << 24 | + gint32(0xff * color.get_green()) << 16 | + gint32(0xff * color.get_blue()) << 8 | + 0xff; + } + + auto context = get_style_context(); + Gdk::RGBA color = context->get_color(get_state_flags()); + auto foreground = + gint32(0xff * color.get_red()) << 24 | + gint32(0xff * color.get_green()) << 16 | + gint32(0xff * color.get_blue()) << 8 | + 0xff; + if (foreground != _foreground_color || background != _background_color) { + _foreground_color = foreground; + _background_color = background; + // theme changed? + init_combo(); + } +} + +/*TODO: background for custom markers + <filter id="softGlow" height="1.2" width="1.2" x="0.0" y="0.0"> + <!-- <feMorphology operator="dilate" radius="1" in="SourceAlpha" result="thicken" id="feMorphology2" /> --> + <!-- Use a gaussian blur to create the soft blurriness of the glow --> + <feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blurred" id="feGaussianBlur4" /> + <!-- Change the color --> + <feFlood flood-color="rgb(255,255,255)" result="glowColor" id="feFlood6" flood-opacity="0.70" /> + <!-- Color in the glows --> + <feComposite in="glowColor" in2="blurred" operator="in" result="softGlow_colored" id="feComposite8" /> + <!-- Layer the effects together --> + <feMerge id="feMerge14"> + <feMergeNode in="softGlow_colored" id="feMergeNode10" /> + <feMergeNode in="SourceGraphic" id="feMergeNode12" /> + </feMerge> + </filter> +*/ + +/** + * Returns a new document containing default start, mid, and end markers. + * Note 1: group IDs are matched against "_combo_id" to render correct preview object. + * Note 2: paths/lines are kept outside of groups, so they don't inflate visible bounds + * Note 3: invisible rects inside groups keep visual bounds from getting too small, so we can see relative marker sizes + */ +std::unique_ptr<SPDocument> MarkerComboBox::ink_markers_preview_doc(const Glib::ustring& group_id) +{ +gchar const *buffer = R"A( + <svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + id="MarkerSample"> + + <defs id="defs"> + <filter id="softGlow" height="1.2" width="1.2" x="0.0" y="0.0"> + <!-- <feMorphology operator="dilate" radius="1" in="SourceAlpha" result="thicken" id="feMorphology2" /> --> + <!-- Use a gaussian blur to create the soft blurriness of the glow --> + <feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blurred" id="feGaussianBlur4" /> + <!-- Change the color --> + <feFlood flood-color="rgb(255,255,255)" result="glowColor" id="feFlood6" flood-opacity="0.70" /> + <!-- Color in the glows --> + <feComposite in="glowColor" in2="blurred" operator="in" result="softGlow_colored" id="feComposite8" /> + <!-- Layer the effects together --> + <feMerge id="feMerge14"> + <feMergeNode in="softGlow_colored" id="feMergeNode10" /> + <feMergeNode in="SourceGraphic" id="feMergeNode12" /> + </feMerge> + </filter> + </defs> + + <!-- cross at the end of the line to help position marker --> + <symbol id="cross" width="25" height="25" viewBox="0 0 25 25"> + <path class="cross" style="mix-blend-mode:difference;stroke:#7ff;stroke-opacity:1;fill:none;display:block" d="M 0,0 M 25,25 M 10,10 15,15 M 10,15 15,10" /> + <!-- <path class="cross" style="mix-blend-mode:difference;stroke:#7ff;stroke-width:1;stroke-opacity:1;fill:none;display:block;-inkscape-stroke:hairline" d="M 0,0 M 25,25 M 10,10 15,15 M 10,15 15,10" /> --> + </symbol> + + <!-- very short path with 1px stroke used to measure size of marker --> + <path id="measure-marker" style="stroke-width:1.0;stroke-opacity:0.01;marker-start:url(#sample)" d="M 0,9999 m 0,0.1" /> + + <path id="line-marker-start" class="line colors" style="stroke-width:2;stroke-opacity:0.2" d="M 12.5,12.5 l 1000,0" /> + <!-- <g id="marker-start" class="group" style="filter:url(#softGlow)"> --> + <g id="marker-start" class="group"> + <path class="colors" style="stroke-width:2;stroke-opacity:0;marker-start:url(#sample)" + d="M 12.5,12.5 L 25,12.5"/> + <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/> + <use xlink:href="#cross" width="25" height="25" /> + </g> + + <path id="line-marker-mid" class="line colors" style="stroke-width:2;stroke-opacity:0.2" d="M -1000,12.5 L 1000,12.5" /> + <g id="marker-mid" class="group"> + <path class="colors" style="stroke-width:2;stroke-opacity:0;marker-mid:url(#sample)" + d="M 0,12.5 L 12.5,12.5 L 25,12.5"/> + <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/> + <use xlink:href="#cross" width="25" height="25" /> + </g> + + <path id="line-marker-end" class="line colors" style="stroke-width:2;stroke-opacity:0.2" d="M -1000,12.5 L 12.5,12.5" /> + <g id="marker-end" class="group"> + <path class="colors" style="stroke-width:2;stroke-opacity:0;marker-end:url(#sample)" + d="M 0,12.5 L 12.5,12.5"/> + <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/> + <use xlink:href="#cross" width="25" height="25" /> + </g> + + </svg> +)A"; + + auto document = std::unique_ptr<SPDocument>(SPDocument::createNewDocFromMem(buffer, strlen(buffer), false)); + // only leave requested group, so nothing else gets rendered + for (auto&& group : document->getObjectsByClass("group")) { + assert(group->getId()); + if (group->getId() != group_id) { + group->deleteObject(); + } + } + auto id = "line-" + group_id; + for (auto&& line : document->getObjectsByClass("line")) { + assert(line->getId()); + if (line->getId() != id) { + line->deleteObject(); + } + } + return document; +} + +} // namespace Widget +} // namespace UI +} // 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 : |