summaryrefslogtreecommitdiffstats
path: root/src/ui/widget/marker-combo-box.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
commitcca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch)
tree146f39ded1c938019e1ed42d30923c2ac9e86789 /src/ui/widget/marker-combo-box.cpp
parentInitial commit. (diff)
downloadinkscape-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/marker-combo-box.cpp1033
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 :