diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:29:01 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:29:01 +0000 |
commit | 35a96bde514a8897f6f0fcc41c5833bf63df2e2a (patch) | |
tree | 657d15a03cc46bd099fc2c6546a7a4ad43815d9f /src/widgets | |
parent | Initial commit. (diff) | |
download | inkscape-upstream.tar.xz inkscape-upstream.zip |
Adding upstream version 1.0.2.upstream/1.0.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
40 files changed, 15008 insertions, 0 deletions
diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt new file mode 100644 index 0000000..3ed2a37 --- /dev/null +++ b/src/widgets/CMakeLists.txt @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(widgets_SRC + desktop-widget.cpp + ege-paint-def.cpp + fill-style.cpp + gradient-image.cpp + gradient-selector.cpp + gradient-vector.cpp + ink-action.cpp + paint-selector.cpp + sp-attribute-widget.cpp + sp-color-selector.cpp + sp-xmlview-tree.cpp + spinbutton-events.cpp + spw-utilities.cpp + stroke-marker-selector.cpp + stroke-style.cpp + swatch-selector.cpp + toolbox.cpp + + # ------- + # Headers + desktop-widget.h + ege-paint-def.h + fill-n-stroke-factory.h + fill-style.h + gradient-image.h + gradient-selector.h + gradient-vector.h + ink-action.h + paint-selector.h + sp-attribute-widget.h + sp-color-selector.h + sp-xmlview-tree.h + spinbutton-events.h + spw-utilities.h + stroke-marker-selector.h + stroke-style.h + swatch-selector.h + toolbox.h + widget-sizes.h +) + +add_inkscape_source("${widgets_SRC}") diff --git a/src/widgets/README b/src/widgets/README new file mode 100644 index 0000000..b1c905a --- /dev/null +++ b/src/widgets/README @@ -0,0 +1,10 @@ + + +This directory contains widgets written in 'C'. + +To do: + +* Replace 'C' widgets by 'C++' widgets. +* Temporary move to 'ui/widgets/legacy'. + + diff --git a/src/widgets/desktop-widget.cpp b/src/widgets/desktop-widget.cpp new file mode 100644 index 0000000..491ce41 --- /dev/null +++ b/src/widgets/desktop-widget.cpp @@ -0,0 +1,2570 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Desktop widget implementation + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * MenTaLguY <mental@rydia.net> + * bulia byak <buliabyak@users.sf.net> + * Ralf Stephan <ralf@ark.in-berlin.de> + * John Bintz <jcoswell@coswellproductions.org> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2006 John Bintz + * Copyright (C) 2004 MenTaLguY + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <2geom/rect.h> + +#include "attributes.h" +#include "cms-system.h" +#include "conn-avoid-ref.h" +#include "desktop-events.h" +#include "desktop-widget.h" +#include "desktop.h" +#include "document-undo.h" +#include "ege-color-prof-tracker.h" +#include "file.h" +#include "inkscape-version.h" +#include "verbs.h" + +#include "display/canvas-arena.h" +#include "display/canvas-axonomgrid.h" +#include "display/guideline.h" +#include "display/sp-canvas.h" + +#include "extension/db.h" + +#include "helper/action.h" + +#include "object/sp-image.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" + +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/swatches.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/tools/box3d-tool.h" +#include "ui/uxmanager.h" +#include "ui/widget/button.h" +#include "ui/widget/dock.h" +#include "ui/widget/ink-ruler.h" +#include "ui/widget/layer-selector.h" +#include "ui/widget/selected-style.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +// TEMP +#include "ui/desktop/menubar.h" + +#include "util/ege-appear-time-tracker.h" +#include "util/units.h" + +// We're in the "widgets" directory, so no need to explicitly prefix these: +#include "spinbutton-events.h" +#include "spw-utilities.h" +#include "toolbox.h" +#include "widget-sizes.h" + +#ifdef GDK_WINDOWING_QUARTZ +#include <gtkosxapplication.h> +#endif + +using Inkscape::DocumentUndo; +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::UI::UXManager; +using Inkscape::UI::ToolboxFactory; +using ege::AppearTimeTracker; +using Inkscape::Util::unit_table; + + +//--------------------------------------------------------------------- +/* SPDesktopWidget */ + +static void sp_desktop_widget_class_init (SPDesktopWidgetClass *klass); + +static void sp_desktop_widget_size_allocate (GtkWidget *widget, GtkAllocation *allocation); +static void sp_desktop_widget_realize (GtkWidget *widget); + +static void sp_desktop_widget_adjustment_value_changed (GtkAdjustment *adj, SPDesktopWidget *dtw); + +static gdouble sp_dtw_zoom_value_to_display (gdouble value); +static gdouble sp_dtw_zoom_display_to_value (gdouble value); +static void sp_dtw_zoom_menu_handler (SPDesktop *dt, gdouble factor); + +SPViewWidgetClass *dtw_parent_class; + +class CMSPrefWatcher { +public: + CMSPrefWatcher() : + _dpw(*this), + _spw(*this), + _tracker(ege_color_prof_tracker_new(nullptr)) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + g_signal_connect( G_OBJECT(_tracker), "modified", G_CALLBACK(hook), this ); + prefs->addObserver(_dpw); + prefs->addObserver(_spw); + } + virtual ~CMSPrefWatcher() = default; + + //virtual void notify(PrefValue &); + void add( SPDesktopWidget* dtw ) { + _widget_list.push_back(dtw); + } + void remove( SPDesktopWidget* dtw ) { + _widget_list.remove(dtw); + } + +private: + static void hook(EgeColorProfTracker *tracker, gint b, CMSPrefWatcher *watcher); + + class DisplayProfileWatcher : public Inkscape::Preferences::Observer { + public: + DisplayProfileWatcher(CMSPrefWatcher &pw) : Observer("/options/displayprofile"), _pw(pw) {} + void notify(Inkscape::Preferences::Entry const &/*val*/) override { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _pw._setCmsSensitive(!prefs->getString("/options/displayprofile/uri").empty()); + _pw._refreshAll(); + } + private: + CMSPrefWatcher &_pw; + }; + + DisplayProfileWatcher _dpw; + + class SoftProofWatcher : public Inkscape::Preferences::Observer { + public: + SoftProofWatcher(CMSPrefWatcher &pw) : Observer("/options/softproof"), _pw(pw) {} + void notify(Inkscape::Preferences::Entry const &) override { + _pw._refreshAll(); + } + private: + CMSPrefWatcher &_pw; + }; + + SoftProofWatcher _spw; + + void _refreshAll(); + void _setCmsSensitive(bool value); + + std::list<SPDesktopWidget*> _widget_list; + EgeColorProfTracker *_tracker; + + friend class DisplayProfileWatcher; + friend class SoftproofWatcher; +}; + +#if defined(HAVE_LIBLCMS2) +void CMSPrefWatcher::hook(EgeColorProfTracker * /*tracker*/, gint monitor, CMSPrefWatcher * /*watcher*/) +{ + unsigned char* buf = nullptr; + guint len = 0; + + ege_color_prof_tracker_get_profile_for( monitor, reinterpret_cast<gpointer*>(&buf), &len ); + Glib::ustring id = Inkscape::CMSSystem::setDisplayPer( buf, len, monitor ); +} +#else +void CMSPrefWatcher::hook(EgeColorProfTracker * /*tracker*/, gint /*monitor*/, CMSPrefWatcher * /*watcher*/) +{ +} +#endif // defined(HAVE_LIBLCMS2) + +/// @todo Use conditional compilation in saner places. The whole PrefWatcher +/// object is unnecessary if defined(HAVE_LIBLCMS2) is not defined. +void CMSPrefWatcher::_refreshAll() +{ +#if defined(HAVE_LIBLCMS2) + for (auto & it : _widget_list) { + it->requestCanvasUpdate(); + } +#endif // defined(HAVE_LIBLCMS2) +} + +void CMSPrefWatcher::_setCmsSensitive(bool enabled) +{ +#if defined(HAVE_LIBLCMS2) + for ( auto dtw : _widget_list ) { + auto cms_adj = dtw->get_cms_adjust(); + if ( cms_adj->get_sensitive() != enabled ) { + dtw->cms_adjust_set_sensitive(enabled); + } + } +#else + (void) enabled; +#endif // defined(HAVE_LIBLCMS2) +} + +static CMSPrefWatcher* watcher = nullptr; + +void +SPDesktopWidget::setMessage (Inkscape::MessageType type, const gchar *message) +{ + _select_status->set_markup(message ? message : ""); + + // make sure the important messages are displayed immediately! + if (type == Inkscape::IMMEDIATE_MESSAGE && _select_status->get_is_drawable()) { + _select_status->queue_draw(); + } + + _select_status->set_tooltip_text(_select_status->get_text()); +} + +Geom::Point +SPDesktopWidget::window_get_pointer() +{ + int x, y; + auto window = Glib::wrap(GTK_WIDGET(_canvas))->get_window(); + auto display = window->get_display(); + auto seat = display->get_default_seat(); + auto device = seat->get_pointer(); + Gdk::ModifierType m; + window->get_device_position(device, x, y, m); + + return Geom::Point(x, y); +} + +static GTimer *overallTimer = nullptr; + +/** + * Registers SPDesktopWidget class and returns its type number. + */ +GType SPDesktopWidget::getType() +{ + static GType type = 0; + if (!type) { + GTypeInfo info = { + sizeof(SPDesktopWidgetClass), + nullptr, // base_init + nullptr, // base_finalize + (GClassInitFunc)sp_desktop_widget_class_init, + nullptr, // class_finalize + nullptr, // class_data + sizeof(SPDesktopWidget), + 0, // n_preallocs + (GInstanceInitFunc)SPDesktopWidget::init, + nullptr // value_table + }; + type = g_type_register_static(SP_TYPE_VIEW_WIDGET, "SPDesktopWidget", &info, static_cast<GTypeFlags>(0)); + // Begin a timer to watch for the first desktop to appear on-screen + overallTimer = g_timer_new(); + } + return type; +} + +/** + * SPDesktopWidget vtable initialization + */ +static void +sp_desktop_widget_class_init (SPDesktopWidgetClass *klass) +{ + dtw_parent_class = SP_VIEW_WIDGET_CLASS(g_type_class_peek_parent(klass)); + + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + + object_class->dispose = SPDesktopWidget::dispose; + + widget_class->size_allocate = sp_desktop_widget_size_allocate; + widget_class->realize = sp_desktop_widget_realize; +} + +/** + * Callback for changes in size of the canvas table (i.e. the container for + * the canvas, the rulers etc). + * + * This adjusts the range of the rulers when the dock container is adjusted + * (fixes lp:950552) + * + * This fix was causing the rulers to be completely redrawn when not needed. + * Added check to see if allocation really changed. + * + *(Question, why is the callback being called when allocation not changed?) + */ +void +SPDesktopWidget::canvas_tbl_size_allocate(Gtk::Allocation& allocation) +{ + if (_allocation == allocation) { + return; + } + + _allocation = allocation; + update_rulers(); +} + +/** + * Callback for SPDesktopWidget object initialization. + */ +void SPDesktopWidget::init( SPDesktopWidget *dtw ) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + new (&dtw->modified_connection) sigc::connection(); + + dtw->_ruler_clicked = false; + dtw->_ruler_dragged = false; + dtw->_active_guide = nullptr; + dtw->_xp = 0; + dtw->_yp = 0; + dtw->window = nullptr; + dtw->desktop = nullptr; + dtw->_interaction_disabled_counter = 0; + + /* Main table */ + dtw->_vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + dtw->_vbox->set_name("DesktopMainTable"); + gtk_container_add( GTK_CONTAINER(dtw), GTK_WIDGET(dtw->_vbox->gobj()) ); + + /* Status bar */ + dtw->_statusbar = Gtk::manage(new Gtk::Box()); + dtw->_statusbar->set_name("DesktopStatusBar"); + dtw->_vbox->pack_end(*dtw->_statusbar, false, true); + + /* Swatches panel */ + { + dtw->_panels = new Inkscape::UI::Dialog::SwatchesPanel("/embedded/swatches"); + dtw->_panels->set_vexpand(false); + dtw->_vbox->pack_end(*dtw->_panels, false, true); + } + + /* DesktopHBox (Vertical toolboxes, canvas) */ + dtw->_hbox = Gtk::manage(new Gtk::Box()); + dtw->_hbox->set_name("DesktopHbox"); + dtw->_vbox->pack_end(*dtw->_hbox, true, true); + + /* Toolboxes */ + dtw->aux_toolbox = ToolboxFactory::createAuxToolbox(); + dtw->_vbox->pack_end(*Glib::wrap(dtw->aux_toolbox), false, true); + + dtw->snap_toolbox = ToolboxFactory::createSnapToolbox(); + ToolboxFactory::setOrientation( dtw->snap_toolbox, GTK_ORIENTATION_VERTICAL ); + dtw->_hbox->pack_end(*Glib::wrap(dtw->snap_toolbox), false, true); + + dtw->commands_toolbox = ToolboxFactory::createCommandsToolbox(); + dtw->_vbox->pack_end(*Glib::wrap(dtw->commands_toolbox), false, true); + + dtw->tool_toolbox = ToolboxFactory::createToolToolbox(); + ToolboxFactory::setOrientation( dtw->tool_toolbox, GTK_ORIENTATION_VERTICAL ); + dtw->_hbox->pack_start(*Glib::wrap(dtw->tool_toolbox), false, true); + + /* Canvas table wrapper */ + auto tbl_wrapper = Gtk::manage(new Gtk::Grid()); // Is this widget really needed? No! + tbl_wrapper->set_name("CanvasTableWrapper"); + dtw->_hbox->pack_start(*tbl_wrapper, true, true, 1); + + /* Canvas table */ + dtw->_canvas_tbl = Gtk::manage(new Gtk::Grid()); + dtw->_canvas_tbl->set_name("CanvasTable"); + // Added to table wrapper later either directly or via paned window shared with dock. + + + + // Lock guides button + dtw->_guides_lock = Gtk::manage(new Inkscape::UI::Widget::Button(GTK_ICON_SIZE_MENU, + Inkscape::UI::Widget::BUTTON_TYPE_TOGGLE, + nullptr, + INKSCAPE_ICON("object-locked"), + _("Toggle lock of all guides in the document"))); + + auto guides_lock_style_provider = Gtk::CssProvider::create(); + guides_lock_style_provider->load_from_data("GtkWidget { padding-left: 0; padding-right: 0; padding-top: 0; padding-bottom: 0; }"); + dtw->_guides_lock->set_name("LockGuides"); + auto context = dtw->_guides_lock->get_style_context(); + context->add_provider(guides_lock_style_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + dtw->_guides_lock->signal_toggled().connect(sigc::mem_fun(dtw, &SPDesktopWidget::update_guides_lock)); + dtw->_canvas_tbl->attach(*dtw->_guides_lock, 0, 0, 1, 1); + + /* Rulers */ + Inkscape::Util::Unit const *pt = unit_table.getUnit("pt"); + + /* Horizontal ruler */ + dtw->_hruler = Gtk::manage(new Inkscape::UI::Widget::Ruler(Gtk::ORIENTATION_HORIZONTAL)); + dtw->_hruler->set_unit(pt); + + // We should probably get rid of this and attach the signals directly rulers. + dtw->_hruler_box = Gtk::manage(new Gtk::EventBox()); + dtw->_hruler_box->set_tooltip_text(gettext(pt->name_plural.c_str())); + dtw->_hruler_box->add(*dtw->_hruler); + dtw->_hruler_box->signal_button_press_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_button_press_event), dtw->_hruler_box, true)); + dtw->_hruler_box->signal_button_release_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_button_release_event), dtw->_hruler_box, true)); + dtw->_hruler_box->signal_motion_notify_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), dtw->_hruler_box, true)); + + dtw->_canvas_tbl->attach(*dtw->_hruler_box, 1, 0, 1, 1); + + /* Vertical ruler */ + dtw->_vruler = Gtk::manage(new Inkscape::UI::Widget::Ruler(Gtk::ORIENTATION_VERTICAL)); + dtw->_vruler->set_unit(pt); + + dtw->_vruler_box = Gtk::manage(new Gtk::EventBox()); + dtw->_vruler_box->set_tooltip_text(gettext(pt->name_plural.c_str())); + dtw->_vruler_box->add(*dtw->_vruler); + dtw->_vruler_box->signal_button_press_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_button_press_event), dtw->_vruler_box, false)); + dtw->_vruler_box->signal_button_release_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_button_release_event), dtw->_vruler_box, false)); + dtw->_vruler_box->signal_motion_notify_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), dtw->_vruler_box, false)); + + dtw->_canvas_tbl->attach(*dtw->_vruler_box, 0, 1, 1, 1); + + // Horizontal scrollbar + dtw->_hadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0); + dtw->_hscrollbar = Gtk::manage(new Gtk::Scrollbar(dtw->_hadj)); + dtw->_hscrollbar->set_name("HorizontalScrollbar"); + dtw->_canvas_tbl->attach(*dtw->_hscrollbar, 1, 2, 1, 1); + + // By packing the sticky zoom button and vertical scrollbar in a box it allows the canvas to + // expand fully to the top if the rulers are hidden. + // (Otherwise, the canvas is pushed down by the height of the sticky zoom button.) + + // Vertical Scrollbar box + dtw->_vscrollbar_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + dtw->_canvas_tbl->attach(*dtw->_vscrollbar_box, 2, 0, 1, 2); + + // Sticky zoom button + dtw->_sticky_zoom = Gtk::manage(new Inkscape::UI::Widget::Button(GTK_ICON_SIZE_MENU, + Inkscape::UI::Widget::BUTTON_TYPE_TOGGLE, + nullptr, + INKSCAPE_ICON("zoom-original"), + _("Zoom drawing if window size changes"))); + dtw->_sticky_zoom->set_name("StickyZoom"); + dtw->_sticky_zoom->set_active(prefs->getBool("/options/stickyzoom/value")); + dtw->_sticky_zoom->signal_toggled().connect(sigc::mem_fun(dtw, &SPDesktopWidget::sticky_zoom_toggled)); + dtw->_vscrollbar_box->pack_start(*dtw->_sticky_zoom, false, false); + + // Vertical scrollbar + dtw->_vadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0); + dtw->_vscrollbar = Gtk::manage(new Gtk::Scrollbar(dtw->_vadj, Gtk::ORIENTATION_VERTICAL)); + dtw->_vscrollbar->set_name("VerticalScrollbar"); + dtw->_vscrollbar_box->pack_start(*dtw->_vscrollbar, true, true, 0); + + gchar const* tip = ""; + Inkscape::Verb* verb = Inkscape::Verb::get( SP_VERB_VIEW_CMS_TOGGLE ); + if ( verb ) { + SPAction *act = verb->get_action( Inkscape::ActionContext( dtw->viewwidget.view ) ); + if ( act && act->tip ) { + tip = act->tip; + } + } + dtw->_cms_adjust = Gtk::manage(new Inkscape::UI::Widget::Button(GTK_ICON_SIZE_MENU, + Inkscape::UI::Widget::BUTTON_TYPE_TOGGLE, + nullptr, + INKSCAPE_ICON("color-management"), + tip )); + dtw->_cms_adjust->set_name("CMS_Adjust"); + +#if defined(HAVE_LIBLCMS2) + { + Glib::ustring current = prefs->getString("/options/displayprofile/uri"); + bool enabled = current.length() > 0; + dtw->cms_adjust_set_sensitive(enabled ); + if ( enabled ) { + bool active = prefs->getBool("/options/displayprofile/enable"); + if ( active ) { + dtw->_cms_adjust->toggle_set_down(true); + } + } + } + g_signal_connect_after( G_OBJECT(dtw->_cms_adjust->gobj()), "clicked", G_CALLBACK(SPDesktopWidget::cms_adjust_toggled), dtw ); +#else + dtw->cms_adjust_set_sensitive(false); +#endif // defined(HAVE_LIBLCMS2) + + dtw->_canvas_tbl->attach(*dtw->_cms_adjust, 2, 2, 1, 1); + { + if (!watcher) { + watcher = new CMSPrefWatcher(); + } + watcher->add(dtw); + } + /* Canvas */ + dtw->_canvas = SP_CANVAS(SPCanvas::createAA()); +#if defined(HAVE_LIBLCMS2) + dtw->_canvas->_enable_cms_display_adj = prefs->getBool("/options/displayprofile/enable"); +#endif // defined(HAVE_LIBLCMS2) + gtk_widget_set_can_focus (GTK_WIDGET (dtw->_canvas), TRUE); + + dtw->_hruler->add_track_widget(*Glib::wrap(GTK_WIDGET(dtw->_canvas))); + dtw->_vruler->add_track_widget(*Glib::wrap(GTK_WIDGET(dtw->_canvas))); + + auto css_provider = gtk_css_provider_new(); + auto style_context = gtk_widget_get_style_context(GTK_WIDGET(dtw->_canvas)); + + gtk_css_provider_load_from_data(css_provider, + "SPCanvas {\n" + " background-color: white;\n" + "}\n", + -1, nullptr); + + gtk_style_context_add_provider(style_context, + GTK_STYLE_PROVIDER(css_provider), + GTK_STYLE_PROVIDER_PRIORITY_USER); + g_signal_connect(G_OBJECT(dtw->_canvas), "event", G_CALLBACK(SPDesktopWidget::event), dtw); + + gtk_widget_set_hexpand(GTK_WIDGET(dtw->_canvas), TRUE); + gtk_widget_set_vexpand(GTK_WIDGET(dtw->_canvas), TRUE); + dtw->_canvas_tbl->attach(*Glib::wrap(GTK_WIDGET(dtw->_canvas)), 1, 1, 1, 1); + + /* Dock */ + bool create_dock = + prefs->getIntLimited("/options/dialogtype/value", Inkscape::UI::Dialog::FLOATING, 0, 1) == + Inkscape::UI::Dialog::DOCK; + + if (create_dock) { + dtw->_dock = new Inkscape::UI::Widget::Dock(); + auto paned = new Gtk::Paned(); + paned->set_name("Canvas_and_Dock"); + + paned->pack1(*dtw->_canvas_tbl); + paned->pack2(dtw->_dock->getWidget(), Gtk::FILL); + + /* Prevent the paned from catching F6 and F8 by unsetting the default callbacks */ + if (GtkPanedClass *paned_class = GTK_PANED_CLASS (G_OBJECT_GET_CLASS (paned->gobj()))) { + paned_class->cycle_child_focus = nullptr; + paned_class->cycle_handle_focus = nullptr; + } + + paned->set_hexpand(true); + paned->set_vexpand(true); + tbl_wrapper->attach(*paned, 1, 1, 1, 1); + } else { + dtw->_canvas_tbl->set_hexpand(true); + dtw->_canvas_tbl->set_vexpand(true); + tbl_wrapper->attach(*(dtw->_canvas_tbl), 1, 1, 1, 1); + } + + // connect scrollbar signals + dtw->_hadj->signal_value_changed().connect(sigc::mem_fun(dtw, &SPDesktopWidget::on_adjustment_value_changed)); + dtw->_vadj->signal_value_changed().connect(sigc::mem_fun(dtw, &SPDesktopWidget::on_adjustment_value_changed)); + + // --------------- Status Tool Bar ------------------// + + // Selected Style (Fill/Stroke/Opacity) + dtw->_selected_style = new Inkscape::UI::Widget::SelectedStyle(true); + dtw->_statusbar->pack_start(*dtw->_selected_style, false, false); + + // Separator + dtw->_statusbar->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_VERTICAL)), + false, false); + + // Layer Selector + dtw->layer_selector = new Inkscape::UI::Widget::LayerSelector(nullptr); + // FIXME: need to unreference on container destruction to avoid leak + dtw->layer_selector->reference(); + dtw->_statusbar->pack_start(*dtw->layer_selector, false, false, 1); + + // Select Status + dtw->_select_status = Gtk::manage(new Gtk::Label()); + dtw->_select_status->set_name("SelectStatus"); + dtw->_select_status->set_ellipsize(Pango::ELLIPSIZE_END); + dtw->_select_status->set_line_wrap(true); + dtw->_select_status->set_lines(2); + dtw->_select_status->set_halign(Gtk::ALIGN_START); + dtw->_select_status->set_size_request(1, -1); + + // Display the initial welcome message in the statusbar + dtw->_select_status->set_markup(_("<b>Welcome to Inkscape!</b> Use shape or freehand tools to create objects; use selector (arrow) to move or transform them.")); + + dtw->_statusbar->pack_start(*dtw->_select_status, true, true); + + + // Zoom status spinbutton --------------- + auto zoom_adj = Gtk::Adjustment::create(100.0, log(SP_DESKTOP_ZOOM_MIN)/log(2), log(SP_DESKTOP_ZOOM_MAX)/log(2), 0.1); + dtw->_zoom_status = Gtk::manage(new Gtk::SpinButton(zoom_adj)); + + dtw->_zoom_status->set_data("dtw", dtw->_canvas); + dtw->_zoom_status->set_tooltip_text(_("Zoom")); + dtw->_zoom_status->set_size_request(STATUS_ZOOM_WIDTH, -1); + dtw->_zoom_status->set_width_chars(6); + dtw->_zoom_status->set_numeric(false); + dtw->_zoom_status->set_update_policy(Gtk::UPDATE_ALWAYS); + + // Callbacks + dtw->_zoom_status_input_connection = dtw->_zoom_status->signal_input().connect(sigc::mem_fun(dtw, &SPDesktopWidget::zoom_input)); + dtw->_zoom_status_output_connection = dtw->_zoom_status->signal_output().connect(sigc::mem_fun(dtw, &SPDesktopWidget::zoom_output)); + g_signal_connect (G_OBJECT (dtw->_zoom_status->gobj()), "focus-in-event", G_CALLBACK (spinbutton_focus_in), dtw->_zoom_status->gobj()); + g_signal_connect (G_OBJECT (dtw->_zoom_status->gobj()), "key-press-event", G_CALLBACK (spinbutton_keypress), dtw->_zoom_status->gobj()); + dtw->_zoom_status_value_changed_connection = dtw->_zoom_status->signal_value_changed().connect(sigc::mem_fun(dtw, &SPDesktopWidget::zoom_value_changed)); + dtw->_zoom_status_populate_popup_connection = dtw->_zoom_status->signal_populate_popup().connect(sigc::mem_fun(dtw, &SPDesktopWidget::zoom_populate_popup)); + + // Style + auto css_provider_spinbutton = Gtk::CssProvider::create(); + css_provider_spinbutton->load_from_data("* { padding-left: 2px; padding-right: 2px; padding-top: 0px; padding-bottom: 0px;}"); // Shouldn't this be in a style sheet? Used also by rotate. + + dtw->_zoom_status->set_name("ZoomStatus"); + auto context_zoom = dtw->_zoom_status->get_style_context(); + context_zoom->add_provider(css_provider_spinbutton, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + // Rotate status spinbutton --------------- + auto rotation_adj = Gtk::Adjustment::create(0, -360.0, 360.0, 1.0); + dtw->_rotation_status = Gtk::manage(new Gtk::SpinButton(rotation_adj)); + dtw->_rotation_status->set_data("dtw", dtw->_canvas); + dtw->_rotation_status->set_tooltip_text(_("Rotation. (Also Ctrl+Shift+Scroll)")); + dtw->_rotation_status->set_size_request(STATUS_ROTATION_WIDTH, -1); + dtw->_rotation_status->set_width_chars(7); + dtw->_rotation_status->set_numeric(false); + dtw->_rotation_status->set_digits(2); + dtw->_rotation_status->set_increments(1.0, 15.0); + dtw->_rotation_status->set_update_policy(Gtk::UPDATE_ALWAYS); + + // Callbacks + dtw->_rotation_status_input_connection = dtw->_rotation_status->signal_input().connect(sigc::mem_fun(dtw, &SPDesktopWidget::rotation_input)); + dtw->_rotation_status_output_connection = dtw->_rotation_status->signal_output().connect(sigc::mem_fun(dtw, &SPDesktopWidget::rotation_output)); + g_signal_connect (G_OBJECT (dtw->_rotation_status->gobj()), "focus-in-event", G_CALLBACK (spinbutton_focus_in), dtw->_rotation_status->gobj()); + g_signal_connect (G_OBJECT (dtw->_rotation_status->gobj()), "key-press-event", G_CALLBACK (spinbutton_keypress), dtw->_rotation_status->gobj()); + dtw->_rotation_status_value_changed_connection = dtw->_rotation_status->signal_value_changed().connect(sigc::mem_fun(dtw, &SPDesktopWidget::rotation_value_changed)); + dtw->_rotation_status_populate_popup_connection = dtw->_rotation_status->signal_populate_popup().connect(sigc::mem_fun(dtw, &SPDesktopWidget::rotation_populate_popup)); + + // Style + dtw->_rotation_status->set_name("RotationStatus"); + auto context_rotation = dtw->_rotation_status->get_style_context(); + context_rotation->add_provider(css_provider_spinbutton, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + + // Cursor coordinates + dtw->_coord_status = Gtk::manage(new Gtk::Grid()); + dtw->_coord_status->set_name("CoordinateAndZStatus"); + dtw->_coord_status->set_row_spacing(0); + dtw->_coord_status->set_column_spacing(10); + dtw->_coord_status->set_margin_end(10); + auto sep = Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_VERTICAL)); + sep->set_name("CoordinateSeparator"); + dtw->_coord_status->attach(*sep, 0, 0, 1, 2); + + dtw->_coord_status->set_tooltip_text(_("Cursor coordinates")); + auto label_x = Gtk::manage(new Gtk::Label(_("X:"))); + auto label_y = Gtk::manage(new Gtk::Label(_("Y:"))); + label_x->set_halign(Gtk::ALIGN_START); + label_y->set_halign(Gtk::ALIGN_START); + dtw->_coord_status->attach(*label_x, 1, 0, 1, 1); + dtw->_coord_status->attach(*label_y, 1, 1, 1, 1); + dtw->_coord_status_x = Gtk::manage(new Gtk::Label()); + dtw->_coord_status_y = Gtk::manage(new Gtk::Label()); + dtw->_coord_status_x->set_name("CoordinateStatusX"); + dtw->_coord_status_y->set_name("CoordinateStatusY"); + dtw->_coord_status_x->set_markup(" 0.00 "); + dtw->_coord_status_y->set_markup(" 0.00 "); + + auto label_z = Gtk::manage(new Gtk::Label(_("Z:"))); + label_z->set_name("ZLabel"); + auto label_r = Gtk::manage(new Gtk::Label(_("R:"))); + label_r->set_name("RLabel"); + + dtw->_coord_status_x->set_halign(Gtk::ALIGN_END); + dtw->_coord_status_y->set_halign(Gtk::ALIGN_END); + dtw->_coord_status->attach(*dtw->_coord_status_x, 2, 0, 1, 1); + dtw->_coord_status->attach(*dtw->_coord_status_y, 2, 1, 1, 1); + + dtw->_coord_status->attach(*label_z, 3, 0, 1, 2); + dtw->_coord_status->attach(*dtw->_zoom_status, 4, 0, 1, 2); + + dtw->_coord_status->attach(*label_r, 5, 0, 1, 2); + dtw->_coord_status->attach(*dtw->_rotation_status, 6, 0, 1, 2); + + dtw->_statusbar->pack_end(*dtw->_coord_status, false, false); + + // --------------- Color Management ---------------- // + dtw->_tracker = ege_color_prof_tracker_new(GTK_WIDGET(dtw->layer_selector->gobj())); +#if defined(HAVE_LIBLCMS2) + bool fromDisplay = prefs->getBool( "/options/displayprofile/from_display"); + if ( fromDisplay ) { + Glib::ustring id = Inkscape::CMSSystem::getDisplayId( 0 ); + + bool enabled = false; + dtw->_canvas->_cms_key = id; + enabled = !dtw->_canvas->_cms_key.empty(); + dtw->cms_adjust_set_sensitive(enabled); + } + g_signal_connect( G_OBJECT(dtw->_tracker), "changed", G_CALLBACK(SPDesktopWidget::color_profile_event), dtw ); +#endif // defined(HAVE_LIBLCMS2) + + // ------------------ Finish Up -------------------- // + dtw->_vbox->show_all(); + + gtk_widget_grab_focus (GTK_WIDGET(dtw->_canvas)); + + // If this is the first desktop created, report the time it takes to show up + if ( overallTimer ) { + if ( prefs->getBool("/dialogs/debug/trackAppear", false) ) { + // Time tracker takes ownership of the timer. + AppearTimeTracker *tracker = new AppearTimeTracker(overallTimer, GTK_WIDGET(dtw), "first SPDesktopWidget"); + tracker->setAutodelete(true); + } else { + g_timer_destroy(overallTimer); + } + overallTimer = nullptr; + } + // Ensure that ruler ranges are updated correctly whenever the canvas table + // is resized + dtw->_canvas_tbl_size_allocate_connection = dtw->_canvas_tbl->signal_size_allocate().connect(sigc::mem_fun(dtw, &SPDesktopWidget::canvas_tbl_size_allocate)); +} + +/** + * Called before SPDesktopWidget destruction. + */ +void +SPDesktopWidget::dispose(GObject *object) +{ + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET (object); + + if (dtw == nullptr) { + return; + } + + UXManager::getInstance()->delTrack(dtw); + + if (dtw->desktop) { + if ( watcher ) { + watcher->remove(dtw); + } + + // Zoom + dtw->_zoom_status_input_connection.disconnect(); + dtw->_zoom_status_output_connection.disconnect(); + g_signal_handlers_disconnect_matched (G_OBJECT (dtw->_zoom_status->gobj()), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, dtw->_zoom_status); + dtw->_zoom_status_value_changed_connection.disconnect(); + dtw->_zoom_status_populate_popup_connection.disconnect(); + + // Rotation + dtw->_rotation_status_input_connection.disconnect(); + dtw->_rotation_status_output_connection.disconnect(); + g_signal_handlers_disconnect_matched (G_OBJECT (dtw->_rotation_status->gobj()), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, dtw->_rotation_status); + dtw->_rotation_status_value_changed_connection.disconnect(); + dtw->_rotation_status_populate_popup_connection.disconnect(); + + // Canvas + g_signal_handlers_disconnect_by_func (G_OBJECT (dtw->_canvas), (gpointer) G_CALLBACK (SPDesktopWidget::event), dtw); + dtw->_canvas_tbl_size_allocate_connection.disconnect(); + + dtw->layer_selector->setDesktop(nullptr); + dtw->layer_selector->unreference(); + INKSCAPE.remove_desktop(dtw->desktop); // clears selection and event_context + dtw->modified_connection.disconnect(); + dtw->desktop->destroy(); + Inkscape::GC::release (dtw->desktop); + dtw->desktop = nullptr; + } + + dtw->modified_connection.~connection(); + + if (G_OBJECT_CLASS (dtw_parent_class)->dispose) { + (* G_OBJECT_CLASS (dtw_parent_class)->dispose) (object); + } +} + +/** + * Set the title in the desktop-window (if desktop has an own window). + * + * The title has form file name: desktop number - Inkscape. + * The desktop number is only shown if it's 2 or higher, + */ +void +SPDesktopWidget::updateTitle(gchar const* uri) +{ + if (window) { + + SPDocument *doc = this->desktop->doc(); + + std::string Name; + if (doc->isModifiedSinceSave()) { + Name += "*"; + } + + Name += uri; + + if (desktop->number > 1) { + Name += ": "; + Name += std::to_string(desktop->number); + } + Name += " ("; + + if (desktop->getMode() == Inkscape::RENDERMODE_OUTLINE) { + Name += N_("outline"); + } else if (desktop->getMode() == Inkscape::RENDERMODE_NO_FILTERS) { + Name += N_("no filters"); + } else if (desktop->getMode() == Inkscape::RENDERMODE_VISIBLE_HAIRLINES) { + Name += N_("visible hairlines"); + } + + if (desktop->getColorMode() != Inkscape::COLORMODE_NORMAL && + desktop->getMode() != Inkscape::RENDERMODE_NORMAL) { + Name += ", "; + } + + if (desktop->getColorMode() == Inkscape::COLORMODE_GRAYSCALE) { + Name += N_("grayscale"); + } else if (desktop->getColorMode() == Inkscape::COLORMODE_PRINT_COLORS_PREVIEW) { + Name += N_("print colors preview"); + } + + if (*Name.rbegin() == '(') { // Can not use C++11 .back() or .pop_back() with ustring! + Name.erase(Name.size() - 2); + } else { + Name += ")"; + } + + Name += " - Inkscape"; + + // Name += " ("; + // Name += Inkscape::version_string; + // Name += ")"; + + window->set_title (Name); + } +} + +Inkscape::UI::Widget::Dock* +SPDesktopWidget::getDock() +{ + return _dock; +} + +/** + * Callback to allocate space for desktop widget. + */ +static void +sp_desktop_widget_size_allocate (GtkWidget *widget, GtkAllocation *allocation) +{ + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET (widget); + GtkAllocation widg_allocation; + gtk_widget_get_allocation(widget, &widg_allocation); + + if ((allocation->x == widg_allocation.x) && + (allocation->y == widg_allocation.y) && + (allocation->width == widg_allocation.width) && + (allocation->height == widg_allocation.height)) { + if (GTK_WIDGET_CLASS (dtw_parent_class)->size_allocate) + GTK_WIDGET_CLASS (dtw_parent_class)->size_allocate (widget, allocation); + return; + } + + if (gtk_widget_get_realized (widget)) { + Geom::Rect const area = dtw->desktop->get_display_area(); + Geom::Rect const d_canvas = dtw->desktop->getCanvas()->getViewbox(); + Geom::Point midpoint = dtw->desktop->w2d(d_canvas.midpoint()); + + double zoom = dtw->desktop->current_zoom(); + + if (GTK_WIDGET_CLASS(dtw_parent_class)->size_allocate) { + GTK_WIDGET_CLASS(dtw_parent_class)->size_allocate (widget, allocation); + } + + if (dtw->get_sticky_zoom_active()) { + /* Find new visible area */ + Geom::Rect newarea = dtw->desktop->get_display_area(); + /* Calculate adjusted zoom */ + double oldshortside = MIN( area.width(), area.height()); + double newshortside = MIN(newarea.width(), newarea.height()); + zoom *= newshortside / oldshortside; + } + dtw->desktop->zoom_absolute_center_point (midpoint, zoom); + + // TODO - Should call show_dialogs() from sp_namedview_window_from_document only. + // But delaying the call to here solves dock sizing issues on OS X, (see #171579) + dtw->desktop->show_dialogs(); + + } else { + if (GTK_WIDGET_CLASS (dtw_parent_class)->size_allocate) { + GTK_WIDGET_CLASS (dtw_parent_class)->size_allocate (widget, allocation); + } +// this->size_allocate (widget, allocation); + } +} + +#ifdef GDK_WINDOWING_QUARTZ +static GtkMenuItem *_get_help_menu(GtkMenuShell *menu) +{ + // Assume "Help" is the last child in menu + GtkMenuItem *last = nullptr; + auto callback = [](GtkWidget *widget, gpointer data) { + *static_cast<GtkMenuItem **>(data) = GTK_MENU_ITEM(widget); + }; + gtk_container_foreach(GTK_CONTAINER(menu), callback, &last); + return last; +} +#endif + +/** + * Callback to realize desktop widget. + */ +static void +sp_desktop_widget_realize (GtkWidget *widget) +{ + + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET (widget); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (GTK_WIDGET_CLASS (dtw_parent_class)->realize) + (* GTK_WIDGET_CLASS (dtw_parent_class)->realize) (widget); + + Geom::Rect d = Geom::Rect::from_xywh(Geom::Point(0,0), (dtw->desktop->doc())->getDimensions()); + + if (d.width() < 1.0 || d.height() < 1.0) return; + + dtw->desktop->set_display_area (d, 10); + + dtw->updateNamedview(); + gchar *gtkThemeName; + gboolean gtkApplicationPreferDarkTheme; + GtkSettings *settings = gtk_settings_get_default(); + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + if (settings && window) { + g_object_get(settings, "gtk-theme-name", >kThemeName, NULL); + g_object_get(settings, "gtk-application-prefer-dark-theme", >kApplicationPreferDarkTheme, NULL); + bool dark = Glib::ustring(gtkThemeName).find(":dark") != std::string::npos; + if (!dark) { + Glib::RefPtr<Gtk::StyleContext> stylecontext = window->get_style_context(); + Gdk::RGBA rgba; + bool background_set = stylecontext->lookup_color("theme_bg_color", rgba); + if (background_set && (0.299 * rgba.get_red() + 0.587 * rgba.get_green() + 0.114 * rgba.get_blue()) < 0.5) { + dark = true; + } + } + if (dark) { + window->get_style_context()->add_class("dark"); + window->get_style_context()->remove_class("bright"); + } else { + window->get_style_context()->add_class("bright"); + window->get_style_context()->remove_class("dark"); + } + if (prefs->getBool("/theme/symbolicIcons", false)) { + window->get_style_context()->add_class("symbolic"); + window->get_style_context()->remove_class("regular"); + } else { + window->get_style_context()->add_class("regular"); + window->get_style_context()->remove_class("symbolic"); + } + INKSCAPE.signal_change_theme.emit(); + } + +#ifdef GDK_WINDOWING_QUARTZ + // native macOS menu + auto osxapp = gtkosx_application_get(); + auto menushell = static_cast<Gtk::MenuShell *>(dtw->menubar()); + if (osxapp && menushell && window) { + menushell->set_parent(*window); + gtkosx_application_set_menu_bar(osxapp, menushell->gobj()); + // using quartz accelerators gives menu shortcuts priority over everything else, + // messes up text input because Inkscape has single key shortcuts (e.g. 1-6). + gtkosx_application_set_use_quartz_accelerators(osxapp, false); + gtkosx_application_set_help_menu(osxapp, _get_help_menu(menushell->gobj())); + + // Window menu disabled because hidden windows which are brought back + // from the menu are non-functional. + // https://gitlab.com/inkscape/inkscape/-/issues/1105 +#if 0 + gtkosx_application_set_window_menu(osxapp, nullptr); +#endif + + // move some items to "Inkscape" menu + unsigned app_menu_verbs[] = { + SP_VERB_NONE, + SP_VERB_DIALOG_INPUT, + SP_VERB_DIALOG_DISPLAY, + SP_VERB_NONE, + SP_VERB_HELP_ABOUT, + }; + for (auto verb : app_menu_verbs) { + GtkWidget *menuitem = nullptr; + if (verb == SP_VERB_NONE) { + menuitem = gtk_separator_menu_item_new(); + } else if (auto item = get_menu_item_for_verb(verb, dtw->desktop)) { + menuitem = static_cast<Gtk::Widget *>(item)->gobj(); + } else { + continue; + } + // Don't use index 0 because it appends the app name. Index 1 + // seems to work perfectly with inserting items in reverse order. + gtkosx_application_insert_app_menu_item(osxapp, menuitem, 1); + } + } +#endif +} + +/* This is just to provide access to common functionality from sp_desktop_widget_realize() above + as well as from SPDesktop::change_document() */ +void SPDesktopWidget::updateNamedview() +{ + // Listen on namedview modification + // originally (prior to the sigc++ conversion) the signal was simply + // connected twice rather than disconnecting the first connection + modified_connection.disconnect(); + + modified_connection = desktop->namedview->connectModified(sigc::mem_fun(*this, &SPDesktopWidget::namedviewModified)); + namedviewModified(desktop->namedview, SP_OBJECT_MODIFIED_FLAG); + + updateTitle( desktop->doc()->getDocumentName() ); +} + +/** + * Callback to handle desktop widget event. + */ +gint +SPDesktopWidget::event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw) +{ + if (event->type == GDK_BUTTON_PRESS) { + // defocus any spinbuttons + gtk_widget_grab_focus (GTK_WIDGET(dtw->_canvas)); + } + + if ((event->type == GDK_BUTTON_PRESS) && (event->button.button == 3)) { + if (event->button.state & GDK_SHIFT_MASK) { + sp_canvas_arena_set_sticky (SP_CANVAS_ARENA (dtw->desktop->drawing), TRUE); + } else { + sp_canvas_arena_set_sticky (SP_CANVAS_ARENA (dtw->desktop->drawing), FALSE); + } + } + + if (GTK_WIDGET_CLASS (dtw_parent_class)->event) { + return (* GTK_WIDGET_CLASS (dtw_parent_class)->event) (widget, event); + } else { + // The key press/release events need to be passed to desktop handler explicitly, + // because otherwise the event contexts only receive key events when the mouse cursor + // is over the canvas. This redirection is only done for key events and only if there's no + // current item on the canvas, because item events and all mouse events are caught + // and passed on by the canvas acetate (I think). --bb + + if ((event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) + && !dtw->_canvas->_current_item) { + return sp_desktop_root_handler (nullptr, event, dtw->desktop); + } + } + + return FALSE; +} + +#if defined(HAVE_LIBLCMS2) +void +SPDesktopWidget::color_profile_event(EgeColorProfTracker */*tracker*/, SPDesktopWidget *dtw) +{ + // Handle profile changes + GdkScreen* screen = gtk_widget_get_screen(GTK_WIDGET(dtw)); + GdkWindow *window = gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(dtw))); + + // Figure out the ID for the monitor + auto display = gdk_display_get_default(); + auto monitor = gdk_display_get_monitor_at_window(display, window); + + int n_monitors = gdk_display_get_n_monitors(display); + + int monitorNum = -1; + + // Now loop through the set of monitors and figure out whether this monitor matches + for (int i_monitor = 0; i_monitor < n_monitors; ++i_monitor) { + auto monitor_at_index = gdk_display_get_monitor(display, i_monitor); + if(monitor_at_index == monitor) monitorNum = i_monitor; + } + + Glib::ustring id = Inkscape::CMSSystem::getDisplayId( monitorNum ); + bool enabled = false; + dtw->_canvas->_cms_key = id; + dtw->requestCanvasUpdate(); + enabled = !dtw->_canvas->_cms_key.empty(); + dtw->cms_adjust_set_sensitive(enabled); +} +#else // defined(HAVE_LIBLCMS2) +void sp_dtw_color_profile_event(EgeColorProfTracker */*tracker*/, SPDesktopWidget * /*dtw*/) +{ +} +#endif // defined(HAVE_LIBLCMS2) + +void +SPDesktopWidget::update_guides_lock() +{ + bool down = _guides_lock->get_active(); + + auto doc = desktop->getDocument(); + auto nv = desktop->getNamedView(); + auto repr = nv->getRepr(); + + if ( down != nv->lockguides ) { + nv->lockguides = down; + sp_namedview_guides_toggle_lock(doc, nv); + if (down) { + setMessage (Inkscape::NORMAL_MESSAGE, _("Locked all guides")); + } else { + setMessage (Inkscape::NORMAL_MESSAGE, _("Unlocked all guides")); + } + } +} + +#if defined(HAVE_LIBLCMS2) +void +SPDesktopWidget::cms_adjust_toggled( GtkWidget */*button*/, gpointer data ) +{ + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(data); + + bool down = dtw->_cms_adjust->get_active(); + if ( down != dtw->_canvas->_enable_cms_display_adj ) { + dtw->_canvas->_enable_cms_display_adj = down; + dtw->desktop->redrawDesktop(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/displayprofile/enable", down); + if (down) { + dtw->setMessage (Inkscape::NORMAL_MESSAGE, _("Color-managed display is <b>enabled</b> in this window")); + } else { + dtw->setMessage (Inkscape::NORMAL_MESSAGE, _("Color-managed display is <b>disabled</b> in this window")); + } + } +} +#endif // defined(HAVE_LIBLCMS2) + +void +SPDesktopWidget::cms_adjust_set_sensitive(bool enabled) +{ + Inkscape::Verb* verb = Inkscape::Verb::get( SP_VERB_VIEW_CMS_TOGGLE ); + if ( verb ) { + SPAction *act = verb->get_action( Inkscape::ActionContext(viewwidget.view) ); + if ( act ) { + sp_action_set_sensitive( act, enabled ); + } + } + _cms_adjust->set_sensitive(enabled); +} + +void +sp_dtw_desktop_activate (SPDesktopWidget */*dtw*/) +{ + /* update active desktop indicator */ +} + +void +sp_dtw_desktop_deactivate (SPDesktopWidget */*dtw*/) +{ + /* update inactive desktop indicator */ +} + +/** + * Shuts down the desktop object for the view being closed. It checks + * to see if the document has been edited, and if so prompts the user + * to save, discard, or cancel. Returns TRUE if the shutdown operation + * is cancelled or if the save is cancelled or fails, FALSE otherwise. + */ +bool +SPDesktopWidget::shutdown() +{ + g_assert(desktop != nullptr); + + if (INKSCAPE.sole_desktop_for_document(*desktop)) { + SPDocument *doc = desktop->doc(); + if (doc->isModifiedSinceSave()) { + Gtk::Window *toplevel_window = Glib::wrap(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(this)))); + Glib::ustring message = g_markup_printf_escaped( + _("<span weight=\"bold\" size=\"larger\">Save changes to document \"%s\" before closing?</span>\n\n" + "If you close without saving, your changes will be discarded."), + doc->getDocumentName()); + Gtk::MessageDialog dialog = Gtk::MessageDialog(*toplevel_window, message, true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE); + dialog.property_destroy_with_parent() = true; + + // fix for bug lp:168809 + Gtk::Container *ma = dialog.get_message_area(); + std::vector<Gtk::Widget*> ma_labels = ma->get_children(); + ma_labels[0]->set_can_focus(false); + + Gtk::Button close_button(_("Close _without saving"), true); + close_button.show(); + dialog.add_action_widget(close_button, Gtk::RESPONSE_NO); + + dialog.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + dialog.add_button(_("_Save"), Gtk::RESPONSE_YES); + dialog.set_default_response(Gtk::RESPONSE_YES); + + gint response = dialog.run(); + + switch (response) { + case GTK_RESPONSE_YES: + { + doc->doRef(); + sp_namedview_document_from_window(desktop); + if (sp_file_save_document(*window, doc)) { + doc->doUnref(); + } else { // save dialog cancelled or save failed + doc->doUnref(); + return TRUE; + } + + break; + } + case GTK_RESPONSE_NO: + break; + default: // cancel pressed, or dialog was closed + return TRUE; + break; + } + } + /* Code to check data loss */ + bool allow_data_loss = FALSE; + while (doc->getReprRoot()->attribute("inkscape:dataloss") != nullptr && allow_data_loss == FALSE) { + Gtk::Window *toplevel_window = Glib::wrap(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(this)))); + Glib::ustring message = g_markup_printf_escaped( + _("<span weight=\"bold\" size=\"larger\">The file \"%s\" was saved with a format that may cause data loss!</span>\n\n" + "Do you want to save this file as Inkscape SVG?"), + doc->getDocumentName() ? doc->getDocumentName() : "Unnamed"); + Gtk::MessageDialog dialog = Gtk::MessageDialog(*toplevel_window, message, true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE); + dialog.property_destroy_with_parent() = true; + + // fix for bug lp:168809 + Gtk::Container *ma = dialog.get_message_area(); + std::vector<Gtk::Widget*> ma_labels = ma->get_children(); + ma_labels[0]->set_can_focus(false); + + Gtk::Button close_button(_("Close _without saving"), true); + close_button.show(); + dialog.add_action_widget(close_button, Gtk::RESPONSE_NO); + + dialog.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + + Gtk::Button save_button(_("_Save as Inkscape SVG"), true); + save_button.set_can_default(true); + save_button.show(); + dialog.add_action_widget(save_button, Gtk::RESPONSE_YES); + dialog.set_default_response(Gtk::RESPONSE_YES); + + gint response = dialog.run(); + + switch (response) { + case GTK_RESPONSE_YES: + { + doc->doRef(); + + if (sp_file_save_dialog(*window, doc, Inkscape::Extension::FILE_SAVE_METHOD_INKSCAPE_SVG)) { + doc->doUnref(); + } else { // save dialog cancelled or save failed + doc->doUnref(); + return TRUE; + } + + break; + } + case GTK_RESPONSE_NO: + allow_data_loss = TRUE; + break; + default: // cancel pressed, or dialog was closed + return TRUE; + break; + } + } + } + + /* Save window geometry to prefs for use as a default. + * Use depends on setting of "options.savewindowgeometry". + * But we save the info here regardless of the setting. + */ + storeDesktopPosition(); + + return FALSE; +} + +/** + * \store dessktop position + */ +void SPDesktopWidget::storeDesktopPosition() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool maxed = desktop->is_maximized(); + bool full = desktop->is_fullscreen(); + prefs->setBool("/desktop/geometry/fullscreen", full); + prefs->setBool("/desktop/geometry/maximized", maxed); + gint w, h, x, y; + desktop->getWindowGeometry(x, y, w, h); + // Don't save geom for maximized windows. It + // just tells you the current maximized size, which is not + // as useful as whatever value it had previously. + if (!maxed && !full) { + prefs->setInt("/desktop/geometry/width", w); + prefs->setInt("/desktop/geometry/height", h); + prefs->setInt("/desktop/geometry/x", x); + prefs->setInt("/desktop/geometry/y", y); + } +} + +/** + * \pre this->desktop->main != 0 + */ +void +SPDesktopWidget::requestCanvasUpdate() { + // ^^ also this->desktop != 0 + g_return_if_fail(this->desktop != nullptr); + g_return_if_fail(this->desktop->main != nullptr); + gtk_widget_queue_draw (GTK_WIDGET (SP_CANVAS_ITEM (this->desktop->main)->canvas)); +} + +void +SPDesktopWidget::requestCanvasUpdateAndWait() { + requestCanvasUpdate(); + + while (gtk_events_pending()) + gtk_main_iteration_do(FALSE); + +} + +void +SPDesktopWidget::enableInteraction() +{ + g_return_if_fail(_interaction_disabled_counter > 0); + + _interaction_disabled_counter--; + + if (_interaction_disabled_counter == 0) { + gtk_widget_set_sensitive(GTK_WIDGET(this), TRUE); + } +} + +void +SPDesktopWidget::disableInteraction() +{ + if (_interaction_disabled_counter == 0) { + gtk_widget_set_sensitive(GTK_WIDGET(this), FALSE); + } + + _interaction_disabled_counter++; +} + +void +SPDesktopWidget::setCoordinateStatus(Geom::Point p) +{ + gchar *cstr; + cstr = g_strdup_printf("%7.2f", _dt2r * p[Geom::X]); + _coord_status_x->set_markup(cstr); + g_free(cstr); + + cstr = g_strdup_printf("%7.2f", _dt2r * p[Geom::Y]); + _coord_status_y->set_markup(cstr); + g_free(cstr); +} + +void +SPDesktopWidget::letZoomGrabFocus() +{ + if (_zoom_status) _zoom_status->grab_focus(); +} + +void +SPDesktopWidget::getWindowGeometry (gint &x, gint &y, gint &w, gint &h) +{ + if (window) + { + window->get_size (w, h); + window->get_position (x, y); + } +} + +void +SPDesktopWidget::setWindowPosition (Geom::Point p) +{ + if (window) + { + window->move (gint(round(p[Geom::X])), gint(round(p[Geom::Y]))); + } +} + +void +SPDesktopWidget::setWindowSize (gint w, gint h) +{ + if (window) + { + window->set_default_size (w, h); + window->resize (w, h); + } +} + +#ifdef __APPLE__ +/** + * This should be a no-op, but on macOS it raises the given transient window to the + * top of other transient windows from the same parent. + */ +static gboolean transient_focus_in_callback(GtkWindow *window, GdkEvent *, gpointer) +{ + gtk_window_set_transient_for(window, gtk_window_get_transient_for(window)); + return FALSE; +} +#endif + +/** + * \note transientizing does not work on windows; when you minimize a document + * and then open it back, only its transient emerges and you cannot access + * the document window. The document window must be restored by rightclicking + * the taskbar button and pressing "Restore" + */ +void +SPDesktopWidget::setWindowTransient (void *p, int transient_policy) +{ + if (window) + { + GtkWindow *w = GTK_WINDOW(window->gobj()); + +#ifdef __APPLE__ + // Workaround for https://gitlab.gnome.org/GNOME/gtk/issues/2436 + // The first time this window is made transient, connect the focus-in event to + // re-transientize the window in order to raise it above other transient windows. + if (gtk_window_get_transient_for(GTK_WINDOW(p)) == nullptr) { + g_signal_connect(GTK_WIDGET(p), "focus-in-event", G_CALLBACK(transient_focus_in_callback), nullptr); + } +#endif + + gtk_window_set_transient_for (GTK_WINDOW(p), w); + + /* + * This enables "aggressive" transientization, + * i.e. dialogs always emerging on top when you switch documents. Note + * however that this breaks "click to raise" policy of a window + * manager because the switched-to document will be raised at once + * (so that its transients also could raise) + */ + if (transient_policy == 2) + // without this, a transient window not always emerges on top + gtk_window_present (w); + } +} + +void +SPDesktopWidget::presentWindow() +{ + GtkWindow *w =GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(this))); + if (w) + gtk_window_present (w); +} + +bool SPDesktopWidget::showInfoDialog( Glib::ustring const &message ) +{ + bool result = false; + Gtk::Window *window = Glib::wrap(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(this)))); + if (window) + { + Gtk::MessageDialog dialog(*window, message, false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK); + dialog.property_destroy_with_parent() = true; + dialog.set_name("InfoDialog"); + dialog.set_title(_("Note:")); // probably want to take this as a parameter. + dialog.run(); + } + return result; +} + +bool SPDesktopWidget::warnDialog (Glib::ustring const &text) +{ + Gtk::MessageDialog dialog (*window, text, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK_CANCEL); + gint response = dialog.run(); + if (response == Gtk::RESPONSE_OK) + return true; + else + return false; +} + +void +SPDesktopWidget::iconify() +{ + GtkWindow *topw = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(_canvas))); + if (GTK_IS_WINDOW(topw)) { + if (desktop->is_iconified()) { + gtk_window_deiconify(topw); + } else { + gtk_window_iconify(topw); + } + } +} + +void +SPDesktopWidget::maximize() +{ + GtkWindow *topw = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(_canvas))); + if (GTK_IS_WINDOW(topw)) { + if (desktop->is_maximized()) { + gtk_window_unmaximize(topw); + } else { + // Save geometry to prefs before maximizing so that + // something useful is stored there, because GTK doesn't maintain + // a separate non-maximized size. + if (!desktop->is_iconified() && !desktop->is_fullscreen()) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint w = -1; + gint h, x, y; + getWindowGeometry(x, y, w, h); + g_assert(w != -1); + prefs->setInt("/desktop/geometry/width", w); + prefs->setInt("/desktop/geometry/height", h); + prefs->setInt("/desktop/geometry/x", x); + prefs->setInt("/desktop/geometry/y", y); + } + gtk_window_maximize(topw); + } + } +} + +void +SPDesktopWidget::fullscreen() +{ + GtkWindow *topw = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(_canvas))); + if (GTK_IS_WINDOW(topw)) { + if (desktop->is_fullscreen()) { + gtk_window_unfullscreen(topw); + // widget layout is triggered by the resulting window_state_event + } else { + // Save geometry to prefs before maximizing so that + // something useful is stored there, because GTK doesn't maintain + // a separate non-maximized size. + if (!desktop->is_iconified() && !desktop->is_maximized()) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint w, h, x, y; + getWindowGeometry(x, y, w, h); + prefs->setInt("/desktop/geometry/width", w); + prefs->setInt("/desktop/geometry/height", h); + prefs->setInt("/desktop/geometry/x", x); + prefs->setInt("/desktop/geometry/y", y); + } + gtk_window_fullscreen(topw); + // widget layout is triggered by the resulting window_state_event + } + } +} + +/** + * Hide whatever the user does not want to see in the window + */ +void SPDesktopWidget::layoutWidgets() +{ + SPDesktopWidget *dtw = this; + Glib::ustring pref_root; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (dtw->desktop->is_focusMode()) { + pref_root = "/focus/"; + } else if (dtw->desktop->is_fullscreen()) { + pref_root = "/fullscreen/"; + } else { + pref_root = "/window/"; + } + + if (!prefs->getBool(pref_root + "commands/state", true)) { + gtk_widget_hide (dtw->commands_toolbox); + } else { + gtk_widget_show_all (dtw->commands_toolbox); + } + + if (!prefs->getBool(pref_root + "snaptoolbox/state", true)) { + gtk_widget_hide (dtw->snap_toolbox); + } else { + gtk_widget_show_all (dtw->snap_toolbox); + } + + if (!prefs->getBool(pref_root + "toppanel/state", true)) { + gtk_widget_hide (dtw->aux_toolbox); + } else { + // we cannot just show_all because that will show all tools' panels; + // this is a function from toolbox.cpp that shows only the current tool's panel + ToolboxFactory::showAuxToolbox(dtw->aux_toolbox); + } + + if (!prefs->getBool(pref_root + "toolbox/state", true)) { + gtk_widget_hide (dtw->tool_toolbox); + } else { + gtk_widget_show_all (dtw->tool_toolbox); + } + + if (!prefs->getBool(pref_root + "statusbar/state", true)) { + dtw->_statusbar->hide(); + } else { + dtw->_statusbar->show_all(); + } + + if (!prefs->getBool(pref_root + "panels/state", true)) { + dtw->_panels->hide(); + } else { + dtw->_panels->show_all(); + } + + if (!prefs->getBool(pref_root + "scrollbars/state", true)) { + dtw->_hscrollbar->hide(); + dtw->_vscrollbar_box->hide(); + dtw->_cms_adjust->hide(); + } else { + dtw->_hscrollbar->show_all(); + dtw->_vscrollbar_box->show_all(); + dtw->_cms_adjust->show_all(); + } + + if (!prefs->getBool(pref_root + "rulers/state", true)) { + dtw->_guides_lock->hide(); + dtw->_hruler->hide(); + dtw->_vruler->hide(); + } else { + dtw->_guides_lock->show_all(); + dtw->_hruler->show_all(); + dtw->_vruler->show_all(); + } +} + +Gtk::Toolbar * +SPDesktopWidget::get_toolbar_by_name(const Glib::ustring& name) +{ + // The name is actually attached to the GtkGrid that contains + // the toolbar, so we need to get the grid first + auto widget = sp_search_by_name_recursive(Glib::wrap(aux_toolbox), name); + auto grid = dynamic_cast<Gtk::Grid*>(widget); + + if (!grid) return nullptr; + + auto child = grid->get_child_at(0,0); + auto tb = dynamic_cast<Gtk::Toolbar*>(child); + + return tb; +} + +void +SPDesktopWidget::setToolboxFocusTo (const gchar* label) +{ + // First try looking for a named widget + auto hb = sp_search_by_name_recursive(Glib::wrap(aux_toolbox), label); + + // Fallback to looking for a named data member (deprecated) + if (!hb) { + hb = Glib::wrap(GTK_WIDGET(sp_search_by_data_recursive(aux_toolbox, (gpointer) label))); + } + + if (hb) + { + hb->grab_focus(); + } +} + +void +SPDesktopWidget::setToolboxAdjustmentValue (gchar const *id, double value) +{ + // First try looking for a named widget + auto hb = sp_search_by_name_recursive(Glib::wrap(aux_toolbox), id); + + // Fallback to looking for a named data member (deprecated) + if (!hb) { + hb = Glib::wrap(GTK_WIDGET(sp_search_by_data_recursive(aux_toolbox, (gpointer)id))); + } + + if (hb) { + auto sb = dynamic_cast<Inkscape::UI::Widget::SpinButtonToolItem *>(hb); + auto a = sb->get_adjustment(); + + if(a) a->set_value(value); + } + + else g_warning ("Could not find GtkAdjustment for %s\n", id); +} + + +bool +SPDesktopWidget::isToolboxButtonActive (const gchar* id) +{ + bool isActive = false; + gpointer thing = sp_search_by_data_recursive(aux_toolbox, (gpointer) id); + if ( !thing ) { + //g_message( "Unable to locate item for {%s}", id ); + } else if ( GTK_IS_TOGGLE_BUTTON(thing) ) { + GtkToggleButton *b = GTK_TOGGLE_BUTTON(thing); + isActive = gtk_toggle_button_get_active( b ) != 0; + } else if ( GTK_IS_TOGGLE_ACTION(thing) ) { + GtkToggleAction* act = GTK_TOGGLE_ACTION(thing); + isActive = gtk_toggle_action_get_active( act ) != 0; + } else if ( GTK_IS_TOGGLE_TOOL_BUTTON(thing) ) { + GtkToggleToolButton *b = GTK_TOGGLE_TOOL_BUTTON(thing); + isActive = gtk_toggle_tool_button_get_active( b ) != 0; + } else { + //g_message( "Item for {%s} is of an unsupported type", id ); + } + + return isActive; +} + +void SPDesktopWidget::setToolboxPosition(Glib::ustring const& id, GtkPositionType pos) +{ + // Note - later on these won't be individual member variables. + GtkWidget* toolbox = nullptr; + if (id == "ToolToolbar") { + toolbox = tool_toolbox; + } else if (id == "AuxToolbar") { + toolbox = aux_toolbox; + } else if (id == "CommandsToolbar") { + toolbox = commands_toolbox; + } else if (id == "SnapToolbar") { + toolbox = snap_toolbox; + } + + + if (toolbox) { + switch(pos) { + case GTK_POS_TOP: + case GTK_POS_BOTTOM: + if ( gtk_widget_is_ancestor(toolbox, GTK_WIDGET(_hbox->gobj())) ) { + // Removing a widget can reduce ref count to zero + g_object_ref(G_OBJECT(toolbox)); + _hbox->remove(*Glib::wrap(toolbox)); + _vbox->add(*Glib::wrap(toolbox)); + g_object_unref(G_OBJECT(toolbox)); + + // Function doesn't seem to be in Gtkmm wrapper yet + gtk_box_set_child_packing(_vbox->gobj(), toolbox, FALSE, TRUE, 0, GTK_PACK_START); + } + ToolboxFactory::setOrientation(toolbox, GTK_ORIENTATION_HORIZONTAL); + break; + case GTK_POS_LEFT: + case GTK_POS_RIGHT: + if ( !gtk_widget_is_ancestor(toolbox, GTK_WIDGET(_hbox->gobj())) ) { + g_object_ref(G_OBJECT(toolbox)); + _vbox->remove(*Glib::wrap(toolbox)); + _hbox->add(*Glib::wrap(toolbox)); + g_object_unref(G_OBJECT(toolbox)); + + // Function doesn't seem to be in Gtkmm wrapper yet + gtk_box_set_child_packing(_hbox->gobj(), toolbox, FALSE, TRUE, 0, GTK_PACK_START); + if (pos == GTK_POS_LEFT) { + _hbox->reorder_child(*Glib::wrap(toolbox), 0 ); + } + } + ToolboxFactory::setOrientation(toolbox, GTK_ORIENTATION_VERTICAL); + break; + } + } +} + + +SPDesktopWidget *sp_desktop_widget_new(SPDocument *document) +{ + SPDesktopWidget* dtw = SPDesktopWidget::createInstance(document); + return dtw; +} + +SPDesktopWidget* SPDesktopWidget::createInstance(SPDocument *document) +{ + SPDesktopWidget *dtw = static_cast<SPDesktopWidget*>(g_object_new(SP_TYPE_DESKTOP_WIDGET, nullptr)); + + SPNamedView *namedview = sp_document_namedview(document, nullptr); + + dtw->_dt2r = 1. / namedview->display_units->factor; + + dtw->_ruler_origin = Geom::Point(0,0); //namedview->gridorigin; Why was the grid origin used here? + + dtw->desktop = new SPDesktop(); + dtw->stub = new SPDesktopWidget::WidgetStub (dtw); + dtw->desktop->init (namedview, dtw->_canvas, dtw->stub); + INKSCAPE.add_desktop (dtw->desktop); + + // Add the shape geometry to libavoid for autorouting connectors. + // This needs desktop set for its spacing preferences. + init_avoided_shape_geometry(dtw->desktop); + + dtw->_selected_style->setDesktop(dtw->desktop); + + /* Once desktop is set, we can update rulers */ + dtw->update_rulers(); + + sp_view_widget_set_view (SP_VIEW_WIDGET (dtw), dtw->desktop); + + /* Listen on namedview modification */ + dtw->modified_connection = namedview->connectModified(sigc::mem_fun(*dtw, &SPDesktopWidget::namedviewModified)); + + dtw->layer_selector->setDesktop(dtw->desktop); + + // TEMP + dtw->_menubar = build_menubar(dtw->desktop); + dtw->_menubar->set_name("MenuBar"); + dtw->_menubar->show_all(); + +#ifdef GDK_WINDOWING_QUARTZ + // native macOS menu: do this later because we don't have the window handle yet +#else + dtw->_vbox->pack_start(*dtw->_menubar, false, false); +#endif + + dtw->layoutWidgets(); + + std::vector<GtkWidget *> toolboxes; + toolboxes.push_back(dtw->tool_toolbox); + toolboxes.push_back(dtw->aux_toolbox); + toolboxes.push_back(dtw->commands_toolbox); + toolboxes.push_back(dtw->snap_toolbox); + + dtw->_panels->setDesktop( dtw->desktop ); + + UXManager::getInstance()->addTrack(dtw); + UXManager::getInstance()->connectToDesktop( toolboxes, dtw->desktop ); + + return dtw; +} + + +void +SPDesktopWidget::update_rulers() +{ + Geom::Rect viewbox = desktop->get_display_area(true); + // "true" means: Use integer values of the canvas for calculating the display area, similar + // to the integer values used for positioning the grid lines. (see SPCanvas::scrollTo(), + // where ix and iy are rounded integer values; these values are stored in SPCanvasBuf->rect, + // and used for drawing the grid). By using the integer values here too, the ruler ticks + // will be perfectly aligned to the grid + + double lower_x = _dt2r * (viewbox.left() - _ruler_origin[Geom::X]); + double upper_x = _dt2r * (viewbox.right() - _ruler_origin[Geom::X]); + _hruler->set_range(lower_x, upper_x); + + double lower_y = _dt2r * (viewbox.bottom() - _ruler_origin[Geom::Y]); + double upper_y = _dt2r * (viewbox.top() - _ruler_origin[Geom::Y]); + if (desktop->is_yaxisdown()) { + std::swap(lower_y, upper_y); + } + _vruler->set_range(lower_y, upper_y); +} + + +void SPDesktopWidget::namedviewModified(SPObject *obj, guint flags) +{ + SPNamedView *nv=SP_NAMEDVIEW(obj); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + _dt2r = 1. / nv->display_units->factor; + _ruler_origin = Geom::Point(0,0); //nv->gridorigin; Why was the grid origin used here? + + _vruler->set_unit(nv->getDisplayUnit()); + _hruler->set_unit(nv->getDisplayUnit()); + + /* This loops through all the grandchildren of aux toolbox, + * and for each that it finds, it performs an sp_search_by_data_recursive(), + * looking for widgets that hold some "tracker" data (this is used by + * all toolboxes to refer to the unit selector). The default document units + * is then selected within these unit selectors. + * + * Of course it would be nice to be able to refer to the toolbox and the + * unit selector directly by name, but I don't yet see a way to do that. + * + * This should solve: https://bugs.launchpad.net/inkscape/+bug/362995 + */ + if (GTK_IS_CONTAINER(aux_toolbox)) { + std::vector<Gtk::Widget*> ch = Glib::wrap(GTK_CONTAINER(aux_toolbox))->get_children(); + for (auto i:ch) { + if (GTK_IS_CONTAINER(i->gobj())) { + std::vector<Gtk::Widget*> grch = dynamic_cast<Gtk::Container*>(i)->get_children(); + for (auto j:grch) { + + if (!GTK_IS_WIDGET(j->gobj())) // wasn't a widget + continue; + + // Don't apply to text toolbar. We want to be able to + // use different units for text. (Bug 1562217) + const Glib::ustring name = j->get_name(); + if ( name == "TextToolbar" || name == "MeasureToolbar") + continue; + + gpointer t = sp_search_by_data_recursive(GTK_WIDGET(j->gobj()), (gpointer) "unit-tracker"); + if (t == nullptr) // didn't find any tracker data + continue; + + UnitTracker *tracker = reinterpret_cast<UnitTracker*>( t ); + if (tracker == nullptr) // it's null when inkscape is first opened + continue; + + tracker->setActiveUnit( nv->display_units ); + } // grandchildren + } // if child is a container + } // children + } // if aux_toolbox is a container + + _hruler_box->set_tooltip_text(gettext(nv->display_units->name_plural.c_str())); + _vruler_box->set_tooltip_text(gettext(nv->display_units->name_plural.c_str())); + + update_rulers(); + ToolboxFactory::updateSnapToolbox(this->desktop, nullptr, this->snap_toolbox); + } +} + +void +SPDesktopWidget::on_adjustment_value_changed() +{ + if (update) + return; + + update = 1; + + // Do not call canvas->scrollTo directly... messes up 'offset'. + desktop->scroll_absolute( Geom::Point(_hadj->get_value(), + _vadj->get_value()), false); + + update = 0; +} + +/* we make the desktop window with focus active, signal is connected in interface.c */ +bool SPDesktopWidget::onFocusInEvent(GdkEventFocus*) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/bitmapautoreload/value", true)) { + std::vector<SPObject *> imageList = (desktop->doc())->getResourceList("image"); + for (auto it : imageList) { + SPImage* image = SP_IMAGE(it); + image->refresh_if_outdated(); + } + } + + INKSCAPE.activate_desktop (desktop); + + return false; +} + +// ------------------------ Zoom ------------------------ +static gdouble +sp_dtw_zoom_value_to_display (gdouble value) +{ + return floor (10 * (pow (2, value) * 100.0 + 0.05)) / 10; +} + +static gdouble +sp_dtw_zoom_display_to_value (gdouble value) +{ + return log (value / 100.0) / log (2); +} + +int +SPDesktopWidget::zoom_input(double *new_val) +{ + gchar *b = g_strdup(_zoom_status->get_text().c_str()); + + gchar *comma = g_strstr_len (b, -1, ","); + if (comma) { + *comma = '.'; + } + + char *oldlocale = g_strdup (setlocale(LC_NUMERIC, nullptr)); + setlocale (LC_NUMERIC, "C"); + gdouble new_typed = atof (b); + setlocale (LC_NUMERIC, oldlocale); + g_free (oldlocale); + g_free (b); + + *new_val = sp_dtw_zoom_display_to_value (new_typed); + return TRUE; +} + +bool +SPDesktopWidget::zoom_output() +{ + gchar b[64]; + double val = sp_dtw_zoom_value_to_display (_zoom_status->get_value()); + if (val < 10) { + g_snprintf (b, 64, "%4.1f%%", val); + } else { + g_snprintf (b, 64, "%4.0f%%", val); + } + _zoom_status->set_text(b); + return true; +} + +void +SPDesktopWidget::zoom_value_changed() +{ + double const zoom_factor = pow (2, _zoom_status->get_value()); + + // Zoom around center of window + Geom::Rect const d_canvas = desktop->getCanvas()->getViewbox(); + Geom::Point midpoint = desktop->w2d(d_canvas.midpoint()); + _zoom_status_value_changed_connection.block(); + desktop->zoom_absolute_center_point (midpoint, zoom_factor); + _zoom_status_value_changed_connection.unblock(); + + spinbutton_defocus(GTK_WIDGET(_zoom_status->gobj())); +} + +void +SPDesktopWidget::zoom_menu_handler(double factor) +{ + Geom::Rect const d = desktop->get_display_area(); + desktop->zoom_absolute_center_point (d.midpoint(), factor); +} + +void +SPDesktopWidget::zoom_populate_popup(Gtk::Menu *menu) +{ + for ( auto iter : menu->get_children()) { + menu->remove(*iter); + } + + auto item_1000 = Gtk::manage(new Gtk::MenuItem("1000%")); + auto item_500 = Gtk::manage(new Gtk::MenuItem("500%")); + auto item_200 = Gtk::manage(new Gtk::MenuItem("200%")); + auto item_100 = Gtk::manage(new Gtk::MenuItem("100%")); + auto item_50 = Gtk::manage(new Gtk::MenuItem( "50%")); + auto item_25 = Gtk::manage(new Gtk::MenuItem( "25%")); + auto item_10 = Gtk::manage(new Gtk::MenuItem( "10%")); + + item_1000->signal_activate().connect(sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 10.00)); + item_500->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 5.00)); + item_200->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 2.00)); + item_100->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 1.00)); + item_50->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 0.50)); + item_25->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 0.25)); + item_10->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 0.10)); + + menu->append(*item_1000); + menu->append(*item_500); + menu->append(*item_200); + menu->append(*item_100); + menu->append(*item_50); + menu->append(*item_25); + menu->append(*item_10); + + auto sep = Gtk::manage(new Gtk::SeparatorMenuItem()); + menu->append(*sep); + + auto item_page = Gtk::manage(new Gtk::MenuItem(_("Page"))); + item_page->signal_activate().connect(sigc::mem_fun(desktop, &SPDesktop::zoom_page)); + menu->append(*item_page); + + auto item_drawing = Gtk::manage(new Gtk::MenuItem(_("Drawing"))); + item_drawing->signal_activate().connect(sigc::mem_fun(desktop, &SPDesktop::zoom_drawing)); + menu->append(*item_drawing); + + auto item_selection = Gtk::manage(new Gtk::MenuItem(_("Selection"))); + item_selection->signal_activate().connect(sigc::mem_fun(desktop, &SPDesktop::zoom_selection)); + menu->append(*item_selection); + + auto item_center_page = Gtk::manage(new Gtk::MenuItem(_("Centre Page"))); + item_center_page->signal_activate().connect(sigc::mem_fun(desktop, &SPDesktop::zoom_center_page)); + menu->append(*item_center_page); + + menu->show_all(); +} + + +void +SPDesktopWidget::sticky_zoom_toggled() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/stickyzoom/value", _sticky_zoom->get_active()); +} + + +void +SPDesktopWidget::update_zoom() +{ + _zoom_status_value_changed_connection.block(); + _zoom_status->set_value(log(desktop->current_zoom()) / log(2)); + _zoom_status->queue_draw(); + _zoom_status_value_changed_connection.unblock(); +} + + +// ---------------------- Rotation ------------------------ +int +SPDesktopWidget::rotation_input(double *new_val) +{ + auto *b = g_strdup(_rotation_status->get_text().c_str()); + + gchar *comma = g_strstr_len (b, -1, ","); + if (comma) { + *comma = '.'; + } + + char *oldlocale = g_strdup (setlocale(LC_NUMERIC, nullptr)); + setlocale (LC_NUMERIC, "C"); + gdouble new_value = atof (b); + setlocale (LC_NUMERIC, oldlocale); + g_free (oldlocale); + g_free (b); + + *new_val = new_value; + return true; +} + +bool +SPDesktopWidget::rotation_output() +{ + gchar b[64]; + double val = _rotation_status->get_value(); + + if (val < -180) val += 360; + if (val > 180) val -= 360; + + g_snprintf (b, 64, "%7.2f°", val); + + _rotation_status->set_text(b); + return true; +} + +void +SPDesktopWidget::rotation_value_changed() +{ + double const rotate_factor = M_PI / 180.0 * _rotation_status->get_value(); + // std::cout << "SPDesktopWidget::rotation_value_changed: " + // << _rotation_status->get_value() + // << " (" << rotate_factor << ")" <<std::endl; + + // Rotate around center of window + Geom::Rect const d_canvas = desktop->getCanvas()->getViewbox(); + _rotation_status_value_changed_connection.block(); + Geom::Point midpoint = desktop->w2d(d_canvas.midpoint()); + desktop->rotate_absolute_center_point (midpoint, rotate_factor); + _rotation_status_value_changed_connection.unblock(); + + spinbutton_defocus(GTK_WIDGET(_rotation_status->gobj())); +} + +void +SPDesktopWidget::rotation_populate_popup(Gtk::Menu *menu) +{ + for ( auto iter : menu->get_children()) { + menu->remove(*iter); + } + + auto item_m135 = Gtk::manage(new Gtk::MenuItem("-135°")); + auto item_m90 = Gtk::manage(new Gtk::MenuItem( "-90°")); + auto item_m45 = Gtk::manage(new Gtk::MenuItem( "-45°")); + auto item_0 = Gtk::manage(new Gtk::MenuItem( "0°")); + auto item_p45 = Gtk::manage(new Gtk::MenuItem( "45°")); + auto item_p90 = Gtk::manage(new Gtk::MenuItem( "90°")); + auto item_p135 = Gtk::manage(new Gtk::MenuItem( "135°")); + auto item_p180 = Gtk::manage(new Gtk::MenuItem( "180°")); + + item_m135->signal_activate().connect(sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), -135)); + item_m90->signal_activate().connect( sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), -90)); + item_m45->signal_activate().connect( sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), -45)); + item_0->signal_activate().connect( sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), 0)); + item_p45->signal_activate().connect( sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), 45)); + item_p90->signal_activate().connect( sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), 90)); + item_p135->signal_activate().connect(sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), 135)); + item_p180->signal_activate().connect(sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), 180)); + + menu->append(*item_m135); + menu->append(*item_m90); + menu->append(*item_m45); + menu->append(*item_0); + menu->append(*item_p45); + menu->append(*item_p90); + menu->append(*item_p135); + menu->append(*item_p180); + + menu->show_all(); +} + + +void +SPDesktopWidget::update_rotation() +{ + _rotation_status_value_changed_connection.block(); + _rotation_status->set_value(desktop->current_rotation() / M_PI * 180.0); + _rotation_status->queue_draw(); + _rotation_status_value_changed_connection.unblock(); + +} + + +// --------------- Rulers/Scrollbars/Etc. ----------------- +void +SPDesktopWidget::toggle_rulers() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (_guides_lock->get_visible()) { + _guides_lock->hide(); + _hruler->hide(); + _vruler->hide(); + prefs->setBool(desktop->is_fullscreen() ? "/fullscreen/rulers/state" : "/window/rulers/state", false); + } else { + _guides_lock->show_all(); + _hruler->show_all(); + _vruler->show_all(); + prefs->setBool(desktop->is_fullscreen() ? "/fullscreen/rulers/state" : "/window/rulers/state", true); + } +} + +void +SPDesktopWidget::toggle_scrollbars() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (_hscrollbar->get_visible()) { + _hscrollbar->hide(); + _vscrollbar_box->hide(); + _cms_adjust->hide(); + prefs->setBool(desktop->is_fullscreen() ? "/fullscreen/scrollbars/state" : "/window/scrollbars/state", false); + } else { + _hscrollbar->show_all(); + _vscrollbar_box->show_all(); + _cms_adjust->show_all(); + prefs->setBool(desktop->is_fullscreen() ? "/fullscreen/scrollbars/state" : "/window/scrollbars/state", true); + } +} + +bool +SPDesktopWidget::get_color_prof_adj_enabled() const +{ + return _cms_adjust->get_sensitive() && _cms_adjust->get_active(); +} + +void +SPDesktopWidget::toggle_color_prof_adj() +{ + if (_cms_adjust->get_sensitive()) { + if (_cms_adjust->get_active()) { + _cms_adjust->toggle_set_down(false); + } else { + _cms_adjust->toggle_set_down(true); + } + } +} + +static void +set_adjustment (Glib::RefPtr<Gtk::Adjustment> &adj, double l, double u, double ps, double si, double pi) +{ + if ((l != adj->get_lower()) || + (u != adj->get_upper()) || + (ps != adj->get_page_size()) || + (si != adj->get_step_increment()) || + (pi != adj->get_page_increment())) { + adj->set_lower(l); + adj->set_upper(u); + adj->set_page_size(ps); + adj->set_step_increment(si); + adj->set_page_increment(pi); + } +} + +void +SPDesktopWidget::update_scrollbars(double scale) +{ + if (update) return; + update = 1; + + /* The desktop region we always show unconditionally */ + SPDocument *doc = desktop->doc(); + Geom::Rect darea ( Geom::Point(-doc->getWidth().value("px"), -doc->getHeight().value("px")), + Geom::Point(2 * doc->getWidth().value("px"), 2 * doc->getHeight().value("px")) ); + + Geom::OptRect deskarea; + if (Inkscape::Preferences::get()->getInt("/tools/bounding_box") == 0) { + deskarea = darea | doc->getRoot()->desktopVisualBounds(); + } else { + deskarea = darea | doc->getRoot()->desktopGeometricBounds(); + } + + /* Canvas region we always show unconditionally */ + double const y_dir = desktop->yaxisdir(); + Geom::Rect carea( Geom::Point(deskarea->left() * scale - 64, (deskarea->top() * scale + 64) * y_dir), + Geom::Point(deskarea->right() * scale + 64, (deskarea->bottom() * scale - 64) * y_dir) ); + + Geom::Rect viewbox = _canvas->getViewbox(); + + /* Viewbox is always included into scrollable region */ + carea = Geom::unify(carea, viewbox); + + set_adjustment(_hadj, carea.min()[Geom::X], carea.max()[Geom::X], + viewbox.dimensions()[Geom::X], + 0.1 * viewbox.dimensions()[Geom::X], + viewbox.dimensions()[Geom::X]); + _hadj->set_value(viewbox.min()[Geom::X]); + + set_adjustment(_vadj, carea.min()[Geom::Y], carea.max()[Geom::Y], + viewbox.dimensions()[Geom::Y], + 0.1 * viewbox.dimensions()[Geom::Y], + viewbox.dimensions()[Geom::Y]); + _vadj->set_value(viewbox.min()[Geom::Y]); + + update = 0; +} + +bool +SPDesktopWidget::get_sticky_zoom_active() const +{ + return _sticky_zoom->get_active(); +} + +double +SPDesktopWidget::get_hruler_thickness() const +{ + auto allocation = _hruler->get_allocation(); + return allocation.get_height(); +} + +double +SPDesktopWidget::get_vruler_thickness() const +{ + auto allocation = _vruler->get_allocation(); + return allocation.get_width(); +} + +gint +SPDesktopWidget::ruler_event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw, bool horiz) +{ + switch (event->type) { + case GDK_BUTTON_PRESS: + dtw->on_ruler_box_button_press_event(&event->button, Glib::wrap(GTK_EVENT_BOX(widget)), horiz); + break; + case GDK_MOTION_NOTIFY: + dtw->on_ruler_box_motion_notify_event(&event->motion, Glib::wrap(GTK_EVENT_BOX(widget)), horiz); + break; + case GDK_BUTTON_RELEASE: + dtw->on_ruler_box_button_release_event(&event->button, Glib::wrap(GTK_EVENT_BOX(widget)), horiz); + break; + default: + break; + } + + return FALSE; +} + +bool +SPDesktopWidget::on_ruler_box_motion_notify_event(GdkEventMotion *event, Gtk::EventBox *widget, bool horiz) +{ + if (horiz) { + sp_event_context_snap_delay_handler(desktop->event_context, (gpointer) widget->gobj(), (gpointer) this, event, Inkscape::UI::Tools::DelayedSnapEvent::GUIDE_HRULER); + } + else { + sp_event_context_snap_delay_handler(desktop->event_context, (gpointer) widget->gobj(), (gpointer) this, event, Inkscape::UI::Tools::DelayedSnapEvent::GUIDE_VRULER); + } + + int wx, wy; + + GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas)); + + gint width, height; + + gdk_window_get_device_position(window, event->device, &wx, &wy, nullptr); + gdk_window_get_geometry(window, nullptr /*x*/, nullptr /*y*/, &width, &height); + + Geom::Point const event_win(wx, wy); + + if (_ruler_clicked) { + Geom::Point const event_w(sp_canvas_window_to_world(_canvas, event_win)); + Geom::Point event_dt(desktop->w2d(event_w)); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + if ( ( abs( (gint) event->x - _xp ) < tolerance ) + && ( abs( (gint) event->y - _yp ) < tolerance ) ) { + return false; + } + + _ruler_dragged = true; + + // explicitly show guidelines; if I draw a guide, I want them on + if ((horiz ? wy : wx) >= 0) { + desktop->namedview->setGuides(true); + } + + if (!(event->state & GDK_SHIFT_MASK)) { + ruler_snap_new_guide(desktop, _active_guide, event_dt, _normal); + } + sp_guideline_set_normal(SP_GUIDELINE(_active_guide), _normal); + sp_guideline_set_position(SP_GUIDELINE(_active_guide), event_dt); + + desktop->set_coordinate_status(event_dt); + } + + return false; +} + +bool +SPDesktopWidget::on_ruler_box_button_release_event(GdkEventButton *event, Gtk::EventBox *widget, bool horiz) +{ + int wx, wy; + + GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas)); + + gint width, height; + + gdk_window_get_device_position(window, event->device, &wx, &wy, nullptr); + gdk_window_get_geometry(window, nullptr /*x*/, nullptr /*y*/, &width, &height); + + Geom::Point const event_win(wx, wy); + + if (_ruler_clicked && event->button == 1) { + sp_event_context_discard_delayed_snap_event(desktop->event_context); + + auto seat = gdk_device_get_seat(event->device); + gdk_seat_ungrab(seat); + + Geom::Point const event_w(sp_canvas_window_to_world(_canvas, event_win)); + Geom::Point event_dt(desktop->w2d(event_w)); + + if (!(event->state & GDK_SHIFT_MASK)) { + ruler_snap_new_guide(desktop, _active_guide, event_dt, _normal); + } + + sp_canvas_item_destroy(_active_guide); + _active_guide = nullptr; + if ((horiz ? wy : wx) >= 0) { + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("sodipodi:guide"); + + // If root viewBox set, interpret guides in terms of viewBox (90/96) + double newx = event_dt.x(); + double newy = event_dt.y(); + + // <sodipodi:guide> stores inverted y-axis coordinates + if (desktop->is_yaxisdown()) { + newy = desktop->doc()->getHeight().value("px") - newy; + _normal[Geom::Y] *= -1.0; + } + + SPRoot *root = desktop->doc()->getRoot(); + if( root->viewBox_set ) { + newx = newx * root->viewBox.width() / root->width.computed; + newy = newy * root->viewBox.height() / root->height.computed; + } + sp_repr_set_point(repr, "position", Geom::Point( newx, newy )); + sp_repr_set_point(repr, "orientation", _normal); + desktop->namedview->appendChild(repr); + Inkscape::GC::release(repr); + DocumentUndo::done(desktop->getDocument(), SP_VERB_NONE, + _("Create guide")); + } + desktop->set_coordinate_status(event_dt); + + if (!_ruler_dragged) { + // Ruler click (without drag) toggle the guide visibility on and off + Inkscape::XML::Node *repr = desktop->namedview->getRepr(); + sp_namedview_toggle_guides(desktop->getDocument(), desktop->namedview); + } + + _ruler_clicked = false; + _ruler_dragged = false; + } + + return false; +} + +bool +SPDesktopWidget::on_ruler_box_button_press_event(GdkEventButton *event, Gtk::EventBox *widget, bool horiz) +{ + if (_ruler_clicked) // event triggerred on a double click: do no process the click + return false; + + int wx, wy; + + GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas)); + + gint width, height; + + gdk_window_get_device_position(window, event->device, &wx, &wy, nullptr); + gdk_window_get_geometry(window, nullptr /*x*/, nullptr /*y*/, &width, &height); + + Geom::Point const event_win(wx, wy); + + if (event->button == 1) { + _ruler_clicked = true; + _ruler_dragged = false; + // save click origin + _xp = (gint) event->x; + _yp = (gint) event->y; + + Geom::Point const event_w(sp_canvas_window_to_world(_canvas, event_win)); + Geom::Point const event_dt(desktop->w2d(event_w)); + + // calculate the normal of the guidelines when dragged from the edges of rulers. + auto const y_dir = desktop->yaxisdir(); + Geom::Point normal_bl_to_tr(1., y_dir); //bottomleft to topright + Geom::Point normal_tr_to_bl(-1., y_dir); //topright to bottomleft + normal_bl_to_tr.normalize(); + normal_tr_to_bl.normalize(); + Inkscape::CanvasGrid * grid = sp_namedview_get_first_enabled_grid(desktop->namedview); + if (grid){ + if (grid->getGridType() == Inkscape::GRID_AXONOMETRIC ) { + Inkscape::CanvasAxonomGrid *axonomgrid = dynamic_cast<Inkscape::CanvasAxonomGrid *>(grid); + if (event->state & GDK_CONTROL_MASK) { + // guidelines normal to gridlines + normal_bl_to_tr = Geom::Point::polar(-axonomgrid->angle_rad[0], 1.0); + normal_tr_to_bl = Geom::Point::polar(axonomgrid->angle_rad[2], 1.0); + } else { + normal_bl_to_tr = rot90(Geom::Point::polar(axonomgrid->angle_rad[2], 1.0)); + normal_tr_to_bl = rot90(Geom::Point::polar(-axonomgrid->angle_rad[0], 1.0)); + } + } + } + if (horiz) { + if (wx < 50) { + _normal = normal_bl_to_tr; + } else if (wx > width - 50) { + _normal = normal_tr_to_bl; + } else { + _normal = Geom::Point(0.,1.); + } + } else { + if (wy < 50) { + _normal = normal_bl_to_tr; + } else if (wy > height - 50) { + _normal = normal_tr_to_bl; + } else { + _normal = Geom::Point(1.,0.); + } + } + + _active_guide = sp_guideline_new(desktop->guides, nullptr, event_dt, _normal); + sp_guideline_set_color(SP_GUIDELINE(_active_guide), desktop->namedview->guidehicolor); + + auto window = widget->get_window()->gobj(); + + auto seat = gdk_device_get_seat(event->device); + gdk_seat_grab(seat, + window, + GDK_SEAT_CAPABILITY_ALL_POINTING, + FALSE, + nullptr, + (GdkEvent*)event, + nullptr, + nullptr); + } + + return false; +} + +GtkAllocation +SPDesktopWidget::get_canvas_allocation() const +{ + GtkAllocation allocation; + gtk_widget_get_allocation(GTK_WIDGET(_canvas), &allocation); + return allocation; +} + +void +SPDesktopWidget::ruler_snap_new_guide(SPDesktop *desktop, SPCanvasItem * /*guide*/, Geom::Point &event_dt, Geom::Point &normal) +{ + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + // We're dragging a brand new guide, just pulled of the rulers seconds ago. When snapping to a + // path this guide will change it slope to become either tangential or perpendicular to that path. It's + // therefore not useful to try tangential or perpendicular snapping, so this will be disabled temporarily + bool pref_perp = m.snapprefs.getSnapPerp(); + bool pref_tang = m.snapprefs.getSnapTang(); + m.snapprefs.setSnapPerp(false); + m.snapprefs.setSnapTang(false); + // We only have a temporary guide which is not stored in our document yet. + // Because the guide snapper only looks in the document for guides to snap to, + // we don't have to worry about a guide snapping to itself here + Geom::Point normal_orig = normal; + m.guideFreeSnap(event_dt, normal, false, false); + // After snapping, both event_dt and normal have been modified accordingly; we'll take the normal (of the + // curve we snapped to) to set the normal the guide. And rotate it by 90 deg. if needed + if (pref_perp) { // Perpendicular snapping to paths is requested by the user, so let's do that + if (normal != normal_orig) { + normal = Geom::rot90(normal); + } + } + if (!(pref_tang || pref_perp)) { // if we don't want to snap either perpendicularly or tangentially, then + normal = normal_orig; // we must restore the normal to it's original state + } + // Restore the preferences + m.snapprefs.setSnapPerp(pref_perp); + m.snapprefs.setSnapTang(pref_tang); + m.unSetup(); +} + +/* + 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 : diff --git a/src/widgets/desktop-widget.h b/src/widgets/desktop-widget.h new file mode 100644 index 0000000..e61cdd5 --- /dev/null +++ b/src/widgets/desktop-widget.h @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_DESKTOP_WIDGET_H +#define SEEN_SP_DESKTOP_WIDGET_H + +/** \file + * SPDesktopWidget: handling Gtk events on a desktop. + * + * Authors: + * Jon A. Cruz <jon@joncruz.org> (c) 2010 + * John Bintz <jcoswell@coswellproductions.org> (c) 2006 + * Ralf Stephan <ralf@ark.in-berlin.de> (c) 2005 + * Abhishek Sharma + * ? -2004 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <gtkmm.h> + +#include "message.h" +#include "ui/view/view-widget.h" +#include "ui/view/edit-widget-interface.h" + +#include <cstddef> +#include <sigc++/connection.h> +#include <2geom/point.h> + +// forward declaration +typedef struct _EgeColorProfTracker EgeColorProfTracker; +struct SPCanvas; +struct SPCanvasItem; +class SPDocument; +class SPDesktop; +struct SPDesktopWidget; +class SPObject; + +namespace Inkscape { +namespace UI { +namespace Dialog { +class SwatchesPanel; +} // namespace Dialog + +namespace Widget { +class Button; +class LayerSelector; +class SelectedStyle; +class Ruler; +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#define SP_TYPE_DESKTOP_WIDGET SPDesktopWidget::getType() +#define SP_DESKTOP_WIDGET(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_DESKTOP_WIDGET, SPDesktopWidget)) +#define SP_DESKTOP_WIDGET_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_DESKTOP_WIDGET, SPDesktopWidgetClass)) +#define SP_IS_DESKTOP_WIDGET(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_DESKTOP_WIDGET)) +#define SP_IS_DESKTOP_WIDGET_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_DESKTOP_WIDGET)) + +/** + * Create a new SPDesktopWidget + */ +SPDesktopWidget *sp_desktop_widget_new(SPDocument* document); + +void sp_desktop_widget_show_decorations(SPDesktopWidget *dtw, gboolean show); +void sp_desktop_widget_update_hruler (SPDesktopWidget *dtw); +void sp_desktop_widget_update_vruler (SPDesktopWidget *dtw); + +/* Show/hide rulers & scrollbars */ +void sp_desktop_widget_update_scrollbars (SPDesktopWidget *dtw, double scale); + +void sp_dtw_desktop_activate (SPDesktopWidget *dtw); +void sp_dtw_desktop_deactivate (SPDesktopWidget *dtw); + +/// A GtkEventBox on an SPDesktop. +struct SPDesktopWidget { + SPViewWidget viewwidget; + + unsigned int update : 1; + + sigc::connection modified_connection; + + SPDesktop *desktop; + + Gtk::Window *window; + + static void dispose(GObject *object); + +private: + // Flags for ruler event handling + bool _ruler_clicked; ///< True if the ruler has been clicked + bool _ruler_dragged; ///< True if a drag on the ruler is occurring + + SPCanvasItem *_active_guide; ///< The guide currently being handled during a ruler event + Geom::Point _normal; ///< Normal to the guide currently being handled during ruler event + int _xp; ///< x coordinate for start of drag + int _yp; ///< y coordinate for start of drag + + // The root vbox of the window layout. + Gtk::Box *_vbox; + + Gtk::Box *_hbox; + + Gtk::MenuBar *_menubar; // TEMP + Gtk::Box *_statusbar; + + Inkscape::UI::Dialog::SwatchesPanel *_panels; + + Glib::RefPtr<Gtk::Adjustment> _hadj; + Glib::RefPtr<Gtk::Adjustment> _vadj; + + Gtk::ToggleButton *_guides_lock; + + Inkscape::UI::Widget::Button *_cms_adjust; + Gtk::ToggleButton *_sticky_zoom; + Gtk::Grid *_coord_status; + + Gtk::Label *_coord_status_x; + Gtk::Label *_coord_status_y; + Gtk::SpinButton *_zoom_status; + sigc::connection _zoom_status_input_connection; + sigc::connection _zoom_status_output_connection; + sigc::connection _zoom_status_value_changed_connection; + sigc::connection _zoom_status_populate_popup_connection; + Gtk::Label *_select_status; + Gtk::SpinButton *_rotation_status; + + sigc::connection _rotation_status_input_connection; + sigc::connection _rotation_status_output_connection; + sigc::connection _rotation_status_value_changed_connection; + sigc::connection _rotation_status_populate_popup_connection; + + Inkscape::UI::Widget::Dock *_dock; + + Gtk::Scrollbar *_hscrollbar; + Gtk::Scrollbar *_vscrollbar; + Gtk::Box *_vscrollbar_box; + + Inkscape::UI::Widget::SelectedStyle *_selected_style; + + /** A table for displaying the canvas, rulers etc */ + Gtk::Grid *_canvas_tbl; + sigc::connection _canvas_tbl_size_allocate_connection; + + Gtk::EventBox *_hruler_box; + Gtk::EventBox *_vruler_box; // eventboxes for setting tooltips + + /* Rulers */ + Inkscape::UI::Widget::Ruler *_hruler; + Inkscape::UI::Widget::Ruler *_vruler; + Gtk::Allocation _allocation; + + unsigned int _interaction_disabled_counter; + + Geom::Point _ruler_origin; + double _dt2r; + + SPCanvas *_canvas; + +public: + Inkscape::UI::Widget::LayerSelector *layer_selector; + + EgeColorProfTracker* _tracker; + + struct WidgetStub : public Inkscape::UI::View::EditWidgetInterface { + SPDesktopWidget *_dtw; + WidgetStub (SPDesktopWidget* dtw) : _dtw(dtw) {} + + void setTitle (gchar const *uri) override + { _dtw->updateTitle (uri); } + Gtk::Window* getWindow() override + { return _dtw->window; } + + void layout() override { + _dtw->layoutWidgets(); + } + + void present() override + { _dtw->presentWindow(); } + void getGeometry (gint &x, gint &y, gint &w, gint &h) override + { _dtw->getWindowGeometry (x, y, w, h); } + void setSize (gint w, gint h) override + { _dtw->setWindowSize (w, h); } + void setPosition (Geom::Point p) override + { _dtw->setWindowPosition (p); } + void setTransient (void* p, int transient_policy) override + { _dtw->setWindowTransient (p, transient_policy); } + Geom::Point getPointer() override + { return _dtw->window_get_pointer(); } + void setIconified() override + { _dtw->iconify(); } + void setMaximized() override + { _dtw->maximize(); } + void setFullscreen() override + { _dtw->fullscreen(); } + bool shutdown() override + { return _dtw->shutdown(); } + void destroy() override + { + if(_dtw->window != nullptr) + delete _dtw->window; + _dtw->window = nullptr; + } + + void storeDesktopPosition() override { _dtw->storeDesktopPosition(); } + void requestCanvasUpdate() override { _dtw->requestCanvasUpdate(); } + void requestCanvasUpdateAndWait() override { _dtw->requestCanvasUpdateAndWait(); } + void enableInteraction() override { _dtw->enableInteraction(); } + void disableInteraction() override { _dtw->disableInteraction(); } + void activateDesktop() override { sp_dtw_desktop_activate(_dtw); } + void deactivateDesktop() override { sp_dtw_desktop_deactivate(_dtw); } + void updateRulers() override { _dtw->update_rulers(); } + void updateScrollbars(double scale) override { _dtw->update_scrollbars(scale); } + void toggleRulers() override { _dtw->toggle_rulers(); } + void toggleScrollbars() override { _dtw->toggle_scrollbars(); } + void toggleColorProfAdjust() override { _dtw->toggle_color_prof_adj(); } + bool colorProfAdjustEnabled() override { return _dtw->get_color_prof_adj_enabled(); } + void updateZoom() override { _dtw->update_zoom(); } + void letZoomGrabFocus() override { _dtw->letZoomGrabFocus(); } + void updateRotation() override { _dtw->update_rotation(); } + Gtk::Toolbar* get_toolbar_by_name(const Glib::ustring& name) override {return _dtw->get_toolbar_by_name(name);} + void setToolboxFocusTo(const gchar *id) override { _dtw->setToolboxFocusTo(id); } + void setToolboxAdjustmentValue(const gchar *id, double val) override + { _dtw->setToolboxAdjustmentValue (id, val); } + bool isToolboxButtonActive (gchar const* id) override + { return _dtw->isToolboxButtonActive (id); } + void setCoordinateStatus (Geom::Point p) override + { _dtw->setCoordinateStatus (p); } + void setMessage (Inkscape::MessageType type, gchar const* msg) override + { _dtw->setMessage (type, msg); } + + bool showInfoDialog( Glib::ustring const &message ) override { + return _dtw->showInfoDialog( message ); + } + + bool warnDialog (Glib::ustring const &text) override + { return _dtw->warnDialog (text); } + + Inkscape::UI::Widget::Dock* getDock () override + { return _dtw->getDock(); } + }; + + WidgetStub *stub; + + void setMessage(Inkscape::MessageType type, gchar const *message); + Geom::Point window_get_pointer(); + bool shutdown(); + void viewSetPosition (Geom::Point p); + void letZoomGrabFocus(); + void getWindowGeometry (gint &x, gint &y, gint &w, gint &h); + void setWindowPosition (Geom::Point p); + void setWindowSize (gint w, gint h); + void setWindowTransient (void *p, int transient_policy); + void presentWindow(); + bool showInfoDialog( Glib::ustring const &message ); + bool warnDialog (Glib::ustring const &text); + Gtk::Toolbar* get_toolbar_by_name(const Glib::ustring& name); + void setToolboxFocusTo (gchar const *); + void setToolboxAdjustmentValue (gchar const * id, double value); + bool isToolboxButtonActive (gchar const *id); + void setToolboxPosition(Glib::ustring const& id, GtkPositionType pos); + void setCoordinateStatus(Geom::Point p); + void storeDesktopPosition(); + void requestCanvasUpdate(); + void requestCanvasUpdateAndWait(); + void enableInteraction(); + void disableInteraction(); + void updateTitle(gchar const *uri); + bool onFocusInEvent(GdkEventFocus*); + + Gtk::MenuBar *menubar() { return _menubar; } + + Inkscape::UI::Widget::Dock* getDock(); + + static GType getType(); + static SPDesktopWidget* createInstance(SPDocument *document); + + void updateNamedview(); + void update_guides_lock(); + + /// Get the CMS adjustment button widget + decltype(_cms_adjust) get_cms_adjust() const {return _cms_adjust;} + + void cms_adjust_set_sensitive(bool enabled); + bool get_color_prof_adj_enabled() const; + void toggle_color_prof_adj(); + bool get_sticky_zoom_active() const; + void update_zoom(); + void update_rotation(); + void update_rulers(); + double get_hruler_thickness() const; + double get_vruler_thickness() const; + GtkAllocation get_canvas_allocation() const; + void iconify(); + void maximize(); + void fullscreen(); + static gint ruler_event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw, bool horiz); + + private: + GtkWidget *tool_toolbox; + GtkWidget *aux_toolbox; + GtkWidget *commands_toolbox; + GtkWidget *snap_toolbox; + + static void init(SPDesktopWidget *widget); + void layoutWidgets(); + + void namedviewModified(SPObject *obj, guint flags); + void on_adjustment_value_changed(); + void toggle_scrollbars(); + void update_scrollbars(double scale); + void toggle_rulers(); + void sticky_zoom_toggled(); + int zoom_input(double *new_val); + bool zoom_output(); + void zoom_value_changed(); + void zoom_menu_handler(double factor); + void zoom_populate_popup(Gtk::Menu *menu); + int rotation_input(double *new_val); + bool rotation_output(); + void rotation_value_changed(); + void rotation_populate_popup(Gtk::Menu *menu); + void canvas_tbl_size_allocate(Gtk::Allocation &allocation); + +#if defined(HAVE_LIBLCMS2) + static void cms_adjust_toggled( GtkWidget *button, gpointer data ); + static void color_profile_event(EgeColorProfTracker *tracker, SPDesktopWidget *dtw); +#endif + static void ruler_snap_new_guide(SPDesktop *desktop, SPCanvasItem *guide, Geom::Point &event_dt, Geom::Point &normal); + static gint event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw); + bool on_ruler_box_button_press_event(GdkEventButton *event, Gtk::EventBox *widget, bool horiz); + bool on_ruler_box_button_release_event(GdkEventButton *event, Gtk::EventBox *widget, bool horiz); + bool on_ruler_box_motion_notify_event(GdkEventMotion *event, Gtk::EventBox *widget, bool horiz); +}; + +/// The SPDesktopWidget vtable +struct SPDesktopWidgetClass { + SPViewWidgetClass parent_class; +}; + +#endif /* !SEEN_SP_DESKTOP_WIDGET_H */ + +/* + 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 : diff --git a/src/widgets/ege-paint-def.cpp b/src/widgets/ege-paint-def.cpp new file mode 100644 index 0000000..fa654cb --- /dev/null +++ b/src/widgets/ege-paint-def.cpp @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Eek Color Definition. + * + * The Initial Developer of the Original Code is + * Jon A. Cruz. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include <libintl.h> + +#include <cstdint> +#include <string> +#include <iostream> +#include <sstream> +#include <cstring> +#include <cstdio> +#include <utility> +#include <glibmm/i18n.h> +#include <glibmm/stringutils.h> + +#if !defined(_) +#define _(s) gettext(s) +#endif // !defined(_) + +#include "ege-paint-def.h" + +namespace ege +{ + +static std::string mimeTEXT("text/plain"); +static std::string mimeX_COLOR("application/x-color"); +static std::string mimeOSWB_COLOR("application/x-oswb-color"); + +PaintDef::PaintDef() : + descr(_("none")), + type(NONE), + r(0), + g(0), + b(0), + editable(false), + _listeners() +{ +} + +PaintDef::PaintDef( ColorType type ) : + descr(), + type(type), + r(0), + g(0), + b(0), + editable(false), + _listeners() +{ + switch (type) { + case CLEAR: + descr = _("remove"); + break; + case NONE: + descr = _("none"); + break; + case RGB: + descr = ""; + break; + } +} + +PaintDef::PaintDef( unsigned int r, unsigned int g, unsigned int b, std::string description ) : + descr(std::move(description)), + type(RGB), + r(r), + g(g), + b(b), + editable(false), + _listeners() +{ +} + +PaintDef::~PaintDef() += default; + +PaintDef::PaintDef( PaintDef const &other ) +{ + if ( this != &other ) { + *this = other; + } +} + +PaintDef& PaintDef::operator=( PaintDef const &other ) +{ + if ( this != & other ) + { + type = other.type; + r = other.r; + g = other.g; + b = other.b; + descr = other.descr; + editable = other.editable; + //TODO: _listeners should be assigned a value + } + return *this; +} + +class PaintDef::HookData { +public: + HookData( ColorCallback cb, void* data ) {_cb = cb; _data = data;} + ColorCallback _cb; + void* _data; +}; + + +std::vector<std::string> PaintDef::getMIMETypes() +{ + std::vector<std::string> listing; + listing.push_back(mimeOSWB_COLOR); + listing.push_back(mimeX_COLOR); + listing.push_back(mimeTEXT); + return listing; +} + +void PaintDef::getMIMEData(std::string const & type, char*& dest, int& len, int& format) +{ + if ( type == mimeTEXT ) { + dest = new char[8]; + snprintf( dest, 8, "#%02x%02x%02x", getR(), getG(), getB() ); + dest[7] = 0; + len = 8; + format = 8; + } else if ( type == mimeX_COLOR ) { + uint16_t* tmp = new uint16_t[4]; + tmp[0] = (getR() << 8) | getR(); + tmp[1] = (getG() << 8) | getG(); + tmp[2] = (getB() << 8) | getB(); + tmp[3] = 0xffff; + dest = reinterpret_cast<char*>(tmp); + len = 8; + format = 16; + } else if ( type == mimeOSWB_COLOR ) { + std::string tmp("<paint>"); + switch ( getType() ) { + case ege::PaintDef::NONE: + { + tmp += "<nocolor/>"; + } + break; + case ege::PaintDef::CLEAR: + { + tmp += "<clear/>"; + } + break; + default: + { + tmp += std::string("<color name=\"") + descr + "\">"; + tmp += "<sRGB r=\""; + tmp += Glib::Ascii::dtostr(getR()/255.0); + tmp += "\" g=\""; + tmp += Glib::Ascii::dtostr(getG()/255.0); + tmp += "\" b=\""; + tmp += Glib::Ascii::dtostr(getB()/255.0); + tmp += "\"/>"; + tmp += "</color>"; + } + } + tmp += "</paint>"; + len = tmp.size(); + dest = new char[len]; + // Note that this is not null-terminated: + memcpy(dest, tmp.c_str(), len); + format = 8; + } else { + // nothing + dest = nullptr; + len = 0; + } +} + +bool PaintDef::fromMIMEData(std::string const & type, char const * data, int len, int /*format*/) +{ + bool worked = false; + bool changed = false; + if ( type == mimeTEXT ) { + } else if ( type == mimeX_COLOR ) { + } else if ( type == mimeOSWB_COLOR ) { + std::string xml(data, len); + if ( xml.find("<nocolor/>") != std::string::npos ) { + if ( (this->type != ege::PaintDef::NONE) + || (this->r != 0) + || (this->g != 0) + || (this->b != 0) ) { + this->type = ege::PaintDef::NONE; + this->r = 0; + this->g = 0; + this->b = 0; + changed = true; + } + worked = true; + } else { + size_t pos = xml.find("<sRGB"); + if ( pos != std::string::npos ) { + size_t endPos = xml.find(">", pos); + std::string srgb = xml.substr(pos, endPos); + this->type = ege::PaintDef::RGB; + size_t numPos = srgb.find("r="); + if (numPos != std::string::npos) { + double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3)); + this->r = static_cast<int>(255 * dbl); + } + numPos = srgb.find("g="); + if (numPos != std::string::npos) { + double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3)); + this->g = static_cast<int>(255 * dbl); + } + numPos = srgb.find("b="); + if (numPos != std::string::npos) { + double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3)); + this->b = static_cast<int>(255 * dbl); + } + + size_t pos = xml.find("<color "); + if ( pos != std::string::npos ) { + size_t endPos = xml.find(">", pos); + std::string colorTag = xml.substr(pos, endPos); + + size_t namePos = colorTag.find("name="); + if (namePos != std::string::npos) { + char quote = colorTag[namePos + 5]; + endPos = colorTag.find(quote, namePos + 6); + descr = colorTag.substr(namePos + 6, endPos - (namePos + 6)); + } + } + changed = true; + worked = true; + } + } + } + if ( changed ) { + // beware of callbacks changing things + for (auto & _listener : _listeners) + { + if ( _listener->_cb ) + { + _listener->_cb( _listener->_data ); + } + } + } + return worked; +} + +void PaintDef::setRGB( unsigned int r, unsigned int g, unsigned int b ) +{ + if ( r != this->r || g != this->g || b != this->b ) { + this->r = r; + this->g = g; + this->b = b; + + // beware of callbacks changing things + for (auto & _listener : _listeners) + { + if ( _listener->_cb ) + { + _listener->_cb( _listener->_data ); + } + } + } +} + +void PaintDef::addCallback( ColorCallback cb, void* data ) +{ + _listeners.push_back( new HookData(cb, data) ); +} + +void PaintDef::removeCallback( ColorCallback /*cb*/, void* /*data*/ ) +{ +} + + +} // namespace ege + +/* + 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 : diff --git a/src/widgets/ege-paint-def.h b/src/widgets/ege-paint-def.h new file mode 100644 index 0000000..63643c5 --- /dev/null +++ b/src/widgets/ege-paint-def.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Eek Color Definition. + * + * The Initial Developer of the Original Code is + * Jon A. Cruz. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#ifndef SEEN_EGE_PAINT_DEF_H +#define SEEN_EGE_PAINT_DEF_H + +#include <string> +#include <vector> + +namespace ege +{ + +typedef void (*ColorCallback)( void* data ); + + +/** + * Pure data representation of a color definition. + */ +class PaintDef +{ +public: + enum ColorType{CLEAR, NONE, RGB}; + + PaintDef(); + PaintDef(ColorType type); + PaintDef( unsigned int r, unsigned int g, unsigned int b, std::string description ); + virtual ~PaintDef(); + + PaintDef( PaintDef const &other ); + virtual PaintDef& operator=( PaintDef const &other ); + + ColorType getType() const { return type; } + + std::vector<std::string> getMIMETypes(); + void getMIMEData(std::string const & type, char*& dest, int& len, int& format); + bool fromMIMEData(std::string const & type, char const * data, int len, int format); + + void setRGB( unsigned int r, unsigned int g, unsigned int b ); + unsigned int getR() const { return r; } + unsigned int getG() const { return g; } + unsigned int getB() const { return b; } + + void addCallback( ColorCallback cb, void* data ); + void removeCallback( ColorCallback cb, void* data ); + + bool isEditable() const { return editable; } + void setEditable( bool edit ) { editable = edit; } + + std::string descr; + +protected: + ColorType type; + unsigned int r; + unsigned int g; + unsigned int b; + bool editable; + +private: + class HookData; + + std::vector<HookData*> _listeners; +}; + + +} // namespace ege + +#endif // SEEN_EGE_PAINT_DEF_H + +/* + 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 : diff --git a/src/widgets/fill-n-stroke-factory.h b/src/widgets/fill-n-stroke-factory.h new file mode 100644 index 0000000..5ca9daa --- /dev/null +++ b/src/widgets/fill-n-stroke-factory.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_FILL_N_STROKE_FACTORY_H +#define SEEN_FILL_N_STROKE_FACTORY_H +/* Authors: + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "fill-or-stroke.h" + +namespace Gtk { +class Widget; +} + +namespace Inkscape { +namespace Widgets { + +Gtk::Widget *createStyleWidget( FillOrStroke kind ); +Gtk::Widget *createStrokeStyleWidget( ); + +} // namespace Widgets +} // namespace Inkscape + +#endif // !SEEN_FILL_N_STROKE_FACTORY_H + +/* + 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 : diff --git a/src/widgets/fill-style.cpp b/src/widgets/fill-style.cpp new file mode 100644 index 0000000..f35a148 --- /dev/null +++ b/src/widgets/fill-style.cpp @@ -0,0 +1,832 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Fill style widget. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noSP_FS_VERBOSE + +#include <gtkmm/box.h> +#include <glibmm/i18n.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "fill-n-stroke-factory.h" +#include "fill-style.h" +#include "gradient-chemistry.h" +#include "inkscape.h" +#include "selection.h" +#include "verbs.h" + +#include "object/sp-defs.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-text.h" +#include "style.h" + +#include "display/sp-canvas.h" + +#include "widgets/paint-selector.h" + + +// These can be deleted once we sort out the libart dependence. + +#define ART_WIND_RULE_NONZERO 0 + +/* Fill */ + + +Gtk::Widget *sp_fill_style_widget_new() +{ + return Inkscape::Widgets::createStyleWidget( FILL ); +} + + +namespace Inkscape { + +class FillNStroke : public Gtk::VBox +{ +public: + FillNStroke( FillOrStroke k ); + ~FillNStroke() override; + + void setFillrule( SPPaintSelector::FillRule mode ); + + void setDesktop(SPDesktop *desktop); + +private: + static void paintModeChangeCB(SPPaintSelector *psel, SPPaintSelector::Mode mode, FillNStroke *self); + static void paintChangedCB(SPPaintSelector *psel, FillNStroke *self); + static void paintDraggedCB(SPPaintSelector *psel, FillNStroke *self); + static gboolean dragDelayCB(gpointer data); + + static void fillruleChangedCB( SPPaintSelector *psel, SPPaintSelector::FillRule mode, FillNStroke *self ); + + void selectionModifiedCB(guint flags); + void eventContextCB(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *eventcontext); + + void dragFromPaint(); + void updateFromPaint(); + + void performUpdate(); + + FillOrStroke kind; + SPDesktop *desktop; + SPPaintSelector *psel; + guint32 lastDrag; + guint dragId; + bool update; + sigc::connection selectChangedConn; + sigc::connection subselChangedConn; + sigc::connection selectModifiedConn; + sigc::connection eventContextConn; +}; + +} // namespace Inkscape + +void sp_fill_style_widget_set_desktop(Gtk::Widget *widget, SPDesktop *desktop) +{ + Inkscape::FillNStroke *fs = dynamic_cast<Inkscape::FillNStroke*>(widget); + if (fs) { + fs->setDesktop(desktop); + } +} + +namespace Inkscape { + +/** + * Create the fill or stroke style widget, and hook up all the signals. + */ +Gtk::Widget *Inkscape::Widgets::createStyleWidget( FillOrStroke kind ) +{ + FillNStroke *filler = new FillNStroke(kind); + + return filler; +} + +FillNStroke::FillNStroke( FillOrStroke k ) : + Gtk::VBox(), + kind(k), + desktop(nullptr), + psel(nullptr), + lastDrag(0), + dragId(0), + update(false), + selectChangedConn(), + subselChangedConn(), + selectModifiedConn(), + eventContextConn() +{ + // Add and connect up the paint selector widget: + psel = sp_paint_selector_new(kind); + gtk_widget_show(GTK_WIDGET(psel)); + gtk_container_add(GTK_CONTAINER(gobj()), GTK_WIDGET(psel)); + g_signal_connect( G_OBJECT(psel), "mode_changed", + G_CALLBACK(paintModeChangeCB), + this ); + + g_signal_connect( G_OBJECT(psel), "dragged", + G_CALLBACK(paintDraggedCB), + this ); + + g_signal_connect( G_OBJECT(psel), "changed", + G_CALLBACK(paintChangedCB), + this ); + if (kind == FILL) { + g_signal_connect( G_OBJECT(psel), "fillrule_changed", + G_CALLBACK(fillruleChangedCB), + this ); + } + + performUpdate(); +} + +FillNStroke::~FillNStroke() +{ + if (dragId) { + g_source_remove(dragId); + dragId = 0; + } + psel = nullptr; + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + eventContextConn.disconnect(); +} + +/** + * On signal modified, invokes an update of the fill or stroke style paint object. + */ +void FillNStroke::selectionModifiedCB( guint flags ) +{ + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { +#ifdef SP_FS_VERBOSE + g_message("selectionModifiedCB(%d) on %p", flags, this); +#endif + performUpdate(); + } +} + +void FillNStroke::setDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (dragId) { + g_source_remove(dragId); + dragId = 0; + } + if (this->desktop) { + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + eventContextConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &FillNStroke::performUpdate))); + subselChangedConn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &FillNStroke::performUpdate))); + eventContextConn = desktop->connectEventContextChanged(sigc::hide(sigc::bind(sigc::mem_fun(*this, &FillNStroke::eventContextCB), (Inkscape::UI::Tools::ToolBase *)nullptr))); + + // Must check flags, so can't call performUpdate() directly. + selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &FillNStroke::selectionModifiedCB))); + } + performUpdate(); + } +} + +/** + * Listen to this "change in tool" event, in case a subselection tool (such as Gradient or Node) selection + * is changed back to a selection tool - especially needed for selected gradient stops. + */ +void FillNStroke::eventContextCB(SPDesktop * /*desktop*/, Inkscape::UI::Tools::ToolBase * /*eventcontext*/) +{ + performUpdate(); +} + + +/** + * Gets the active fill or stroke style property, then sets the appropriate + * color, alpha, gradient, pattern, etc. for the paint-selector. + * + * @param sel Selection to use, or NULL. + */ +void FillNStroke::performUpdate() +{ + if ( update || !desktop ) { + return; + } + + if ( dragId ) { + // local change; do nothing, but reset the flag + g_source_remove(dragId); + dragId = 0; + return; + } + + update = true; + + // create temporary style + SPStyle query(desktop->doc()); + + // query style from desktop into it. This returns a result flag and fills query with the style of subselection, if any, or selection + int result = sp_desktop_query_style(desktop, &query, (kind == FILL) ? QUERY_STYLE_PROPERTY_FILL : QUERY_STYLE_PROPERTY_STROKE); + + SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL); + SPIScale24 &targOpacity = *(kind == FILL ? query.fill_opacity.upcast() : query.stroke_opacity.upcast()); + + switch (result) { + case QUERY_STYLE_NOTHING: + { + /* No paint at all */ + psel->setMode(SPPaintSelector::MODE_EMPTY); + break; + } + + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: // TODO: treat this slightly differently, e.g. display "averaged" somewhere in paint selector + case QUERY_STYLE_MULTIPLE_SAME: + { + SPPaintSelector::Mode pselmode = SPPaintSelector::getModeForStyle(query, kind); + psel->setMode(pselmode); + + if (kind == FILL) { + psel->setFillrule(query.fill_rule.computed == ART_WIND_RULE_NONZERO? + SPPaintSelector::FILLRULE_NONZERO : SPPaintSelector::FILLRULE_EVENODD); + } + + if (targPaint.set && targPaint.isColor()) { + psel->setColorAlpha(targPaint.value.color, SP_SCALE24_TO_FLOAT(targOpacity.value)); + } else if (targPaint.set && targPaint.isPaintserver()) { + + SPPaintServer *server = (kind == FILL) ? query.getFillPaintServer() : query.getStrokePaintServer(); + + if (server) { + if (SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()) { + SPGradient *vector = SP_GRADIENT(server)->getVector(); + psel->setSwatch( vector ); + } else if (SP_IS_LINEARGRADIENT(server)) { + SPGradient *vector = SP_GRADIENT(server)->getVector(); + psel->setGradientLinear( vector ); + + SPLinearGradient *lg = SP_LINEARGRADIENT(server); + psel->setGradientProperties( lg->getUnits(), + lg->getSpread() ); + } else if (SP_IS_RADIALGRADIENT(server)) { + SPGradient *vector = SP_GRADIENT(server)->getVector(); + psel->setGradientRadial( vector ); + + SPRadialGradient *rg = SP_RADIALGRADIENT(server); + psel->setGradientProperties( rg->getUnits(), + rg->getSpread() ); +#ifdef WITH_MESH + } else if (SP_IS_MESHGRADIENT(server)) { + SPGradient *array = SP_GRADIENT(server)->getArray(); + psel->setGradientMesh( SP_MESHGRADIENT(array) ); + SPMeshGradient *mg = SP_MESHGRADIENT(server); + psel->updateMeshList( SP_MESHGRADIENT( array )); +#endif + } else if (SP_IS_PATTERN(server)) { + SPPattern *pat = SP_PATTERN(server)->rootPattern(); + psel->updatePatternList( pat ); + } + } + } + break; + } + + case QUERY_STYLE_MULTIPLE_DIFFERENT: + { + psel->setMode(SPPaintSelector::MODE_MULTIPLE); + break; + } + } + + update = false; +} + +/** + * When the mode is changed, invoke a regular changed handler. + */ +void FillNStroke::paintModeChangeCB( SPPaintSelector * /*psel*/, + SPPaintSelector::Mode /*mode*/, + FillNStroke *self ) +{ +#ifdef SP_FS_VERBOSE + g_message("paintModeChangeCB(psel, mode, self:%p)", self); +#endif + if (self && !self->update) { + self->updateFromPaint(); + } +} + +void FillNStroke::fillruleChangedCB( SPPaintSelector * /*psel*/, + SPPaintSelector::FillRule mode, + FillNStroke *self ) +{ + if (self) { + self->setFillrule(mode); + } +} + +void FillNStroke::setFillrule( SPPaintSelector::FillRule mode ) +{ + if (!update && desktop) { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-rule", (mode == SPPaintSelector::FILLRULE_EVENODD) ? "evenodd":"nonzero"); + + sp_desktop_set_style(desktop, css); + + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(desktop->doc(), SP_VERB_DIALOG_FILL_STROKE, + _("Change fill rule")); + } +} + +static gchar const *undo_F_label_1 = "fill:flatcolor:1"; +static gchar const *undo_F_label_2 = "fill:flatcolor:2"; + +static gchar const *undo_S_label_1 = "stroke:flatcolor:1"; +static gchar const *undo_S_label_2 = "stroke:flatcolor:2"; + +static gchar const *undo_F_label = undo_F_label_1; +static gchar const *undo_S_label = undo_S_label_1; + + +void FillNStroke::paintDraggedCB(SPPaintSelector * /*psel*/, FillNStroke *self) +{ +#ifdef SP_FS_VERBOSE + g_message("paintDraggedCB(psel, spw:%p)", self); +#endif + if (self && !self->update) { + self->dragFromPaint(); + } +} + + +gboolean FillNStroke::dragDelayCB(gpointer data) +{ + gboolean keepGoing = TRUE; + if (data) { + FillNStroke *self = reinterpret_cast<FillNStroke*>(data); + if (!self->update) { + if (self->dragId) { + g_source_remove(self->dragId); + self->dragId = 0; + + self->dragFromPaint(); + self->performUpdate(); + } + keepGoing = FALSE; + } + } else { + keepGoing = FALSE; + } + return keepGoing; +} + +/** + * This is called repeatedly while you are dragging a color slider, only for flat color + * modes. Previously it set the color in style but did not update the repr for efficiency, however + * this was flakey and didn't buy us almost anything. So now it does the same as _changed, except + * lumps all its changes for undo. + */ +void FillNStroke::dragFromPaint() +{ + if (!desktop || update) { + return; + } + + guint32 when = gtk_get_current_event_time(); + + // Don't attempt too many updates per second. + // Assume a base 15.625ms resolution on the timer. + if (!dragId && lastDrag && when && ((when - lastDrag) < 32)) { + // local change, do not update from selection + dragId = g_timeout_add_full(G_PRIORITY_DEFAULT, 33, dragDelayCB, this, nullptr); + } + + if (dragId) { + // previous local flag not cleared yet; + // this means dragged events come too fast, so we better skip this one to speed up display + // (it's safe to do this in any case) + return; + } + lastDrag = when; + + update = true; + + switch (psel->mode) { + case SPPaintSelector::MODE_SOLID_COLOR: + { + // local change, do not update from selection + dragId = g_timeout_add_full(G_PRIORITY_DEFAULT, 100, dragDelayCB, this, nullptr); + psel->setFlatColor( desktop, (kind == FILL) ? "fill" : "stroke", (kind == FILL) ? "fill-opacity" : "stroke-opacity" ); + DocumentUndo::maybeDone(desktop->doc(), (kind == FILL) ? undo_F_label : undo_S_label, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Set fill color") : _("Set stroke color")); + break; + } + + default: + g_warning( "file %s: line %d: Paint %d should not emit 'dragged'", + __FILE__, __LINE__, psel->mode ); + break; + } + update = false; +} + +/** +This is called (at least) when: +1 paint selector mode is switched (e.g. flat color -> gradient) +2 you finished dragging a gradient node and released mouse +3 you changed a gradient selector parameter (e.g. spread) +Must update repr. + */ +void FillNStroke::paintChangedCB( SPPaintSelector * /*psel*/, FillNStroke *self ) +{ +#ifdef SP_FS_VERBOSE + g_message("paintChangedCB(psel, spw:%p)", self); +#endif + if (self && !self->update) { + self->updateFromPaint(); + } +} + +void FillNStroke::updateFromPaint() +{ + if (!desktop) { + return; + } + update = true; + + SPDocument *document = desktop->getDocument(); + Inkscape::Selection *selection = desktop->getSelection(); + + std::vector<SPItem*> const items(selection->items().begin(), selection->items().end()); + + switch (psel->mode) { + case SPPaintSelector::MODE_EMPTY: + // This should not happen. + g_warning( "file %s: line %d: Paint %d should not emit 'changed'", + __FILE__, __LINE__, psel->mode); + break; + case SPPaintSelector::MODE_MULTIPLE: + // This happens when you switch multiple objects with different gradients to flat color; + // nothing to do here. + break; + + case SPPaintSelector::MODE_NONE: + { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, (kind == FILL) ? "fill" : "stroke", "none"); + + sp_desktop_set_style(desktop, css); + + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Remove fill") : _("Remove stroke")); + break; + } + + case SPPaintSelector::MODE_SOLID_COLOR: + { + if (kind == FILL) { + // FIXME: fix for GTK breakage, see comment in SelectedStyle::on_opacity_changed; here it results in losing release events + desktop->getCanvas()->forceFullRedrawAfterInterruptions(0); + } + + psel->setFlatColor( desktop, + (kind == FILL) ? "fill" : "stroke", + (kind == FILL) ? "fill-opacity" : "stroke-opacity" ); + DocumentUndo::maybeDone(desktop->getDocument(), (kind == FILL) ? undo_F_label : undo_S_label, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Set fill color") : _("Set stroke color")); + + if (kind == FILL) { + // resume interruptibility + desktop->getCanvas()->endForcedFullRedraws(); + } + + // on release, toggle undo_label so that the next drag will not be lumped with this one + if (undo_F_label == undo_F_label_1) { + undo_F_label = undo_F_label_2; + undo_S_label = undo_S_label_2; + } else { + undo_F_label = undo_F_label_1; + undo_S_label = undo_S_label_1; + } + + break; + } + + case SPPaintSelector::MODE_GRADIENT_LINEAR: + case SPPaintSelector::MODE_GRADIENT_RADIAL: + case SPPaintSelector::MODE_SWATCH: + if (!items.empty()) { + SPGradientType const gradient_type = ( psel->mode != SPPaintSelector::MODE_GRADIENT_RADIAL + ? SP_GRADIENT_TYPE_LINEAR + : SP_GRADIENT_TYPE_RADIAL ); + bool createSwatch = (psel->mode == SPPaintSelector::MODE_SWATCH); + + SPCSSAttr *css = nullptr; + if (kind == FILL) { + // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs + css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-opacity", "1.0"); + } + + SPGradient *vector = psel->getGradientVector(); + if (!vector) { + /* No vector in paint selector should mean that we just changed mode */ + + SPStyle query(desktop->doc()); + int result = objects_query_fillstroke(items, &query, kind == FILL); + if (result == QUERY_STYLE_MULTIPLE_SAME) { + SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL); + SPColor common; + if (!targPaint.isColor()) { + common = sp_desktop_get_color(desktop, kind == FILL); + } else { + common = targPaint.value.color; + } + vector = sp_document_default_gradient_vector( document, common, createSwatch ); + if ( vector && createSwatch ) { + vector->setSwatch(); + } + } + + for(auto item : items){ + //FIXME: see above + if (kind == FILL) { + sp_repr_css_change_recursive(item->getRepr(), css, "style"); + } + + if (!vector) { + SPGradient *gr = sp_gradient_vector_for_object( document, + desktop, + reinterpret_cast<SPObject*>(item), + (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE, + createSwatch ); + if ( gr && createSwatch ) { + gr->setSwatch(); + } + sp_item_set_gradient(item, + gr, + gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE); + } else { + sp_item_set_gradient(item, vector, gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE); + } + } + } else { + // We have changed from another gradient type, or modified spread/units within + // this gradient type. + vector = sp_gradient_ensure_vector_normalized(vector); + for(auto item : items){ + //FIXME: see above + if (kind == FILL) { + sp_repr_css_change_recursive(item->getRepr(), css, "style"); + } + + SPGradient *gr = sp_item_set_gradient(item, vector, gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE); + psel->pushAttrsToGradient( gr ); + } + } + + if (css) { + sp_repr_css_attr_unref(css); + css = nullptr; + } + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Set gradient on fill") : _("Set gradient on stroke")); + } + break; + +#ifdef WITH_MESH + case SPPaintSelector::MODE_GRADIENT_MESH: + + if (!items.empty()) { + SPGradientType const gradient_type = SP_GRADIENT_TYPE_MESH; + + SPCSSAttr *css = nullptr; + if (kind == FILL) { + // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs + css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-opacity", "1.0"); + } + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + SPDefs *defs = document->getDefs(); + + SPMeshGradient * mesh = psel->getMeshGradient(); + + for(auto item : items){ + + //FIXME: see above + if (kind == FILL) { + sp_repr_css_change_recursive(item->getRepr(), css, "style"); + } + + // Check if object already has mesh. + bool has_mesh = false; + SPStyle *style = item->style; + if (style) { + SPPaintServer *server = + (kind==FILL) ? style->getFillPaintServer():style->getStrokePaintServer(); + if (server && SP_IS_MESHGRADIENT(server)) + has_mesh = true; + } + + if (!mesh || !has_mesh) { + // No mesh in document or object does not already have mesh -> + // Create new mesh. + + // Create mesh element + Inkscape::XML::Node *repr = xml_doc->createElement("svg:meshgradient"); + + // privates are garbage-collectable + repr->setAttribute("inkscape:collect", "always"); + + // Attach to document + defs->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + + // Get corresponding object + SPMeshGradient *mg = static_cast<SPMeshGradient *>(document->getObjectByRepr(repr)); + mg->array.create(mg, item, (kind==FILL) ? + item->geometricBounds() : item->visualBounds()); + + bool isText = SP_IS_TEXT(item); + sp_style_set_property_url (item, ((kind == FILL) ? "fill":"stroke"), + mg, isText); + + // (*i)->requestModified(SP_OBJECT_MODIFIED_FLAG|SP_OBJECT_STYLE_MODIFIED_FLAG); + + } else { + // Using found mesh + + // Duplicate + Inkscape::XML::Node *mesh_repr = mesh->getRepr(); + Inkscape::XML::Node *copy_repr = mesh_repr->duplicate(xml_doc); + + // privates are garbage-collectable + copy_repr->setAttribute("inkscape:collect", "always"); + + // Attach to document + defs->getRepr()->appendChild(copy_repr); + Inkscape::GC::release(copy_repr); + + // Get corresponding object + SPMeshGradient *mg = + static_cast<SPMeshGradient *>(document->getObjectByRepr(copy_repr)); + // std::cout << " " << (mg->getId()?mg->getId():"null") << std::endl; + mg->array.read(mg); + + Geom::OptRect item_bbox = (kind==FILL) ? + item->geometricBounds() : item->visualBounds(); + mg->array.fill_box( item_bbox ); + + bool isText = SP_IS_TEXT(item); + sp_style_set_property_url (item, ((kind == FILL) ? "fill":"stroke"), + mg, isText); + } + } + + if (css) { + sp_repr_css_attr_unref(css); + css = nullptr; + } + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Set mesh on fill") : _("Set mesh on stroke")); + } + break; +#endif + + case SPPaintSelector::MODE_PATTERN: + + if (!items.empty()) { + + SPPattern *pattern = psel->getPattern(); + if (!pattern) { + + /* No Pattern in paint selector should mean that we just + * changed mode - don't do jack. + */ + + } else { + Inkscape::XML::Node *patrepr = pattern->getRepr(); + SPCSSAttr *css = sp_repr_css_attr_new(); + gchar *urltext = g_strdup_printf("url(#%s)", patrepr->attribute("id")); + sp_repr_css_set_property(css, (kind == FILL) ? "fill" : "stroke", urltext); + + // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs + if (kind == FILL) { + sp_repr_css_set_property(css, "fill-opacity", "1.0"); + } + + // cannot just call sp_desktop_set_style, because we don't want to touch those + // objects who already have the same root pattern but through a different href + // chain. FIXME: move this to a sp_item_set_pattern + for(auto item : items){ + Inkscape::XML::Node *selrepr = item->getRepr(); + if ( (kind == STROKE) && !selrepr) { + continue; + } + SPObject *selobj = item; + + SPStyle *style = selobj->style; + if (style && ((kind == FILL) ? style->fill.isPaintserver() : style->stroke.isPaintserver())) { + SPPaintServer *server = (kind == FILL) ? + selobj->style->getFillPaintServer() : + selobj->style->getStrokePaintServer(); + if (SP_IS_PATTERN(server) && SP_PATTERN(server)->rootPattern() == pattern) + // only if this object's pattern is not rooted in our selected pattern, apply + continue; + } + + if (kind == FILL) { + sp_desktop_apply_css_recursive(selobj, css, true); + } else { + sp_repr_css_change_recursive(selrepr, css, "style"); + } + } + + sp_repr_css_attr_unref(css); + css = nullptr; + g_free(urltext); + + } // end if + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Set pattern on fill") : + _("Set pattern on stroke")); + } // end if + + break; + + case SPPaintSelector::MODE_UNSET: + if (!items.empty()) { + SPCSSAttr *css = sp_repr_css_attr_new(); + if (kind == FILL) { + sp_repr_css_unset_property(css, "fill"); + } else { + sp_repr_css_unset_property(css, "stroke"); + sp_repr_css_unset_property(css, "stroke-opacity"); + sp_repr_css_unset_property(css, "stroke-width"); + sp_repr_css_unset_property(css, "stroke-miterlimit"); + sp_repr_css_unset_property(css, "stroke-linejoin"); + sp_repr_css_unset_property(css, "stroke-linecap"); + sp_repr_css_unset_property(css, "stroke-dashoffset"); + sp_repr_css_unset_property(css, "stroke-dasharray"); + } + + sp_desktop_set_style(desktop, css); + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Unset fill") : _("Unset stroke")); + } + break; + + default: + g_warning( "file %s: line %d: Paint selector should not be in " + "mode %d", + __FILE__, __LINE__, + psel->mode ); + break; + } + + update = false; +} + +} // 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 : diff --git a/src/widgets/fill-style.h b/src/widgets/fill-style.h new file mode 100644 index 0000000..3a64f2c --- /dev/null +++ b/src/widgets/fill-style.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Fill style configuration + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_SP_FILL_STYLE_H +#define SEEN_DIALOGS_SP_FILL_STYLE_H + +namespace Gtk { +class Widget; +} + +class SPDesktop; + +Gtk::Widget *sp_fill_style_widget_new(); + +void sp_fill_style_widget_set_desktop(Gtk::Widget *widget, SPDesktop *desktop); + +#endif // SEEN_DIALOGS_SP_FILL_STYLE_H + +/* + 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 : diff --git a/src/widgets/gradient-image.cpp b/src/widgets/gradient-image.cpp new file mode 100644 index 0000000..1a0e253 --- /dev/null +++ b/src/widgets/gradient-image.cpp @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A simple gradient preview + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/sigc++.h> + +#include <glibmm/refptr.h> +#include <gdkmm/pixbuf.h> + +#include <cairomm/surface.h> + +#include "gradient-image.h" + +#include "display/cairo-utils.h" + +#include "object/sp-gradient.h" +#include "object/sp-stop.h" + +static void sp_gradient_image_size_request (GtkWidget *widget, GtkRequisition *requisition); + +static void sp_gradient_image_destroy(GtkWidget *object); +static void sp_gradient_image_get_preferred_width(GtkWidget *widget, + gint *minimal_width, + gint *natural_width); + +static void sp_gradient_image_get_preferred_height(GtkWidget *widget, + gint *minimal_height, + gint *natural_height); +static gboolean sp_gradient_image_draw(GtkWidget *widget, cairo_t *cr); +static void sp_gradient_image_gradient_release (SPObject *, SPGradientImage *im); +static void sp_gradient_image_gradient_modified (SPObject *, guint flags, SPGradientImage *im); +static void sp_gradient_image_update (SPGradientImage *img); + +G_DEFINE_TYPE(SPGradientImage, sp_gradient_image, GTK_TYPE_WIDGET); + +static void sp_gradient_image_class_init(SPGradientImageClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + + widget_class->get_preferred_width = sp_gradient_image_get_preferred_width; + widget_class->get_preferred_height = sp_gradient_image_get_preferred_height; + widget_class->draw = sp_gradient_image_draw; + widget_class->destroy = sp_gradient_image_destroy; +} + +static void +sp_gradient_image_init (SPGradientImage *image) +{ + gtk_widget_set_has_window (GTK_WIDGET(image), FALSE); + + image->gradient = nullptr; + + new (&image->release_connection) sigc::connection(); + new (&image->modified_connection) sigc::connection(); +} + +static void sp_gradient_image_destroy(GtkWidget *object) +{ + SPGradientImage *image = SP_GRADIENT_IMAGE (object); + + if (image->gradient) { + image->release_connection.disconnect(); + image->modified_connection.disconnect(); + image->gradient = nullptr; + } + + image->release_connection.~connection(); + image->modified_connection.~connection(); + + if (GTK_WIDGET_CLASS(sp_gradient_image_parent_class)->destroy) + GTK_WIDGET_CLASS(sp_gradient_image_parent_class)->destroy(object); +} + +static void sp_gradient_image_size_request(GtkWidget * /*widget*/, GtkRequisition *requisition) +{ + requisition->width = 54; + requisition->height = 12; +} + +static void sp_gradient_image_get_preferred_width(GtkWidget *widget, gint *minimal_width, gint *natural_width) +{ + GtkRequisition requisition; + sp_gradient_image_size_request(widget, &requisition); + *minimal_width = *natural_width = requisition.width; +} + +static void sp_gradient_image_get_preferred_height(GtkWidget *widget, gint *minimal_height, gint *natural_height) +{ + GtkRequisition requisition; + sp_gradient_image_size_request(widget, &requisition); + *minimal_height = *natural_height = requisition.height; +} + +static gboolean sp_gradient_image_draw(GtkWidget *widget, cairo_t *ct) +{ + SPGradientImage *image = SP_GRADIENT_IMAGE(widget); + SPGradient *gr = image->gradient; + GtkAllocation allocation; + gtk_widget_get_allocation(widget, &allocation); + + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_set_source(ct, check); + cairo_paint(ct); + cairo_pattern_destroy(check); + + if (gr) { + cairo_pattern_t *p = gr->create_preview_pattern(allocation.width); + cairo_set_source(ct, p); + cairo_paint(ct); + cairo_pattern_destroy(p); + } + + return TRUE; +} + +GtkWidget * +sp_gradient_image_new (SPGradient *gradient) +{ + SPGradientImage *image = SP_GRADIENT_IMAGE(g_object_new(SP_TYPE_GRADIENT_IMAGE, nullptr)); + + sp_gradient_image_set_gradient (image, gradient); + + return GTK_WIDGET(image); +} + +GdkPixbuf* +sp_gradient_to_pixbuf (SPGradient *gr, int width, int height) +{ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *ct = cairo_create(s); + + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_set_source(ct, check); + cairo_paint(ct); + cairo_pattern_destroy(check); + + if (gr) { + cairo_pattern_t *p = gr->create_preview_pattern(width); + cairo_set_source(ct, p); + cairo_paint(ct); + cairo_pattern_destroy(p); + } + + cairo_destroy(ct); + cairo_surface_flush(s); + + // no need to free s - the call below takes ownership + GdkPixbuf *pixbuf = ink_pixbuf_create_from_cairo_surface(s); + return pixbuf; +} + + +Glib::RefPtr<Gdk::Pixbuf> +sp_gradient_to_pixbuf_ref (SPGradient *gr, int width, int height) +{ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *ct = cairo_create(s); + + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_set_source(ct, check); + cairo_paint(ct); + cairo_pattern_destroy(check); + + if (gr) { + cairo_pattern_t *p = gr->create_preview_pattern(width); + cairo_set_source(ct, p); + cairo_paint(ct); + cairo_pattern_destroy(p); + } + + cairo_destroy(ct); + cairo_surface_flush(s); + + Cairo::RefPtr<Cairo::Surface> sref = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s)); + Glib::RefPtr<Gdk::Pixbuf> pixbuf = + Gdk::Pixbuf::create(sref, 0, 0, width, height); + + cairo_surface_destroy(s); + + return pixbuf; +} + + +Glib::RefPtr<Gdk::Pixbuf> +sp_gradstop_to_pixbuf_ref (SPStop *stop, int width, int height) +{ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *ct = cairo_create(s); + + /* Checkerboard background */ + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_rectangle(ct, 0, 0, width, height); + cairo_set_source(ct, check); + cairo_fill_preserve(ct); + cairo_pattern_destroy(check); + + if (stop) { + /* Alpha area */ + cairo_rectangle(ct, 0, 0, width/2, height); + ink_cairo_set_source_rgba32(ct, stop->get_rgba32()); + cairo_fill(ct); + + /* Solid area */ + cairo_rectangle(ct, width/2, 0, width, height); + ink_cairo_set_source_rgba32(ct, stop->get_rgba32() | 0xff); + cairo_fill(ct); + } + + cairo_destroy(ct); + cairo_surface_flush(s); + + Cairo::RefPtr<Cairo::Surface> sref = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s)); + Glib::RefPtr<Gdk::Pixbuf> pixbuf = + Gdk::Pixbuf::create(sref, 0, 0, width, height); + + cairo_surface_destroy(s); + + return pixbuf; +} + + +void +sp_gradient_image_set_gradient (SPGradientImage *image, SPGradient *gradient) +{ + if (image->gradient) { + image->release_connection.disconnect(); + image->modified_connection.disconnect(); + } + + image->gradient = gradient; + + if (gradient) { + image->release_connection = gradient->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_gradient_image_gradient_release), image)); + image->modified_connection = gradient->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_gradient_image_gradient_modified), image)); + } + + sp_gradient_image_update (image); +} + +static void +sp_gradient_image_gradient_release (SPObject *, SPGradientImage *image) +{ + if (image->gradient) { + image->release_connection.disconnect(); + image->modified_connection.disconnect(); + } + + image->gradient = nullptr; + + sp_gradient_image_update (image); +} + +static void +sp_gradient_image_gradient_modified (SPObject *, guint /*flags*/, SPGradientImage *image) +{ + sp_gradient_image_update (image); +} + +static void +sp_gradient_image_update (SPGradientImage *image) +{ + if (gtk_widget_is_drawable (GTK_WIDGET(image))) { + gtk_widget_queue_draw (GTK_WIDGET (image)); + } +} + +/* + 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 : diff --git a/src/widgets/gradient-image.h b/src/widgets/gradient-image.h new file mode 100644 index 0000000..4fcc4ca --- /dev/null +++ b/src/widgets/gradient-image.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_GRADIENT_IMAGE_H +#define SEEN_SP_GRADIENT_IMAGE_H + +/** + * A simple gradient preview + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtk/gtk.h> +#include <glibmm/refptr.h> + +class SPGradient; +class SPStop; +namespace Gdk { + class Pixbuf; +} + +#include <sigc++/connection.h> + +#define SP_TYPE_GRADIENT_IMAGE (sp_gradient_image_get_type ()) +#define SP_GRADIENT_IMAGE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_GRADIENT_IMAGE, SPGradientImage)) +#define SP_GRADIENT_IMAGE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_GRADIENT_IMAGE, SPGradientImageClass)) +#define SP_IS_GRADIENT_IMAGE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_GRADIENT_IMAGE)) +#define SP_IS_GRADIENT_IMAGE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_GRADIENT_IMAGE)) + +struct SPGradientImage { + GtkWidget widget; + SPGradient *gradient; + + sigc::connection release_connection; + sigc::connection modified_connection; +}; + +struct SPGradientImageClass { + GtkWidgetClass parent_class; +}; + +GType sp_gradient_image_get_type (); + +GtkWidget *sp_gradient_image_new (SPGradient *gradient); +GdkPixbuf *sp_gradient_to_pixbuf (SPGradient *gr, int width, int height); +Glib::RefPtr<Gdk::Pixbuf> sp_gradient_to_pixbuf_ref (SPGradient *gr, int width, int height); +Glib::RefPtr<Gdk::Pixbuf> sp_gradstop_to_pixbuf_ref (SPStop *gr, int width, int height); +void sp_gradient_image_set_gradient (SPGradientImage *gi, SPGradient *gr); + +#endif + +/* + 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 : diff --git a/src/widgets/gradient-selector.cpp b/src/widgets/gradient-selector.cpp new file mode 100644 index 0000000..387f077 --- /dev/null +++ b/src/widgets/gradient-selector.cpp @@ -0,0 +1,660 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gradient vector widget + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gtkmm/treeview.h> +#include <vector> + +#include "document-undo.h" +#include "document.h" +#include "gradient-chemistry.h" +#include "gradient-vector.h" +#include "id-clash.h" +#include "inkscape.h" +#include "paint-selector.h" +#include "preferences.h" +#include "verbs.h" + +#include "object/sp-defs.h" +#include "style.h" + +#include "helper/action.h" +#include "ui/icon-loader.h" + +#include "ui/icon-names.h" + +enum { + GRABBED, + DRAGGED, + RELEASED, + CHANGED, + LAST_SIGNAL +}; + + +static void sp_gradient_selector_dispose(GObject *object); + +/* Signal handlers */ +static void sp_gradient_selector_vector_set (SPGradientVectorSelector *gvs, SPGradient *gr, SPGradientSelector *sel); +static void sp_gradient_selector_edit_vector_clicked (GtkWidget *w, SPGradientSelector *sel); +static void sp_gradient_selector_add_vector_clicked (GtkWidget *w, SPGradientSelector *sel); +static void sp_gradient_selector_delete_vector_clicked (GtkWidget *w, SPGradientSelector *sel); + +static guint signals[LAST_SIGNAL] = {0}; + +G_DEFINE_TYPE(SPGradientSelector, sp_gradient_selector, GTK_TYPE_BOX); + +static void sp_gradient_selector_class_init(SPGradientSelectorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + signals[GRABBED] = g_signal_new ("grabbed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET (SPGradientSelectorClass, grabbed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + signals[DRAGGED] = g_signal_new ("dragged", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET (SPGradientSelectorClass, dragged), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + signals[RELEASED] = g_signal_new ("released", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET (SPGradientSelectorClass, released), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + signals[CHANGED] = g_signal_new ("changed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET (SPGradientSelectorClass, changed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + object_class->dispose = sp_gradient_selector_dispose; +} + +static void gradsel_style_button(GtkWidget *gtkbtn, char const *iconName) +{ + Gtk::Button *btn = Glib::wrap(GTK_BUTTON(gtkbtn)); + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show(child); + btn->add(*manage(Glib::wrap(child))); + btn->set_relief(Gtk::RELIEF_NONE); +} + +static void sp_gradient_selector_init(SPGradientSelector *sel) +{ + sel->safelyInit = true; + sel->blocked = false; + + gtk_orientable_set_orientation(GTK_ORIENTABLE(sel), GTK_ORIENTATION_VERTICAL); + + new (&sel->nonsolid) std::vector<GtkWidget*>(); + new (&sel->swatch_widgets) std::vector<GtkWidget*>(); + + sel->mode = SPGradientSelector::MODE_LINEAR; + + sel->gradientUnits = SP_GRADIENT_UNITS_USERSPACEONUSE; + sel->gradientSpread = SP_GRADIENT_SPREAD_PAD; + + /* Vectors */ + sel->vectors = sp_gradient_vector_selector_new (nullptr, nullptr); + SPGradientVectorSelector *gvs = SP_GRADIENT_VECTOR_SELECTOR(sel->vectors); + sel->store = gvs->store; + sel->columns = gvs->columns; + + sel->treeview = Gtk::manage(new Gtk::TreeView()); + sel->treeview->set_model(gvs->store); + sel->treeview->set_headers_clickable (true); + sel->treeview->set_search_column(1); + sel->treeview->set_vexpand(); + sel->icon_renderer = Gtk::manage(new Gtk::CellRendererPixbuf()); + sel->text_renderer = Gtk::manage(new Gtk::CellRendererText()); + + sel->treeview->append_column(_("Gradient"), *sel->icon_renderer); + Gtk::TreeView::Column* icon_column = sel->treeview->get_column(0); + icon_column->add_attribute(sel->icon_renderer->property_pixbuf(), sel->columns->pixbuf); + icon_column->set_sort_column(sel->columns->color); + icon_column->set_clickable(true); + + sel->treeview->append_column(_("Name"), *sel->text_renderer); + Gtk::TreeView::Column* name_column = sel->treeview->get_column(1); + sel->text_renderer->property_editable() = true; + name_column->add_attribute(sel->text_renderer->property_text(), sel->columns->name); + name_column->set_min_width(180); + name_column->set_clickable(true); + name_column->set_resizable(true); + + sel->treeview->append_column("#", sel->columns->refcount); + Gtk::TreeView::Column* count_column = sel->treeview->get_column(2); + count_column->set_clickable(true); + count_column->set_resizable(true); + + sel->treeview->signal_key_press_event().connect(sigc::mem_fun(*sel, &SPGradientSelector::onKeyPressEvent), false); + + sel->treeview->show(); + + icon_column->signal_clicked().connect( sigc::mem_fun(*sel, &SPGradientSelector::onTreeColorColClick) ); + name_column->signal_clicked().connect( sigc::mem_fun(*sel, &SPGradientSelector::onTreeNameColClick) ); + count_column->signal_clicked().connect( sigc::mem_fun(*sel, &SPGradientSelector::onTreeCountColClick) ); + + gvs->tree_select_connection = sel->treeview->get_selection()->signal_changed().connect( sigc::mem_fun(*sel, &SPGradientSelector::onTreeSelection) ); + sel->text_renderer->signal_edited().connect( sigc::mem_fun(*sel, &SPGradientSelector::onGradientRename) ); + + sel->scrolled_window = Gtk::manage(new Gtk::ScrolledWindow()); + sel->scrolled_window->add(*sel->treeview); + sel->scrolled_window->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + sel->scrolled_window->set_shadow_type(Gtk::SHADOW_IN); + sel->scrolled_window->set_size_request(0, 180); + sel->scrolled_window->set_hexpand(); + sel->scrolled_window->show(); + + gtk_box_pack_start (GTK_BOX (sel), GTK_WIDGET(sel->scrolled_window->gobj()), TRUE, TRUE, 4); + + + /* Create box for buttons */ + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_start( GTK_BOX(sel), hb, FALSE, FALSE, 0 ); + + sel->add = gtk_button_new(); + gradsel_style_button(sel->add, INKSCAPE_ICON("list-add")); + + sel->nonsolid.push_back(sel->add); + gtk_box_pack_start (GTK_BOX (hb), sel->add, FALSE, FALSE, 0); + + g_signal_connect (G_OBJECT (sel->add), "clicked", G_CALLBACK (sp_gradient_selector_add_vector_clicked), sel); + gtk_widget_set_sensitive (sel->add, FALSE); + gtk_button_set_relief(GTK_BUTTON(sel->add), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text( sel->add, _("Create a duplicate gradient")); + + sel->edit = gtk_button_new(); + gradsel_style_button(sel->edit, INKSCAPE_ICON("edit")); + + sel->nonsolid.push_back(sel->edit); + gtk_box_pack_start (GTK_BOX (hb), sel->edit, FALSE, FALSE, 0); + g_signal_connect (G_OBJECT (sel->edit), "clicked", G_CALLBACK (sp_gradient_selector_edit_vector_clicked), sel); + gtk_widget_set_sensitive (sel->edit, FALSE); + gtk_button_set_relief(GTK_BUTTON(sel->edit), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text( sel->edit, _("Edit gradient")); + + sel->del = gtk_button_new (); + gradsel_style_button(sel->del, INKSCAPE_ICON("list-remove")); + + sel->swatch_widgets.push_back(sel->del); + gtk_box_pack_start (GTK_BOX (hb), sel->del, FALSE, FALSE, 0); + g_signal_connect (G_OBJECT (sel->del), "clicked", G_CALLBACK (sp_gradient_selector_delete_vector_clicked), sel); + gtk_widget_set_sensitive (sel->del, FALSE); + gtk_button_set_relief(GTK_BUTTON(sel->del), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text( sel->del, _("Delete swatch")); + + gtk_widget_show_all(hb); +} + +static void sp_gradient_selector_dispose(GObject *object) +{ + SPGradientSelector *sel = SP_GRADIENT_SELECTOR( object ); + + if ( sel->safelyInit ) { + sel->safelyInit = false; + sel->nonsolid.~vector<GtkWidget*>(); + sel->swatch_widgets.~vector<GtkWidget*>(); + } + + if (sel->icon_renderer) { + delete sel->icon_renderer; + sel->icon_renderer = nullptr; + } + if (sel->text_renderer) { + delete sel->text_renderer; + sel->text_renderer = nullptr; + } + + if ((G_OBJECT_CLASS(sp_gradient_selector_parent_class))->dispose) { + (G_OBJECT_CLASS(sp_gradient_selector_parent_class))->dispose(object); + } +} + +void SPGradientSelector::setSpread(SPGradientSpread spread) +{ + gradientSpread = spread; + //gtk_combo_box_set_active (GTK_COMBO_BOX(this->spread), gradientSpread); +} + + +GtkWidget *sp_gradient_selector_new() +{ + SPGradientSelector *sel = SP_GRADIENT_SELECTOR(g_object_new (SP_TYPE_GRADIENT_SELECTOR, nullptr)); + + return GTK_WIDGET(sel); +} + +void SPGradientSelector::setMode(SelectorMode mode) +{ + if (mode != this->mode) { + this->mode = mode; + if (mode == MODE_SWATCH) { + for (auto & it : nonsolid) + { + gtk_widget_hide(it); + } + for (auto & swatch_widget : swatch_widgets) + { + gtk_widget_show_all(swatch_widget); + } + + Gtk::TreeView::Column* icon_column = treeview->get_column(0); + icon_column->set_title(_("Swatch")); + + SPGradientVectorSelector* vs = SP_GRADIENT_VECTOR_SELECTOR(vectors); + vs->setSwatched(); + } else { + for (auto & it : nonsolid) + { + gtk_widget_show_all(it); + } + for (auto & swatch_widget : swatch_widgets) + { + gtk_widget_hide(swatch_widget); + } + Gtk::TreeView::Column* icon_column = treeview->get_column(0); + icon_column->set_title(_("Gradient")); + + } + } +} + +void SPGradientSelector::setUnits(SPGradientUnits units) +{ + gradientUnits = units; +} + +SPGradientUnits SPGradientSelector::getUnits() +{ + return gradientUnits; +} + +SPGradientSpread SPGradientSelector::getSpread() +{ + return gradientSpread; +} + +void SPGradientSelector::onGradientRename( const Glib::ustring& path_string, const Glib::ustring& new_text) +{ + Gtk::TreePath path(path_string); + Gtk::TreeModel::iterator iter = store->get_iter(path); + + if( iter ) + { + Gtk::TreeModel::Row row = *iter; + if ( row ) { + SPObject* obj = row[columns->data]; + if ( obj ) { + row[columns->name] = gr_prepare_label(obj); + if (!new_text.empty() && new_text != row[columns->name]) { + rename_id(obj, new_text ); + Inkscape::DocumentUndo::done(obj->document, SP_VERB_CONTEXT_GRADIENT, + _("Rename gradient")); + } + } + } + } +} + +void SPGradientSelector::onTreeColorColClick() { + Gtk::TreeView::Column* column = treeview->get_column(0); + column->set_sort_column(columns->color); +} + +void SPGradientSelector::onTreeNameColClick() { + Gtk::TreeView::Column* column = treeview->get_column(1); + column->set_sort_column(columns->name); +} + + +void SPGradientSelector::onTreeCountColClick() { + Gtk::TreeView::Column* column = treeview->get_column(2); + column->set_sort_column(columns->refcount); +} + +void SPGradientSelector::moveSelection(int amount, bool down, bool toEnd) +{ + Glib::RefPtr<Gtk::TreeSelection> select = treeview->get_selection(); + auto iter = select->get_selected(); + + if (amount < 0) { + down = !down; + amount = -amount; + } + + auto canary = iter; + if (down) { + ++canary; + } else { + --canary; + } + while (canary && (toEnd || amount > 0)) { + --amount; + if (down) { + ++canary; + ++iter; + } else { + --canary; + --iter; + } + } + + select->select(iter); + treeview->scroll_to_row(store->get_path(iter), 0.5); +} + +bool SPGradientSelector::onKeyPressEvent(GdkEventKey *event) +{ + bool consume = false; + auto display = Gdk::Display::get_default(); + auto keymap = display->get_keymap(); + guint key = 0; + gdk_keymap_translate_keyboard_state(keymap, event->hardware_keycode, + static_cast<GdkModifierType>(event->state), + 0, &key, 0, 0, 0); + + switch (key) { + case GDK_KEY_Up: + case GDK_KEY_KP_Up: { + moveSelection(-1); + consume = true; + break; + } + case GDK_KEY_Down: + case GDK_KEY_KP_Down: { + moveSelection(1); + consume = true; + break; + } + case GDK_KEY_Page_Up: + case GDK_KEY_KP_Page_Up: { + moveSelection(-5); + consume = true; + break; + } + + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Down: { + moveSelection(5); + consume = true; + break; + } + + case GDK_KEY_End: + case GDK_KEY_KP_End: { + moveSelection(0, true, true); + consume = true; + break; + } + + case GDK_KEY_Home: + case GDK_KEY_KP_Home: { + moveSelection(0, false, true); + consume = true; + break; + } + } + return consume; +} + +void SPGradientSelector::onTreeSelection() +{ + if (!treeview) { + return; + } + + if (blocked) { + return; + } + + if (!treeview->has_focus()) { + /* Workaround for GTK bug on Windows/OS X + * When the treeview initially doesn't have focus and is clicked + * sometimes get_selection()->signal_changed() has the wrong selection + */ + treeview->grab_focus(); + } + + const Glib::RefPtr<Gtk::TreeSelection> sel = treeview->get_selection(); + if (!sel) { + return; + } + + SPGradient *obj = nullptr; + /* Single selection */ + Gtk::TreeModel::iterator iter = sel->get_selected(); + if ( iter ) { + Gtk::TreeModel::Row row = *iter; + obj = row[columns->data]; + } + + if (obj) { + sp_gradient_selector_vector_set (nullptr, SP_GRADIENT(obj), this); + } +} + +bool SPGradientSelector::_checkForSelected(const Gtk::TreePath &path, const Gtk::TreeIter& iter, SPGradient *vector) +{ + bool found = false; + + Gtk::TreeModel::Row row = *iter; + if ( vector == row[columns->data] ) + { + treeview->scroll_to_row(path, 0.5); + Glib::RefPtr<Gtk::TreeSelection> select = treeview->get_selection(); + bool wasBlocked = blocked; + blocked = true; + select->select(iter); + blocked = wasBlocked; + found = true; + } + + return found; +} + +void SPGradientSelector::selectGradientInTree(SPGradient *vector) +{ + store->foreach( sigc::bind<SPGradient*>(sigc::mem_fun(*this, &SPGradientSelector::_checkForSelected), vector) ); +} + +void SPGradientSelector::setVector(SPDocument *doc, SPGradient *vector) +{ + g_return_if_fail(!vector || SP_IS_GRADIENT(vector)); + g_return_if_fail(!vector || (vector->document == doc)); + + if (vector && !vector->hasStops()) { + return; + } + + sp_gradient_vector_selector_set_gradient(SP_GRADIENT_VECTOR_SELECTOR(vectors), doc, vector); + + selectGradientInTree(vector); + + if (vector) { + if ( (mode == MODE_SWATCH) && vector->isSwatch() ) { + if ( vector->isSolid() ) { + for (auto & it : nonsolid) + { + gtk_widget_hide(it); + } + } else { + for (auto & it : nonsolid) + { + gtk_widget_show_all(it); + } + } + } else if (mode != MODE_SWATCH) { + + for (auto & swatch_widget : swatch_widgets) + { + gtk_widget_hide(swatch_widget); + } + for (auto & it : nonsolid) + { + gtk_widget_show_all(it); + } + + } + + if (edit) { + gtk_widget_set_sensitive(edit, TRUE); + } + if (add) { + gtk_widget_set_sensitive(add, TRUE); + } + if (del) { + gtk_widget_set_sensitive(del, TRUE); + } + } else { + if (edit) { + gtk_widget_set_sensitive(edit, FALSE); + } + if (add) { + gtk_widget_set_sensitive(add, (doc != nullptr)); + } + if (del) { + gtk_widget_set_sensitive(del, FALSE); + } + } +} + +SPGradient *SPGradientSelector::getVector() +{ + /* fixme: */ + return SP_GRADIENT_VECTOR_SELECTOR(vectors)->gr; +} + + +static void sp_gradient_selector_vector_set(SPGradientVectorSelector * /*gvs*/, SPGradient *gr, SPGradientSelector *sel) +{ + + if (!sel->blocked) { + sel->blocked = TRUE; + gr = sp_gradient_ensure_vector_normalized (gr); + sel->setVector((gr) ? gr->document : nullptr, gr); + g_signal_emit (G_OBJECT (sel), signals[CHANGED], 0, gr); + sel->blocked = FALSE; + } +} + + +static void +sp_gradient_selector_delete_vector_clicked (GtkWidget */*w*/, SPGradientSelector *sel) +{ + const Glib::RefPtr<Gtk::TreeSelection> selection = sel->treeview->get_selection(); + if (!selection) { + return; + } + + SPGradient *obj = nullptr; + /* Single selection */ + Gtk::TreeModel::iterator iter = selection->get_selected(); + if ( iter ) { + Gtk::TreeModel::Row row = *iter; + obj = row[sel->columns->data]; + } + + if (obj) { + sp_gradient_unset_swatch(SP_ACTIVE_DESKTOP, obj->getId()); + } + +} + +static void +sp_gradient_selector_edit_vector_clicked (GtkWidget */*w*/, SPGradientSelector *sel) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/dialogs/gradienteditor/showlegacy", false)) { + // Legacy gradient dialog + GtkWidget *dialog; + dialog = sp_gradient_vector_editor_new (SP_GRADIENT_VECTOR_SELECTOR (sel->vectors)->gr); + gtk_widget_show (dialog); + } else { + // Invoke the gradient tool + Inkscape::Verb *verb = Inkscape::Verb::get( SP_VERB_CONTEXT_GRADIENT ); + if ( verb ) { + SPAction *action = verb->get_action( Inkscape::ActionContext( ( Inkscape::UI::View::View * ) SP_ACTIVE_DESKTOP ) ); + if ( action ) { + sp_action_perform( action, nullptr ); + } + } + } +} + +static void +sp_gradient_selector_add_vector_clicked (GtkWidget */*w*/, SPGradientSelector *sel) +{ + SPDocument *doc = sp_gradient_vector_selector_get_document (SP_GRADIENT_VECTOR_SELECTOR (sel->vectors)); + + if (!doc) + return; + + SPGradient *gr = sp_gradient_vector_selector_get_gradient( SP_GRADIENT_VECTOR_SELECTOR (sel->vectors)); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + Inkscape::XML::Node *repr = nullptr; + + if (gr) { + repr = gr->getRepr()->duplicate(xml_doc); + // Rename the new gradients id to be similar to the cloned gradients + Glib::ustring old_id = gr->getId(); + rename_id(gr, old_id); + doc->getDefs()->getRepr()->addChild(repr, nullptr); + } else { + repr = xml_doc->createElement("svg:linearGradient"); + Inkscape::XML::Node *stop = xml_doc->createElement("svg:stop"); + stop->setAttribute("offset", "0"); + stop->setAttribute("style", "stop-color:#000;stop-opacity:1;"); + repr->appendChild(stop); + Inkscape::GC::release(stop); + stop = xml_doc->createElement("svg:stop"); + stop->setAttribute("offset", "1"); + stop->setAttribute("style", "stop-color:#fff;stop-opacity:1;"); + repr->appendChild(stop); + Inkscape::GC::release(stop); + doc->getDefs()->getRepr()->addChild(repr, nullptr); + gr = SP_GRADIENT(doc->getObjectByRepr(repr)); + } + + sp_gradient_vector_selector_set_gradient( SP_GRADIENT_VECTOR_SELECTOR (sel->vectors), doc, gr); + + sel->selectGradientInTree(gr); + + Inkscape::GC::release(repr); +} + +/* + 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 : diff --git a/src/widgets/gradient-selector.h b/src/widgets/gradient-selector.h new file mode 100644 index 0000000..c787552 --- /dev/null +++ b/src/widgets/gradient-selector.h @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_GRADIENT_SELECTOR_H +#define SEEN_GRADIENT_SELECTOR_H + +/* + * Gradient vector and position widget + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/liststore.h> +#include <gtkmm/scrolledwindow.h> + +#include <vector> +#include "object/sp-gradient-spread.h" +#include "object/sp-gradient-units.h" + +class SPDocument; +class SPGradient; + +namespace Gtk { +class CellRendererPixbuf; +class CellRendererText; +class ScrolledWindow; +class TreeView; +} + +#define SP_TYPE_GRADIENT_SELECTOR (sp_gradient_selector_get_type ()) +#define SP_GRADIENT_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_GRADIENT_SELECTOR, SPGradientSelector)) +#define SP_GRADIENT_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_GRADIENT_SELECTOR, SPGradientSelectorClass)) +#define SP_IS_GRADIENT_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_GRADIENT_SELECTOR)) +#define SP_IS_GRADIENT_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_GRADIENT_SELECTOR)) + + + +struct SPGradientSelector { + GtkBox vbox; + + enum SelectorMode { + MODE_LINEAR, + MODE_RADIAL, + MODE_SWATCH + }; + + SelectorMode mode; + + SPGradientUnits gradientUnits; + SPGradientSpread gradientSpread; + + /* Vector selector */ + GtkWidget *vectors; + + /* Tree */ + bool _checkForSelected(const Gtk::TreePath& path, const Gtk::TreeIter& iter, SPGradient *vector); + bool onKeyPressEvent(GdkEventKey *event); + void onTreeSelection(); + void onGradientRename( const Glib::ustring& path_string, const Glib::ustring& new_text); + void onTreeNameColClick(); + void onTreeColorColClick(); + void onTreeCountColClick(); + + Gtk::TreeView *treeview; + Gtk::ScrolledWindow *scrolled_window; + class ModelColumns : public Gtk::TreeModel::ColumnRecord + { + public: + ModelColumns() + { + add(name); + add(refcount); + add(color); + add(data); + add(pixbuf); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<unsigned long> color; + Gtk::TreeModelColumn<gint> refcount; + Gtk::TreeModelColumn<SPGradient*> data; + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > pixbuf; + + }; + + ModelColumns *columns; + Glib::RefPtr<Gtk::ListStore> store; + Gtk::CellRendererPixbuf* icon_renderer; + Gtk::CellRendererText* text_renderer; + + /* Editing buttons */ + GtkWidget *edit; + GtkWidget *add; + GtkWidget *del; + GtkWidget *merge; + + /* Position widget */ + GtkWidget *position; + + bool safelyInit; + bool blocked; + + std::vector<GtkWidget*> nonsolid; + std::vector<GtkWidget*> swatch_widgets; + + void setMode(SelectorMode mode); + void setUnits(SPGradientUnits units); + void setSpread(SPGradientSpread spread); + void setVector(SPDocument *doc, SPGradient *vector); + void selectGradientInTree(SPGradient *vector); + void moveSelection(int amount, bool down = true, bool toEnd = false); + + SPGradientUnits getUnits(); + SPGradientSpread getSpread(); + SPGradient *getVector(); +}; + +struct SPGradientSelectorClass { + GtkBoxClass parent_class; + + void (* grabbed) (SPGradientSelector *sel); + void (* dragged) (SPGradientSelector *sel); + void (* released) (SPGradientSelector *sel); + void (* changed) (SPGradientSelector *sel); +}; + +GType sp_gradient_selector_get_type(); + +GtkWidget *sp_gradient_selector_new (); + +void sp_gradient_selector_set_bbox (SPGradientSelector *sel, gdouble x0, gdouble y0, gdouble x1, gdouble y1); + +#endif // SEEN_GRADIENT_SELECTOR_H + + +/* + 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 : diff --git a/src/widgets/gradient-vector.cpp b/src/widgets/gradient-vector.cpp new file mode 100644 index 0000000..a2ca973 --- /dev/null +++ b/src/widgets/gradient-vector.cpp @@ -0,0 +1,1314 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gradient vector selection widget + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * MenTaLguY <mental@rydia.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2004 Monash University + * Copyright (C) 2004 David Turner + * Copyright (C) 2006 MenTaLguY + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include <set> + +#include <glibmm.h> +#include <glibmm/i18n.h> + + + + +#include "gradient-chemistry.h" +#include "inkscape.h" +#include "preferences.h" +#include "desktop.h" +#include "document-undo.h" +#include "gradient-vector.h" +#include "layer-manager.h" +#include "include/macros.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "io/resource.h" + +#include "object/sp-defs.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-root.h" +#include "object/sp-stop.h" +#include "style.h" + +#include "svg/css-ostringstream.h" + +#include "ui/dialog-events.h" +#include "ui/selected-color.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/color-preview.h" + +#include "widgets/gradient-image.h" + +#include "xml/repr.h" + +using Inkscape::DocumentUndo; +using Inkscape::UI::SelectedColor; + +enum { + VECTOR_SET, + LAST_SIGNAL +}; + +static void sp_gradient_vector_selector_destroy(GtkWidget *object); + +static void sp_gvs_gradient_release(SPObject *obj, SPGradientVectorSelector *gvs); +static void sp_gvs_defs_release(SPObject *defs, SPGradientVectorSelector *gvs); +static void sp_gvs_defs_modified(SPObject *defs, guint flags, SPGradientVectorSelector *gvs); + +static void sp_gvs_rebuild_gui_full(SPGradientVectorSelector *gvs); +static SPStop *get_selected_stop( GtkWidget *vb); +void gr_get_usage_counts(SPDocument *doc, std::map<SPGradient *, gint> *mapUsageCount ); +unsigned long sp_gradient_to_hhssll(SPGradient *gr); + +static guint signals[LAST_SIGNAL] = {0}; + +// TODO FIXME kill these globals!!! +static GtkWidget *dlg = nullptr; +static win_data wd; +static gint x = -1000, y = -1000, w = 0, h = 0; // impossible original values to make sure they are read from prefs +static Glib::ustring const prefs_path = "/dialogs/gradienteditor/"; + +G_DEFINE_TYPE(SPGradientVectorSelector, sp_gradient_vector_selector, GTK_TYPE_BOX); + +static void sp_gradient_vector_selector_class_init(SPGradientVectorSelectorClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); + + signals[VECTOR_SET] = g_signal_new( "vector_set", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET(SPGradientVectorSelectorClass, vector_set), + nullptr, nullptr, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + widget_class->destroy = sp_gradient_vector_selector_destroy; +} + +static void sp_gradient_vector_selector_init(SPGradientVectorSelector *gvs) +{ + gtk_orientable_set_orientation(GTK_ORIENTABLE(gvs), GTK_ORIENTATION_VERTICAL); + + gvs->idlabel = TRUE; + + gvs->swatched = false; + + gvs->doc = nullptr; + gvs->gr = nullptr; + + new (&gvs->gradient_release_connection) sigc::connection(); + new (&gvs->defs_release_connection) sigc::connection(); + new (&gvs->defs_modified_connection) sigc::connection(); + + gvs->columns = new SPGradientSelector::ModelColumns(); + gvs->store = Gtk::ListStore::create(*gvs->columns); + new (&gvs->tree_select_connection) sigc::connection(); + +} + +static void sp_gradient_vector_selector_destroy(GtkWidget *object) +{ + SPGradientVectorSelector *gvs = SP_GRADIENT_VECTOR_SELECTOR(object); + + if (gvs->gr) { + gvs->gradient_release_connection.disconnect(); + gvs->tree_select_connection.disconnect(); + gvs->gr = nullptr; + } + + if (gvs->doc) { + gvs->defs_release_connection.disconnect(); + gvs->defs_modified_connection.disconnect(); + gvs->doc = nullptr; + } + + gvs->gradient_release_connection.~connection(); + gvs->defs_release_connection.~connection(); + gvs->defs_modified_connection.~connection(); + gvs->tree_select_connection.~connection(); + + if ((GTK_WIDGET_CLASS(sp_gradient_vector_selector_parent_class))->destroy) { + (GTK_WIDGET_CLASS(sp_gradient_vector_selector_parent_class))->destroy(object); + } +} + +GtkWidget *sp_gradient_vector_selector_new(SPDocument *doc, SPGradient *gr) +{ + GtkWidget *gvs; + + g_return_val_if_fail(!gr || SP_IS_GRADIENT(gr), NULL); + g_return_val_if_fail(!gr || (gr->document == doc), NULL); + + gvs = static_cast<GtkWidget*>(g_object_new(SP_TYPE_GRADIENT_VECTOR_SELECTOR, nullptr)); + + if (doc) { + sp_gradient_vector_selector_set_gradient(SP_GRADIENT_VECTOR_SELECTOR(gvs), doc, gr); + } else { + sp_gvs_rebuild_gui_full(SP_GRADIENT_VECTOR_SELECTOR(gvs)); + } + + return gvs; +} + +void sp_gradient_vector_selector_set_gradient(SPGradientVectorSelector *gvs, SPDocument *doc, SPGradient *gr) +{ +// g_message("sp_gradient_vector_selector_set_gradient(%p, %p, %p) [%s] %d %d", gvs, doc, gr, +// (gr ? gr->getId():"N/A"), +// (gr ? gr->isSwatch() : -1), +// (gr ? gr->isSolid() : -1)); + static gboolean suppress = FALSE; + + g_return_if_fail(gvs != nullptr); + g_return_if_fail(SP_IS_GRADIENT_VECTOR_SELECTOR(gvs)); + g_return_if_fail(!gr || (doc != nullptr)); + g_return_if_fail(!gr || SP_IS_GRADIENT(gr)); + g_return_if_fail(!gr || (gr->document == doc)); + g_return_if_fail(!gr || gr->hasStops()); + + if (doc != gvs->doc) { + /* Disconnect signals */ + if (gvs->gr) { + gvs->gradient_release_connection.disconnect(); + gvs->gr = nullptr; + } + if (gvs->doc) { + gvs->defs_release_connection.disconnect(); + gvs->defs_modified_connection.disconnect(); + gvs->doc = nullptr; + } + + // Connect signals + if (doc) { + gvs->defs_release_connection = doc->getDefs()->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_gvs_defs_release), gvs)); + gvs->defs_modified_connection = doc->getDefs()->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_gvs_defs_modified), gvs)); + } + if (gr) { + gvs->gradient_release_connection = gr->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_gvs_gradient_release), gvs)); + } + gvs->doc = doc; + gvs->gr = gr; + sp_gvs_rebuild_gui_full(gvs); + if (!suppress) g_signal_emit(G_OBJECT(gvs), signals[VECTOR_SET], 0, gr); + } else if (gr != gvs->gr) { + // Harder case - keep document, rebuild list and stuff + // fixme: (Lauris) + suppress = TRUE; + sp_gradient_vector_selector_set_gradient(gvs, nullptr, nullptr); + sp_gradient_vector_selector_set_gradient(gvs, doc, gr); + suppress = FALSE; + g_signal_emit(G_OBJECT(gvs), signals[VECTOR_SET], 0, gr); + } + /* The case of setting NULL -> NULL is not very interesting */ +} + +SPDocument *sp_gradient_vector_selector_get_document(SPGradientVectorSelector *gvs) +{ + g_return_val_if_fail(gvs != nullptr, NULL); + g_return_val_if_fail(SP_IS_GRADIENT_VECTOR_SELECTOR(gvs), NULL); + + return gvs->doc; +} + +SPGradient *sp_gradient_vector_selector_get_gradient(SPGradientVectorSelector *gvs) +{ + g_return_val_if_fail(gvs != nullptr, NULL); + g_return_val_if_fail(SP_IS_GRADIENT_VECTOR_SELECTOR(gvs), NULL); + + return gvs->gr; +} + +Glib::ustring gr_prepare_label (SPObject *obj) +{ + const gchar *id = obj->label() ? obj->label() : obj->getId(); + if (!id) { + id = obj->getRepr()->name(); + } + + if (strlen(id) > 14 && (!strncmp (id, "linearGradient", 14) || !strncmp (id, "radialGradient", 14))) + return gr_ellipsize_text (g_strdup_printf ("%s", id+14), 35); + return gr_ellipsize_text (id, 35); +} + +/* + * Ellipse text if longer than maxlen, "50% start text + ... + ~50% end text" + * Text should be > length 8 or just return the original text + */ +Glib::ustring gr_ellipsize_text(Glib::ustring const &src, size_t maxlen) +{ + if (src.length() > maxlen && maxlen > 8) { + size_t p1 = (size_t) maxlen / 2; + size_t p2 = (size_t) src.length() - (maxlen - p1 - 1); + return src.substr(0, p1) + "…" + src.substr(p2); + } + return src; +} + +static void sp_gvs_rebuild_gui_full(SPGradientVectorSelector *gvs) +{ + + gvs->tree_select_connection.block(); + + /* Clear old list, if there is any */ + gvs->store->clear(); + + /* Pick up all gradients with vectors */ + std::vector<SPGradient *> gl; + if (gvs->gr) { + std::vector<SPObject *> gradients = gvs->gr->document->getResourceList("gradient"); + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + if ( grad->hasStops() && (grad->isSwatch() == gvs->swatched) ) { + gl.push_back(SP_GRADIENT(gradient)); + } + } + } + + /* Get usage count of all the gradients */ + std::map<SPGradient *, gint> usageCount; + gr_get_usage_counts(gvs->doc, &usageCount); + + if (!gvs->doc) { + Gtk::TreeModel::Row row = *(gvs->store->append()); + row[gvs->columns->name] = _("No document selected"); + + } else if (gl.empty()) { + Gtk::TreeModel::Row row = *(gvs->store->append()); + row[gvs->columns->name] = _("No gradients in document"); + + } else if (!gvs->gr) { + Gtk::TreeModel::Row row = *(gvs->store->append()); + row[gvs->columns->name] = _("No gradient selected"); + + } else { + for (auto gr:gl) { + unsigned long hhssll = sp_gradient_to_hhssll(gr); + GdkPixbuf *pixb = sp_gradient_to_pixbuf (gr, 64, 18); + Glib::ustring label = gr_prepare_label(gr); + + Gtk::TreeModel::Row row = *(gvs->store->append()); + row[gvs->columns->name] = label.c_str(); + row[gvs->columns->color] = hhssll; + row[gvs->columns->refcount] = usageCount[gr]; + row[gvs->columns->data] = gr; + row[gvs->columns->pixbuf] = Glib::wrap(pixb); + } + } + + gvs->tree_select_connection.unblock(); + +} + +/* + * Return a "HHSSLL" version of the first stop color so we can sort by it + */ +unsigned long sp_gradient_to_hhssll(SPGradient *gr) +{ + SPStop *stop = gr->getFirstStop(); + unsigned long rgba = stop->get_rgba32(); + float hsl[3]; + SPColor::rgb_to_hsl_floatv (hsl, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba)); + + return ((int)(hsl[0]*100 * 10000)) + ((int)(hsl[1]*100 * 100)) + ((int)(hsl[2]*100 * 1)); +} + +static void get_all_doc_items(std::vector<SPItem*> &list, SPObject *from) +{ + for (auto& child: from->children) { + if (SP_IS_ITEM(&child)) { + list.push_back(SP_ITEM(&child)); + } + get_all_doc_items(list, &child); + } +} + +/* + * Return a SPItem's gradient + */ +static SPGradient * gr_item_get_gradient(SPItem *item, gboolean fillorstroke) +{ + SPIPaint *item_paint = item->style->getFillOrStroke(fillorstroke); + if (item_paint->isPaintserver()) { + + SPPaintServer *item_server = (fillorstroke) ? + item->style->getFillPaintServer() : item->style->getStrokePaintServer(); + + if (SP_IS_LINEARGRADIENT(item_server) || SP_IS_RADIALGRADIENT(item_server) || + (SP_IS_GRADIENT(item_server) && SP_GRADIENT(item_server)->getVector()->isSwatch())) { + + return SP_GRADIENT(item_server)->getVector(); + } + } + + return nullptr; +} + +/* + * Map each gradient to its usage count for both fill and stroke styles + */ +void gr_get_usage_counts(SPDocument *doc, std::map<SPGradient *, gint> *mapUsageCount ) +{ + if (!doc) + return; + + std::vector<SPItem *> all_list; + get_all_doc_items(all_list, doc->getRoot()); + + for (auto item:all_list) { + if (!item->getId()) + continue; + SPGradient *gr = nullptr; + gr = gr_item_get_gradient(item, true); // fill + if (gr) { + mapUsageCount->count(gr) > 0 ? (*mapUsageCount)[gr] += 1 : (*mapUsageCount)[gr] = 1; + } + gr = gr_item_get_gradient(item, false); // stroke + if (gr) { + mapUsageCount->count(gr) > 0 ? (*mapUsageCount)[gr] += 1 : (*mapUsageCount)[gr] = 1; + } + } +} + +static void sp_gvs_gradient_release(SPObject */*obj*/, SPGradientVectorSelector *gvs) +{ + /* Disconnect gradient */ + if (gvs->gr) { + gvs->gradient_release_connection.disconnect(); + gvs->gr = nullptr; + } + + /* Rebuild GUI */ + sp_gvs_rebuild_gui_full(gvs); +} + +static void sp_gvs_defs_release(SPObject */*defs*/, SPGradientVectorSelector *gvs) +{ + gvs->doc = nullptr; + + gvs->defs_release_connection.disconnect(); + gvs->defs_modified_connection.disconnect(); + + /* Disconnect gradient as well */ + if (gvs->gr) { + gvs->gradient_release_connection.disconnect(); + gvs->gr = nullptr; + } + + /* Rebuild GUI */ + sp_gvs_rebuild_gui_full(gvs); +} + +static void sp_gvs_defs_modified(SPObject */*defs*/, guint /*flags*/, SPGradientVectorSelector *gvs) +{ + /* fixme: We probably have to check some flags here (Lauris) */ + sp_gvs_rebuild_gui_full(gvs); +} + +void SPGradientVectorSelector::setSwatched() +{ + swatched = true; + sp_gvs_rebuild_gui_full(this); +} + +/*################################################################## + ### Vector Editing Widget + ##################################################################*/ + +#include "widgets/widget-sizes.h" +#include "xml/node-event-vector.h" +#include "svg/svg-color.h" + +#define PAD 4 + +static GtkWidget *sp_gradient_vector_widget_new(SPGradient *gradient, SPStop *stop); + +static void sp_gradient_vector_widget_load_gradient(GtkWidget *widget, SPGradient *gradient); +static gint sp_gradient_vector_dialog_delete(GtkWidget *widget, GdkEvent *event, GtkWidget *dialog); +static void sp_gradient_vector_dialog_destroy(GtkWidget *object, gpointer data); +static void sp_gradient_vector_widget_destroy(GtkWidget *object, gpointer data); +static void sp_gradient_vector_gradient_release(SPObject *obj, GtkWidget *widget); +static void sp_gradient_vector_gradient_modified(SPObject *obj, guint flags, GtkWidget *widget); +static void sp_gradient_vector_color_dragged(Inkscape::UI::SelectedColor *selected_color, GObject *object); +static void sp_gradient_vector_color_changed(Inkscape::UI::SelectedColor *selected_color, GObject *object); +static void update_stop_list( GtkWidget *vb, SPGradient *gradient, SPStop *new_stop); + +static gboolean blocked = FALSE; + +static void grad_edit_dia_stop_added_or_removed(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node */*child*/, Inkscape::XML::Node */*ref*/, gpointer data) +{ + GtkWidget *vb = GTK_WIDGET(data); + SPGradient *gradient = static_cast<SPGradient *>(g_object_get_data(G_OBJECT(vb), "gradient")); + update_stop_list(vb, gradient, nullptr); +} + +//FIXME!!! We must also listen to attr changes on all children (i.e. stops) too, +//otherwise the dialog does not reflect undoing color or offset change. This is a major +//hassle, unless we have a "one of the descendants changed in some way" signal. +static Inkscape::XML::NodeEventVector grad_edit_dia_repr_events = +{ + grad_edit_dia_stop_added_or_removed, /* child_added */ + grad_edit_dia_stop_added_or_removed, /* child_removed */ + nullptr, /* attr_changed*/ + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +static void verify_grad(SPGradient *gradient) +{ + int i = 0; + SPStop *stop = nullptr; + /* count stops */ + for (auto& ochild: gradient->children) { + if (SP_IS_STOP(&ochild)) { + i++; + stop = SP_STOP(&ochild); + } + } + + Inkscape::XML::Document *xml_doc; + xml_doc = gradient->getRepr()->document(); + + if (i < 1) { + Inkscape::CSSOStringStream os; + os << "stop-color: #000000;stop-opacity:" << 1.0 << ";"; + + Inkscape::XML::Node *child; + + child = xml_doc->createElement("svg:stop"); + sp_repr_set_css_double(child, "offset", 0.0); + child->setAttribute("style", os.str()); + gradient->getRepr()->addChild(child, nullptr); + Inkscape::GC::release(child); + + child = xml_doc->createElement("svg:stop"); + sp_repr_set_css_double(child, "offset", 1.0); + child->setAttribute("style", os.str()); + gradient->getRepr()->addChild(child, nullptr); + Inkscape::GC::release(child); + return; + } + if (i < 2) { + sp_repr_set_css_double(stop->getRepr(), "offset", 0.0); + Inkscape::XML::Node *child = stop->getRepr()->duplicate(gradient->getRepr()->document()); + sp_repr_set_css_double(child, "offset", 1.0); + gradient->getRepr()->addChild(child, stop->getRepr()); + Inkscape::GC::release(child); + } +} + +static void select_stop_in_list( GtkWidget *vb, SPGradient *gradient, SPStop *new_stop) +{ + GtkWidget *combo_box = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(vb), "combo_box")); + + int i = 0; + for (auto& ochild: gradient->children) { + if (SP_IS_STOP(&ochild)) { + if (&ochild == new_stop) { + gtk_combo_box_set_active (GTK_COMBO_BOX(combo_box) , i); + break; + } + i++; + } + } +} + +static void update_stop_list( GtkWidget *vb, SPGradient *gradient, SPStop *new_stop) +{ + + if (!SP_IS_GRADIENT(gradient)) { + return; + } + + blocked = TRUE; + + /* Clear old list, if there is any */ + GtkWidget *combo_box = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(vb), "combo_box")); + if (!combo_box) { + return; + } + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo_box))); + if (!store) { + return; + } + gtk_list_store_clear(store); + GtkTreeIter iter; + + /* Populate the combobox store */ + std::vector<SPStop *> sl; + if ( gradient->hasStops() ) { + for (auto& ochild: gradient->children) { + if (SP_IS_STOP(&ochild)) { + sl.push_back(SP_STOP(&ochild)); + } + } + } + if (sl.empty()) { + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, NULL, 1, _("No stops in gradient"), 2, NULL, -1); + gtk_widget_set_sensitive (combo_box, FALSE); + + } else { + + for (auto stop:sl) { + Inkscape::XML::Node *repr = stop->getRepr(); + Inkscape::UI::Widget::ColorPreview *cpv = Gtk::manage(new Inkscape::UI::Widget::ColorPreview(stop->get_rgba32())); + GdkPixbuf *pb = cpv->toPixbuf(64, 16); + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, pb, 1, repr->attribute("id"), 2, stop, -1); + gtk_widget_set_sensitive (combo_box, FALSE); + } + gtk_widget_set_sensitive(combo_box, TRUE); + } + + /* Set history */ + if (new_stop == nullptr) { + gtk_combo_box_set_active (GTK_COMBO_BOX(combo_box) , 0); + } else { + select_stop_in_list(vb, gradient, new_stop); + } + + blocked = FALSE; +} + + +// user selected existing stop from list +static void sp_grad_edit_combo_box_changed (GtkComboBox * /*widget*/, GtkWidget *tbl) +{ + SPStop *stop = get_selected_stop(tbl); + if (!stop) { + return; + } + + blocked = TRUE; + + SelectedColor *csel = static_cast<SelectedColor*>(g_object_get_data(G_OBJECT(tbl), "cselector")); + // set its color, from the stored array + g_object_set_data(G_OBJECT(tbl), "updating_color", reinterpret_cast<void*>(1)); + csel->setColorAlpha(stop->getColor(), stop->getOpacity()); + g_object_set_data(G_OBJECT(tbl), "updating_color", reinterpret_cast<void*>(0)); + GtkWidget *offspin = GTK_WIDGET(g_object_get_data(G_OBJECT(tbl), "offspn")); + GtkWidget *offslide =GTK_WIDGET(g_object_get_data(G_OBJECT(tbl), "offslide")); + + GtkAdjustment *adj = static_cast<GtkAdjustment*>(g_object_get_data(G_OBJECT(tbl), "offset")); + + bool isEndStop = false; + + SPStop *prev = nullptr; + prev = stop->getPrevStop(); + if (prev != nullptr ) { + gtk_adjustment_set_lower (adj, prev->offset); + } else { + isEndStop = true; + gtk_adjustment_set_lower (adj, 0); + } + + SPStop *next = nullptr; + next = stop->getNextStop(); + if (next != nullptr ) { + gtk_adjustment_set_upper (adj, next->offset); + } else { + isEndStop = true; + gtk_adjustment_set_upper (adj, 1.0); + } + + //fixme: does this work on all possible input gradients? + if (!isEndStop) { + gtk_widget_set_sensitive(offslide, TRUE); + gtk_widget_set_sensitive(GTK_WIDGET(offspin), TRUE); + } else { + gtk_widget_set_sensitive(offslide, FALSE); + gtk_widget_set_sensitive(GTK_WIDGET(offspin), FALSE); + } + + gtk_adjustment_set_value(adj, stop->offset); + + blocked = FALSE; +} + +static SPStop *get_selected_stop( GtkWidget *vb) +{ + SPStop *stop = nullptr; + GtkWidget *combo_box = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(vb), "combo_box")); + if (combo_box) { + GtkTreeIter iter; + if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX(combo_box), &iter)) { + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo_box))); + gtk_tree_model_get (GTK_TREE_MODEL(store), &iter, 2, &stop, -1); + } + } + return stop; +} + +static void offadjustmentChanged( GtkAdjustment *adjustment, GtkWidget *vb) +{ + if (!blocked) { + blocked = TRUE; + + SPStop *stop = get_selected_stop(vb); + if (stop) { + stop->offset = gtk_adjustment_get_value (adjustment); + sp_repr_set_css_double(stop->getRepr(), "offset", stop->offset); + + DocumentUndo::maybeDone(stop->document, "gradient:stop:offset", SP_VERB_CONTEXT_GRADIENT, + _("Change gradient stop offset")); + + } + + blocked = FALSE; + } +} + +guint32 sp_average_color(guint32 c1, guint32 c2, gdouble p/* = 0.5*/) +{ + guint32 r = (guint32) (SP_RGBA32_R_U(c1) * p + SP_RGBA32_R_U(c2) * (1 - p)); + guint32 g = (guint32) (SP_RGBA32_G_U(c1) * p + SP_RGBA32_G_U(c2) * (1 - p)); + guint32 b = (guint32) (SP_RGBA32_B_U(c1) * p + SP_RGBA32_B_U(c2) * (1 - p)); + guint32 a = (guint32) (SP_RGBA32_A_U(c1) * p + SP_RGBA32_A_U(c2) * (1 - p)); + + return SP_RGBA32_U_COMPOSE(r, g, b, a); +} + + +static void sp_grd_ed_add_stop(GtkWidget */*widget*/, GtkWidget *vb) +{ + SPGradient *gradient = static_cast<SPGradient *>(g_object_get_data(G_OBJECT(vb), "gradient")); + verify_grad(gradient); + + SPStop *stop = get_selected_stop(vb); + if (!stop) { + return; + } + + Inkscape::XML::Node *new_stop_repr = nullptr; + + SPStop *next = stop->getNextStop(); + + if (next == nullptr) { + SPStop *prev = stop->getPrevStop(); + if (prev != nullptr) { + next = stop; + stop = prev; + } + } + + if (next != nullptr) { + new_stop_repr = stop->getRepr()->duplicate(gradient->getRepr()->document()); + gradient->getRepr()->addChild(new_stop_repr, stop->getRepr()); + } else { + next = stop; + new_stop_repr = stop->getPrevStop()->getRepr()->duplicate(gradient->getRepr()->document()); + gradient->getRepr()->addChild(new_stop_repr, stop->getPrevStop()->getRepr()); + } + + SPStop *newstop = reinterpret_cast<SPStop *>(gradient->document->getObjectByRepr(new_stop_repr)); + + newstop->offset = (stop->offset + next->offset) * 0.5 ; + + guint32 const c1 = stop->get_rgba32(); + guint32 const c2 = next->get_rgba32(); + guint32 cnew = sp_average_color(c1, c2); + + Inkscape::CSSOStringStream os; + gchar c[64]; + sp_svg_write_color(c, sizeof(c), cnew); + gdouble opacity = static_cast<gdouble>(SP_RGBA32_A_F(cnew)); + os << "stop-color:" << c << ";stop-opacity:" << opacity <<";"; + newstop->setAttribute("style", os.str()); + sp_repr_set_css_double( newstop->getRepr(), "offset", (double)newstop->offset); + + sp_gradient_vector_widget_load_gradient(vb, gradient); + Inkscape::GC::release(new_stop_repr); + update_stop_list(GTK_WIDGET(vb), gradient, newstop); + GtkWidget *offspin = GTK_WIDGET(g_object_get_data(G_OBJECT(vb), "offspn")); + GtkWidget *offslide =GTK_WIDGET(g_object_get_data(G_OBJECT(vb), "offslide")); + gtk_widget_set_sensitive(offslide, TRUE); + gtk_widget_set_sensitive(GTK_WIDGET(offspin), TRUE); + DocumentUndo::done(gradient->document, SP_VERB_CONTEXT_GRADIENT, + _("Add gradient stop")); +} + +static void sp_grd_ed_del_stop(GtkWidget */*widget*/, GtkWidget *vb) +{ + SPGradient *gradient = static_cast<SPGradient *>(g_object_get_data(G_OBJECT(vb), "gradient")); + + SPStop *stop = get_selected_stop(vb); + if (!stop) { + return; + } + + if (gradient->vector.stops.size() > 2) { // 2 is the minimum + + // if we delete first or last stop, move the next/previous to the edge + if (stop->offset == 0) { + SPStop *next = stop->getNextStop(); + if (next) { + next->offset = 0; + sp_repr_set_css_double(next->getRepr(), "offset", 0); + } + } else if (stop->offset == 1) { + SPStop *prev = stop->getPrevStop(); + if (prev) { + prev->offset = 1; + sp_repr_set_css_double(prev->getRepr(), "offset", 1); + } + } + + gradient->getRepr()->removeChild(stop->getRepr()); + sp_gradient_vector_widget_load_gradient(vb, gradient); + update_stop_list(GTK_WIDGET(vb), gradient, nullptr); + DocumentUndo::done(gradient->document, SP_VERB_CONTEXT_GRADIENT, + _("Delete gradient stop")); + } + +} + +static GtkWidget * sp_gradient_vector_widget_new(SPGradient *gradient, SPStop *select_stop) +{ + using Inkscape::UI::Widget::ColorNotebook; + + GtkWidget *vb, *w, *f; + + g_return_val_if_fail(gradient != nullptr, NULL); + g_return_val_if_fail(SP_IS_GRADIENT(gradient), NULL); + + vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, PAD); + gtk_box_set_homogeneous(GTK_BOX(vb), FALSE); + g_signal_connect(G_OBJECT(vb), "destroy", G_CALLBACK(sp_gradient_vector_widget_destroy), NULL); + + w = sp_gradient_image_new(gradient); + g_object_set_data(G_OBJECT(vb), "preview", w); + gtk_widget_show(w); + gtk_box_pack_start(GTK_BOX(vb), w, TRUE, TRUE, PAD); + + sp_repr_add_listener(gradient->getRepr(), &grad_edit_dia_repr_events, vb); + + /* ComboBox of stops with 3 columns, + * The color preview, the label and a pointer to the SPStop + */ + GtkListStore *store = gtk_list_store_new (3, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_POINTER); + GtkWidget *combo_box = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store)); + + GtkCellRenderer *renderer = gtk_cell_renderer_pixbuf_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo_box), renderer, FALSE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo_box), renderer, "pixbuf", 0, NULL); + gtk_cell_renderer_set_padding(renderer, 5, 0); + + renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo_box), renderer, TRUE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo_box), renderer, "text", 1, NULL); + gtk_widget_show(combo_box); + gtk_box_pack_start(GTK_BOX(vb), combo_box, FALSE, FALSE, 0); + g_object_set_data(G_OBJECT(vb), "combo_box", combo_box); + + update_stop_list(GTK_WIDGET(vb), gradient, nullptr); + + g_signal_connect(G_OBJECT(combo_box), "changed", G_CALLBACK(sp_grad_edit_combo_box_changed), vb); + + /* Add and Remove buttons */ + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + // TRANSLATORS: "Stop" means: a "phase" of a gradient + GtkWidget *b = gtk_button_new_with_label(_("Add stop")); + gtk_widget_show(b); + gtk_container_add(GTK_CONTAINER(hb), b); + gtk_widget_set_tooltip_text(b, _("Add another control stop to gradient")); + g_signal_connect(G_OBJECT(b), "clicked", G_CALLBACK(sp_grd_ed_add_stop), vb); + b = gtk_button_new_with_label(_("Delete stop")); + gtk_widget_show(b); + gtk_container_add(GTK_CONTAINER(hb), b); + gtk_widget_set_tooltip_text(b, _("Delete current control stop from gradient")); + g_signal_connect(G_OBJECT(b), "clicked", G_CALLBACK(sp_grd_ed_del_stop), vb); + + gtk_widget_show(hb); + gtk_box_pack_start(GTK_BOX(vb),hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS); + + /* Offset Slider and stuff */ + hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + /* Label */ + GtkWidget *l = gtk_label_new(C_("Gradient","Offset:")); + gtk_widget_set_halign(l, GTK_ALIGN_END); + + gtk_box_pack_start(GTK_BOX(hb),l, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS); + gtk_widget_show(l); + + /* Adjustment */ + GtkAdjustment *Offset_adj = nullptr; + Offset_adj= GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 1.0, 0.01, 0.01, 0.0)); + g_object_set_data(G_OBJECT(vb), "offset", Offset_adj); + + SPStop *stop = get_selected_stop(vb); + if (!stop) { + return nullptr; + } + + gtk_adjustment_set_value(Offset_adj, stop->offset); + + /* Slider */ + auto slider = gtk_scale_new(GTK_ORIENTATION_HORIZONTAL, Offset_adj); + gtk_scale_set_draw_value( GTK_SCALE(slider), FALSE ); + gtk_widget_show(slider); + gtk_box_pack_start(GTK_BOX(hb),slider, TRUE, TRUE, AUX_BETWEEN_BUTTON_GROUPS); + g_object_set_data(G_OBJECT(vb), "offslide", slider); + + /* Spinbutton */ + GtkWidget *sbtn = gtk_spin_button_new(GTK_ADJUSTMENT(Offset_adj), 0.01, 2); + sp_dialog_defocus_on_enter(sbtn); + gtk_widget_show(sbtn); + gtk_box_pack_start(GTK_BOX(hb),sbtn, FALSE, TRUE, AUX_BETWEEN_BUTTON_GROUPS); + g_object_set_data(G_OBJECT(vb), "offspn", sbtn); + + if (stop->offset>0 && stop->offset<1) { + gtk_widget_set_sensitive(slider, TRUE); + gtk_widget_set_sensitive(GTK_WIDGET(sbtn), TRUE); + } else { + gtk_widget_set_sensitive(slider, FALSE); + gtk_widget_set_sensitive(GTK_WIDGET(sbtn), FALSE); + } + + + /* Signals */ + g_signal_connect(G_OBJECT(Offset_adj), "value_changed", + G_CALLBACK(offadjustmentChanged), vb); + + // g_signal_connect(G_OBJECT(slider), "changed", G_CALLBACK(offsliderChanged), vb); + gtk_widget_show(hb); + gtk_box_pack_start(GTK_BOX(vb), hb, FALSE, FALSE, PAD); + + // TRANSLATORS: "Stop" means: a "phase" of a gradient + f = gtk_frame_new(_("Stop Color")); + gtk_widget_show(f); + gtk_box_pack_start(GTK_BOX(vb), f, TRUE, TRUE, PAD); + + Inkscape::UI::SelectedColor *selected_color = new Inkscape::UI::SelectedColor; + g_object_set_data(G_OBJECT(vb), "cselector", selected_color); + g_object_set_data(G_OBJECT(vb), "updating_color", reinterpret_cast<void*>(0)); + selected_color->signal_changed.connect(sigc::bind(sigc::ptr_fun(&sp_gradient_vector_color_changed), selected_color, G_OBJECT(vb))); + selected_color->signal_dragged.connect(sigc::bind(sigc::ptr_fun(&sp_gradient_vector_color_changed), selected_color, G_OBJECT(vb))); + + Gtk::Widget *color_selector = Gtk::manage(new ColorNotebook(*selected_color)); + color_selector->show(); + gtk_container_add(GTK_CONTAINER(f), color_selector->gobj()); + + /* + gtk_widget_show(csel); + gtk_container_add(GTK_CONTAINER(f), csel); + g_signal_connect(G_OBJECT(csel), "dragged", G_CALLBACK(sp_gradient_vector_color_dragged), vb); + g_signal_connect(G_OBJECT(csel), "changed", G_CALLBACK(sp_gradient_vector_color_changed), vb); + */ + + gtk_widget_show(vb); + + sp_gradient_vector_widget_load_gradient(vb, gradient); + + if (select_stop) { + select_stop_in_list(GTK_WIDGET(vb), gradient, select_stop); + } + + return vb; +} + + + +GtkWidget * sp_gradient_vector_editor_new(SPGradient *gradient, SPStop *stop) +{ + if (dlg == nullptr) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + dlg = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_title ((GtkWindow *) dlg, _("Gradient editor")); + gtk_window_set_resizable ((GtkWindow *) dlg, true); + + if (x == -1000 || y == -1000) { + x = prefs->getInt(prefs_path + "x", -1000); + y = prefs->getInt(prefs_path + "y", -1000); + } + if (w ==0 || h == 0) { + w = prefs->getInt(prefs_path + "w", 0); + h = prefs->getInt(prefs_path + "h", 0); + } + + if (x<0) { + x=0; + } + if (y<0) { + y=0; + } + + if (x != 0 || y != 0) { + gtk_window_move(reinterpret_cast<GtkWindow *>(dlg), x, y); + } else { + gtk_window_set_position(GTK_WINDOW(dlg), GTK_WIN_POS_CENTER); + } + if (w && h) { + gtk_window_resize(reinterpret_cast<GtkWindow *>(dlg), w, h); + } + sp_transientize(dlg); + wd.win = dlg; + wd.stop = 0; + + GObject *obj = G_OBJECT(dlg); + sigc::connection *conn = nullptr; + + conn = new sigc::connection(INKSCAPE.signal_activate_desktop.connect(sigc::bind(sigc::ptr_fun(&sp_transientize_callback), &wd))); + g_object_set_data(obj, "desktop-activate-connection", conn); + + g_signal_connect(obj, "event", G_CALLBACK(sp_dialog_event_handler), dlg); + g_signal_connect(obj, "destroy", G_CALLBACK(sp_gradient_vector_dialog_destroy), dlg); + g_signal_connect(obj, "delete_event", G_CALLBACK(sp_gradient_vector_dialog_delete), dlg); + + conn = new sigc::connection(INKSCAPE.signal_shut_down.connect( + sigc::hide_return( + sigc::bind(sigc::ptr_fun(&sp_gradient_vector_dialog_delete), (GtkWidget *) nullptr, (GdkEvent *) nullptr, (GtkWidget *) nullptr) + ))); + g_object_set_data(obj, "shutdown-connection", conn); + + conn = new sigc::connection(INKSCAPE.signal_dialogs_hide.connect(sigc::bind(sigc::ptr_fun(>k_widget_hide), dlg))); + g_object_set_data(obj, "dialog-hide-connection", conn); + + conn = new sigc::connection(INKSCAPE.signal_dialogs_unhide.connect(sigc::bind(sigc::ptr_fun(>k_widget_show), dlg))); + g_object_set_data(obj, "dialog-unhide-connection", conn); + + gtk_container_set_border_width(GTK_CONTAINER(dlg), PAD); + + GtkWidget *wid = static_cast<GtkWidget*>(sp_gradient_vector_widget_new(gradient, stop)); + g_object_set_data(G_OBJECT(dlg), "gradient-vector-widget", wid); + /* Connect signals */ + gtk_widget_show(wid); + gtk_container_add(GTK_CONTAINER(dlg), wid); + } else { + // FIXME: temp fix for 0.38 + // Simply load_gradient into the editor does not work for multi-stop gradients, + // as the stop list and other widgets are in a wrong state and crash readily. + // Instead we just delete the window (by sending the delete signal) + // and call sp_gradient_vector_editor_new again, so it creates the window anew. + + GdkEventAny event; + GtkWidget *widget = static_cast<GtkWidget *>(dlg); + event.type = GDK_DELETE; + event.window = gtk_widget_get_window (widget); + event.send_event = TRUE; + g_object_ref(G_OBJECT(event.window)); + gtk_main_do_event(reinterpret_cast<GdkEvent*>(&event)); + g_object_unref(G_OBJECT(event.window)); + + g_assert(dlg == nullptr); + sp_gradient_vector_editor_new(gradient, stop); + } + + return dlg; +} + +static void sp_gradient_vector_widget_load_gradient(GtkWidget *widget, SPGradient *gradient) +{ + blocked = TRUE; + + SPGradient *old; + + old = static_cast<SPGradient*>(g_object_get_data(G_OBJECT(widget), "gradient")); + + if (old != gradient) { + sigc::connection *release_connection; + sigc::connection *modified_connection; + + release_connection = static_cast<sigc::connection *>(g_object_get_data(G_OBJECT(widget), "gradient_release_connection")); + modified_connection = static_cast<sigc::connection *>(g_object_get_data(G_OBJECT(widget), "gradient_modified_connection")); + + if (old) { + g_assert( release_connection != nullptr ); + g_assert( modified_connection != nullptr ); + release_connection->disconnect(); + modified_connection->disconnect(); + sp_signal_disconnect_by_data(old, widget); + } + + if (gradient) { + if (!release_connection) { + release_connection = new sigc::connection(); + } + if (!modified_connection) { + modified_connection = new sigc::connection(); + } + *release_connection = gradient->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_gradient_vector_gradient_release), widget)); + *modified_connection = gradient->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_gradient_vector_gradient_modified), widget)); + } else { + if (release_connection) { + delete release_connection; + release_connection = nullptr; + } + if (modified_connection) { + delete modified_connection; + modified_connection = nullptr; + } + } + + g_object_set_data(G_OBJECT(widget), "gradient_release_connection", release_connection); + g_object_set_data(G_OBJECT(widget), "gradient_modified_connection", modified_connection); + } + + g_object_set_data(G_OBJECT(widget), "gradient", gradient); + + if (gradient) { + gtk_widget_set_sensitive(widget, TRUE); + + gradient->ensureVector(); + + SPStop *stop = get_selected_stop(widget); + if (!stop) { + return; + } + + // get the color selector + SelectedColor *csel = static_cast<SelectedColor*>(g_object_get_data(G_OBJECT(widget), "cselector")); + + g_object_set_data(G_OBJECT(widget), "updating_color", reinterpret_cast<void*>(1)); + csel->setColorAlpha(stop->getColor(), stop->getOpacity()); + g_object_set_data(G_OBJECT(widget), "updating_color", reinterpret_cast<void*>(0)); + + /* Fill preview */ + GtkWidget *w = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(widget), "preview")); + sp_gradient_image_set_gradient(SP_GRADIENT_IMAGE(w), gradient); + + update_stop_list(GTK_WIDGET(widget), gradient, nullptr); + + // Once the user edits a gradient, it stops being auto-collectable + if (gradient->getRepr()->attribute("inkscape:collect")) { + SPDocument *document = gradient->document; + DocumentUndo::ScopedInsensitive _no_undo(document); + gradient->removeAttribute("inkscape:collect"); + } + } else { // no gradient, disable everything + gtk_widget_set_sensitive(widget, FALSE); + } + + blocked = FALSE; +} + +static void sp_gradient_vector_dialog_destroy(GtkWidget * /*object*/, gpointer /*data*/) +{ + GObject *obj = G_OBJECT(dlg); + assert(obj != NULL); + + sigc::connection *conn = static_cast<sigc::connection *>(g_object_get_data(obj, "desktop-activate-connection")); + assert(conn != NULL); + conn->disconnect(); + delete conn; + + conn = static_cast<sigc::connection *>(g_object_get_data(obj, "shutdown-connection")); + assert(conn != NULL); + conn->disconnect(); + delete conn; + + conn = static_cast<sigc::connection *>(g_object_get_data(obj, "dialog-hide-connection")); + assert(conn != NULL); + conn->disconnect(); + delete conn; + + conn = static_cast<sigc::connection *>(g_object_get_data(obj, "dialog-unhide-connection")); + assert(conn != NULL); + conn->disconnect(); + delete conn; + + wd.win = dlg = nullptr; + wd.stop = 0; +} + +static gboolean sp_gradient_vector_dialog_delete(GtkWidget */*widget*/, GdkEvent */*event*/, GtkWidget */*dialog*/) +{ + gtk_window_get_position(GTK_WINDOW(dlg), &x, &y); + gtk_window_get_size(GTK_WINDOW(dlg), &w, &h); + + if (x<0) { + x=0; + } + if (y<0) { + y=0; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt(prefs_path + "x", x); + prefs->setInt(prefs_path + "y", y); + prefs->setInt(prefs_path + "w", w); + prefs->setInt(prefs_path + "h", h); + + return FALSE; // which means, go ahead and destroy it +} + +/* Widget destroy handler */ +static void sp_gradient_vector_widget_destroy(GtkWidget *object, gpointer /*data*/) +{ + SPObject *gradient = SP_OBJECT(g_object_get_data(G_OBJECT(object), "gradient")); + + sigc::connection *release_connection = static_cast<sigc::connection *>(g_object_get_data(G_OBJECT(object), "gradient_release_connection")); + sigc::connection *modified_connection = static_cast<sigc::connection *>(g_object_get_data(G_OBJECT(object), "gradient_modified_connection")); + + if (gradient) { + g_assert( release_connection != nullptr ); + g_assert( modified_connection != nullptr ); + release_connection->disconnect(); + modified_connection->disconnect(); + sp_signal_disconnect_by_data(gradient, object); + + if (gradient->getRepr()) { + sp_repr_remove_listener_by_data(gradient->getRepr(), object); + } + } + + SelectedColor *selected_color = static_cast<SelectedColor *>(g_object_get_data(G_OBJECT(object), "cselector")); + if (selected_color) { + delete selected_color; + g_object_set_data(G_OBJECT(object), "cselector", nullptr); + } +} + +static void sp_gradient_vector_gradient_release(SPObject */*object*/, GtkWidget *widget) +{ + sp_gradient_vector_widget_load_gradient(widget, nullptr); +} + +static void sp_gradient_vector_gradient_modified(SPObject *object, guint /*flags*/, GtkWidget *widget) +{ + SPGradient *gradient=SP_GRADIENT(object); + if (!blocked) { + blocked = TRUE; + sp_gradient_vector_widget_load_gradient(widget, gradient); + blocked = FALSE; + } +} + +static void sp_gradient_vector_color_dragged(Inkscape::UI::SelectedColor *selected_color, GObject *object) +{ + SPGradient *gradient, *ngr; + + if (blocked) { + return; + } + + gradient = static_cast<SPGradient*>(g_object_get_data(G_OBJECT(object), "gradient")); + if (!gradient) { + return; + } + + blocked = TRUE; + + ngr = sp_gradient_ensure_vector_normalized(gradient); + if (ngr != gradient) { + /* Our master gradient has changed */ + sp_gradient_vector_widget_load_gradient(GTK_WIDGET(object), ngr); + } + + ngr->ensureVector(); + + SPStop *stop = get_selected_stop(GTK_WIDGET(object)); + if (!stop) { + return; + } + + SPColor color = stop->getColor(); + gfloat opacity = stop->getOpacity(); + selected_color->colorAlpha(color, opacity); + stop->style->stop_color.currentcolor = false; + + blocked = FALSE; +} + +static void sp_gradient_vector_color_changed(Inkscape::UI::SelectedColor *selected_color, GObject *object) +{ + (void)selected_color; + + void* updating_color = g_object_get_data(G_OBJECT(object), "updating_color"); + if (updating_color) { + return; + } + + if (blocked) { + return; + } + + SPGradient *gradient = static_cast<SPGradient*>(g_object_get_data(G_OBJECT(object), "gradient")); + if (!gradient) { + return; + } + + blocked = TRUE; + + SPGradient *ngr = sp_gradient_ensure_vector_normalized(gradient); + if (ngr != gradient) { + /* Our master gradient has changed */ + sp_gradient_vector_widget_load_gradient(GTK_WIDGET(object), ngr); + } + + ngr->ensureVector(); + + /* Set start parameters */ + /* We rely on normalized vector, i.e. stops HAVE to exist */ + g_return_if_fail(ngr->getFirstStop() != nullptr); + + SPStop *stop = get_selected_stop(GTK_WIDGET(object)); + if (!stop) { + return; + } + + SelectedColor *csel = static_cast<SelectedColor *>(g_object_get_data(G_OBJECT(object), "cselector")); + SPColor color; + float alpha = 0; + csel->colorAlpha(color, alpha); + + sp_repr_set_css_double(stop->getRepr(), "offset", stop->offset); + Inkscape::CSSOStringStream os; + os << "stop-color:" << color.toString() << ";stop-opacity:" << static_cast<gdouble>(alpha) <<";"; + stop->setAttribute("style", os.str()); + // g_snprintf(c, 256, "stop-color:#%06x;stop-opacity:%g;", rgb >> 8, static_cast<gdouble>(alpha)); + //stop->setAttribute("style", c); + + DocumentUndo::done(ngr->document, SP_VERB_CONTEXT_GRADIENT, + _("Change gradient stop color")); + + blocked = FALSE; + + // Set the color in the selected stop after change + GtkWidget *combo_box = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(object), "combo_box")); + if (combo_box) { + GtkTreeIter iter; + if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX(combo_box), &iter)) { + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo_box))); + + Inkscape::UI::Widget::ColorPreview *cp = Gtk::manage(new Inkscape::UI::Widget::ColorPreview(stop->get_rgba32())); + GdkPixbuf *pb = cp->toPixbuf(64, 16); + + gtk_list_store_set (store, &iter, 0, pb, /*1, repr->attribute("id"),*/ 2, stop, -1); + } + } + +} + +/* + 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 : diff --git a/src/widgets/gradient-vector.h b/src/widgets/gradient-vector.h new file mode 100644 index 0000000..c30cb32 --- /dev/null +++ b/src/widgets/gradient-vector.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_GRADIENT_VECTOR_H +#define SEEN_GRADIENT_VECTOR_H + +/* + * Gradient vector selection widget + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/liststore.h> +#include <sigc++/connection.h> +#include "gradient-selector.h" + +#define SP_TYPE_GRADIENT_VECTOR_SELECTOR (sp_gradient_vector_selector_get_type ()) +#define SP_GRADIENT_VECTOR_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_GRADIENT_VECTOR_SELECTOR, SPGradientVectorSelector)) +#define SP_GRADIENT_VECTOR_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_GRADIENT_VECTOR_SELECTOR, SPGradientVectorSelectorClass)) +#define SP_IS_GRADIENT_VECTOR_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_GRADIENT_VECTOR_SELECTOR)) +#define SP_IS_GRADIENT_VECTOR_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_GRADIENT_VECTOR_SELECTOR)) + +class SPDocument; +class SPObject; +class SPGradient; +class SPStop; + +struct SPGradientVectorSelector { + GtkBox vbox; + + guint idlabel : 1; + + bool swatched; + + SPDocument *doc; + SPGradient *gr; + + /* Gradient vectors store */ + Glib::RefPtr<Gtk::ListStore> store; + SPGradientSelector::ModelColumns *columns; + + sigc::connection gradient_release_connection; + sigc::connection defs_release_connection; + sigc::connection defs_modified_connection; + sigc::connection tree_select_connection; + + void setSwatched(); +}; + +struct SPGradientVectorSelectorClass { + GtkBoxClass parent_class; + + void (* vector_set) (SPGradientVectorSelector *gvs, SPGradient *gr); +}; + +GType sp_gradient_vector_selector_get_type(); + +GtkWidget *sp_gradient_vector_selector_new (SPDocument *doc, SPGradient *gradient); + +void sp_gradient_vector_selector_set_gradient (SPGradientVectorSelector *gvs, SPDocument *doc, SPGradient *gr); + +SPDocument *sp_gradient_vector_selector_get_document (SPGradientVectorSelector *gvs); +SPGradient *sp_gradient_vector_selector_get_gradient (SPGradientVectorSelector *gvs); + +/* fixme: rethink this (Lauris) */ +GtkWidget *sp_gradient_vector_editor_new (SPGradient *gradient, SPStop *stop = nullptr); + +guint32 sp_average_color(guint32 c1, guint32 c2, gdouble p = 0.5); + +Glib::ustring gr_prepare_label (SPObject *obj); +Glib::ustring gr_ellipsize_text(Glib::ustring const &src, size_t maxlen); + +#endif // SEEN_GRADIENT_VECTOR_H + +/* + 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 : diff --git a/src/widgets/ink-action.cpp b/src/widgets/ink-action.cpp new file mode 100644 index 0000000..0696228 --- /dev/null +++ b/src/widgets/ink-action.cpp @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "ink-action.h" +#include "ui/icon-loader.h" +#include <gtk/gtk.h> + +static void ink_action_finalize( GObject* obj ); +static void ink_action_get_property( GObject* obj, guint propId, GValue* value, GParamSpec * pspec ); +static void ink_action_set_property( GObject* obj, guint propId, const GValue *value, GParamSpec* pspec ); + +static GtkWidget* ink_action_create_menu_item( GtkAction* action ); +static GtkWidget* ink_action_create_tool_item( GtkAction* action ); + +typedef struct +{ + gchar* iconId; + GtkIconSize iconSize; +} InkActionPrivate; + +#define INK_ACTION_GET_PRIVATE( o ) \ + reinterpret_cast<InkActionPrivate *>(ink_action_get_instance_private (o)) + +G_DEFINE_TYPE_WITH_PRIVATE(InkAction, ink_action, GTK_TYPE_ACTION); + +enum { + PROP_INK_ID = 1, + PROP_INK_SIZE +}; + +static void ink_action_class_init( InkActionClass* klass ) +{ + if ( klass ) { + GObjectClass * objClass = G_OBJECT_CLASS( klass ); + + objClass->finalize = ink_action_finalize; + objClass->get_property = ink_action_get_property; + objClass->set_property = ink_action_set_property; + + klass->parent_class.create_menu_item = ink_action_create_menu_item; + klass->parent_class.create_tool_item = ink_action_create_tool_item; + /*klass->parent_class.connect_proxy = connect_proxy;*/ + /*klass->parent_class.disconnect_proxy = disconnect_proxy;*/ + + g_object_class_install_property( objClass, + PROP_INK_ID, + g_param_spec_string( "iconId", + "Icon ID", + "The id for the icon", + "", + (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT) ) ); + + g_object_class_install_property( objClass, + PROP_INK_SIZE, + g_param_spec_int( "iconSize", + "Icon Size", + "The size the icon", + (int)GTK_ICON_SIZE_MENU, + (int)GTK_ICON_SIZE_DIALOG, + (int)GTK_ICON_SIZE_SMALL_TOOLBAR, + (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT) ) ); + } +} + +static void ink_action_init( InkAction* action ) +{ + auto priv = INK_ACTION_GET_PRIVATE (action); + priv->iconId = nullptr; + priv->iconSize = GTK_ICON_SIZE_SMALL_TOOLBAR; +} + +static void ink_action_finalize( GObject* obj ) +{ + InkAction* action = INK_ACTION( obj ); + auto priv = INK_ACTION_GET_PRIVATE (action); + + g_free( priv->iconId ); + g_free( priv ); + +} + +//Any strings passed in should already be localised +InkAction* ink_action_new( const gchar *name, + const gchar *label, + const gchar *tooltip, + const gchar *inkId, + GtkIconSize size ) +{ + GObject* obj = (GObject*)g_object_new( INK_ACTION_TYPE, + "name", name, + "label", label, + "tooltip", tooltip, + "iconId", inkId, + "iconSize", size, + NULL ); + + InkAction* action = INK_ACTION( obj ); + + return action; +} + +static void ink_action_get_property( GObject* obj, guint propId, GValue* value, GParamSpec * pspec ) +{ + InkAction* action = INK_ACTION( obj ); + auto priv = INK_ACTION_GET_PRIVATE (action); + + switch ( propId ) { + case PROP_INK_ID: + { + g_value_set_string( value, priv->iconId ); + } + break; + + case PROP_INK_SIZE: + { + g_value_set_int( value, priv->iconSize ); + } + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( obj, propId, pspec ); + } +} + +void ink_action_set_property( GObject* obj, guint propId, const GValue *value, GParamSpec* pspec ) +{ + InkAction* action = INK_ACTION( obj ); + auto priv = INK_ACTION_GET_PRIVATE (action); + + switch ( propId ) { + case PROP_INK_ID: + { + gchar* tmp = priv->iconId; + priv->iconId = g_value_dup_string( value ); + g_free( tmp ); + } + break; + + case PROP_INK_SIZE: + { + priv->iconSize = (GtkIconSize)g_value_get_int( value ); + } + break; + + default: + { + G_OBJECT_WARN_INVALID_PROPERTY_ID( obj, propId, pspec ); + } + } +} + +static GtkWidget* ink_action_create_menu_item( GtkAction* action ) +{ + InkAction* act = INK_ACTION( action ); + GtkWidget* item = GTK_ACTION_CLASS(ink_action_parent_class)->create_menu_item( action ); + + return item; +} + +static GtkWidget* ink_action_create_tool_item( GtkAction* action ) +{ + InkAction* act = INK_ACTION( action ); + auto priv = INK_ACTION_GET_PRIVATE (act); + GtkWidget* item = GTK_ACTION_CLASS(ink_action_parent_class)->create_tool_item(action); + + if ( priv->iconId ) { + if ( GTK_IS_TOOL_BUTTON(item) ) { + GtkToolButton* button = GTK_TOOL_BUTTON(item); + + GtkWidget *child = sp_get_icon_image(priv->iconId, priv->iconSize); + gtk_tool_button_set_icon_widget( button, child ); + } else { + // For now trigger a warning but don't do anything else + GtkToolButton* button = GTK_TOOL_BUTTON(item); + (void)button; + } + } + + // TODO investigate if needed + gtk_widget_show_all( item ); + + return item; +} + +/* + 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 : diff --git a/src/widgets/ink-action.h b/src/widgets/ink-action.h new file mode 100644 index 0000000..d97afe1 --- /dev/null +++ b/src/widgets/ink-action.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INK_ACTION +#define SEEN_INK_ACTION + +#include <gtk/gtk.h> +#include "attributes.h" + +/* Equivalent to GTK Actions of the same type, but can support Inkscape SVG icons */ + +G_BEGIN_DECLS + +#define INK_ACTION_TYPE ( ink_action_get_type() ) +#define INK_ACTION( obj ) ( G_TYPE_CHECK_INSTANCE_CAST( (obj), INK_ACTION_TYPE, InkAction) ) +#define INK_ACTION_CLASS( klass ) ( G_TYPE_CHECK_CLASS_CAST( (klass), INK_ACTION_TYPE, InkActionClass) ) +#define IS_INK_ACTION( obj ) ( G_TYPE_CHECK_INSTANCE_TYPE( (obj), INK_ACTION_TYPE) ) +#define IS_INK_ACTION_CLASS( klass ) ( G_TYPE_CHECK_CLASS_TYPE( (klass), INK_ACTION_TYPE) ) +#define INK_ACTION_GET_CLASS( obj ) ( G_TYPE_INSTANCE_GET_CLASS( (obj), INK_ACTION_TYPE, InkActionClass) ) + +typedef struct _InkAction InkAction; +typedef struct _InkActionClass InkActionClass; + +struct _InkAction +{ + GtkAction action; +}; + +struct _InkActionClass +{ + GtkActionClass parent_class; +}; + +GType ink_action_get_type( void ); + +InkAction* ink_action_new( const gchar *name, + const gchar *label, + const gchar *tooltip, + const gchar *inkId, + GtkIconSize size ); + + +G_END_DECLS + +#endif /* SEEN_INK_ACTION */ +/* + 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 : diff --git a/src/widgets/mappings.xml b/src/widgets/mappings.xml new file mode 100644 index 0000000..3ea9cb3 --- /dev/null +++ b/src/widgets/mappings.xml @@ -0,0 +1,334 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<mappings> + + <!-- file menu --> + <remap id='file_import' newid='document-import'/> + <remap id='file_export' newid='document-export'/> + <remap id='ocal_import' newid='document-import-ocal'/> + <remap id='ocal_export' newid='document-export-ocal'/> + <remap id='document_metadata' newid='document-metadata'/> + <remap id='input_devices' newid='dialog-input-devices'/> + + <!-- edit menu --> + <remap id='edit_duplicate' newid='edit-duplicate'/> + <remap id='edit_clone' newid='edit-clone'/> + <remap id='edit_unlink_clone' newid='edit-clone-unlink'/> + <remap id='edit_select_original' newid='edit-select-original'/> + <remap id='edit_undo_history' newid='edit-undo-history'/> + <remap id='selection_paste_in_place' newid='edit-paste-in-place'/> + <remap id='selection_paste_style' newid='edit-paste-style'/> + <remap id='selection_bitmap' newid='selection-make-bitmap-copy'/> + <remap id='selection_select_all' newid='edit-select-all'/> + <remap id='selection_select_all_in_all_layers' newid='edit-select-all-layers'/> + <remap id='selection_invert' newid='edit-select-invert'/> + <remap id='selection_deselect' newid='edit-select-none'/> + <remap id='xml_editor' newid='dialog-xml-editor'/> + + <!-- view menu --> + <!-- submenu: zoom --> + <remap id='zoom_1_to_1' newid='zoom-original'/> + <remap id='zoom_1_to_2' newid='zoom-half-size'/> + <remap id='zoom_2_to_1' newid='zoom-double-size'/> + <remap id='zoom_select' newid='zoom-fit-selection'/> + <remap id='zoom_draw' newid='zoom-fit-drawing'/> + <remap id='zoom_page' newid='zoom-fit-page'/> + <remap id='zoom_pagewidth' newid='zoom-fit-width'/> + <remap id='zoom_previous' newid='zoom-previous'/> + <remap id='zoom_next' newid='zoom-next'/> + <remap id='zoom_in' newid='zoom-in'/> + <remap id='zoom_out' newid='zoom-out'/> + + <remap id='grid' newid='show-grid'/> + <remap id='guides' newid='show-guides'/> + <remap id='color_management' newid='color-management'/> + <remap id='dialog_toggle' newid='show-dialogs'/> + <remap id='messages' newid='dialog-messages'/> + <remap id='scripts' newid='dialog-scripts'/> + <remap id='window_previous' newid='window-previous'/> + <remap id='window_next' newid='window-next'/> + <remap id='view_icon_preview' newid='dialog-icon-preview'/> + <remap id='view_new' newid='window-new'/> + <remap id='fullscreen' newid='view-fullscreen'/> + + <!-- layers menu --> + <remap id='new_layer' newid='layer-new'/> + <remap id='rename_layer' newid='layer-rename'/> + <remap id='switch_to_layer_above' newid='layer-previous'/> + <remap id='switch_to_layer_below' newid='layer-next'/> + <remap id='move_selection_above' newid='selection-move-to-layer-above'/> + <remap id='move_selection_below' newid='selection-move-to-layer-below'/> + <remap id='raise_layer' newid='layer-raise'/> + <remap id='lower_layer' newid='layer-lower'/> + <remap id='layer_to_top' newid='layer-top'/> + <remap id='layer_to_bottom' newid='layer-bottom'/> + <remap id='delete_layer' newid='layer-delete'/> + <remap id='layers' newid='dialog-layers'/> + + <!-- object menu --> + <remap id='fill_and_stroke' newid='dialog-fill-and-stroke'/> + <remap id='dialog_item_properties' newid='dialog-object-properties'/> + <remap id='selection_group' newid='object-group'/> + <remap id='selection_ungroup' newid='object-ungroup'/> + <remap id='selection_up' newid='selection-raise'/> + <remap id='selection_down' newid='selection-lower'/> + <remap id='selection_top' newid='selection-top'/> + <remap id='selection_bot' newid='selection-bottom'/> + <remap id='object_rotate_90_CCW' newid='object-rotate-left'/> + <remap id='object_rotate_90_CW' newid='object-rotate-right'/> + <remap id='object_flip_hor' newid='object-flip-horizontal'/> + <remap id='object_flip_ver' newid='object-flip-vertical'/> + <remap id='object_trans' newid='dialog-transform'/> + <remap id='object_align' newid='dialog-align-and-distribute'/> + <remap id='grid_arrange' newid='dialog-rows-and-columns'/> + + <!-- path menu --> + <remap id='object_tocurve' newid='object-to-path'/> + <remap id='stroke_tocurve' newid='stroke-to-path'/> + <remap id='selection_trace' newid='bitmap-trace'/> + <remap id='union' newid='path-union'/> + <remap id='difference' newid='path-difference'/> + <remap id='intersection' newid='path-intersection'/> + <remap id='exclusion' newid='path-exclusion'/> + <remap id='division' newid='path-division'/> + <remap id='cut_path' newid='path-cut'/> + <remap id='selection_combine' newid='path-combine'/> + <remap id='selection_break' newid='path-break-apart'/> + <remap id='outset_path' newid='path-outset'/> + <remap id='inset_path' newid='path-inset'/> + <remap id='dynamic_offset' newid='path-offset-dynamic'/> + <remap id='linked_offset' newid='path-offset-linked'/> + <remap id='simplify' newid='path-simplify'/> + <remap id='selection_reverse' newid='path-reverse'/> + + + <!-- text menu --> + <remap id='object_font' newid='dialog-text-and-font'/> + <remap id='put_on_path' newid='text-put-on-path'/> + <remap id='remove_from_path' newid='text-remove-from-path'/> + <remap id='flow_into_frame' newid='text-flow-into-frame'/> + <remap id='unflow' newid='text-unflow'/> + <remap id='convert_to_text' newid='text-convert-to-regular'/> + <remap id='remove_manual_kerns' newid='text-unkern'/> + + <!-- help menu --> + <remap id='help_keys' newid='help-keyboard-shortcuts'/> + <remap id='help_tutorials' newid='help-contents'/> + <remap id='inkscape_options' newid='inkscape-logo'/> + <remap id='about_memory' newid='dialog-memory'/> + + <!-- tools --> + <remap id='draw_select' newid='tool-pointer'/> + <remap id='draw_node' newid='tool-node-editor'/> + <remap id='draw_tweak' newid='tool-tweak'/> + <remap id='draw_zoom' newid='zoom'/> + <remap id='draw_rect' newid='draw-rectangle'/> + <remap id='draw_3dbox' newid='draw-cuboid'/> + <remap id='draw_arc' newid='draw-ellipse'/> + <remap id='draw_star' newid='draw-polygon-star'/> + <remap id='draw_spiral' newid='draw-spiral'/> + <remap id='draw_freehand' newid='draw-freehand'/> + <remap id='draw_pen' newid='draw-path'/> + <remap id='draw_calligraphic' newid='draw-calligraphic'/> + <remap id='draw_erase' newid='draw-eraser'/> + <remap id='draw_paintbucket' newid='color-fill'/> + <remap id='draw_text' newid='draw-text'/> + <remap id='draw_connector' newid='draw-connector'/> + <remap id='draw_gradient' newid='color-gradient'/> + <remap id='draw_dropper' newid='color-picker'/> + + <!-- TOOLBARS --> + <!-- select toolbar --> + <remap id='transform_stroke' newid='transform-affect-stroke'/> + <remap id='transform_corners' newid='transform-affect-rounded-corners'/> + <remap id='transform_gradient' newid='transform-affect-gradient'/> + <remap id='transform_pattern' newid='transform-affect-pattern'/> + + <!-- node editor toolbar --> + <remap id='node_insert' newid='node-add'/> + <remap id='node_delete' newid='node-delete'/> + <remap id='node_join' newid='node-join'/> + <remap id='node_break' newid='node-break'/> + <remap id='node_join_segment' newid='node-join-segment'/> + <remap id='node_delete_segment' newid='node-delete-segment'/> + <remap id='node_cusp' newid='node-type-cusp'/> + <remap id='node_smooth' newid='node-type-smooth'/> + <remap id='node_symmetric' newid='node-type-symmetric'/> + <remap id='node_auto' newid='node-type-auto-smooth'/> + <remap id='node_curve' newid='node-segment-curve'/> + <remap id='node_line' newid='node-segment-line'/> + <remap id='nodes_show_handles' newid='show-node-handles'/> + <remap id='edit_next_parameter' newid='path-effect-parameter-next'/> + <remap id='nodes_show_helperpath' newid='show-path-outline'/> + <remap id='nodeedit-clippath' newid='path-clip-edit'/> + <remap id='nodeedit-mask' newid='path-mask-edit'/> + <remap id='node_cusp' newid='node-type-cusp'/> + + <!-- tweak toolbar --> + <remap id='tweak_move_mode' newid='object-tweak-push'/> + <remap id='tweak_move_mode_inout' newid='object-tweak-attract'/> + <remap id='tweak_move_mode_jitter' newid='object-tweak-randomize'/> + <remap id='tweak_scale_mode' newid='object-tweak-shrink'/> + <remap id='tweak_rotate_mode' newid='object-tweak-rotate'/> + <remap id='tweak_moreless_mode' newid='object-tweak-duplicate'/> + <remap id='tweak_move_mode' newid='object-tweak-push'/> + <remap id='tweak_push_mode' newid='path-tweak-push'/> + <remap id='tweak_shrink_mode' newid='path-tweak-shrink'/> + <remap id='tweak_attract_mode' newid='path-tweak-attract'/> + <remap id='tweak_roughen_mode' newid='path-tweak-roughen'/> + <remap id='tweak_colorpaint_mode' newid='object-tweak-paint'/> + <remap id='tweak_colorjitter_mode' newid='object-tweak-jitter-color'/> + <remap id='tweak_blur_mode' newid='object-tweak-blur'/> + + <!-- rectangle toolbar --> + <remap id='squared_corner' newid='rectangle-make-corners-sharp'/> + + <!-- cuboid toolbar --> + <remap id='toggle_vp_x' newid='perspective-parallel'/> + + <!-- ellipse toolbar --> + <remap id='reset_circle' newid='draw-ellipse-whole'/> + <remap id='circle_closed_arc' newid='draw-ellipse-segment'/> + <remap id='circle_open_arc' newid='draw-ellipse-arc'/> + + <!-- polygon toolbar --> + <remap id='star_flat' newid='draw-polygon'/> + <remap id='star_angled' newid='draw-star'/> + + <!-- bezier toolbar --> + <remap id='bezier_mode' newid='path-mode-bezier'/> + <remap id='spiro_splines_mode' newid='path-mode-spiro'/> + <remap id='bspline_mode' newid='path-mode-bspline'/> + <remap id='polylines_mode' newid='path-mode-polyline'/> + <remap id='paraxial_lines_mode' newid='path-mode-polyline-paraxial'/> + + <!-- calligraphic toolbar --> + <remap id='guse_tilt' newid='draw-use-tilt'/> + <remap id='guse_pressure' newid='draw-use-pressure'/> + <remap id='trace_background' newid='draw-trace-background'/> + + <!-- eraser toolbar --> + <remap id='delete_object' newid='draw-eraser-delete-objects'/> + + <!-- text toolbar --> + <remap id='writing_mode_tb' newid='format-text-direction-vertical'/> + <remap id='writing_mode_lr' newid='format-text-direction-horizontal'/> + + <!-- connector toolbar --> + <remap id='connector_avoid' newid='connector-avoid'/> + <remap id='connector_ignore' newid='connector-ignore'/> + + <!-- gradient toolbar --> + <remap id='controls_fill' newid='object-fill'/> + <remap id='controls_stroke' newid='object-stroke'/> + + <!-- lpe toolbar --> + <remap id='lpetool_show_bbox' newid='show-bounding-box'/> + <remap id='all_inactive_old' newid='draw-geometry-inactive'/> + <remap id='angle_bisector' newid='draw-geometry-angle-bisector'/> + <remap id='circle_3pts' newid='draw-geometry-circle-from-three-points'/> + <remap id='line_segment' newid='draw-geometry-line-segment'/> + <remap id='mirror_symmetry' newid='draw-geometry-mirror'/> + <remap id='parralel' newid='draw-geometry-line-parallel'/> + <remap id='perp_bisector' newid='draw-geometry-line-perpendicular'/> + + <!-- snapping toolbar --> + <remap id='toggle_snap_global' newid='snap'/> + <remap id='toggle_snap_bbox' newid='snap-bounding-box'/> + <remap id='toggle_snap_to_bbox_path' newid='snap-bounding-box-edges'/> + <remap id='toggle_snap_to_bbox_node' newid='snap-bounding-box-corners'/> + <remap id='toggle_snap_to_bbox_edge_midpoints' newid='snap-bounding-box-midpoints'/> + <remap id='toggle_snap_to_bbox_midpoints' newid='snap-bounding-box-center'/> + <remap id='toggle_snap_nodes' newid='snap-nodes'/> + <remap id='toggle_snap_to_paths' newid='snap-nodes-path'/> + <remap id='toggle_snap_to_nodes' newid='snap-nodes-cusp'/> + <remap id='toggle_snap_to_smooth_nodes' newid='snap-nodes-smooth'/> + <remap id='toggle_snap_to_midpoints' newid='snap-nodes-midpoint'/> + <remap id='toggle_snap_to_path_intersections' newid='snap-nodes-intersection'/> + <remap id='toggle_snap_to_bbox_midpoints-3' newid='snap-nodes-center'/> + <remap id='toggle_snap_center' newid='snap-nodes-rotation-center'/> + <remap id='toggle_snap_page_border' newid='snap-page'/> + <remap id='toggle_snap_grid_guide_intersections' newid='snap-grid-guide-intersections'/> + + <!-- DIALOGS --> + <!-- align and distribute dialog --> + <remap id='al_left_out' newid='align-horizontal-right-to-anchor'/> + <remap id='al_left_in' newid='align-horizontal-left'/> + <remap id='al_center_hor' newid='align-horizontal-center'/> + <remap id='al_right_in' newid='align-horizontal-right'/> + <remap id='al_right_out' newid='align-horizontal-left-to-anchor'/> + <remap id='al_baselines_vert' newid='align-horizontal-baseline'/> + + <remap id='al_top_out' newid='align-vertical-bottom-to-anchor'/> + <remap id='al_top_in' newid='align-vertical-top'/> + <remap id='al_center_ver' newid='align-vertical-center'/> + <remap id='al_bottom_in' newid='align-vertical-bottom'/> + <remap id='al_bottom_out' newid='align-vertical-top-to-anchor'/> + <remap id='al_baselines_hor' newid='align-vertical-baseline'/> + + <remap id='distribute_left' newid='distribute-horizontal-left'/> + <remap id='distribute_hcentre' newid='distribute-horizontal-center'/> + <remap id='distribute_right' newid='distribute-horizontal-right'/> + <remap id='distrobute-hdist' newid='distribute-horizontal-gaps'/> + <remap id='distribute_baselines_hor' newid='distribute-horizontal-baseline'/> + + <remap id='distribute_bottom' newid='distribute-vertical-bottom'/> + <remap id='distribute_vcentre' newid='distribute-vertical-center'/> + <remap id='distribute_top' newid='distribute-vertical-top'/> + <remap id='distrobute-vdist' newid='distribute-vertical-gaps'/> + <remap id='distribute_baselines_vert' newid='distribute-vertical-baseline'/> + + <remap id='distribute_randomize' newid='distribute-randomize'/> + <remap id='unclump' newid='distribute-unclump'/> + <remap id='graph_layout' newid='distribute-graph'/> + <remap id='directed_graph' newid='distribute-graph-directed'/> + <remap id='remove_overlaps' newid='distribute-remove-overlaps'/> + + <remap id='node_valign' newid='align-horizontal-node'/> + <remap id='node_halign' newid='align-vertical-node'/> + <remap id='node_vdistribute' newid='distribute-vertical-node'/> + <remap id='node_hdistribute' newid='distribute-horizontal-node'/> + + <!-- XML editor --> + <remap id='add_xml_element_node' newid='xml-element-new'/> + <remap id='add_xml_text_node' newid='xml-text-new'/> + <remap id='delete_xml_node' newid='xml-node-delete'/> + <remap id='duplicate_xml_node' newid='xml-node-duplicate'/> + <remap id='delete_xml_attribute' newid='xml-attribute-delete'/> + + <!-- transform dialog --> + <remap id='arrows_hor' newid='transform-move-horizontal'/> + <remap id='arrows_ver' newid='transform-move-vertical'/> + <remap id='transform_scale_hor' newid='transform-scale-horizontal'/> + <remap id='transform_scale_ver' newid='transform-scale-vertical'/> + <remap id='transform_scew_hor' newid='transform-skew-horizontal'/> + <remap id='transform_scew_ver' newid='transform-skew-vertical'/> + + <!-- fill and stroke dialog --> + <remap id='properties_fill' newid='object-fill'/> + <remap id='properties_stroke_paint' newid='object-stroke'/> + <remap id='properties_stroke' newid='object-stroke-style'/> + <remap id='fill_none' newid='paint-none'/> + <remap id='fill_solid' newid='paint-solid'/> + <remap id='fill_gradient' newid='paint-gradient-linear'/> + <remap id='fill_radial' newid='paint-gradient-radial'/> + <remap id='fill_pattern' newid='paint-pattern'/> + <remap id='fill_unset' newid='paint-unknown'/> + <remap id='fillrule_evenodd' newid='fill-rule-even-odd'/> + <remap id='fillrule_nonzero' newid='fill-rule-nonzero'/> + <remap id='join_miter' newid='stroke-join-miter'/> + <remap id='join_bevel' newid='stroke-join-bevel'/> + <remap id='join_round' newid='stroke-join-round'/> + <remap id='cap_butt' newid='stroke-cap-butt'/> + <remap id='cap_square' newid='stroke-cap-square'/> + <remap id='cap_round' newid='stroke-cap-round'/> + + <!-- MISCELLANEOUS --> + <remap id='guide' newid='guides'/> + <remap id='grid_xy' newid='grid-rectangular'/> + <remap id='grid_axonom' newid='grid-axonometric'/> + <remap id='visible' newid='object-visible'/> + <remap id='hidden' newid='object-hidden'/> + <remap id='lock_unlocked' newid='object-unlocked'/> + <remap id='width_height_lock' newid='object-locked'/> + <remap id='sticky_zoom' newid='zoom'/> +</mappings> diff --git a/src/widgets/paint-selector.cpp b/src/widgets/paint-selector.cpp new file mode 100644 index 0000000..1e1f99e --- /dev/null +++ b/src/widgets/paint-selector.cpp @@ -0,0 +1,1601 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * SPPaintSelector: Generic paint selector widget. + *//* + * Authors: + * see git history + * Lauris Kaplinski + * bulia byak <buliabyak@users.sf.net> + * John Cliff <simarilius@yahoo.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noSP_PS_VERBOSE + +#include <cstring> +#include <string> +#include <vector> + +#include <glibmm/i18n.h> + +#include "desktop-style.h" +#include "gradient-selector.h" +#include "inkscape.h" +#include "paint-selector.h" +#include "path-prefix.h" + +#include "helper/stock-items.h" +#include "ui/icon-loader.h" + +#include "style.h" + +#include "io/sys.h" + +#include "object/sp-hatch.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-stop.h" + +#include "svg/css-ostringstream.h" + +#include "ui/icon-names.h" +#include "ui/widget/color-notebook.h" + +#include "widgets/swatch-selector.h" +#include "widgets/widget-sizes.h" + +#include "xml/repr.h" + +#ifdef SP_PS_VERBOSE +#include "svg/svg-icc-color.h" +#endif // SP_PS_VERBOSE + +using Inkscape::Widgets::SwatchSelector; +using Inkscape::UI::SelectedColor; + +enum { + MODE_CHANGED, + GRABBED, + DRAGGED, + RELEASED, + CHANGED, + FILLRULE_CHANGED, + LAST_SIGNAL +}; + +static void sp_paint_selector_dispose(GObject *object); + +static GtkWidget *sp_paint_selector_style_button_add(SPPaintSelector *psel, gchar const *px, SPPaintSelector::Mode mode, gchar const *tip); +static void sp_paint_selector_style_button_toggled(GtkToggleButton *tb, SPPaintSelector *psel); +static void sp_paint_selector_fillrule_toggled(GtkToggleButton *tb, SPPaintSelector *psel); + +static void sp_paint_selector_set_mode_empty(SPPaintSelector *psel); +static void sp_paint_selector_set_mode_multiple(SPPaintSelector *psel); +static void sp_paint_selector_set_mode_none(SPPaintSelector *psel); +static void sp_paint_selector_set_mode_color(SPPaintSelector *psel, SPPaintSelector::Mode mode); +static void sp_paint_selector_set_mode_gradient(SPPaintSelector *psel, SPPaintSelector::Mode mode); +#ifdef WITH_MESH +static void sp_paint_selector_set_mode_mesh(SPPaintSelector *psel, SPPaintSelector::Mode mode); +#endif +static void sp_paint_selector_set_mode_pattern(SPPaintSelector *psel, SPPaintSelector::Mode mode); +static void sp_paint_selector_set_mode_hatch(SPPaintSelector *psel, SPPaintSelector::Mode mode); +static void sp_paint_selector_set_mode_swatch(SPPaintSelector *psel, SPPaintSelector::Mode mode); +static void sp_paint_selector_set_mode_unset(SPPaintSelector *psel); + + +static void sp_paint_selector_set_style_buttons(SPPaintSelector *psel, GtkWidget *active); + +static guint psel_signals[LAST_SIGNAL] = {0}; + +#ifdef SP_PS_VERBOSE +static gchar const* modeStrings[] = { + "MODE_EMPTY", + "MODE_MULTIPLE", + "MODE_NONE", + "MODE_SOLID_COLOR", + "MODE_GRADIENT_LINEAR", + "MODE_GRADIENT_RADIAL", +#ifdef WITH_MESH + "MODE_GRADIENT_MESH", +#endif + "MODE_PATTERN", + "MODE_SWATCH", + "MODE_UNSET", + ".", + ".", +}; +#endif + + +static bool isPaintModeGradient(SPPaintSelector::Mode mode) +{ + bool isGrad = (mode == SPPaintSelector::MODE_GRADIENT_LINEAR) || + (mode == SPPaintSelector::MODE_GRADIENT_RADIAL) || + (mode == SPPaintSelector::MODE_SWATCH); + + return isGrad; +} + +static SPGradientSelector *getGradientFromData(SPPaintSelector const *psel) +{ + SPGradientSelector *grad = nullptr; + if (psel->mode == SPPaintSelector::MODE_SWATCH) { + SwatchSelector *swatchsel = static_cast<SwatchSelector*>(g_object_get_data(G_OBJECT(psel->selector), "swatch-selector")); + if (swatchsel) { + grad = swatchsel->getGradientSelector(); + } + } else { + grad = reinterpret_cast<SPGradientSelector*>(g_object_get_data(G_OBJECT(psel->selector), "gradient-selector")); + } + return grad; +} + +G_DEFINE_TYPE(SPPaintSelector, sp_paint_selector, GTK_TYPE_BOX); + +static void +sp_paint_selector_class_init(SPPaintSelectorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + psel_signals[MODE_CHANGED] = g_signal_new("mode_changed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPPaintSelectorClass, mode_changed), + nullptr, nullptr, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + psel_signals[GRABBED] = g_signal_new("grabbed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPPaintSelectorClass, grabbed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + psel_signals[DRAGGED] = g_signal_new("dragged", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPPaintSelectorClass, dragged), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + psel_signals[RELEASED] = g_signal_new("released", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPPaintSelectorClass, released), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + psel_signals[CHANGED] = g_signal_new("changed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPPaintSelectorClass, changed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + psel_signals[FILLRULE_CHANGED] = g_signal_new("fillrule_changed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPPaintSelectorClass, fillrule_changed), + nullptr, nullptr, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + + object_class->dispose = sp_paint_selector_dispose; +} + +#define XPAD 4 +#define YPAD 1 + +static void +sp_paint_selector_init(SPPaintSelector *psel) +{ + gtk_orientable_set_orientation(GTK_ORIENTABLE(psel), GTK_ORIENTATION_VERTICAL); + + psel->mode = static_cast<SPPaintSelector::Mode>(-1); // huh? do you mean 0xff? -- I think this means "not in the enum" + + /* Paint style button box */ + psel->style = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(psel->style), FALSE); + gtk_widget_set_name(psel->style,"PaintSelector"); + gtk_widget_show(psel->style); + gtk_container_set_border_width(GTK_CONTAINER(psel->style), 4); + gtk_box_pack_start(GTK_BOX(psel), psel->style, FALSE, FALSE, 0); + + /* Buttons */ + psel->none = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-none"), + SPPaintSelector::MODE_NONE, _("No paint")); + psel->solid = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-solid"), + SPPaintSelector::MODE_SOLID_COLOR, _("Flat color")); + psel->gradient = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-gradient-linear"), + SPPaintSelector::MODE_GRADIENT_LINEAR, _("Linear gradient")); + psel->radial = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-gradient-radial"), + SPPaintSelector::MODE_GRADIENT_RADIAL, _("Radial gradient")); +#ifdef WITH_MESH + psel->mesh = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-gradient-mesh"), + SPPaintSelector::MODE_GRADIENT_MESH, _("Mesh gradient")); +#endif + psel->pattern = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-pattern"), + SPPaintSelector::MODE_PATTERN, _("Pattern")); + psel->swatch = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-swatch"), + SPPaintSelector::MODE_SWATCH, _("Swatch")); + psel->unset = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-unknown"), + SPPaintSelector::MODE_UNSET, _("Unset paint (make it undefined so it can be inherited)")); + + /* Fillrule */ + { + psel->fillrulebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(psel->fillrulebox), FALSE); + gtk_box_pack_end(GTK_BOX(psel->style), psel->fillrulebox, FALSE, FALSE, 0); + + GtkWidget *w; + psel->evenodd = gtk_radio_button_new(nullptr); + gtk_button_set_relief(GTK_BUTTON(psel->evenodd), GTK_RELIEF_NONE); + gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(psel->evenodd), FALSE); + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/painting.html#FillRuleProperty + gtk_widget_set_tooltip_text(psel->evenodd, _("Any path self-intersections or subpaths create holes in the fill (fill-rule: evenodd)")); + g_object_set_data(G_OBJECT(psel->evenodd), "mode", GUINT_TO_POINTER(SPPaintSelector::FILLRULE_EVENODD)); + w = sp_get_icon_image("fill-rule-even-odd", GTK_ICON_SIZE_MENU); + gtk_container_add(GTK_CONTAINER(psel->evenodd), w); + gtk_box_pack_start(GTK_BOX(psel->fillrulebox), psel->evenodd, FALSE, FALSE, 0); + g_signal_connect(G_OBJECT(psel->evenodd), "toggled", G_CALLBACK(sp_paint_selector_fillrule_toggled), psel); + + psel->nonzero = gtk_radio_button_new(gtk_radio_button_get_group(GTK_RADIO_BUTTON(psel->evenodd))); + gtk_button_set_relief(GTK_BUTTON(psel->nonzero), GTK_RELIEF_NONE); + gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(psel->nonzero), FALSE); + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/painting.html#FillRuleProperty + gtk_widget_set_tooltip_text(psel->nonzero, _("Fill is solid unless a subpath is counterdirectional (fill-rule: nonzero)")); + g_object_set_data(G_OBJECT(psel->nonzero), "mode", GUINT_TO_POINTER(SPPaintSelector::FILLRULE_NONZERO)); + w = sp_get_icon_image("fill-rule-nonzero", GTK_ICON_SIZE_MENU); + gtk_container_add(GTK_CONTAINER(psel->nonzero), w); + gtk_box_pack_start(GTK_BOX(psel->fillrulebox), psel->nonzero, FALSE, FALSE, 0); + g_signal_connect(G_OBJECT(psel->nonzero), "toggled", G_CALLBACK(sp_paint_selector_fillrule_toggled), psel); + } + + /* Frame */ + psel->label = gtk_label_new(""); + auto lbbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); + gtk_box_set_homogeneous(GTK_BOX(lbbox), FALSE); + gtk_widget_show(psel->label); + gtk_box_pack_start(GTK_BOX(lbbox), psel->label, false, false, 4); + gtk_box_pack_start(GTK_BOX(psel), lbbox, false, false, 4); + + psel->frame = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_set_homogeneous(GTK_BOX(psel->frame), FALSE); + gtk_widget_show(psel->frame); + //gtk_container_set_border_width(GTK_CONTAINER(psel->frame), 0); + gtk_box_pack_start(GTK_BOX(psel), psel->frame, TRUE, TRUE, 0); + + + /* Last used color */ + psel->selected_color = new SelectedColor; + psel->updating_color = false; + + psel->selected_color->signal_grabbed.connect(sigc::mem_fun(psel, &SPPaintSelector::onSelectedColorGrabbed)); + psel->selected_color->signal_dragged.connect(sigc::mem_fun(psel, &SPPaintSelector::onSelectedColorDragged)); + psel->selected_color->signal_released.connect(sigc::mem_fun(psel, &SPPaintSelector::onSelectedColorReleased)); + psel->selected_color->signal_changed.connect(sigc::mem_fun(psel, &SPPaintSelector::onSelectedColorChanged)); +} + +static void sp_paint_selector_dispose(GObject *object) +{ + SPPaintSelector *psel = SP_PAINT_SELECTOR(object); + + // clean up our long-living pattern menu + g_object_set_data(G_OBJECT(psel),"patternmenu",nullptr); + +#ifdef WITH_MESH + // clean up our long-living mesh menu + g_object_set_data(G_OBJECT(psel),"meshmenu",nullptr); +#endif + + if (psel->selected_color) { + delete psel->selected_color; + psel->selected_color = nullptr; + } + + if ((G_OBJECT_CLASS(sp_paint_selector_parent_class))->dispose) + (G_OBJECT_CLASS(sp_paint_selector_parent_class))->dispose(object); +} + +static GtkWidget *sp_paint_selector_style_button_add(SPPaintSelector *psel, + gchar const *pixmap, SPPaintSelector::Mode mode, + gchar const *tip) +{ + GtkWidget *b, *w; + + b = gtk_toggle_button_new(); + gtk_widget_set_tooltip_text(b, tip); + gtk_widget_show(b); + + gtk_container_set_border_width(GTK_CONTAINER(b), 0); + + gtk_button_set_relief(GTK_BUTTON(b), GTK_RELIEF_NONE); + + gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(b), FALSE); + g_object_set_data(G_OBJECT(b), "mode", GUINT_TO_POINTER(mode)); + + w = sp_get_icon_image(pixmap, GTK_ICON_SIZE_BUTTON); + gtk_container_add(GTK_CONTAINER(b), w); + + gtk_box_pack_start(GTK_BOX(psel->style), b, FALSE, FALSE, 0); + g_signal_connect(G_OBJECT(b), "toggled", G_CALLBACK(sp_paint_selector_style_button_toggled), psel); + + return b; +} + +static void +sp_paint_selector_style_button_toggled(GtkToggleButton *tb, SPPaintSelector *psel) +{ + if (!psel->update && gtk_toggle_button_get_active(tb)) { + psel->setMode(static_cast<SPPaintSelector::Mode>(GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(tb), "mode")))); + } +} + +static void +sp_paint_selector_fillrule_toggled(GtkToggleButton *tb, SPPaintSelector *psel) +{ + if (!psel->update && gtk_toggle_button_get_active(tb)) { + SPPaintSelector::FillRule fr = static_cast<SPPaintSelector::FillRule>(GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(tb), "mode"))); + g_signal_emit(G_OBJECT(psel), psel_signals[FILLRULE_CHANGED], 0, fr); + } +} + +static void +sp_paint_selector_show_fillrule(SPPaintSelector *psel, bool is_fill) +{ + if (psel->fillrulebox) { + if (is_fill) { + gtk_widget_show_all(psel->fillrulebox); + } else { + gtk_widget_destroy(psel->fillrulebox); + psel->fillrulebox = nullptr; + } + } +} + + +SPPaintSelector *sp_paint_selector_new(FillOrStroke kind) +{ + SPPaintSelector *psel = static_cast<SPPaintSelector*>(g_object_new(SP_TYPE_PAINT_SELECTOR, nullptr)); + + psel->setMode(SPPaintSelector::MODE_MULTIPLE); + + // This silliness is here because I don't know how to pass a parameter to the + // GtkObject "constructor" (sp_paint_selector_init). Remove it when paint_selector + // becomes a normal class. + sp_paint_selector_show_fillrule(psel, kind == FILL); + + return psel; +} + +void SPPaintSelector::setMode(Mode mode) +{ + if (this->mode != mode) { + update = TRUE; +#ifdef SP_PS_VERBOSE + g_print("Mode change %d -> %d %s -> %s\n", this->mode, mode, modeStrings[this->mode], modeStrings[mode]); +#endif + switch (mode) { + case MODE_EMPTY: + sp_paint_selector_set_mode_empty(this); + break; + case MODE_MULTIPLE: + sp_paint_selector_set_mode_multiple(this); + break; + case MODE_NONE: + sp_paint_selector_set_mode_none(this); + break; + case MODE_SOLID_COLOR: + sp_paint_selector_set_mode_color(this, mode); + break; + case MODE_GRADIENT_LINEAR: + case MODE_GRADIENT_RADIAL: + sp_paint_selector_set_mode_gradient(this, mode); + break; +#ifdef WITH_MESH + case MODE_GRADIENT_MESH: + sp_paint_selector_set_mode_mesh(this, mode); + break; +#endif + case MODE_PATTERN: + sp_paint_selector_set_mode_pattern(this, mode); + break; + case MODE_HATCH: + sp_paint_selector_set_mode_hatch(this, mode); + break; + case MODE_SWATCH: + sp_paint_selector_set_mode_swatch(this, mode); + break; + case MODE_UNSET: + sp_paint_selector_set_mode_unset(this); + break; + default: + g_warning("file %s: line %d: Unknown paint mode %d", __FILE__, __LINE__, mode); + break; + } + this->mode = mode; + g_signal_emit(G_OBJECT(this), psel_signals[MODE_CHANGED], 0, this->mode); + update = FALSE; + } +} + +void SPPaintSelector::setFillrule(FillRule fillrule) +{ + if (fillrulebox) { + // TODO this flips widgets but does not use a member to store state. Revisit + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(evenodd), (fillrule == FILLRULE_EVENODD)); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(nonzero), (fillrule == FILLRULE_NONZERO)); + } +} + +void SPPaintSelector::setColorAlpha(SPColor const &color, float alpha) +{ + g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) ); +/* + guint32 rgba = 0; + + if ( sp_color_get_colorspace_type(color) == SP_COLORSPACE_TYPE_CMYK ) + { +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set CMYKA\n"); +#endif + sp_paint_selector_set_mode(psel, MODE_COLOR_CMYK); + } + else +*/ + { +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set RGBA\n"); +#endif + setMode(MODE_SOLID_COLOR); + } + + updating_color = true; + selected_color->setColorAlpha(color, alpha); + updating_color = false; + //rgba = color.toRGBA32( alpha ); +} + +void SPPaintSelector::setSwatch(SPGradient *vector ) +{ +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set SWATCH\n"); +#endif + setMode(MODE_SWATCH); + + SwatchSelector *swatchsel = static_cast<SwatchSelector*>(g_object_get_data(G_OBJECT(selector), "swatch-selector")); + if (swatchsel) { + swatchsel->setVector( (vector) ? vector->document : nullptr, vector ); + } +} + +void SPPaintSelector::setGradientLinear(SPGradient *vector) +{ +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set GRADIENT LINEAR\n"); +#endif + setMode(MODE_GRADIENT_LINEAR); + + SPGradientSelector *gsel = getGradientFromData(this); + + gsel->setMode(SPGradientSelector::MODE_LINEAR); + gsel->setVector((vector) ? vector->document : nullptr, vector); +} + +void SPPaintSelector::setGradientRadial(SPGradient *vector) +{ +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set GRADIENT RADIAL\n"); +#endif + setMode(MODE_GRADIENT_RADIAL); + + SPGradientSelector *gsel = getGradientFromData(this); + + gsel->setMode(SPGradientSelector::MODE_RADIAL); + + gsel->setVector((vector) ? vector->document : nullptr, vector); +} + +#ifdef WITH_MESH +void SPPaintSelector::setGradientMesh(SPMeshGradient *array) +{ +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set GRADIENT MESH\n"); +#endif + setMode(MODE_GRADIENT_MESH); + + // SPGradientSelector *gsel = getGradientFromData(this); + + // gsel->setMode(SPGradientSelector::MODE_GRADIENT_MESH); + // gsel->setVector((mesh) ? mesh->document : 0, mesh); +} +#endif + +void SPPaintSelector::setGradientProperties( SPGradientUnits units, SPGradientSpread spread ) +{ + g_return_if_fail(isPaintModeGradient(mode)); + + SPGradientSelector *gsel = getGradientFromData(this); + gsel->setUnits(units); + gsel->setSpread(spread); +} + +void SPPaintSelector::getGradientProperties( SPGradientUnits &units, SPGradientSpread &spread) const +{ + g_return_if_fail(isPaintModeGradient(mode)); + + SPGradientSelector *gsel = getGradientFromData(this); + units = gsel->getUnits(); + spread = gsel->getSpread(); +} + + +/** + * \post (alpha == NULL) || (*alpha in [0.0, 1.0]). + */ +void SPPaintSelector::getColorAlpha(SPColor &color, gfloat &alpha) const +{ + selected_color->colorAlpha(color, alpha); + + g_assert( ( 0.0 <= alpha ) + && ( alpha <= 1.0 ) ); +} + +SPGradient *SPPaintSelector::getGradientVector() +{ + SPGradient* vect = nullptr; + + if (isPaintModeGradient(mode)) { + SPGradientSelector *gsel = getGradientFromData(this); + vect = gsel->getVector(); + } + + return vect; +} + + +void SPPaintSelector::pushAttrsToGradient( SPGradient *gr ) const +{ + SPGradientUnits units = SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX; + SPGradientSpread spread = SP_GRADIENT_SPREAD_PAD; + getGradientProperties( units, spread ); + gr->setUnits(units); + gr->setSpread(spread); + gr->updateRepr(); +} + +static void +sp_paint_selector_clear_frame(SPPaintSelector *psel) +{ + g_return_if_fail( psel != nullptr); + + if (psel->selector) { + + //This is a hack to work around GtkNotebook bug in ColorSelector. Is sends signal switch-page on destroy + //The widget is hidden first so it can recognize that it should not process signals from notebook child + gtk_widget_set_visible(psel->selector, false); + gtk_widget_destroy(psel->selector); + psel->selector = nullptr; + } +} + +static void +sp_paint_selector_set_mode_empty(SPPaintSelector *psel) +{ + sp_paint_selector_set_style_buttons(psel, nullptr); + gtk_widget_set_sensitive(psel->style, FALSE); + + sp_paint_selector_clear_frame(psel); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>No objects</b>")); +} + +static void +sp_paint_selector_set_mode_multiple(SPPaintSelector *psel) +{ + sp_paint_selector_set_style_buttons(psel, nullptr); + gtk_widget_set_sensitive(psel->style, TRUE); + + sp_paint_selector_clear_frame(psel); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Multiple styles</b>")); +} + +static void +sp_paint_selector_set_mode_unset(SPPaintSelector *psel) +{ + sp_paint_selector_set_style_buttons(psel, psel->unset); + gtk_widget_set_sensitive(psel->style, TRUE); + + sp_paint_selector_clear_frame(psel); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Paint is undefined</b>")); +} + +static void +sp_paint_selector_set_mode_none(SPPaintSelector *psel) +{ + sp_paint_selector_set_style_buttons(psel, psel->none); + gtk_widget_set_sensitive(psel->style, TRUE); + + sp_paint_selector_clear_frame(psel); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>No paint</b>")); + +} + +/* Color paint */ + +void SPPaintSelector::onSelectedColorGrabbed() { + g_signal_emit(G_OBJECT(this), psel_signals[GRABBED], 0); +} + +void SPPaintSelector::onSelectedColorDragged() { + if (updating_color) { + return; + } + g_signal_emit(G_OBJECT(this), psel_signals[DRAGGED], 0); +} + +void SPPaintSelector::onSelectedColorReleased() { + g_signal_emit(G_OBJECT(this), psel_signals[RELEASED], 0); +} + +void SPPaintSelector::onSelectedColorChanged() { + if (updating_color) { + return; + } + + if (mode == MODE_SOLID_COLOR) { + g_signal_emit(G_OBJECT(this), psel_signals[CHANGED], 0); + } else { + g_warning("SPPaintSelector::onSelectedColorChanged(): selected color changed while not in color selection mode"); + } +} + +static void sp_paint_selector_set_mode_color(SPPaintSelector *psel, SPPaintSelector::Mode /*mode*/) +{ + using Inkscape::UI::Widget::ColorNotebook; + + if ((psel->mode == SPPaintSelector::MODE_SWATCH) + || (psel->mode == SPPaintSelector::MODE_GRADIENT_LINEAR) + || (psel->mode == SPPaintSelector::MODE_GRADIENT_RADIAL) ) { + SPGradientSelector *gsel = getGradientFromData(psel); + if (gsel) { + SPGradient *gradient = gsel->getVector(); + + // Gradient can be null if object paint is changed externally (ie. with a color picker tool) + if (gradient) + { + SPColor color = gradient->getFirstStop()->getColor(); + float alpha = gradient->getFirstStop()->getOpacity(); + psel->selected_color->setColorAlpha(color, alpha, false); + } + } + } + + sp_paint_selector_set_style_buttons(psel, psel->solid); + gtk_widget_set_sensitive(psel->style, TRUE); + + if (psel->mode == SPPaintSelector::MODE_SOLID_COLOR) { + /* Already have color selector */ + // Do nothing + } else { + + sp_paint_selector_clear_frame(psel); + /* Create new color selector */ + /* Create vbox */ + auto vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_set_homogeneous(GTK_BOX(vb), FALSE); + gtk_widget_show(vb); + + /* Color selector */ + Gtk::Widget *color_selector = Gtk::manage(new ColorNotebook(*(psel->selected_color))); + color_selector->show(); + gtk_box_pack_start(GTK_BOX(vb), color_selector->gobj(), TRUE, TRUE, 0); + + /* Pack everything to frame */ + gtk_container_add(GTK_CONTAINER(psel->frame), vb); + + psel->selector = vb; + } + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Flat color</b>")); + +#ifdef SP_PS_VERBOSE + g_print("Color req\n"); +#endif +} + +/* Gradient */ + +static void sp_paint_selector_gradient_grabbed(SPGradientSelector * /*csel*/, SPPaintSelector *psel) +{ + g_signal_emit(G_OBJECT(psel), psel_signals[GRABBED], 0); +} + +static void sp_paint_selector_gradient_dragged(SPGradientSelector * /*csel*/, SPPaintSelector *psel) +{ + g_signal_emit(G_OBJECT(psel), psel_signals[DRAGGED], 0); +} + +static void sp_paint_selector_gradient_released(SPGradientSelector * /*csel*/, SPPaintSelector *psel) +{ + g_signal_emit(G_OBJECT(psel), psel_signals[RELEASED], 0); +} + +static void sp_paint_selector_gradient_changed(SPGradientSelector * /*csel*/, SPPaintSelector *psel) +{ + g_signal_emit(G_OBJECT(psel), psel_signals[CHANGED], 0); +} + +static void sp_paint_selector_set_mode_gradient(SPPaintSelector *psel, SPPaintSelector::Mode mode) +{ + GtkWidget *gsel; + + /* fixme: We do not need function-wide gsel at all */ + + if (mode == SPPaintSelector::MODE_GRADIENT_LINEAR) { + sp_paint_selector_set_style_buttons(psel, psel->gradient); + } else if (mode == SPPaintSelector::MODE_GRADIENT_RADIAL) { + sp_paint_selector_set_style_buttons(psel, psel->radial); + } + gtk_widget_set_sensitive(psel->style, TRUE); + + if ((psel->mode == SPPaintSelector::MODE_GRADIENT_LINEAR) || (psel->mode == SPPaintSelector::MODE_GRADIENT_RADIAL)) { + /* Already have gradient selector */ + gsel = GTK_WIDGET(g_object_get_data(G_OBJECT(psel->selector), "gradient-selector")); + } else { + sp_paint_selector_clear_frame(psel); + /* Create new gradient selector */ + gsel = sp_gradient_selector_new(); + gtk_widget_show(gsel); + g_signal_connect(G_OBJECT(gsel), "grabbed", G_CALLBACK(sp_paint_selector_gradient_grabbed), psel); + g_signal_connect(G_OBJECT(gsel), "dragged", G_CALLBACK(sp_paint_selector_gradient_dragged), psel); + g_signal_connect(G_OBJECT(gsel), "released", G_CALLBACK(sp_paint_selector_gradient_released), psel); + g_signal_connect(G_OBJECT(gsel), "changed", G_CALLBACK(sp_paint_selector_gradient_changed), psel); + /* Pack everything to frame */ + gtk_container_add(GTK_CONTAINER(psel->frame), gsel); + psel->selector = gsel; + g_object_set_data(G_OBJECT(psel->selector), "gradient-selector", gsel); + } + + /* Actually we have to set option menu history here */ + if (mode == SPPaintSelector::MODE_GRADIENT_LINEAR) { + SP_GRADIENT_SELECTOR(gsel)->setMode(SPGradientSelector::MODE_LINEAR); + //sp_gradient_selector_set_mode(SP_GRADIENT_SELECTOR(gsel), SP_GRADIENT_SELECTOR_MODE_LINEAR); + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Linear gradient</b>")); + } else if (mode == SPPaintSelector::MODE_GRADIENT_RADIAL) { + SP_GRADIENT_SELECTOR(gsel)->setMode(SPGradientSelector::MODE_RADIAL); + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Radial gradient</b>")); + } + +#ifdef SP_PS_VERBOSE + g_print("Gradient req\n"); +#endif +} + +// ************************* MESH ************************ +#ifdef WITH_MESH +static void sp_psel_mesh_destroy(GtkWidget *widget, SPPaintSelector * /*psel*/) +{ + // drop our reference to the mesh menu widget + g_object_unref( G_OBJECT(widget) ); +} + +static void sp_psel_mesh_change(GtkWidget * /*widget*/, SPPaintSelector *psel) +{ + g_signal_emit(G_OBJECT(psel), psel_signals[CHANGED], 0); +} + + +/** + * Returns a list of meshes in the defs of the given source document as a vector + */ +static std::vector<SPMeshGradient *> +ink_mesh_list_get (SPDocument *source) +{ + std::vector<SPMeshGradient *> pl; + if (source == nullptr) + return pl; + + + std::vector<SPObject *> meshes = source->getResourceList("gradient"); + for (auto meshe : meshes) { + if (SP_IS_MESHGRADIENT(meshe) && + SP_GRADIENT(meshe) == SP_GRADIENT(meshe)->getArray()) { // only if this is a root mesh + pl.push_back(SP_MESHGRADIENT(meshe)); + } + } + return pl; +} + +/** + * Adds menu items for mesh list. + */ +static void +sp_mesh_menu_build (GtkWidget *combo, std::vector<SPMeshGradient *> &mesh_list, SPDocument */*source*/) +{ + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + + for (auto i:mesh_list) { + + Inkscape::XML::Node *repr = i->getRepr(); + + gchar const *meshid = repr->attribute("id"); + gchar const *label = meshid; + + // Only relevant if we supply a set of canned meshes. + gboolean stockid = false; + if (repr->attribute("inkscape:stockid")) { + label = _(repr->attribute("inkscape:stockid")); + stockid = true; + } + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, + COMBO_COL_LABEL, label, COMBO_COL_STOCK, stockid, COMBO_COL_MESH, meshid, COMBO_COL_SEP, FALSE, -1); + + } +} + +/** + * Pick up all meshes from source, except those that are in + * current_doc (if non-NULL), and add items to the mesh menu. + */ +static void sp_mesh_list_from_doc(GtkWidget *combo, SPDocument * /*current_doc*/, SPDocument *source, SPDocument * /*mesh_doc*/) +{ + std::vector<SPMeshGradient *> pl = ink_mesh_list_get(source); + sp_mesh_menu_build (combo, pl, source); +} + + +static void +ink_mesh_menu_populate_menu(GtkWidget *combo, SPDocument *doc) +{ + static SPDocument *meshes_doc = nullptr; + + // If we ever add a list of canned mesh gradients, uncomment following: + + // find and load meshes.svg + // if (meshes_doc == NULL) { + // char *meshes_source = g_build_filename(INKSCAPE_MESHESDIR, "meshes.svg", NULL); + // if (Inkscape::IO::file_test(meshes_source, G_FILE_TEST_IS_REGULAR)) { + // meshes_doc = SPDocument::createNewDoc(meshes_source, FALSE); + // } + // g_free(meshes_source); + // } + + // suck in from current doc + sp_mesh_list_from_doc ( combo, nullptr, doc, meshes_doc ); + + // add separator + // { + // GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + // GtkTreeIter iter; + // gtk_list_store_append (store, &iter); + // gtk_list_store_set(store, &iter, + // COMBO_COL_LABEL, "", COMBO_COL_STOCK, false, COMBO_COL_MESH, "", COMBO_COL_SEP, true, -1); + // } + + // suck in from meshes.svg + // if (meshes_doc) { + // doc->ensureUpToDate(); + // sp_mesh_list_from_doc ( combo, doc, meshes_doc, NULL ); + // } + +} + + +static GtkWidget* +ink_mesh_menu(GtkWidget *combo) +{ + SPDocument *doc = SP_ACTIVE_DOCUMENT; + + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + + if (!doc) { + + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, + COMBO_COL_LABEL, _("No document selected"), COMBO_COL_STOCK, false, COMBO_COL_MESH, "", COMBO_COL_SEP, false, -1); + gtk_widget_set_sensitive(combo, FALSE); + + } else { + + ink_mesh_menu_populate_menu(combo, doc); + gtk_widget_set_sensitive(combo, TRUE); + + } + + // Select the first item that is not a separator + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL(store), &iter)) { + gboolean sep = false; + gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, COMBO_COL_SEP, &sep, -1); + if (sep) { + gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter); + } + gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter); + } + + return combo; +} + + +/*update mesh list*/ +void SPPaintSelector::updateMeshList( SPMeshGradient *mesh ) +{ + if (update) { + return; + } + + GtkWidget *combo = GTK_WIDGET(g_object_get_data(G_OBJECT(this), "meshmenu")); + g_assert( combo != nullptr ); + + /* Clear existing menu if any */ + GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo)); + gtk_list_store_clear(GTK_LIST_STORE(store)); + + ink_mesh_menu(combo); + + /* Set history */ + + if (mesh && !g_object_get_data(G_OBJECT(combo), "update")) { + + g_object_set_data(G_OBJECT(combo), "update", GINT_TO_POINTER(TRUE)); + gchar const *meshname = mesh->getRepr()->attribute("id"); + + // Find this mesh and set it active in the combo_box + GtkTreeIter iter ; + gchar *meshid = nullptr; + bool valid = gtk_tree_model_get_iter_first (store, &iter); + if (!valid) { + return; + } + gtk_tree_model_get (store, &iter, COMBO_COL_MESH, &meshid, -1); + while (valid && strcmp(meshid, meshname) != 0) { + valid = gtk_tree_model_iter_next (store, &iter); + g_free(meshid); + meshid = nullptr; + gtk_tree_model_get (store, &iter, COMBO_COL_MESH, &meshid, -1); + } + + if (valid) { + gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter); + } + + g_object_set_data(G_OBJECT(combo), "update", GINT_TO_POINTER(FALSE)); + g_free(meshid); + } +} + +static void sp_paint_selector_set_mode_mesh(SPPaintSelector *psel, SPPaintSelector::Mode mode) +{ + if (mode == SPPaintSelector::MODE_GRADIENT_MESH) { + sp_paint_selector_set_style_buttons(psel, psel->mesh); + } + gtk_widget_set_sensitive(psel->style, TRUE); + + GtkWidget *tbl = nullptr; + + if (psel->mode == SPPaintSelector::MODE_GRADIENT_MESH) { + /* Already have mesh menu */ + tbl = GTK_WIDGET(g_object_get_data(G_OBJECT(psel->selector), "mesh-selector")); + } else { + sp_paint_selector_clear_frame(psel); + + /* Create vbox */ + tbl = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_set_homogeneous(GTK_BOX(tbl), FALSE); + gtk_widget_show(tbl); + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + /** + * Create a combo_box and store with 4 columns, + * The label, a pointer to the mesh, is stockid or not, is a separator or not. + */ + GtkListStore *store = gtk_list_store_new (COMBO_N_COLS, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_BOOLEAN); + GtkWidget *combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store)); + gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(combo), SPPaintSelector::isSeparator, nullptr, nullptr); + + GtkCellRenderer *renderer = gtk_cell_renderer_text_new (); + gtk_cell_renderer_set_padding (renderer, 2, 0); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), renderer, "text", COMBO_COL_LABEL, NULL); + + ink_mesh_menu(combo); + g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(sp_psel_mesh_change), psel); + g_signal_connect(G_OBJECT(combo), "destroy", G_CALLBACK(sp_psel_mesh_destroy), psel); + g_object_set_data(G_OBJECT(psel), "meshmenu", combo); + g_object_ref( G_OBJECT(combo)); + + gtk_container_add(GTK_CONTAINER(hb), combo); + gtk_box_pack_start(GTK_BOX(tbl), hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS); + + g_object_unref( G_OBJECT(store)); + } + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + auto l = gtk_label_new(nullptr); + gtk_label_set_markup(GTK_LABEL(l), _("Use the <b>Mesh tool</b> to modify the mesh.")); + gtk_label_set_line_wrap(GTK_LABEL(l), true); + gtk_widget_set_size_request(l, 180, -1); + gtk_box_pack_start(GTK_BOX(hb), l, TRUE, TRUE, AUX_BETWEEN_BUTTON_GROUPS); + gtk_box_pack_start(GTK_BOX(tbl), hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS); + } + + gtk_widget_show_all(tbl); + + gtk_container_add(GTK_CONTAINER(psel->frame), tbl); + psel->selector = tbl; + g_object_set_data(G_OBJECT(psel->selector), "mesh-selector", tbl); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Mesh fill</b>")); + } +#ifdef SP_PS_VERBOSE + g_print("Mesh req\n"); +#endif +} + +SPMeshGradient *SPPaintSelector::getMeshGradient() +{ + g_return_val_if_fail((mode == MODE_GRADIENT_MESH) , NULL); + + GtkWidget *combo = GTK_WIDGET(g_object_get_data(G_OBJECT(this), "meshmenu")); + + /* no mesh menu if we were just selected */ + if ( combo == nullptr ) { + return nullptr; + } + GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo)); + + /* Get the selected mesh */ + GtkTreeIter iter; + if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX(combo), &iter) || + !gtk_list_store_iter_is_valid(GTK_LIST_STORE(store), &iter)) { + return nullptr; + } + + gchar *meshid = nullptr; + gboolean stockid = FALSE; + // gchar *label = nullptr; + gtk_tree_model_get (store, &iter, COMBO_COL_STOCK, &stockid, COMBO_COL_MESH, &meshid, -1); + // gtk_tree_model_get (store, &iter, COMBO_COL_LABEL, &label, COMBO_COL_STOCK, &stockid, COMBO_COL_MESH, &meshid, -1); + // std::cout << " .. meshid: " << (meshid?meshid:"null") << " label: " << (label?label:"null") << std::endl; + // g_free(label); + if (meshid == nullptr) { + return nullptr; + } + + SPMeshGradient *mesh = nullptr; + if (strcmp(meshid, "none")){ + + gchar *mesh_name; + if (stockid) { + mesh_name = g_strconcat("urn:inkscape:mesh:", meshid, NULL); + } else { + mesh_name = g_strdup(meshid); + } + + SPObject *mesh_obj = get_stock_item(mesh_name); + if (mesh_obj && SP_IS_MESHGRADIENT(mesh_obj)) { + mesh = SP_MESHGRADIENT(mesh_obj); + } + g_free(mesh_name); + } else { + std::cerr << "SPPaintSelector::getMeshGradient: Unexpected meshid value." << std::endl; + } + + g_free(meshid); + + return mesh; +} + +#endif +// ************************ End Mesh ************************ + +static void +sp_paint_selector_set_style_buttons(SPPaintSelector *psel, GtkWidget *active) +{ + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->none), (active == psel->none)); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->solid), (active == psel->solid)); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->gradient), (active == psel->gradient)); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->radial), (active == psel->radial)); +#ifdef WITH_MESH + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->mesh), (active == psel->mesh)); +#endif + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->pattern), (active == psel->pattern)); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->swatch), (active == psel->swatch)); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->unset), (active == psel->unset)); +} + +static void sp_psel_pattern_destroy(GtkWidget *widget, SPPaintSelector * /*psel*/) +{ + // drop our reference to the pattern menu widget + g_object_unref( G_OBJECT(widget) ); +} + +static void sp_psel_pattern_change(GtkWidget * /*widget*/, SPPaintSelector *psel) +{ + g_signal_emit(G_OBJECT(psel), psel_signals[CHANGED], 0); +} + + + +/** + * Returns a list of patterns in the defs of the given source document as a vector + */ +static std::vector<SPPattern*> +ink_pattern_list_get (SPDocument *source) +{ + std::vector<SPPattern *> pl; + if (source == nullptr) + return pl; + + std::vector<SPObject *> patterns = source->getResourceList("pattern"); + for (auto pattern : patterns) { + if (SP_PATTERN(pattern) == SP_PATTERN(pattern)->rootPattern()) { // only if this is a root pattern + pl.push_back(SP_PATTERN(pattern)); + } + } + + return pl; +} + +/** + * Adds menu items for pattern list - derived from marker code, left hb etc in to make addition of previews easier at some point. + */ +static void +sp_pattern_menu_build (GtkWidget *combo, std::vector<SPPattern *> &pl, SPDocument */*source*/) +{ + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + + for (auto i=pl.rbegin(); i!=pl.rend(); ++i) { + + Inkscape::XML::Node *repr = (*i)->getRepr(); + + // label for combobox + gchar const *label; + if (repr->attribute("inkscape:stockid")) { + label = _(repr->attribute("inkscape:stockid")); + } else { + label = _(repr->attribute("id")); + } + + gchar const *patid = repr->attribute("id"); + + gboolean stockid = false; + if (repr->attribute("inkscape:stockid")) { + stockid = true; + } + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, + COMBO_COL_LABEL, label, COMBO_COL_STOCK, stockid, COMBO_COL_PATTERN, patid, COMBO_COL_SEP, FALSE, -1); + + } +} + +/** + * Pick up all patterns from source, except those that are in + * current_doc (if non-NULL), and add items to the pattern menu. + */ +static void sp_pattern_list_from_doc(GtkWidget *combo, SPDocument * /*current_doc*/, SPDocument *source, SPDocument * /*pattern_doc*/) +{ + std::vector<SPPattern *> pl = ink_pattern_list_get(source); + sp_pattern_menu_build (combo, pl, source); +} + + +static void +ink_pattern_menu_populate_menu(GtkWidget *combo, SPDocument *doc) +{ + static SPDocument *patterns_doc = nullptr; + + // find and load patterns.svg + if (patterns_doc == nullptr) { + char *patterns_source = g_build_filename(INKSCAPE_PAINTDIR, "patterns.svg", NULL); + if (Inkscape::IO::file_test(patterns_source, G_FILE_TEST_IS_REGULAR)) { + patterns_doc = SPDocument::createNewDoc(patterns_source, FALSE); + } + g_free(patterns_source); + } + + // suck in from current doc + sp_pattern_list_from_doc ( combo, nullptr, doc, patterns_doc ); + + // add separator + { + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + gtk_list_store_append (store, &iter); + gtk_list_store_set(store, &iter, + COMBO_COL_LABEL, "", COMBO_COL_STOCK, false, COMBO_COL_PATTERN, "", COMBO_COL_SEP, true, -1); + } + + // suck in from patterns.svg + if (patterns_doc) { + doc->ensureUpToDate(); + sp_pattern_list_from_doc ( combo, doc, patterns_doc, nullptr ); + } + +} + + +static GtkWidget* +ink_pattern_menu(GtkWidget *combo) +{ + SPDocument *doc = SP_ACTIVE_DOCUMENT; + + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + + if (!doc) { + + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, + COMBO_COL_LABEL, _("No document selected"), COMBO_COL_STOCK, false, COMBO_COL_PATTERN, "", COMBO_COL_SEP, false, -1); + gtk_widget_set_sensitive(combo, FALSE); + + } else { + + ink_pattern_menu_populate_menu(combo, doc); + gtk_widget_set_sensitive(combo, TRUE); + + } + + // Select the first item that is not a separator + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL(store), &iter)) { + gboolean sep = false; + gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, COMBO_COL_SEP, &sep, -1); + if (sep) { + gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter); + } + gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter); + } + + return combo; +} + + +/*update pattern list*/ +void SPPaintSelector::updatePatternList( SPPattern *pattern ) +{ + if (update) { + return; + } + GtkWidget *combo = GTK_WIDGET(g_object_get_data(G_OBJECT(this), "patternmenu")); + g_assert( combo != nullptr ); + + /* Clear existing menu if any */ + GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo)); + gtk_list_store_clear(GTK_LIST_STORE(store)); + + ink_pattern_menu(combo); + + /* Set history */ + + if (pattern && !g_object_get_data(G_OBJECT(combo), "update")) { + + g_object_set_data(G_OBJECT(combo), "update", GINT_TO_POINTER(TRUE)); + gchar const *patname = pattern->getRepr()->attribute("id"); + + // Find this pattern and set it active in the combo_box + GtkTreeIter iter ; + gchar *patid = nullptr; + bool valid = gtk_tree_model_get_iter_first (store, &iter); + if (!valid) { + return; + } + gtk_tree_model_get (store, &iter, COMBO_COL_PATTERN, &patid, -1); + while (valid && strcmp(patid, patname) != 0) { + valid = gtk_tree_model_iter_next (store, &iter); + g_free(patid); + patid = nullptr; + gtk_tree_model_get (store, &iter, COMBO_COL_PATTERN, &patid, -1); + } + g_free(patid); + + if (valid) { + gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter); + } + + g_object_set_data(G_OBJECT(combo), "update", GINT_TO_POINTER(FALSE)); + } +} + +static void sp_paint_selector_set_mode_pattern(SPPaintSelector *psel, SPPaintSelector::Mode mode) +{ + if (mode == SPPaintSelector::MODE_PATTERN) { + sp_paint_selector_set_style_buttons(psel, psel->pattern); + } + + gtk_widget_set_sensitive(psel->style, TRUE); + + GtkWidget *tbl = nullptr; + + if (psel->mode == SPPaintSelector::MODE_PATTERN) { + /* Already have pattern menu */ + tbl = GTK_WIDGET(g_object_get_data(G_OBJECT(psel->selector), "pattern-selector")); + } else { + sp_paint_selector_clear_frame(psel); + + /* Create vbox */ + tbl = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_set_homogeneous(GTK_BOX(tbl), FALSE); + gtk_widget_show(tbl); + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + /** + * Create a combo_box and store with 4 columns, + * The label, a pointer to the pattern, is stockid or not, is a separator or not. + */ + GtkListStore *store = gtk_list_store_new (COMBO_N_COLS, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_BOOLEAN); + GtkWidget *combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store)); + gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(combo), SPPaintSelector::isSeparator, nullptr, nullptr); + + GtkCellRenderer *renderer = gtk_cell_renderer_text_new (); + gtk_cell_renderer_set_padding (renderer, 2, 0); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), renderer, "text", COMBO_COL_LABEL, NULL); + + ink_pattern_menu(combo); + g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(sp_psel_pattern_change), psel); + g_signal_connect(G_OBJECT(combo), "destroy", G_CALLBACK(sp_psel_pattern_destroy), psel); + g_object_set_data(G_OBJECT(psel), "patternmenu", combo); + g_object_ref( G_OBJECT(combo)); + + gtk_container_add(GTK_CONTAINER(hb), combo); + gtk_box_pack_start(GTK_BOX(tbl), hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS); + + g_object_unref( G_OBJECT(store)); + } + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + auto l = gtk_label_new(nullptr); + gtk_label_set_markup(GTK_LABEL(l), _("Use the <b>Node tool</b> to adjust position, scale, and rotation of the pattern on canvas. Use <b>Object > Pattern > Objects to Pattern</b> to create a new pattern from selection.")); + gtk_label_set_line_wrap(GTK_LABEL(l), true); + gtk_widget_set_size_request(l, 180, -1); + gtk_box_pack_start(GTK_BOX(hb), l, TRUE, TRUE, AUX_BETWEEN_BUTTON_GROUPS); + gtk_box_pack_start(GTK_BOX(tbl), hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS); + } + + gtk_widget_show_all(tbl); + + gtk_container_add(GTK_CONTAINER(psel->frame), tbl); + psel->selector = tbl; + g_object_set_data(G_OBJECT(psel->selector), "pattern-selector", tbl); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Pattern fill</b>")); + } +#ifdef SP_PS_VERBOSE + g_print("Pattern req\n"); +#endif +} + +static void sp_paint_selector_set_mode_hatch(SPPaintSelector *psel, SPPaintSelector::Mode mode) +{ + if (mode == SPPaintSelector::MODE_HATCH) { + sp_paint_selector_set_style_buttons(psel, psel->unset); + } + + gtk_widget_set_sensitive(psel->style, TRUE); + + if (psel->mode == SPPaintSelector::MODE_HATCH) { + /* Already have hatch menu, for the moment unset */ + } else { + sp_paint_selector_clear_frame(psel); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Hatch fill</b>")); + } +#ifdef SP_PS_VERBOSE + g_print("Hatch req\n"); +#endif +} + +gboolean SPPaintSelector::isSeparator (GtkTreeModel *model, GtkTreeIter *iter, gpointer /*data*/) { + + gboolean sep = FALSE; + gtk_tree_model_get(model, iter, COMBO_COL_SEP, &sep, -1); + return sep; +} + +SPPattern *SPPaintSelector::getPattern() +{ + SPPattern *pat = nullptr; + g_return_val_if_fail(mode == MODE_PATTERN, NULL); + + GtkWidget *combo = GTK_WIDGET(g_object_get_data(G_OBJECT(this), "patternmenu")); + + /* no pattern menu if we were just selected */ + if (combo == nullptr) { + return nullptr; + } + + GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo)); + + /* Get the selected pattern */ + GtkTreeIter iter; + if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combo), &iter) || + !gtk_list_store_iter_is_valid(GTK_LIST_STORE(store), &iter)) { + return nullptr; + } + + gchar *patid = nullptr; + gboolean stockid = FALSE; + // gchar *label = nullptr; + gtk_tree_model_get(store, &iter, + // COMBO_COL_LABEL, &label, + COMBO_COL_STOCK, &stockid, + COMBO_COL_PATTERN, &patid, -1); + // g_free(label); + if (patid == nullptr) { + return nullptr; + } + + if (strcmp(patid, "none") != 0) { + gchar *paturn; + + if (stockid) { + paturn = g_strconcat("urn:inkscape:pattern:", patid, NULL); + } else { + paturn = g_strdup(patid); + } + SPObject *pat_obj = get_stock_item(paturn); + if (pat_obj) { + pat = SP_PATTERN(pat_obj); + } + g_free(paturn); + } else { + SPDocument *doc = SP_ACTIVE_DOCUMENT; + SPObject *pat_obj = doc->getObjectById(patid); + + if (pat_obj && SP_IS_PATTERN(pat_obj)) { + pat = SP_PATTERN(pat_obj)->rootPattern(); + } + } + + g_free(patid); + + return pat; +} + +static void sp_paint_selector_set_mode_swatch(SPPaintSelector *psel, SPPaintSelector::Mode mode) +{ + if (mode == SPPaintSelector::MODE_SWATCH) { + sp_paint_selector_set_style_buttons(psel, psel->swatch); + } + + gtk_widget_set_sensitive(psel->style, TRUE); + + if (psel->mode == SPPaintSelector::MODE_SWATCH){ + // swatchsel = static_cast<SwatchSelector*>(g_object_get_data(G_OBJECT(psel->selector), "swatch-selector")); + } else { + sp_paint_selector_clear_frame(psel); + // Create new gradient selector + SwatchSelector *swatchsel = new SwatchSelector(); + swatchsel->show(); + + swatchsel->connectGrabbedHandler( G_CALLBACK(sp_paint_selector_gradient_grabbed), psel ); + swatchsel->connectDraggedHandler( G_CALLBACK(sp_paint_selector_gradient_dragged), psel ); + swatchsel->connectReleasedHandler( G_CALLBACK(sp_paint_selector_gradient_released), psel ); + swatchsel->connectchangedHandler( G_CALLBACK(sp_paint_selector_gradient_changed), psel ); + + // Pack everything to frame + gtk_container_add(GTK_CONTAINER(psel->frame), GTK_WIDGET(swatchsel->gobj())); + psel->selector = GTK_WIDGET(swatchsel->gobj()); + g_object_set_data(G_OBJECT(psel->selector), "swatch-selector", swatchsel); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Swatch fill</b>")); + } + +#ifdef SP_PS_VERBOSE + g_print("Swatch req\n"); +#endif +} + +// TODO this seems very bad to be taking in a desktop pointer to muck with. Logic probably belongs elsewhere +void SPPaintSelector::setFlatColor( SPDesktop *desktop, gchar const *color_property, gchar const *opacity_property ) +{ + SPCSSAttr *css = sp_repr_css_attr_new(); + + SPColor color; + gfloat alpha = 0; + getColorAlpha( color, alpha ); + + std::string colorStr = color.toString(); + +#ifdef SP_PS_VERBOSE + guint32 rgba = color.toRGBA32( alpha ); + g_message("sp_paint_selector_set_flat_color() to '%s' from 0x%08x::%s", + colorStr.c_str(), + rgba, + (color.icc ? color.icc->colorProfile.c_str():"<null>") ); +#endif // SP_PS_VERBOSE + + sp_repr_css_set_property(css, color_property, colorStr.c_str()); + Inkscape::CSSOStringStream osalpha; + osalpha << alpha; + sp_repr_css_set_property(css, opacity_property, osalpha.str().c_str()); + + sp_desktop_set_style(desktop, css); + + sp_repr_css_attr_unref(css); +} + +SPPaintSelector::Mode SPPaintSelector::getModeForStyle(SPStyle const & style, FillOrStroke kind) +{ + Mode mode = MODE_UNSET; + SPIPaint const &target = *style.getFillOrStroke(kind == FILL); + + if ( !target.set ) { + mode = MODE_UNSET; + } else if ( target.isPaintserver() ) { + SPPaintServer const *server = (kind == FILL) ? style.getFillPaintServer() : style.getStrokePaintServer(); + +#ifdef SP_PS_VERBOSE + g_message("SPPaintSelector::getModeForStyle(%p, %d)", &style, kind); + g_message("==== server:%p %s grad:%s swatch:%s", server, server->getId(), (SP_IS_GRADIENT(server)?"Y":"n"), (SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()?"Y":"n")); +#endif // SP_PS_VERBOSE + + + if (server && SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()) { + mode = MODE_SWATCH; + } else if (SP_IS_LINEARGRADIENT(server)) { + mode = MODE_GRADIENT_LINEAR; + } else if (SP_IS_RADIALGRADIENT(server)) { + mode = MODE_GRADIENT_RADIAL; +#ifdef WITH_MESH + } else if (SP_IS_MESHGRADIENT(server)) { + mode = MODE_GRADIENT_MESH; +#endif + } else if (SP_IS_PATTERN(server)) { + mode = MODE_PATTERN; + } else if (SP_IS_HATCH(server)) { + mode = MODE_HATCH; + } else { + g_warning( "file %s: line %d: Unknown paintserver", __FILE__, __LINE__ ); + mode = MODE_NONE; + } + } else if ( target.isColor() ) { + // TODO this is no longer a valid assertion: + mode = MODE_SOLID_COLOR; // so far only rgb can be read from svg + } else if ( target.isNone() ) { + mode = MODE_NONE; + } else { + g_warning( "file %s: line %d: Unknown paint type", __FILE__, __LINE__ ); + mode = MODE_NONE; + } + + return mode; +} + +/* + 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 : diff --git a/src/widgets/paint-selector.h b/src/widgets/paint-selector.h new file mode 100644 index 0000000..7d142f3 --- /dev/null +++ b/src/widgets/paint-selector.h @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Generic paint selector widget + *//* + * Authors: + * Lauris + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_PAINT_SELECTOR_H +#define SEEN_SP_PAINT_SELECTOR_H + +#include <glib.h> +#include <gtk/gtk.h> + +#include "color.h" +#include "fill-or-stroke.h" + +#include "object/sp-gradient-spread.h" +#include "object/sp-gradient-units.h" + +#include "ui/selected-color.h" + +class SPGradient; +#ifdef WITH_MESH +class SPMeshGradient; +#endif +class SPDesktop; +class SPPattern; +class SPStyle; + +#define SP_TYPE_PAINT_SELECTOR (sp_paint_selector_get_type ()) +#define SP_PAINT_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_PAINT_SELECTOR, SPPaintSelector)) +#define SP_PAINT_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_PAINT_SELECTOR, SPPaintSelectorClass)) +#define SP_IS_PAINT_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_PAINT_SELECTOR)) +#define SP_IS_PAINT_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_PAINT_SELECTOR)) + +/** + * Generic paint selector widget. + */ +struct SPPaintSelector { + GtkBox vbox; + + enum Mode { + MODE_EMPTY, + MODE_MULTIPLE, + MODE_NONE, + MODE_SOLID_COLOR, + MODE_GRADIENT_LINEAR, + MODE_GRADIENT_RADIAL, +#ifdef WITH_MESH + MODE_GRADIENT_MESH, +#endif + MODE_PATTERN, + MODE_HATCH, + MODE_SWATCH, + MODE_UNSET + } ; + + enum FillRule { + FILLRULE_NONZERO, + FILLRULE_EVENODD + } ; + + guint update : 1; + + Mode mode; + + GtkWidget *style; + GtkWidget *none; + GtkWidget *solid; + GtkWidget *gradient; + GtkWidget *radial; +#ifdef WITH_MESH + GtkWidget *mesh; +#endif + GtkWidget *pattern; + GtkWidget *swatch; + GtkWidget *unset; + + GtkWidget *fillrulebox; + GtkWidget *evenodd, *nonzero; + + GtkWidget *frame, *selector; + GtkWidget *label; + + Inkscape::UI::SelectedColor *selected_color; + bool updating_color; + + static Mode getModeForStyle(SPStyle const & style, FillOrStroke kind); + + void setMode( Mode mode ); + void setFillrule( FillRule fillrule ); + + void setColorAlpha( SPColor const &color, float alpha ); + void getColorAlpha( SPColor &color, gfloat &alpha ) const; + + void setGradientLinear( SPGradient *vector ); + void setGradientRadial( SPGradient *vector ); +#ifdef WITH_MESH + void setGradientMesh(SPMeshGradient *array); +#endif + void setSwatch( SPGradient *vector ); + + void setGradientProperties( SPGradientUnits units, SPGradientSpread spread ); + void getGradientProperties( SPGradientUnits &units, SPGradientSpread &spread ) const; + + void pushAttrsToGradient( SPGradient *gr ) const; + SPGradient *getGradientVector(); + +#ifdef WITH_MESH + SPMeshGradient * getMeshGradient(); + void updateMeshList( SPMeshGradient *pat ); +#endif + + SPPattern * getPattern(); + void updatePatternList( SPPattern *pat ); + + static gboolean isSeparator (GtkTreeModel *model, GtkTreeIter *iter, gpointer data); + + // TODO move this elsewhere: + void setFlatColor( SPDesktop *desktop, const gchar *color_property, const gchar *opacity_property ); + + void onSelectedColorGrabbed(); + void onSelectedColorDragged(); + void onSelectedColorReleased(); + void onSelectedColorChanged(); +}; + +enum { + COMBO_COL_LABEL = 0, + COMBO_COL_STOCK = 1, + COMBO_COL_PATTERN = 2, + COMBO_COL_MESH = COMBO_COL_PATTERN, + COMBO_COL_SEP = 3, + COMBO_N_COLS = 4 +}; + +/// The SPPaintSelector vtable +struct SPPaintSelectorClass { + GtkBoxClass parent_class; + + void (* mode_changed) (SPPaintSelector *psel, SPPaintSelector::Mode mode); + + void (* grabbed) (SPPaintSelector *psel); + void (* dragged) (SPPaintSelector *psel); + void (* released) (SPPaintSelector *psel); + void (* changed) (SPPaintSelector *psel); + void (* fillrule_changed) (SPPaintSelector *psel, SPPaintSelector::FillRule fillrule); +}; + +GType sp_paint_selector_get_type (); + +SPPaintSelector *sp_paint_selector_new(FillOrStroke kind); + + + +#endif // SEEN_SP_PAINT_SELECTOR_H + +/* + 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 : diff --git a/src/widgets/sp-attribute-widget.cpp b/src/widgets/sp-attribute-widget.cpp new file mode 100644 index 0000000..a82d06d --- /dev/null +++ b/src/widgets/sp-attribute-widget.cpp @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Base widget for user input of object properties. + */ +/* Authors: + * Lauris Kaplinski <lauris@ximian.com> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2012, authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gtkmm/entry.h> +#include <gtkmm/grid.h> + +#include "sp-attribute-widget.h" + +#include "include/macros.h" +#include "document.h" +#include "document-undo.h" +#include "verbs.h" + +#include "include/gtkmm_version.h" + +#include "object/sp-object.h" + +#include "xml/repr.h" + +using Inkscape::DocumentUndo; + +/** + * Callback for user input in one of the entries. + * + * sp_attribute_table_entry_changed set the object property + * to the new value and updates history. It is a callback from + * the entries created by SPAttributeTable. + * + * @param editable pointer to the entry box. + * @param spat pointer to the SPAttributeTable instance. + */ +static void sp_attribute_table_entry_changed (Gtk::Entry *editable, SPAttributeTable *spat); +/** + * Callback for a modification of the selected object (size, color, properties, etc.). + * + * sp_attribute_table_object_modified rereads the object properties + * and shows the values in the entry boxes. It is a callback from a + * connection of the SPObject. + * + * @param object the SPObject to which this instance is referring to. + * @param flags gives the applied modifications + * @param spat pointer to the SPAttributeTable instance. + */ +static void sp_attribute_table_object_modified (SPObject *object, guint flags, SPAttributeTable *spaw); +/** + * Callback for the deletion of the selected object. + * + * sp_attribute_table_object_release invalidates all data of + * SPAttributeTable and disables the widget. + */ +static void sp_attribute_table_object_release (SPObject */*object*/, SPAttributeTable *spat); + +#define XPAD 4 +#define YPAD 0 + + +SPAttributeTable::SPAttributeTable () : + _object(nullptr), + blocked(false), + table(nullptr), + _attributes(), + _entries(), + modified_connection(), + release_connection() +{ +} + +SPAttributeTable::SPAttributeTable (SPObject *object, std::vector<Glib::ustring> &labels, std::vector<Glib::ustring> &attributes, GtkWidget* parent) : + _object(nullptr), + blocked(false), + table(nullptr), + _attributes(), + _entries(), + modified_connection(), + release_connection() +{ + set_object(object, labels, attributes, parent); +} + +SPAttributeTable::~SPAttributeTable () +{ + clear(); +} + +void SPAttributeTable::clear() +{ + if (table) + { + std::vector<Gtk::Widget*> ch = table->get_children(); + for (int i = (ch.size())-1; i >=0 ; i--) + { + Gtk::Widget *w = ch[i]; + ch.pop_back(); + if (w != nullptr) + { + try + { + sp_signal_disconnect_by_data (w->gobj(), this); + delete w; + } + catch(...) + { + } + } + } + ch.clear(); + _attributes.clear(); + _entries.clear(); + + delete table; + table = nullptr; + } + + if (_object) + { + modified_connection.disconnect(); + release_connection.disconnect(); + _object = nullptr; + } +} + +void SPAttributeTable::set_object(SPObject *object, + std::vector<Glib::ustring> &labels, + std::vector<Glib::ustring> &attributes, + GtkWidget* parent) +{ + g_return_if_fail (!object || SP_IS_OBJECT (object)); + g_return_if_fail (!object || !labels.empty() || !attributes.empty()); + g_return_if_fail (labels.size() == attributes.size()); + + clear(); + _object = object; + + if (object) { + blocked = true; + + // Set up object + modified_connection = object->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_attribute_table_object_modified), this)); + release_connection = object->connectRelease (sigc::bind<1>(sigc::ptr_fun(&sp_attribute_table_object_release), this)); + + // Create table + table = new Gtk::Grid(); + + if (!(parent == nullptr)) + gtk_container_add(GTK_CONTAINER(parent), (GtkWidget*)table->gobj()); + + // Fill rows + _attributes = attributes; + for (guint i = 0; i < (attributes.size()); i++) { + Gtk::Label *ll = new Gtk::Label (_(labels[i].c_str())); + ll->show(); + ll->set_halign(Gtk::ALIGN_START); + ll->set_valign(Gtk::ALIGN_CENTER); + ll->set_vexpand(); + ll->set_margin_start(XPAD); + ll->set_margin_end(XPAD); + ll->set_margin_top(XPAD); + ll->set_margin_bottom(XPAD); + table->attach(*ll, 0, i, 1, 1); + + Gtk::Entry *ee = new Gtk::Entry(); + ee->show(); + const gchar *val = object->getRepr()->attribute(attributes[i].c_str()); + ee->set_text (val ? val : (const gchar *) ""); + ee->set_hexpand(); + ee->set_vexpand(); + ee->set_margin_start(XPAD); + ee->set_margin_end(XPAD); + ee->set_margin_top(XPAD); + ee->set_margin_bottom(XPAD); + table->attach(*ee, 1, i, 1, 1); + + _entries.push_back(ee); + g_signal_connect ( ee->gobj(), "changed", + G_CALLBACK (sp_attribute_table_entry_changed), + this ); + } + /* Show table */ + table->show (); + blocked = false; + } +} + +void SPAttributeTable::change_object(SPObject *object) +{ + g_return_if_fail (!object || SP_IS_OBJECT (object)); + if (_object) + { + modified_connection.disconnect(); + release_connection.disconnect(); + _object = nullptr; + } + + _object = object; + if (_object) { + blocked = true; + + // Set up object + modified_connection = _object->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_attribute_table_object_modified), this)); + release_connection = _object->connectRelease (sigc::bind<1>(sigc::ptr_fun(&sp_attribute_table_object_release), this)); + for (guint i = 0; i < (_attributes.size()); i++) { + const gchar *val = _object->getRepr()->attribute(_attributes[i].c_str()); + _entries[i]->set_text(val ? val : ""); + } + + blocked = false; + } + +} + +void SPAttributeTable::reread_properties() +{ + blocked = true; + for (guint i = 0; i < (_attributes.size()); i++) + { + const gchar *val = _object->getRepr()->attribute(_attributes[i].c_str()); + _entries[i]->set_text(val ? val : ""); + } + blocked = false; +} + +static void sp_attribute_table_object_modified ( SPObject */*object*/, + guint flags, + SPAttributeTable *spat ) +{ + if (flags & SP_OBJECT_MODIFIED_FLAG) + { + std::vector<Glib::ustring> attributes = spat->get_attributes(); + std::vector<Gtk::Entry *> entries = spat->get_entries(); + Glib::ustring text=""; + for (guint i = 0; i < (attributes.size()); i++) { + Gtk::Entry* e = entries[i]; + const gchar *val = spat->_object->getRepr()->attribute(attributes[i].c_str()); + text = e->get_text (); + if (val || !text.empty()) { + if (text != val) { + // We are different + spat->blocked = true; + e->set_text (val ? val : (const gchar *) ""); + spat->blocked = false; + } + } + } + } + +} // end of sp_attribute_table_object_modified() + +static void sp_attribute_table_entry_changed ( Gtk::Entry *editable, + SPAttributeTable *spat ) +{ + if (!spat->blocked) + { + std::vector<Glib::ustring> attributes = spat->get_attributes(); + std::vector<Gtk::Entry *> entries = spat->get_entries(); + for (guint i = 0; i < (attributes.size()); i++) { + Gtk::Entry *e = entries[i]; + if ((GtkWidget*)editable == (GtkWidget*)e->gobj()) { + spat->blocked = true; + Glib::ustring text = e->get_text (); + if (spat->_object) { + spat->_object->getRepr()->setAttribute(attributes[i], text); + DocumentUndo::done(spat->_object->document, SP_VERB_NONE, + _("Set attribute")); + } + spat->blocked = false; + return; + } + } + g_warning ("file %s: line %d: Entry signalled change, but there is no such entry", __FILE__, __LINE__); + } + +} // end of sp_attribute_table_entry_changed() + +static void sp_attribute_table_object_release (SPObject */*object*/, SPAttributeTable *spat) +{ + std::vector<Glib::ustring> labels; + std::vector<Glib::ustring> attributes; + spat->set_object (nullptr, labels, attributes, nullptr); +} + +/* + 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 : diff --git a/src/widgets/sp-attribute-widget.h b/src/widgets/sp-attribute-widget.h new file mode 100644 index 0000000..f43fe84 --- /dev/null +++ b/src/widgets/sp-attribute-widget.h @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Widget that listens and modifies repr attributes. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2002,2011-2012 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_SP_ATTRIBUTE_WIDGET_H +#define SEEN_DIALOGS_SP_ATTRIBUTE_WIDGET_H + +#include <gtkmm/widget.h> +#include <cstddef> +#include <sigc++/connection.h> + +namespace Gtk { +class Entry; +class Grid; +} + +namespace Inkscape { +namespace XML { +class Node; +} +} + +class SPObject; + +/** + * A base class for dialogs to enter the value of several properties. + * + * SPAttributeTable is used if you want to alter several properties of + * an object. For each property, it creates an entry next to a label and + * positiones these labels and entries one by one below each other. + */ +class SPAttributeTable : public Gtk::Widget { +public: + /** + * Constructor defaulting to no content. + */ + SPAttributeTable (); + + /** + * Constructor referring to a specific object. + * + * This constructor initializes all data fields and creates the necessary widgets. + * set_object is called for this purpose. + * + * @param object the SPObject to which this instance is referring to. It should be the object that is currently selected and whose properties are being shown by this SPAttributeTable instance. + * @param labels list of labels to be shown for the different attributes. + * @param attributes list of attributes whose value can be edited. + * @param parent the parent object owning the SPAttributeTable instance. + * + * @see set_object + */ + SPAttributeTable (SPObject *object, std::vector<Glib::ustring> &labels, std::vector<Glib::ustring> &attributes, GtkWidget* parent); + + ~SPAttributeTable () override; + + /** + * Sets class properties and creates child widgets + * + * set_object initializes all data fields, creates links to the + * SPOject item and creates the necessary widgets. For n properties + * n labels and n entries are created and shown in tabular format. + * + * @param object the SPObject to which this instance is referring to. It should be the object that is currently selected and whose properties are being shown by this SPAttribuTable instance. + * @param labels list of labels to be shown for the different attributes. + * @param attributes list of attributes whose value can be edited. + * @param parent the parent object owning the SPAttributeTable instance. + */ + void set_object(SPObject *object, std::vector<Glib::ustring> &labels, std::vector<Glib::ustring> &attributes, GtkWidget* parent); + + /** + * Update values in entry boxes on change of object. + * + * change_object updates the values of the entry boxes in case the user + * of Inkscape selects an other object. + * change_object is a subset of set_object and should only be called by + * the parent class (holding the SPAttributeTable instance). This function + * should only be called when the number of properties/entries nor + * the labels do not change. + * + * @param object the SPObject to which this instance is referring to. It should be the object that is currently selected and whose properties are being shown by this SPAttribuTable instance. + */ + void change_object(SPObject *object); + + /** + * Clears data of SPAttributeTable instance, destroys all child widgets and closes connections. + */ + void clear(); + + /** + * Reads the object attributes. + * + * Reads the object attributes and shows the new object attributes in the + * entry boxes. Caution: function should only be used when which there is + * no change in which objects are selected. + */ + void reread_properties(); + + /** + * Gives access to the attributes list. + */ + std::vector<Glib::ustring> get_attributes() {return _attributes;}; + + /** + * Gives access to the Gtk::Entry list. + */ + std::vector<Gtk::Entry *> get_entries() {return _entries;}; + + /** + * Stores pointer to the selected object. + */ + SPObject *_object; + + /** + * Indicates whether SPAttributeTable is processing callbacks and whether it should accept any updating. + */ + bool blocked; + +private: + /** + * Container widget for the dynamically created child widgets (labels and entry boxes). + */ + Gtk::Grid *table; + + /** + * List of attributes. + * + * _attributes stores the attribute names of the selected object that + * are valid and can be modified through this widget. + */ + std::vector<Glib::ustring> _attributes; + /** + * List of pointers to the respective entry boxes. + * + * _entries stores pointers to the dynamically created entry boxes in which + * the user can midify the attributes of the selected object. + */ + std::vector<Gtk::Entry *> _entries; + + /** + * Sets the callback for a modification of the selection. + */ + sigc::connection modified_connection; + + /** + * Sets the callback for the deletion of the selected object. + */ + sigc::connection release_connection; +}; + +#endif + +/* + 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 : diff --git a/src/widgets/sp-color-selector.cpp b/src/widgets/sp-color-selector.cpp new file mode 100644 index 0000000..c1c6975 --- /dev/null +++ b/src/widgets/sp-color-selector.cpp @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> +#include <gtk/gtk.h> +#include <glibmm/i18n.h> +#include "sp-color-selector.h" + +enum { + GRABBED, + DRAGGED, + RELEASED, + CHANGED, + LAST_SIGNAL +}; + +#define noDUMP_CHANGE_INFO +#define FOO_NAME(x) g_type_name( G_TYPE_FROM_INSTANCE(x) ) + +static void sp_color_selector_dispose(GObject *object); + +static void sp_color_selector_show_all( GtkWidget *widget ); +static void sp_color_selector_hide( GtkWidget *widget ); + +static guint csel_signals[LAST_SIGNAL] = {0}; + +double ColorSelector::_epsilon = 1e-4; + +G_DEFINE_TYPE(SPColorSelector, sp_color_selector, GTK_TYPE_BOX); + +void sp_color_selector_class_init( SPColorSelectorClass *klass ) +{ + static const gchar* nameset[] = {N_("Unnamed"), nullptr}; + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GtkWidgetClass *widget_class; + widget_class = GTK_WIDGET_CLASS(klass); + + csel_signals[GRABBED] = g_signal_new( "grabbed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPColorSelectorClass, grabbed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0 ); + csel_signals[DRAGGED] = g_signal_new( "dragged", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPColorSelectorClass, dragged), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0 ); + csel_signals[RELEASED] = g_signal_new( "released", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPColorSelectorClass, released), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0 ); + csel_signals[CHANGED] = g_signal_new( "changed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPColorSelectorClass, changed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0 ); + + klass->name = nameset; + klass->submode_count = 1; + + object_class->dispose = sp_color_selector_dispose; + + widget_class->show_all = sp_color_selector_show_all; + widget_class->hide = sp_color_selector_hide; + +} + +void sp_color_selector_init( SPColorSelector *csel ) +{ + gtk_orientable_set_orientation(GTK_ORIENTABLE(csel), GTK_ORIENTATION_VERTICAL); + + if ( csel->base ) + { + csel->base->init(); + } +/* g_signal_connect(G_OBJECT(csel->rgbae), "changed", G_CALLBACK(sp_color_selector_rgba_entry_changed), csel); */ +} + +void sp_color_selector_dispose(GObject *object) +{ + SPColorSelector *csel = SP_COLOR_SELECTOR( object ); + if ( csel->base ) + { + delete csel->base; + csel->base = nullptr; + } + + if ((G_OBJECT_CLASS(sp_color_selector_parent_class))->dispose ) { + (G_OBJECT_CLASS(sp_color_selector_parent_class))->dispose(object); + } +} + +void sp_color_selector_show_all( GtkWidget *widget ) +{ + gtk_widget_show( widget ); +} + +void sp_color_selector_hide(GtkWidget *widget) +{ + gtk_widget_hide( widget ); +} + +GtkWidget *sp_color_selector_new( GType selector_type ) +{ + g_return_val_if_fail( g_type_is_a( selector_type, SP_TYPE_COLOR_SELECTOR ), NULL ); + + SPColorSelector *csel = SP_COLOR_SELECTOR( g_object_new( selector_type, nullptr ) ); + + return GTK_WIDGET( csel ); +} + +void ColorSelector::setSubmode( guint /*submode*/ ) +{ +} + +guint ColorSelector::getSubmode() const +{ + guint mode = 0; + return mode; +} + +ColorSelector::ColorSelector( SPColorSelector* csel ) + : _csel(csel), + _color( 0 ), + _alpha(1.0), + _held(FALSE), + virgin(true) +{ + g_return_if_fail( SP_IS_COLOR_SELECTOR(_csel) ); +} + +ColorSelector::~ColorSelector() += default; + +void ColorSelector::init() +{ + _csel->base = new ColorSelector( _csel ); +} + +void ColorSelector::setColor( const SPColor& color ) +{ + setColorAlpha( color, _alpha ); +} + +SPColor ColorSelector::getColor() const +{ + return _color; +} + +void ColorSelector::setAlpha( gfloat alpha ) +{ + g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) ); + setColorAlpha( _color, alpha ); +} + +gfloat ColorSelector::getAlpha() const +{ + return _alpha; +} + +/** +Called from the outside to set the color; optionally emits signal (only when called from +downstream, e.g. the RGBA value field, but not from the rest of the program) +*/ +void ColorSelector::setColorAlpha( const SPColor& color, gfloat alpha, bool emit ) +{ +#ifdef DUMP_CHANGE_INFO + g_message("ColorSelector::setColorAlpha( this=%p, %f, %f, %f, %s, %f, %s) in %s", this, color.v.c[0], color.v.c[1], color.v.c[2], (color.icc?color.icc->colorProfile.c_str():"<null>"), alpha, (emit?"YES":"no"), FOO_NAME(_csel)); +#endif + g_return_if_fail( _csel != nullptr ); + g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) ); + +#ifdef DUMP_CHANGE_INFO + g_message("---- ColorSelector::setColorAlpha virgin:%s !close:%s alpha is:%s in %s", + (virgin?"YES":"no"), + (!color.isClose( _color, _epsilon )?"YES":"no"), + ((fabs((_alpha) - (alpha)) >= _epsilon )?"YES":"no"), + FOO_NAME(_csel) + ); +#endif + + if ( virgin || !color.isClose( _color, _epsilon ) || + (fabs((_alpha) - (alpha)) >= _epsilon )) { + + virgin = false; + + _color = color; + _alpha = alpha; + _colorChanged(); + + if (emit) { + g_signal_emit(G_OBJECT(_csel), csel_signals[CHANGED], 0); + } +#ifdef DUMP_CHANGE_INFO + } else { + g_message("++++ ColorSelector::setColorAlpha color:%08x ==> _color:%08X isClose:%s in %s", color.toRGBA32(alpha), _color.toRGBA32(_alpha), + (color.isClose( _color, _epsilon )?"YES":"no"), FOO_NAME(_csel)); +#endif + } +} + +void ColorSelector::_grabbed() +{ + _held = TRUE; +#ifdef DUMP_CHANGE_INFO + g_message("%s:%d: About to signal %s in %s", __FILE__, __LINE__, + "GRABBED", + FOO_NAME(_csel)); +#endif + g_signal_emit(G_OBJECT(_csel), csel_signals[GRABBED], 0); +} + +void ColorSelector::_released() +{ + _held = false; +#ifdef DUMP_CHANGE_INFO + g_message("%s:%d: About to signal %s in %s", __FILE__, __LINE__, + "RELEASED", + FOO_NAME(_csel)); +#endif + g_signal_emit(G_OBJECT(_csel), csel_signals[RELEASED], 0); + g_signal_emit(G_OBJECT(_csel), csel_signals[CHANGED], 0); +} + +// Called from subclasses to update color and broadcast if needed +void ColorSelector::_updateInternals( const SPColor& color, gfloat alpha, gboolean held ) +{ + g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) ); + gboolean colorDifferent = ( !color.isClose( _color, _epsilon ) + || ( fabs((_alpha) - (alpha)) >= _epsilon ) ); + + gboolean grabbed = held && !_held; + gboolean released = !held && _held; + + // Store these before emitting any signals + _held = held; + if ( colorDifferent ) + { + _color = color; + _alpha = alpha; + } + + if ( grabbed ) + { +#ifdef DUMP_CHANGE_INFO + g_message("%s:%d: About to signal %s to color %08x::%s in %s", __FILE__, __LINE__, + "GRABBED", + color.toRGBA32( alpha ), (color.icc?color.icc->colorProfile.c_str():"<null>"), FOO_NAME(_csel)); +#endif + g_signal_emit(G_OBJECT(_csel), csel_signals[GRABBED], 0); + } + else if ( released ) + { +#ifdef DUMP_CHANGE_INFO + g_message("%s:%d: About to signal %s to color %08x::%s in %s", __FILE__, __LINE__, + "RELEASED", + color.toRGBA32( alpha ), (color.icc?color.icc->colorProfile.c_str():"<null>"), FOO_NAME(_csel)); +#endif + g_signal_emit(G_OBJECT(_csel), csel_signals[RELEASED], 0); + } + + if ( colorDifferent || released ) + { +#ifdef DUMP_CHANGE_INFO + g_message("%s:%d: About to signal %s to color %08x::%s in %s", __FILE__, __LINE__, + (_held ? "CHANGED" : "DRAGGED" ), + color.toRGBA32( alpha ), (color.icc?color.icc->colorProfile.c_str():"<null>"), FOO_NAME(_csel)); +#endif + g_signal_emit(G_OBJECT(_csel), csel_signals[_held ? DRAGGED : CHANGED], 0); + } +} + +/** + * Called once the color actually changes. Allows subclasses to react to changes. + */ +void ColorSelector::_colorChanged() +{ +} + +void ColorSelector::getColorAlpha( SPColor &color, gfloat &alpha ) const +{ + gint i = 0; + + color = _color; + alpha = _alpha; + + // Try to catch uninitialized value usage + if ( color.v.c[0] ) + { + i++; + } + if ( color.v.c[1] ) + { + i++; + } + if ( color.v.c[2] ) + { + i++; + } + if ( alpha ) + { + i++; + } +} + +/* + 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 : diff --git a/src/widgets/sp-color-selector.h b/src/widgets/sp-color-selector.h new file mode 100644 index 0000000..eb77250 --- /dev/null +++ b/src/widgets/sp-color-selector.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_COLOR_SELECTOR_H +#define SEEN_SP_COLOR_SELECTOR_H + +#include <gtk/gtk.h> +#include "color.h" + +struct SPColorSelector; + +class ColorSelector +{ +public: + ColorSelector( SPColorSelector* csel ); + virtual ~ColorSelector(); + + virtual void init(); + + void setColor( const SPColor& color ); + SPColor getColor() const; + + void setAlpha( gfloat alpha ); + gfloat getAlpha() const; + + void setColorAlpha( const SPColor& color, gfloat alpha, bool emit = false ); + void getColorAlpha( SPColor &color, gfloat &alpha ) const; + + virtual void setSubmode( guint submode ); + virtual guint getSubmode() const; + +protected: + void _grabbed(); + void _released(); + void _updateInternals( const SPColor& color, gfloat alpha, gboolean held ); + gboolean _isHeld() const { return _held; } + + virtual void _colorChanged(); + + static double _epsilon; + + SPColorSelector* _csel; + SPColor _color; + gfloat _alpha; // guaranteed to be in [0, 1]. + +private: + // By default, disallow copy constructor and assignment operator + ColorSelector( const ColorSelector& obj ) = delete; + ColorSelector& operator=( const ColorSelector& obj ) = delete; + + gboolean _held; + + bool virgin; // if true, no color is set yet +}; + + + +#define SP_TYPE_COLOR_SELECTOR (sp_color_selector_get_type ()) +#define SP_COLOR_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_COLOR_SELECTOR, SPColorSelector)) +#define SP_COLOR_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_COLOR_SELECTOR, SPColorSelectorClass)) +#define SP_IS_COLOR_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_COLOR_SELECTOR)) +#define SP_IS_COLOR_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_COLOR_SELECTOR)) +#define SP_COLOR_SELECTOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SP_TYPE_COLOR_SELECTOR, SPColorSelectorClass)) + +struct SPColorSelector { + GtkBox vbox; + ColorSelector* base; +}; + +struct SPColorSelectorClass { + GtkBoxClass parent_class; + + const gchar **name; + guint submode_count; + + void (* grabbed) (SPColorSelector *rgbsel); + void (* dragged) (SPColorSelector *rgbsel); + void (* released) (SPColorSelector *rgbsel); + void (* changed) (SPColorSelector *rgbsel); +}; + +GType sp_color_selector_get_type(); + +GtkWidget *sp_color_selector_new( GType selector_type ); + + + +#endif // SEEN_SP_COLOR_SELECTOR_H + +/* + 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 : diff --git a/src/widgets/sp-xmlview-tree.cpp b/src/widgets/sp-xmlview-tree.cpp new file mode 100644 index 0000000..6f45e64 --- /dev/null +++ b/src/widgets/sp-xmlview-tree.cpp @@ -0,0 +1,866 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Specialization of GtkTreeView for the XML tree view + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2002 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> + +#include "xml/node-event-vector.h" +#include "sp-xmlview-tree.h" + +namespace { +struct NodeData { + SPXMLViewTree * tree; + GtkTreeRowReference *rowref; + Inkscape::XML::Node * repr; + bool expanded = false; //< true if tree view has been expanded to this node + bool dragging = false; + + NodeData(SPXMLViewTree *tree, GtkTreeIter *node, Inkscape::XML::Node *repr); + ~NodeData(); +}; + +// currently dragged node +Inkscape::XML::Node *dragging_repr = nullptr; +} // namespace + +enum { STORE_TEXT_COL = 0, STORE_DATA_COL, STORE_N_COLS }; + +static void sp_xmlview_tree_destroy(GtkWidget * object); + +static NodeData *sp_xmlview_tree_node_get_data(GtkTreeModel *model, GtkTreeIter *iter); + +static void add_node(SPXMLViewTree *tree, GtkTreeIter *parent, GtkTreeIter *before, Inkscape::XML::Node *repr); + +static void element_child_added (Inkscape::XML::Node * repr, Inkscape::XML::Node * child, Inkscape::XML::Node * ref, gpointer data); +static void element_attr_changed (Inkscape::XML::Node * repr, const gchar * key, const gchar * old_value, const gchar * new_value, bool is_interactive, gpointer data); +static void element_child_removed (Inkscape::XML::Node * repr, Inkscape::XML::Node * child, Inkscape::XML::Node * ref, gpointer data); +static void element_order_changed (Inkscape::XML::Node * repr, Inkscape::XML::Node * child, Inkscape::XML::Node * oldref, Inkscape::XML::Node * newref, gpointer data); +static void element_name_changed (Inkscape::XML::Node* repr, gchar const* oldname, gchar const* newname, gpointer data); +static void element_attr_or_name_change_update(Inkscape::XML::Node* repr, NodeData* data); + +static void text_content_changed (Inkscape::XML::Node * repr, const gchar * old_content, const gchar * new_content, gpointer data); +static void comment_content_changed (Inkscape::XML::Node * repr, const gchar * old_content, const gchar * new_content, gpointer data); +static void pi_content_changed (Inkscape::XML::Node * repr, const gchar * old_content, const gchar * new_content, gpointer data); + +static gboolean ref_to_sibling (NodeData *node, Inkscape::XML::Node * ref, GtkTreeIter *); +static gboolean repr_to_child (NodeData *node, Inkscape::XML::Node * repr, GtkTreeIter *); +static GtkTreeRowReference *tree_iter_to_ref(SPXMLViewTree *, GtkTreeIter *); +static gboolean tree_ref_to_iter (SPXMLViewTree * tree, GtkTreeIter* iter, GtkTreeRowReference *ref); + +static gboolean search_equal_func(GtkTreeModel *, gint column, const gchar *key, GtkTreeIter *, gpointer search_data); +static gboolean foreach_func(GtkTreeModel *, GtkTreePath *, GtkTreeIter *, gpointer user_data); + +static void on_row_changed(GtkTreeModel *, GtkTreePath *, GtkTreeIter *, gpointer user_data); +static void on_drag_begin(GtkWidget *, GdkDragContext *, gpointer userdata); +static void on_drag_end(GtkWidget *, GdkDragContext *, gpointer userdata); +static gboolean do_drag_motion(GtkWidget *, GdkDragContext *, gint x, gint y, guint time, gpointer user_data); + +static const Inkscape::XML::NodeEventVector element_repr_events = { + element_child_added, + element_child_removed, + element_attr_changed, + nullptr, /* content_changed */ + element_order_changed, + element_name_changed +}; + +static const Inkscape::XML::NodeEventVector text_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + nullptr, /* attr_changed */ + text_content_changed, + nullptr /* order_changed */, + nullptr /* element_name_changed */ +}; + +static const Inkscape::XML::NodeEventVector comment_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + nullptr, /* attr_changed */ + comment_content_changed, + nullptr /* order_changed */, + nullptr /* element_name_changed */ +}; + +static const Inkscape::XML::NodeEventVector pi_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + nullptr, /* attr_changed */ + pi_content_changed, + nullptr /* order_changed */, + nullptr /* element_name_changed */ +}; + +/** + * Get an iterator to the first child of `data` + * @param data handle which references a row + * @param[out] child_iter On success: valid iterator to first child + * @return False if the node has no children + */ +static bool get_first_child(NodeData *data, GtkTreeIter *child_iter) +{ + GtkTreeIter iter; + return tree_ref_to_iter(data->tree, &iter, data->rowref) && + gtk_tree_model_iter_children(GTK_TREE_MODEL(data->tree->store), child_iter, &iter); +} + +/** + * @param iter First dummy row on that level + * @pre all rows on the same level are dummies + * @pre iter is valid + * @post iter is invalid + * @post level is empty + */ +static void remove_dummy_rows(GtkTreeStore *store, GtkTreeIter *iter) +{ + do { + g_assert(nullptr == sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(store), iter)); + gtk_tree_store_remove(store, iter); + } while (gtk_tree_store_iter_is_valid(store, iter)); +} + +static gboolean on_test_expand_row( // + GtkTreeView *tree_view, // + GtkTreeIter *iter, // + GtkTreePath *path, // + gpointer) +{ + auto tree = SP_XMLVIEW_TREE(tree_view); + auto model = GTK_TREE_MODEL(tree->store); + + GtkTreeIter childiter; + bool has_children = gtk_tree_model_iter_children(model, &childiter, iter); + g_assert(has_children); + + if (sp_xmlview_tree_node_get_repr(model, &childiter) == nullptr) { + NodeData *data = sp_xmlview_tree_node_get_data(model, iter); + + remove_dummy_rows(tree->store, &childiter); + + // insert real rows + data->expanded = true; + sp_repr_synthesize_events(data->repr, &element_repr_events, data); + } + + return false; +} + +GtkWidget *sp_xmlview_tree_new(Inkscape::XML::Node * repr, void * /*factory*/, void * /*data*/) +{ + SPXMLViewTree *tree = SP_XMLVIEW_TREE(g_object_new (SP_TYPE_XMLVIEW_TREE, nullptr)); + + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW(tree), FALSE); + gtk_tree_view_set_reorderable (GTK_TREE_VIEW(tree), TRUE); + gtk_tree_view_set_enable_search (GTK_TREE_VIEW(tree), TRUE); + gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW(tree), search_equal_func, nullptr, nullptr); + + GtkCellRenderer *renderer = gtk_cell_renderer_text_new (); + GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes ("", renderer, "text", STORE_TEXT_COL, NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column); + gtk_cell_renderer_set_padding (renderer, 2, 0); + gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_AUTOSIZE); + + sp_xmlview_tree_set_repr (tree, repr); + + g_signal_connect(GTK_TREE_VIEW(tree), "drag-begin", G_CALLBACK(on_drag_begin), tree); + g_signal_connect(GTK_TREE_VIEW(tree), "drag-end", G_CALLBACK(on_drag_end), tree); + g_signal_connect(GTK_TREE_VIEW(tree), "drag-motion", G_CALLBACK(do_drag_motion), tree); + g_signal_connect(GTK_TREE_VIEW(tree), "test-expand-row", G_CALLBACK(on_test_expand_row), nullptr); + + return GTK_WIDGET(tree); +} + +G_DEFINE_TYPE(SPXMLViewTree, sp_xmlview_tree, GTK_TYPE_TREE_VIEW); + +void sp_xmlview_tree_class_init(SPXMLViewTreeClass * klass) +{ + auto widget_class = GTK_WIDGET_CLASS(klass); + widget_class->destroy = sp_xmlview_tree_destroy; + + // Signal for when a tree drag and drop has completed + g_signal_new ( "tree_move", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, + G_TYPE_UINT); +} + +void +sp_xmlview_tree_init (SPXMLViewTree * tree) +{ + tree->repr = nullptr; + tree->blocked = 0; +} + +void sp_xmlview_tree_destroy(GtkWidget * object) +{ + SPXMLViewTree * tree = SP_XMLVIEW_TREE (object); + + sp_xmlview_tree_set_repr (tree, nullptr); + + GTK_WIDGET_CLASS(sp_xmlview_tree_parent_class)->destroy (object); +} + +/* + * Add a new row to the tree + */ +void +add_node (SPXMLViewTree * tree, GtkTreeIter *parent, GtkTreeIter *before, Inkscape::XML::Node * repr) +{ + const Inkscape::XML::NodeEventVector * vec; + + g_assert (tree != nullptr); + + if (before && !gtk_tree_store_iter_is_valid(tree->store, before)) { + before = nullptr; + } + + GtkTreeIter iter; + gtk_tree_store_insert_before (tree->store, &iter, parent, before); + + if (!gtk_tree_store_iter_is_valid(tree->store, &iter)) { + return; + } + + if (!repr) { + // no need to store any data + return; + } + + auto data = new NodeData(tree, &iter, repr); + + g_assert (data != nullptr); + + gtk_tree_store_set(tree->store, &iter, STORE_DATA_COL, data, -1); + + if ( repr->type() == Inkscape::XML::TEXT_NODE ) { + vec = &text_repr_events; + } else if ( repr->type() == Inkscape::XML::COMMENT_NODE ) { + vec = &comment_repr_events; + } else if ( repr->type() == Inkscape::XML::PI_NODE ) { + vec = &pi_repr_events; + } else if ( repr->type() == Inkscape::XML::ELEMENT_NODE ) { + vec = &element_repr_events; + } else { + vec = nullptr; + } + + if (vec) { + /* cheat a little to get the text updated on nodes without id */ + if (repr->type() == Inkscape::XML::ELEMENT_NODE && repr->attribute("id") == nullptr) { + element_attr_changed (repr, "id", nullptr, nullptr, false, data); + } + sp_repr_add_listener (repr, vec, data); + sp_repr_synthesize_events (repr, vec, data); + } +} + +static gboolean remove_all_listeners(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer) +{ + NodeData *data = sp_xmlview_tree_node_get_data(model, iter); + delete data; + return false; +} + +NodeData::NodeData(SPXMLViewTree *tree, GtkTreeIter *iter, Inkscape::XML::Node *repr) + : tree(tree) + , rowref(tree_iter_to_ref(tree, iter)) + , repr(repr) +{ + if (repr) { + Inkscape::GC::anchor(repr); + } +} + +NodeData::~NodeData() +{ + if (repr) { + sp_repr_remove_listener_by_data(repr, this); + Inkscape::GC::release(repr); + } + gtk_tree_row_reference_free(rowref); +} + +void element_child_added (Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node * child, Inkscape::XML::Node * ref, gpointer ptr) +{ + NodeData *data = static_cast<NodeData *>(ptr); + GtkTreeIter before; + + if (data->tree->blocked) return; + + if (!ref_to_sibling (data, ref, &before)) { + return; + } + + GtkTreeIter data_iter; + tree_ref_to_iter(data->tree, &data_iter, data->rowref); + + if (!data->expanded) { + auto model = GTK_TREE_MODEL(data->tree->store); + GtkTreeIter childiter; + if (!gtk_tree_model_iter_children(model, &childiter, &data_iter)) { + // no children yet, add a dummy + child = nullptr; + } else if (sp_xmlview_tree_node_get_repr(model, &childiter) == nullptr) { + // already has a dummy child + return; + } + } + + add_node (data->tree, &data_iter, &before, child); +} + +void element_attr_changed( + Inkscape::XML::Node* repr, gchar const* key, + gchar const* /*old_value*/, gchar const* /*new_value*/, bool /*is_interactive*/, + gpointer ptr) +{ + if (0 != strcmp (key, "id") && 0 != strcmp (key, "inkscape:label")) + return; + element_attr_or_name_change_update(repr, static_cast<NodeData*>(ptr)); +} + +void element_name_changed( + Inkscape::XML::Node* repr, + gchar const* /*oldname*/, gchar const* /*newname*/, gpointer ptr) +{ + element_attr_or_name_change_update(repr, static_cast<NodeData*>(ptr)); +} + +void element_attr_or_name_change_update(Inkscape::XML::Node* repr, NodeData* data) +{ + if (data->tree->blocked) { + return; + } + + gchar const* node_name = repr->name(); + gchar const* id_value = repr->attribute("id"); + gchar const* label_value = repr->attribute("inkscape:label"); + gchar* display_text; + + if (id_value && label_value) { + display_text = g_strdup_printf ("<%s id=\"%s\" inkscape:label=\"%s\">", node_name, id_value, label_value); + } else if (id_value) { + display_text = g_strdup_printf ("<%s id=\"%s\">", node_name, id_value); + } else if (label_value) { + display_text = g_strdup_printf ("<%s inkscape:label=\"%s\">", node_name, label_value); + } else { + display_text = g_strdup_printf ("<%s>", node_name); + } + + GtkTreeIter iter; + if (tree_ref_to_iter(data->tree, &iter, data->rowref)) { + gtk_tree_store_set (GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, display_text, -1); + } + + g_free(display_text); +} + +void element_child_removed(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node * /*ref*/, + gpointer ptr) +{ + NodeData *data = static_cast<NodeData *>(ptr); + + if (data->tree->blocked) return; + + GtkTreeIter iter; + if (repr_to_child(data, child, &iter)) { + delete sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(data->tree->store), &iter); + gtk_tree_store_remove(data->tree->store, &iter); + } else if (!repr->firstChild() && get_first_child(data, &iter)) { + // remove dummy when all children gone + remove_dummy_rows(data->tree->store, &iter); + } else { + return; + } + +#ifndef GTK_ISSUE_2510_IS_FIXED + // https://gitlab.gnome.org/GNOME/gtk/issues/2510 + gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(GTK_TREE_VIEW(data->tree))); +#endif +} + +void element_order_changed(Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node * child, Inkscape::XML::Node * /*oldref*/, Inkscape::XML::Node * newref, gpointer ptr) +{ + NodeData *data = static_cast<NodeData *>(ptr); + GtkTreeIter before, node; + + if (data->tree->blocked) return; + + ref_to_sibling (data, newref, &before); + repr_to_child (data, child, &node); + + if (gtk_tree_store_iter_is_valid(data->tree->store, &before)) { + gtk_tree_store_move_before (data->tree->store, &node, &before); + } else { + repr_to_child (data, newref, &before); + gtk_tree_store_move_after (data->tree->store, &node, &before); + } +} + +Glib::ustring sp_remove_newlines_and_tabs(Glib::ustring val) +{ + int pos; + Glib::ustring newlinesign = ""; + Glib::ustring tabsign = "⇥"; + while ((pos = val.find("\r\n")) != std::string::npos) { + val.erase(pos, 2); + val.insert(pos, newlinesign); + } + while ((pos = val.find('\n')) != std::string::npos) { + val.erase(pos, 1); + val.insert(pos, newlinesign); + } + while ((pos = val.find('\t')) != std::string::npos) { + val.erase(pos, 1); + val.insert(pos, tabsign); + } + return val; +} + +void text_content_changed(Inkscape::XML::Node * /*repr*/, const gchar * /*old_content*/, const gchar * new_content, gpointer ptr) +{ + NodeData *data = static_cast<NodeData *>(ptr); + + if (data->tree->blocked) return; + + gchar *label = g_strdup_printf ("\"%s\"", new_content); + Glib::ustring nolinecontent = label; + nolinecontent = sp_remove_newlines_and_tabs(nolinecontent); + + GtkTreeIter iter; + if (tree_ref_to_iter(data->tree, &iter, data->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, nolinecontent.c_str(), -1); + } + + g_free (label); +} + +void comment_content_changed(Inkscape::XML::Node * /*repr*/, const gchar * /*old_content*/, const gchar *new_content, gpointer ptr) +{ + NodeData *data = static_cast<NodeData*>(ptr); + + if (data->tree->blocked) return; + + gchar *label = g_strdup_printf ("<!--%s-->", new_content); + Glib::ustring nolinecontent = label; + nolinecontent = sp_remove_newlines_and_tabs(nolinecontent); + + GtkTreeIter iter; + if (tree_ref_to_iter(data->tree, &iter, data->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, nolinecontent.c_str(), -1); + } + g_free (label); +} + +void pi_content_changed(Inkscape::XML::Node *repr, const gchar * /*old_content*/, const gchar *new_content, gpointer ptr) +{ + NodeData *data = static_cast<NodeData *>(ptr); + + if (data->tree->blocked) return; + + gchar *label = g_strdup_printf ("<?%s %s?>", repr->name(), new_content); + Glib::ustring nolinecontent = label; + nolinecontent = sp_remove_newlines_and_tabs(nolinecontent); + + GtkTreeIter iter; + if (tree_ref_to_iter(data->tree, &iter, data->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, nolinecontent.c_str(), -1); + } + g_free (label); +} + +/* + * Save the source path on drag start, will need it in on_row_changed() when moving a row + */ +void on_drag_begin(GtkWidget *, GdkDragContext *, gpointer userdata) +{ + SPXMLViewTree *tree = static_cast<SPXMLViewTree *>(userdata); + if (!tree) { + return; + } + + GtkTreeModel *model = nullptr; + GtkTreeIter iter; + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + if (gtk_tree_selection_get_selected(selection, &model, &iter)) { + NodeData *data = sp_xmlview_tree_node_get_data(model, &iter); + if (data) { + data->dragging = true; + dragging_repr = data->repr; + } + } +} + +/** + * Finalize what happended in `on_row_changed` and clean up what was set up in `on_drag_begin` + */ +void on_drag_end(GtkWidget *, GdkDragContext *, gpointer userdata) +{ + if (!dragging_repr) + return; + + auto tree = static_cast<SPXMLViewTree *>(userdata); + auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + bool failed = false; + + GtkTreeIter iter; + if (sp_xmlview_tree_get_repr_node(tree, dragging_repr, &iter)) { + NodeData *data = sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(tree->store), &iter); + + if (data && data->dragging) { + // dragging flag was not cleared in `on_row_changed`, this indicates a failed drag + data->dragging = false; + failed = true; + } else { + // Reselect the dragged row + gtk_tree_selection_select_iter(selection, &iter); + } + } else { +#ifndef GTK_ISSUE_2510_IS_FIXED + // https://gitlab.gnome.org/GNOME/gtk/issues/2510 + gtk_tree_selection_unselect_all(selection); +#endif + } + + dragging_repr = nullptr; + + if (!failed) { + // Signal that a drag and drop has completed successfully + g_signal_emit_by_name(G_OBJECT(tree), "tree_move", GUINT_TO_POINTER(1)); + } +} + +/* + * Main drag & drop function + * Get the old and new paths, and change the Inkscape::XML::Node repr's + */ +void on_row_changed(GtkTreeModel *tree_model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data) +{ + NodeData *data = sp_xmlview_tree_node_get_data(tree_model, iter); + + if (!data || !data->dragging) { + return; + } + data->dragging = false; + + SPXMLViewTree *tree = SP_XMLVIEW_TREE(user_data); + + gtk_tree_row_reference_free(data->rowref); + data->rowref = tree_iter_to_ref(tree, iter); + + GtkTreeIter new_parent; + if (!gtk_tree_model_iter_parent(tree_model, &new_parent, iter)) { + //No parent of drop location + return; + } + + Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(tree_model, iter); + Inkscape::XML::Node *before_repr = nullptr; + + // Find the sibling node before iter + GtkTreeIter before_iter = *iter; + if (gtk_tree_model_iter_previous(tree_model, &before_iter)) { + before_repr = sp_xmlview_tree_node_get_repr(tree_model, &before_iter); + } + + // Drop onto oneself causes assert in changeOrder() below, ignore + if (repr == before_repr) + return; + + auto repr_old_parent = repr->parent(); + auto repr_new_parent = sp_xmlview_tree_node_get_repr(tree_model, &new_parent); + + tree->blocked++; + + if (repr_old_parent == repr_new_parent) { + repr_old_parent->changeOrder(repr, before_repr); + } else { + repr_old_parent->removeChild(repr); + repr_new_parent->addChild(repr, before_repr); + } + + NodeData *data_new_parent = sp_xmlview_tree_node_get_data(tree_model, &new_parent); + if (data_new_parent && data_new_parent->expanded) { + // Reselect the dragged row in `on_drag_end` instead of here, because of + // https://gitlab.gnome.org/GNOME/gtk/-/issues/2510 + } else { + // convert to dummy node + delete data; + gtk_tree_store_set(tree->store, iter, STORE_DATA_COL, nullptr, -1); + } + + tree->blocked--; +} + +/* + * Set iter to ref or node data's child with the same repr or first child + */ +gboolean ref_to_sibling (NodeData *data, Inkscape::XML::Node *repr, GtkTreeIter *iter) +{ + if (repr) { + if (!repr_to_child (data, repr, iter)) { + return false; + } + gtk_tree_model_iter_next (GTK_TREE_MODEL(data->tree->store), iter); + } else { + GtkTreeIter data_iter; + if (!tree_ref_to_iter(data->tree, &data_iter, data->rowref)) { + return false; + } + gtk_tree_model_iter_children(GTK_TREE_MODEL(data->tree->store), iter, &data_iter); + } + return true; +} + +/* + * Set iter to the node data's child with the same repr + */ +gboolean repr_to_child (NodeData *data, Inkscape::XML::Node * repr, GtkTreeIter *iter) +{ + GtkTreeIter data_iter; + GtkTreeModel *model = GTK_TREE_MODEL(data->tree->store); + gboolean valid = false; + + if (!tree_ref_to_iter(data->tree, &data_iter, data->rowref)) { + return false; + } + + /* + * The node we are looking for is likely to be the last one, so check it first. + */ + gint n_children = gtk_tree_model_iter_n_children (model, &data_iter); + if (n_children > 1) { + valid = gtk_tree_model_iter_nth_child (model, iter, &data_iter, n_children-1); + if (valid && sp_xmlview_tree_node_get_repr (model, iter) == repr) { + //g_message("repr_to_child hit %d", n_children); + return valid; + } + } + + valid = gtk_tree_model_iter_children(model, iter, &data_iter); + while (valid && sp_xmlview_tree_node_get_repr (model, iter) != repr) { + valid = gtk_tree_model_iter_next(model, iter); + } + + return valid; +} + +/* + * Get a matching GtkTreeRowReference for a GtkTreeIter + */ +GtkTreeRowReference *tree_iter_to_ref (SPXMLViewTree * tree, GtkTreeIter* iter) +{ + GtkTreePath* path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), iter); + GtkTreeRowReference *ref = gtk_tree_row_reference_new(GTK_TREE_MODEL(tree->store), path); + gtk_tree_path_free(path); + + return ref; +} + +/* + * Get a matching GtkTreeIter for a GtkTreeRowReference + */ +gboolean tree_ref_to_iter (SPXMLViewTree * tree, GtkTreeIter* iter, GtkTreeRowReference *ref) +{ + GtkTreePath* path = gtk_tree_row_reference_get_path(ref); + if (!path) { + return false; + } + gboolean const valid = // + gtk_tree_model_get_iter(GTK_TREE_MODEL(tree->store), iter, path); + gtk_tree_path_free(path); + + return valid; +} + +/* + * Disable drag and drop target on : root node and non-element nodes + */ +gboolean do_drag_motion(GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, gpointer user_data) +{ + GtkTreePath *path = nullptr; + GtkTreeViewDropPosition pos; + gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW(widget), x, y, &path, &pos); + + int action = 0; + + if (!dragging_repr) { + goto finally; + } + + if (path) { + SPXMLViewTree *tree = SP_XMLVIEW_TREE(user_data); + GtkTreeIter iter; + gtk_tree_model_get_iter(GTK_TREE_MODEL(tree->store), &iter, path); + auto repr = sp_xmlview_tree_node_get_repr(GTK_TREE_MODEL(tree->store), &iter); + + bool const drop_into = pos != GTK_TREE_VIEW_DROP_BEFORE && // + pos != GTK_TREE_VIEW_DROP_AFTER; + + // 0. don't drop on self (also handled by on_row_changed but nice to not have drop highlight for it) + if (repr == dragging_repr) { + goto finally; + } + + // 1. only xml elements can have children + if (drop_into && repr->type() != Inkscape::XML::ELEMENT_NODE) { + goto finally; + } + + // 3. elements must be at least children of the root <svg:svg> element + if (gtk_tree_path_get_depth(path) < 2) { + goto finally; + } + + // 4. drag node specific limitations + { + // nodes which can't be re-parented (because the document holds pointers to them which must stay valid) + static GQuark const CODE_sodipodi_namedview = g_quark_from_static_string("sodipodi:namedview"); + static GQuark const CODE_svg_defs = g_quark_from_static_string("svg:defs"); + + bool const no_reparenting = dragging_repr->code() == CODE_sodipodi_namedview || // + dragging_repr->code() == CODE_svg_defs; + + if (no_reparenting && (drop_into || dragging_repr->parent() != repr->parent())) { + goto finally; + } + } + + action = GDK_ACTION_MOVE; + } + +finally: + if (action == 0) { + // remove drop highlight + gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(widget), nullptr, pos /* ignored */); + } + + gtk_tree_path_free(path); + gdk_drag_status (context, (GdkDragAction)action, time); + + return (action == 0); +} + +/* + * Set the tree selection and scroll to the row with the given repr + */ +void +sp_xmlview_tree_set_repr (SPXMLViewTree * tree, Inkscape::XML::Node * repr) +{ + if ( tree->repr == repr ) return; + + if (tree->store) { + gtk_tree_view_set_model(GTK_TREE_VIEW(tree), nullptr); + gtk_tree_model_foreach(GTK_TREE_MODEL(tree->store), remove_all_listeners, nullptr); + g_object_unref(tree->store); + tree->store = nullptr; + } + + if (tree->repr) { + Inkscape::GC::release(tree->repr); + } + tree->repr = repr; + if (repr) { + tree->store = gtk_tree_store_new(STORE_N_COLS, G_TYPE_STRING, G_TYPE_POINTER); + + Inkscape::GC::anchor(repr); + add_node(tree, nullptr, nullptr, repr); + + // Set the tree model here, after all data is inserted + gtk_tree_view_set_model (GTK_TREE_VIEW(tree), GTK_TREE_MODEL(tree->store)); + g_signal_connect(G_OBJECT(tree->store), "row-changed", G_CALLBACK(on_row_changed), tree); + + GtkTreePath *path = gtk_tree_path_new_from_indices(0, -1); + gtk_tree_view_expand_to_path (GTK_TREE_VIEW(tree), path); + gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW(tree), path, nullptr, true, 0.5, 0.0); + gtk_tree_path_free(path); + } +} + +/* + * Return the node data at a given GtkTreeIter position + */ +NodeData *sp_xmlview_tree_node_get_data(GtkTreeModel *model, GtkTreeIter *iter) +{ + NodeData *data = nullptr; + gtk_tree_model_get(model, iter, STORE_DATA_COL, &data, -1); + return data; +} + +/* + * Return the repr at a given GtkTreeIter position + */ +Inkscape::XML::Node * +sp_xmlview_tree_node_get_repr (GtkTreeModel *model, GtkTreeIter * iter) +{ + NodeData *data = sp_xmlview_tree_node_get_data(model, iter); + return data ? data->repr : nullptr; +} + +struct IterByReprData { + const Inkscape::XML::Node *repr; //< in + GtkTreeIter *iter; //< out +}; + +/* + * Find a GtkTreeIter position in the tree by repr + * @return True if the node was found + */ +gboolean +sp_xmlview_tree_get_repr_node (SPXMLViewTree * tree, Inkscape::XML::Node * repr, GtkTreeIter *iter) +{ + iter->stamp = 0; // invalidate iterator + IterByReprData funcdata = { repr, iter }; + gtk_tree_model_foreach(GTK_TREE_MODEL(tree->store), foreach_func, &funcdata); + return iter->stamp != 0; +} + +gboolean foreach_func(GtkTreeModel *model, GtkTreePath * /*path*/, GtkTreeIter *iter, gpointer user_data) +{ + auto funcdata = static_cast<IterByReprData *>(user_data); + if (sp_xmlview_tree_node_get_repr(model, iter) == funcdata->repr) { + *funcdata->iter = *iter; + return TRUE; + } + + return FALSE; +} + +/* + * Callback function for string searches in the tree + * Return a match on any substring + */ +gboolean search_equal_func(GtkTreeModel *model, gint /*column*/, const gchar *key, GtkTreeIter *iter, gpointer /*search_data*/) +{ + gchar *text = nullptr; + gtk_tree_model_get(model, iter, STORE_TEXT_COL, &text, -1); + + gboolean match = (strstr(text, key) != nullptr); + + g_free(text); + + return !match; +} + +/* + 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=4:softtabstop=4:fileencoding=utf-8 : diff --git a/src/widgets/sp-xmlview-tree.h b/src/widgets/sp-xmlview-tree.h new file mode 100644 index 0000000..8839ee9 --- /dev/null +++ b/src/widgets/sp-xmlview-tree.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2002 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_XMLVIEW_TREE_H +#define SEEN_SP_XMLVIEW_TREE_H + +#include <gtk/gtk.h> +#include <glib.h> + +/** + * Specialization of GtkTreeView for the XML editor + */ + +#define SP_TYPE_XMLVIEW_TREE (sp_xmlview_tree_get_type ()) +#define SP_XMLVIEW_TREE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_XMLVIEW_TREE, SPXMLViewTree)) +#define SP_IS_XMLVIEW_TREE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_XMLVIEW_TREE)) +#define SP_XMLVIEW_TREE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_XMLVIEW_TREE)) + +struct SPXMLViewTree; +struct SPXMLViewTreeClass; + +struct SPXMLViewTree +{ + GtkTreeView tree; + GtkTreeStore *store; + Inkscape::XML::Node * repr; + gint blocked; +}; + +struct SPXMLViewTreeClass +{ + GtkTreeViewClass parent_class; +}; + +GType sp_xmlview_tree_get_type (); +GtkWidget * sp_xmlview_tree_new (Inkscape::XML::Node * repr, void * factory, void * data); + +#define SP_XMLVIEW_TREE_REPR(tree) (SP_XMLVIEW_TREE (tree)->repr) + +void sp_xmlview_tree_set_repr (SPXMLViewTree * tree, Inkscape::XML::Node * repr); + +Inkscape::XML::Node * sp_xmlview_tree_node_get_repr (GtkTreeModel *model, GtkTreeIter * node); +gboolean sp_xmlview_tree_get_repr_node (SPXMLViewTree * tree, Inkscape::XML::Node * repr, GtkTreeIter *node); + + +#endif // !SEEN_SP_XMLVIEW_TREE_H + +/* + 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 : diff --git a/src/widgets/spinbutton-events.cpp b/src/widgets/spinbutton-events.cpp new file mode 100644 index 0000000..9800cf5 --- /dev/null +++ b/src/widgets/spinbutton-events.cpp @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Common callbacks for spinbuttons + * + * Authors: + * bulia byak <bulia@users.sourceforge.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2013 authors + * Copyright (C) 2003 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> + +#include "ui/tools/tool-base.h" + +#include "spinbutton-events.h" + +gboolean spinbutton_focus_in(GtkWidget *w, GdkEventKey * /*event*/, gpointer /*data*/) +{ + gdouble *ini = static_cast<gdouble *>(g_object_get_data(G_OBJECT(w), "ini")); + if (ini) { + g_free(ini); // free the old value if any + } + + // retrieve the value + ini = g_new(gdouble, 1); + *ini = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w)); + + // remember it + g_object_set_data(G_OBJECT(w), "ini", ini); + + return FALSE; // I didn't consume the event +} + +void spinbutton_undo(GtkWidget *w) +{ + gdouble *ini = static_cast<gdouble *>(g_object_get_data(G_OBJECT(w), "ini")); + if (ini) { + gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), *ini); + } +} + +void spinbutton_defocus(GtkWidget *container) +{ + // defocus spinbuttons by moving focus to the canvas, unless "stay" is on + gboolean stay = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(container), "stay")); + if (stay) { + g_object_set_data(G_OBJECT(container), "stay", GINT_TO_POINTER(FALSE)); + } else { + GtkWidget *canvas = GTK_WIDGET(g_object_get_data(G_OBJECT(container), "dtw")); + if (canvas) { + gtk_widget_grab_focus(GTK_WIDGET(canvas)); + } + } +} + +gboolean spinbutton_keypress(GtkWidget *w, GdkEventKey *event, gpointer /*data*/) +{ + gboolean result = FALSE; // I didn't consume the event + + switch (Inkscape::UI::Tools::get_latin_keyval(event)) { + case GDK_KEY_Escape: // defocus + spinbutton_undo(w); + spinbutton_defocus(w); + result = TRUE; // I consumed the event + break; + case GDK_KEY_Return: // defocus + case GDK_KEY_KP_Enter: + spinbutton_defocus(w); + result = TRUE; // I consumed the event + break; + case GDK_KEY_Tab: + case GDK_KEY_ISO_Left_Tab: + // set the flag meaning "do not leave toolbar when changing value" + g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE)); + result = FALSE; // I didn't consume the event + break; + + // The following keys are processed manually because GTK implements them in strange ways + // (increments start with double step value and seem to grow as you press the key continuously) + + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + { + g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE)); + gdouble v = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w)); + gdouble step = 0; + gdouble page = 0; + gtk_spin_button_get_increments(GTK_SPIN_BUTTON(w), &step, &page); + v += step; + gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), v); + result = TRUE; // I consumed the event + break; + } + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + { + g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE)); + gdouble v = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w)); + gdouble step = 0; + gdouble page = 0; + gtk_spin_button_get_increments(GTK_SPIN_BUTTON(w), &step, &page); + v -= step; + gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), v); + result = TRUE; // I consumed the event + break; + } + case GDK_KEY_Page_Up: + case GDK_KEY_KP_Page_Up: + { + g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE)); + gdouble v = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w)); + gdouble step = 0; + gdouble page = 0; + gtk_spin_button_get_increments(GTK_SPIN_BUTTON(w), &step, &page); + v += page; + gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), v); + result = TRUE; // I consumed the event + break; + } + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Down: + { + g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE)); + gdouble v = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w)); + gdouble step = 0; + gdouble page = 0; + gtk_spin_button_get_increments(GTK_SPIN_BUTTON(w), &step, &page); + v -= page; + gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), v); + result = TRUE; // I consumed the event + break; + } + case GDK_KEY_z: + case GDK_KEY_Z: + g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE)); + if (event->state & GDK_CONTROL_MASK) { + spinbutton_undo(w); + result = TRUE; // I consumed the event + } + break; + default: + result = FALSE; + break; + } + + return result; +} + +/* + 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 : diff --git a/src/widgets/spinbutton-events.h b/src/widgets/spinbutton-events.h new file mode 100644 index 0000000..e32ef18 --- /dev/null +++ b/src/widgets/spinbutton-events.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Common callbacks for spinbuttons + * + * Authors: + * bulia byak <bulia@users.sourceforge.net> + * + * Copyright (C) 2003 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> + +typedef struct _GdkEventKey GdkEventKey; +typedef struct _GtkWidget GtkWidget; + +gboolean spinbutton_focus_in (GtkWidget *w, GdkEventKey *event, gpointer data); +void spinbutton_undo (GtkWidget *w); +gboolean spinbutton_keypress (GtkWidget *w, GdkEventKey *event, gpointer data); +void spinbutton_defocus (GtkWidget *container); + +/* + 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 : diff --git a/src/widgets/spw-utilities.cpp b/src/widgets/spw-utilities.cpp new file mode 100644 index 0000000..38759ef --- /dev/null +++ b/src/widgets/spw-utilities.cpp @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape Widget Utilities + * + * Authors: + * Bryce W. Harrington <brycehar@bryceharrington.org> + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2003 Bryce W. Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/grid.h> + +#include "selection.h" + +#include "spw-utilities.h" + +/** + * Creates a label widget with the given text, at the given col, row + * position in the table. + */ +Gtk::Label * spw_label(Gtk::Grid *table, const gchar *label_text, int col, int row, Gtk::Widget* target) +{ + Gtk::Label *label_widget = new Gtk::Label(); + g_assert(label_widget != nullptr); + if (target != nullptr) { + label_widget->set_text_with_mnemonic(label_text); + label_widget->set_mnemonic_widget(*target); + } else { + label_widget->set_text(label_text); + } + label_widget->show(); + + label_widget->set_halign(Gtk::ALIGN_START); + label_widget->set_valign(Gtk::ALIGN_CENTER); + label_widget->set_margin_start(4); + label_widget->set_margin_end(4); + + table->attach(*label_widget, col, row, 1, 1); + + return label_widget; +} + +/** + * Creates a horizontal layout manager with 4-pixel spacing between children + * and space for 'width' columns. + */ +Gtk::HBox * spw_hbox(Gtk::Grid * table, int width, int col, int row) +{ + /* Create a new hbox with a 4-pixel spacing between children */ + Gtk::HBox *hb = new Gtk::HBox(false, 4); + g_assert(hb != nullptr); + hb->show(); + hb->set_hexpand(); + hb->set_halign(Gtk::ALIGN_FILL); + hb->set_valign(Gtk::ALIGN_CENTER); + table->attach(*hb, col, row, width, 1); + + return hb; +} + +/** + * Finds the descendant of w which has the data with the given key and returns the data, or NULL if there's none. + */ +gpointer sp_search_by_data_recursive(GtkWidget *w, gpointer key) +{ + gpointer r = nullptr; + + if (w && G_IS_OBJECT(w)) { + r = g_object_get_data(G_OBJECT(w), (gchar *) key); + } + if (r) return r; + + if (GTK_IS_CONTAINER(w)) { + std::vector<Gtk::Widget*> children = Glib::wrap(GTK_CONTAINER(w))->get_children(); + for (auto i:children) { + r = sp_search_by_data_recursive(GTK_WIDGET(i->gobj()), key); + if (r) return r; + } + } + + return nullptr; +} + +/** + * Returns a named descendent of parent, which has the given name, or nullptr if there's none. + * + * \param[in] parent The widget to search + * \param[in] name The name of the desired child widget + * + * \return The specified child widget, or nullptr if it cannot be found + */ +Gtk::Widget * +sp_search_by_name_recursive(Gtk::Widget *parent, const Glib::ustring& name) +{ + auto parent_bin = dynamic_cast<Gtk::Bin *>(parent); + auto parent_container = dynamic_cast<Gtk::Container *>(parent); + + if (parent && parent->get_name() == name) { + return parent; + } + else if (parent_bin) { + auto child = parent_bin->get_child(); + return sp_search_by_name_recursive(child, name); + } + else if (parent_container) { + auto children = parent_container->get_children(); + + for (auto child : children) { + auto tmp = sp_search_by_name_recursive(child, name); + + if (tmp) { + return tmp; + } + } + } + + return nullptr; +} + +/** + * Returns the descendant of w which has the given key and value pair, or NULL if there's none. + */ +GtkWidget *sp_search_by_value_recursive(GtkWidget *w, gchar *key, gchar *value) +{ + gchar *r = nullptr; + + if (w && G_IS_OBJECT(w)) { + r = (gchar *) g_object_get_data(G_OBJECT(w), key); + } + if (r && !strcmp (r, value)) return w; + + if (GTK_IS_CONTAINER(w)) { + std::vector<Gtk::Widget*> children = Glib::wrap(GTK_CONTAINER(w))->get_children(); + for (auto i:children) { + GtkWidget *child = sp_search_by_value_recursive(GTK_WIDGET(i->gobj()), key, value); + if (child) return child; + } + } + + return nullptr; +} + +/* + 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 : diff --git a/src/widgets/spw-utilities.h b/src/widgets/spw-utilities.h new file mode 100644 index 0000000..4751e81 --- /dev/null +++ b/src/widgets/spw-utilities.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SPW_UTILITIES_H__ +#define __SPW_UTILITIES_H__ + +/* + * Inkscape Widget Utilities + * + * Author: + * Bryce W. Harrington <brycehar@bryceharrington.org> + * + * Copyright (C) 2003 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* The following are helper routines for making Inkscape dialog widgets. + All are prefixed with spw_, short for inkscape_widget. This is not to + be confused with SPWidget, an existing datatype associated with Inkscape::XML::Node/ + SPObject, that reacts to modification. +*/ + +namespace Gtk { + class Label; + class Grid; + class HBox; + class Widget; +} + +Gtk::Label * spw_label(Gtk::Grid *table, gchar const *label_text, int col, int row, Gtk::Widget *target); +Gtk::HBox * spw_hbox(Gtk::Grid *table, int width, int col, int row); + +gpointer sp_search_by_data_recursive(GtkWidget *w, gpointer data); +GtkWidget *sp_search_by_value_recursive(GtkWidget *w, gchar *key, gchar *value); + +Gtk::Widget * sp_search_by_name_recursive(Gtk::Widget *parent, + const Glib::ustring& name); +#endif + +/* + 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 : diff --git a/src/widgets/stroke-marker-selector.cpp b/src/widgets/stroke-marker-selector.cpp new file mode 100644 index 0000000..337ba16 --- /dev/null +++ b/src/widgets/stroke-marker-selector.cpp @@ -0,0 +1,567 @@ +// 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 "stroke-marker-selector.h" + +#include <glibmm/i18n.h> +#include <gtkmm/icontheme.h> + +#include "desktop-style.h" +#include "path-prefix.h" +#include "stroke-style.h" + +#include "helper/stock-items.h" +#include "ui/icon-loader.h" + +#include "io/sys.h" + +#include "object/sp-defs.h" +#include "object/sp-marker.h" +#include "object/sp-root.h" +#include "style.h" + +#include "ui/cache/svg_preview_cache.h" +#include "ui/dialog-events.h" +#include "ui/util.h" +#include "ui/widget/spinbutton.h" + +static Inkscape::UI::Cache::SvgPreview svg_preview_cache; + +MarkerComboBox::MarkerComboBox(gchar const *id, int l) : + Gtk::ComboBox(), + combo_id(id), + loc(l), + updating(false), + markerCount(0) +{ + + marker_store = Gtk::ListStore::create(marker_columns); + set_model(marker_store); + pack_start(image_renderer, false); + set_cell_data_func(image_renderer, sigc::mem_fun(*this, &MarkerComboBox::prepareImageRenderer)); + gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(gobj()), MarkerComboBox::separator_cb, nullptr, nullptr); + empty_image = sp_get_icon_image("no-marker", Gtk::ICON_SIZE_SMALL_TOOLBAR); + + sandbox = ink_markers_preview_doc (); + + init_combo(); + this->get_style_context()->add_class("combobright"); + + show(); +} + +MarkerComboBox::~MarkerComboBox() { + delete combo_id; + delete sandbox; + delete empty_image; + + if (doc) { + modified_connection.disconnect(); + } +} + +void MarkerComboBox::setDocument(SPDocument *document) +{ + if (doc != document) { + + if (doc) { + modified_connection.disconnect(); + } + + doc = document; + + if (doc) { + modified_connection = doc->getDefs()->connectModified( sigc::hide(sigc::hide(sigc::bind(sigc::ptr_fun(&MarkerComboBox::handleDefsModified), this))) ); + } + + refreshHistory(); + } +} + +void +MarkerComboBox::handleDefsModified(MarkerComboBox *self) +{ + self->refreshHistory(); +} + +void +MarkerComboBox::refreshHistory() +{ + if (updating) + return; + + updating = true; + + std::vector<SPMarker *> ml = get_marker_list(doc); + + /* + * 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 + */ + if (markerCount != ml.size()) { + const char *active = get_active()->get_value(marker_columns.marker); + sp_marker_list_from_doc(doc, true); + set_selected(active); + markerCount = ml.size(); + } + + updating = false; +} + +/** + * Init the combobox widget to display markers from markers.svg + */ +void +MarkerComboBox::init_combo() +{ + if (updating) + return; + + static SPDocument *markers_doc = nullptr; + + // add separator + Gtk::TreeModel::Row row_sep = *(marker_store->append()); + row_sep[marker_columns.label] = "Separator"; + row_sep[marker_columns.marker] = g_strdup("None"); + row_sep[marker_columns.image] = NULL; + row_sep[marker_columns.stock] = false; + row_sep[marker_columns.history] = false; + row_sep[marker_columns.separator] = true; + + // find and load markers.svg + if (markers_doc == nullptr) { + char *markers_source = g_build_filename(INKSCAPE_MARKERSDIR, "markers.svg", NULL); + if (Inkscape::IO::file_test(markers_source, G_FILE_TEST_IS_REGULAR)) { + markers_doc = SPDocument::createNewDoc(markers_source, FALSE); + } + g_free(markers_source); + } + + // load markers from markers.svg + if (markers_doc) { + sp_marker_list_from_doc(markers_doc, false); + } + + set_sensitive(true); +} + +/** + * Sets the current marker in the marker combobox. + */ +void MarkerComboBox::set_current(SPObject *marker) +{ + updating = true; + + if (marker != nullptr) { + gchar *markname = g_strdup(marker->getRepr()->attribute("id")); + set_selected(markname); + g_free (markname); + } + else { + set_selected(nullptr); + } + + updating = false; + +} +/** + * Return a uri string representing the current selected marker used for setting the marker style in the document + */ +const gchar * MarkerComboBox::get_active_marker_uri() +{ + /* Get Marker */ + const gchar *markid = get_active()->get_value(marker_columns.marker); + if (!markid) + { + return nullptr; + } + + gchar const *marker = ""; + if (strcmp(markid, "none")) { + bool stockid = get_active()->get_value(marker_columns.stock); + + gchar *markurn; + if (stockid) + { + markurn = g_strconcat("urn:inkscape:marker:",markid,NULL); + } + else + { + markurn = g_strdup(markid); + } + SPObject *mark = get_stock_item(markurn, stockid); + g_free(markurn); + if (mark) { + Inkscape::XML::Node *repr = mark->getRepr(); + marker = g_strconcat("url(#", repr->attribute("id"), ")", NULL); + } + } else { + marker = g_strdup(markid); + } + + return marker; +} + +void MarkerComboBox::set_active_history() { + + const gchar *markid = get_active()->get_value(marker_columns.marker); + + // If forked from a stockid, add the stockid + SPObject const *marker = doc->getObjectById(markid); + if (marker && marker->getRepr()->attribute("inkscape:stockid")) { + markid = marker->getRepr()->attribute("inkscape:stockid"); + } + + set_selected(markid); +} + + +void MarkerComboBox::set_selected(const gchar *name, gboolean retry/*=true*/) { + + if (!name) { + set_active(0); + return; + } + + for(Gtk::TreeIter iter = marker_store->children().begin(); + iter != marker_store->children().end(); ++iter) { + Gtk::TreeModel::Row row = (*iter); + if (row[marker_columns.marker] && + !strcmp(row[marker_columns.marker], name)) { + set_active(iter); + return; + } + } + + // Didn't find it in the list, try refreshing from the doc + if (retry) { + sp_marker_list_from_doc(doc, true); + set_selected(name, false); + } +} + + +/** + * Pick up all markers from source, except those that are in + * current_doc (if non-NULL), and add items to the combo. + */ +void MarkerComboBox::sp_marker_list_from_doc(SPDocument *source, gboolean history) +{ + std::vector<SPMarker *> ml = get_marker_list(source); + + remove_markers(history); // Seem to need to remove 2x + remove_markers(history); + add_markers(ml, source, history); +} + +/** + * Returns a list of markers in the defs of the given source document as a vector + * Returns NULL if there are no markers in the document. + */ +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)) { + ml.push_back(SP_MARKER(&child)); + } + } + return ml; +} + +/** + * Remove history or non-history markers from the combo + */ +void MarkerComboBox::remove_markers (gboolean history) +{ + // Having the model set causes assertions when erasing rows, temporarily disconnect + unset_model(); + for(Gtk::TreeIter iter = marker_store->children().begin(); + iter != marker_store->children().end(); ++iter) { + Gtk::TreeModel::Row row = (*iter); + if (row[marker_columns.history] == history && row[marker_columns.separator] == false) { + marker_store->erase(iter); + iter = marker_store->children().begin(); + } + } + + set_model(marker_store); +} + +/** + * 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)); + // Find the separator, + Gtk::TreeIter sep_iter; + for(Gtk::TreeIter iter = marker_store->children().begin(); + iter != marker_store->children().end(); ++iter) { + Gtk::TreeModel::Row row = (*iter); + if (row[marker_columns.separator]) { + sep_iter = iter; + } + } + + if (history) { + // add "None" + Gtk::TreeModel::Row row = *(marker_store->prepend()); + row[marker_columns.label] = C_("Marker", "None"); + row[marker_columns.stock] = false; + row[marker_columns.marker] = g_strdup("None"); + row[marker_columns.image] = NULL; + row[marker_columns.history] = true; + row[marker_columns.separator] = false; + } + + 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 + Gtk::Image *prv = create_marker_image (24, repr->attribute("id"), source, drawing, visionkey); + prv->show(); + + // Add history before separator, others after + Gtk::TreeModel::Row row; + if (history) + row = *(marker_store->insert(sep_iter)); + else + row = *(marker_store->append()); + + row[marker_columns.label] = ink_ellipsize_text(markid, 20); + // Non "stock" markers can also have "inkscape:stockid" (when using extension ColorMarkers), + // So use !is_history instead to determine is it is "stock" (ie in the markers.svg file) + row[marker_columns.stock] = !history; + row[marker_columns.marker] = repr->attribute("id"); + row[marker_columns.image] = prv; + row[marker_columns.history] = history; + row[marker_columns.separator] = false; + + } + + sandbox->getRoot()->invoke_hide(visionkey); +} + +/* + * Remove from the cache and recreate a marker image + */ +void +MarkerComboBox::update_marker_image(gchar const *mname) +{ + gchar *cache_name = g_strconcat(combo_id, mname, NULL); + Glib::ustring key = svg_preview_cache.cache_key(doc->getDocumentURI(), cache_name, 24); + g_free (cache_name); + svg_preview_cache.remove_preview_from_cache(key); + + Inkscape::Drawing drawing; + unsigned const visionkey = SPItem::display_key_new(1); + drawing.setRoot(sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY)); + Gtk::Image *prv = create_marker_image(24, mname, doc, drawing, visionkey); + if (prv) { + prv->show(); + } + sandbox->getRoot()->invoke_hide(visionkey); + + for(const auto & iter : marker_store->children()) { + Gtk::TreeModel::Row row = iter; + if (row[marker_columns.marker] && row[marker_columns.history] && + !strcmp(row[marker_columns.marker], mname)) { + row[marker_columns.image] = prv; + return; + } + } + +} +/** + * 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. + */ +Gtk::Image * +MarkerComboBox::create_marker_image(unsigned psize, gchar const *mname, + SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/) +{ + // Retrieve the marker named 'mname' from the source SVG document + SPObject const *marker = source->getObjectById(mname); + if (marker == nullptr) { + return nullptr; + } + + // 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(); + SPObject *oldmarker = sandbox->getObjectById("sample"); + if (oldmarker) { + oldmarker->deleteObject(false); + } + + // 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", NULL), "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); + sandbox->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + sandbox->ensureUpToDate(); + + if (object == nullptr || !SP_IS_ITEM(object)) { + return nullptr; // sandbox broken? + } + + SPItem *item = SP_ITEM(object); + // Find object's bbox in document + Geom::OptRect dbox = item->documentVisualBounds(); + + if (!dbox) { + return nullptr; + } + + /* Update to renderable state */ + gchar *cache_name = g_strconcat(combo_id, mname, NULL); + Glib::ustring key = svg_preview_cache.cache_key(source->getDocumentURI(), cache_name, psize); + g_free (cache_name); + GdkPixbuf *pixbuf = svg_preview_cache.get_preview_from_cache(key); // no ref created + + if (!pixbuf) { + pixbuf = render_pixbuf(drawing, 0.8, *dbox, psize); + svg_preview_cache.set_preview_in_cache(key, pixbuf); + g_object_unref(pixbuf); // reference is held by svg_preview_cache + } + + // Create widget + Gtk::Image *pb = Glib::wrap(GTK_IMAGE(gtk_image_new_from_pixbuf(pixbuf))); + return pb; +} + +void MarkerComboBox::prepareImageRenderer( Gtk::TreeModel::const_iterator const &row ) { + + Gtk::Image *image = (*row)[marker_columns.image]; + if (image) + image_renderer.property_pixbuf() = image->get_pixbuf(); + else + image_renderer.property_pixbuf() = empty_image->get_pixbuf(); +} + +gboolean MarkerComboBox::separator_cb (GtkTreeModel *model, GtkTreeIter *iter, gpointer /*data*/) { + + gboolean sep = FALSE; + gtk_tree_model_get(model, iter, 4, &sep, -1); + return sep; +} + +/** + * Returns a new document containing default start, mid, and end markers. + */ +SPDocument *MarkerComboBox::ink_markers_preview_doc () +{ +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"/> + + <g id="marker-start"> + <path style="fill:gray;stroke:darkgray;stroke-width:1.7;marker-start:url(#sample)" + d="M 12.5,13 L 25,13"/> + <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/> + </g> + + <g id="marker-mid"> + <path style="fill:gray;stroke:darkgray;stroke-width:1.7;marker-mid:url(#sample)" + d="M 0,113 L 12.5,113 L 25,113"/> + <rect x="0" y="100" width="25" height="25" style="fill:none;stroke:none"/> + </g> + + <g id="marker-end"> + <path style="fill:gray;stroke:darkgray;stroke-width:1.7;marker-end:url(#sample)" + d="M 0,213 L 12.5,213"/> + <rect x="0" y="200" width="25" height="25" style="fill:none;stroke:none"/> + </g> + + </svg> +)A"; + + return SPDocument::createNewDocFromMem (buffer, strlen(buffer), FALSE); +} + +/* + 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 : diff --git a/src/widgets/stroke-marker-selector.h b/src/widgets/stroke-marker-selector.h new file mode 100644 index 0000000..c9bd2e8 --- /dev/null +++ b/src/widgets/stroke-marker-selector.h @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MARKER_SELECTOR_NEW_H +#define SEEN_SP_MARKER_SELECTOR_NEW_H + +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Maximilian Albert <maximilian.albert> (gtkmm-ification) + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <vector> + +#include <gtkmm/box.h> +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> + +#include <sigc++/signal.h> + +#include "document.h" +#include "inkscape.h" + +#include "display/drawing.h" + +class SPMarker; + +namespace Gtk { + +class Container; +class Adjustment; +} + +/** + * ComboBox derived class for selecting stroke markers. + */ + +class MarkerComboBox : public Gtk::ComboBox { +public: + MarkerComboBox(gchar const *id, int loc); + ~MarkerComboBox() override; + + void setDocument(SPDocument *); + + sigc::signal<void> changed_signal; + + void set_current(SPObject *marker); + void set_active_history(); + void set_selected(const gchar *name, gboolean retry=true); + const gchar *get_active_marker_uri(); + bool update() { return updating; }; + gchar const *get_id() { return combo_id; }; + void update_marker_image(gchar const *mname); + int get_loc() { return loc; }; + +private: + + + Glib::RefPtr<Gtk::ListStore> marker_store; + gchar const *combo_id; + int loc; + bool updating; + guint markerCount; + SPDocument *doc = nullptr; + SPDocument *sandbox; + Gtk::Image *empty_image; + Gtk::CellRendererPixbuf image_renderer; + + class MarkerColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn<Glib::ustring> label; + Gtk::TreeModelColumn<const gchar *> marker; // ustring doesn't work here on windows due to unicode + Gtk::TreeModelColumn<gboolean> stock; + Gtk::TreeModelColumn<Gtk::Image *> image; + Gtk::TreeModelColumn<gboolean> history; + Gtk::TreeModelColumn<gboolean> separator; + + MarkerColumns() { + add(label); add(stock); add(marker); add(history); add(separator); add(image); + } + }; + MarkerColumns marker_columns; + + void init_combo(); + void set_history(Gtk::TreeModel::Row match_row); + void sp_marker_list_from_doc(SPDocument *source, gboolean history); + std::vector <SPMarker*> get_marker_list (SPDocument *source); + void add_markers (std::vector<SPMarker *> const& marker_list, SPDocument *source, gboolean history); + void remove_markers (gboolean history); + SPDocument *ink_markers_preview_doc (); + Gtk::Image * create_marker_image(unsigned psize, gchar const *mname, + SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/); + + /* + * Callbacks for drawing the combo box + */ + void prepareImageRenderer( Gtk::TreeModel::const_iterator const &row ); + static gboolean separator_cb (GtkTreeModel *model, GtkTreeIter *iter, gpointer data); + + static void handleDefsModified(MarkerComboBox *self); + + void refreshHistory(); + + sigc::connection modified_connection; +}; + +#endif // SEEN_SP_MARKER_SELECTOR_NEW_H + +/* + 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 : diff --git a/src/widgets/stroke-style.cpp b/src/widgets/stroke-style.cpp new file mode 100644 index 0000000..f2fe0e1 --- /dev/null +++ b/src/widgets/stroke-style.cpp @@ -0,0 +1,1346 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Bryce Harrington <brycehar@bryceharrington.org> + * bulia byak <buliabyak@users.sf.net> + * Maximilian Albert <maximilian.albert@gmail.com> + * Josh Andler <scislac@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2001-2005 authors + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2004 John Cliff + * Copyright (C) 2008 Maximilian Albert (gtkmm-ification) + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noSP_SS_VERBOSE + +#include "desktop-widget.h" +#include "stroke-style.h" + +#include "object/sp-marker.h" +#include "object/sp-namedview.h" +#include "object/sp-rect.h" +#include "object/sp-stop.h" +#include "object/sp-text.h" + +#include "svg/svg-color.h" + +#include "ui/icon-loader.h" +#include "ui/widget/dash-selector.h" +#include "ui/widget/unit-menu.h" + +#include "widgets/style-utils.h" + +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +/** + * Creates a new widget for the line stroke paint. + */ +Gtk::Widget *sp_stroke_style_paint_widget_new() +{ + return Inkscape::Widgets::createStyleWidget( STROKE ); +} + +/** + * Creates a new widget for the line stroke style. + */ +Gtk::Widget *sp_stroke_style_line_widget_new() +{ + return Inkscape::Widgets::createStrokeStyleWidget(); +} + +void sp_stroke_style_widget_set_desktop(Gtk::Widget *widget, SPDesktop *desktop) +{ + Inkscape::StrokeStyle *ss = dynamic_cast<Inkscape::StrokeStyle*>(widget); + if (ss) { + ss->setDesktop(desktop); + } +} + + +/** + * Extract the actual name of the link + * e.g. get mTriangle from url(#mTriangle). + * \return Buffer containing the actual name, allocated from GLib; + * the caller should free the buffer when they no longer need it. + */ +SPObject* getMarkerObj(gchar const *n, SPDocument *doc) +{ + gchar const *p = n; + while (*p != '\0' && *p != '#') { + p++; + } + + if (*p == '\0' || p[1] == '\0') { + return nullptr; + } + + p++; + int c = 0; + while (p[c] != '\0' && p[c] != ')') { + c++; + } + + if (p[c] == '\0') { + return nullptr; + } + + gchar* b = g_strdup(p); + b[c] = '\0'; + + // FIXME: get the document from the object and let the caller pass it in + SPObject *marker = doc->getObjectById(b); + + g_free(b); + return marker; +} + +namespace Inkscape { + + +/** + * Construct a stroke-style radio button with a given icon + * + * \param[in] grp The Gtk::RadioButtonGroup to which to add the new button + * \param[in] icon The icon to use for the button + * \param[in] button_type The type of stroke-style radio button (join/cap) + * \param[in] stroke_style The style attribute to associate with the button + */ +StrokeStyle::StrokeStyleButton::StrokeStyleButton(Gtk::RadioButtonGroup &grp, + char const *icon, + StrokeStyleButtonType button_type, + gchar const *stroke_style) + : + Gtk::RadioButton(grp), + button_type(button_type), + stroke_style(stroke_style) +{ + show(); + set_mode(false); + + auto px = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_LARGE_TOOLBAR)); + g_assert(px != nullptr); + px->show(); + add(*px); +} + +/** + * Create the fill or stroke style widget, and hook up all the signals. + */ +Gtk::Widget *Inkscape::Widgets::createStrokeStyleWidget( ) +{ + StrokeStyle *strokeStyle = new StrokeStyle(); + + return strokeStyle; +} + +StrokeStyle::StrokeStyle() : + Gtk::Box(), + miterLimitSpin(), + widthSpin(), + unitSelector(), + joinMiter(), + joinRound(), + joinBevel(), + capButt(), + capRound(), + capSquare(), + dashSelector(), + update(false), + desktop(nullptr), + selectChangedConn(), + selectModifiedConn(), + startMarkerConn(), + midMarkerConn(), + endMarkerConn(), + _old_unit(nullptr) +{ + table = new Gtk::Grid(); + table->set_border_width(4); + table->set_row_spacing(4); + table->set_hexpand(false); + table->set_halign(Gtk::ALIGN_CENTER); + table->show(); + add(*table); + + Gtk::HBox *hb; + gint i = 0; + + //spw_label(t, C_("Stroke width", "_Width:"), 0, i); + + hb = spw_hbox(table, 3, 1, i); + +// TODO: when this is gtkmmified, use an Inkscape::UI::Widget::ScalarUnit instead of the separate +// spinbutton and unit selector for stroke width. In sp_stroke_style_line_update, use +// setHundredPercent to remember the averaged width corresponding to 100%. Then the +// stroke_width_set_unit will be removed (because ScalarUnit takes care of conversions itself), and +// with it, the two remaining calls of stroke_average_width, allowing us to get rid of that +// function in desktop-style. + widthAdj = new Glib::RefPtr<Gtk::Adjustment>(Gtk::Adjustment::create(1.0, 0.0, 1000.0, 0.1, 10.0, 0.0)); + widthSpin = new Inkscape::UI::Widget::SpinButton(*widthAdj, 0.1, 3); + widthSpin->set_tooltip_text(_("Stroke width")); + widthSpin->show(); + spw_label(table, C_("Stroke width", "_Width:"), 0, i, widthSpin); + + sp_dialog_defocus_on_enter_cpp(widthSpin); + + hb->pack_start(*widthSpin, false, false, 0); + unitSelector = new Inkscape::UI::Widget::UnitMenu(); + unitSelector->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR); + Gtk::Widget *us = Gtk::manage(unitSelector); + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + unitSelector->addUnit(*unit_table.getUnit("%")); + _old_unit = unitSelector->getUnit(); + if (desktop) { + unitSelector->setUnit(desktop->getNamedView()->display_units->abbr); + _old_unit = desktop->getNamedView()->display_units; + } + widthSpin->setUnitMenu(unitSelector); + unitChangedConn = unitSelector->signal_changed().connect(sigc::mem_fun(*this, &StrokeStyle::unitChangedCB)); + + us->show(); + + hb->pack_start(*us, FALSE, FALSE, 0); + (*widthAdj)->signal_value_changed().connect(sigc::mem_fun(*this, &StrokeStyle::widthChangedCB)); + i++; + + /* Dash */ + spw_label(table, _("Dashes:"), 0, i, nullptr); //no mnemonic for now + //decide what to do: + // implement a set_mnemonic_source function in the + // Inkscape::UI::Widget::DashSelector class, so that we do not have to + // expose any of the underlying widgets? + dashSelector = Gtk::manage(new Inkscape::UI::Widget::DashSelector); + + dashSelector->show(); + dashSelector->set_hexpand(); + dashSelector->set_halign(Gtk::ALIGN_FILL); + dashSelector->set_valign(Gtk::ALIGN_CENTER); + table->attach(*dashSelector, 1, i, 3, 1); + dashSelector->changed_signal.connect(sigc::mem_fun(*this, &StrokeStyle::lineDashChangedCB)); + + i++; + + /* Drop down marker selectors*/ + // TRANSLATORS: Path markers are an SVG feature that allows you to attach arbitrary shapes + // (arrowheads, bullets, faces, whatever) to the start, end, or middle nodes of a path. + + spw_label(table, _("Markers:"), 0, i, nullptr); + + hb = spw_hbox(table, 1, 1, i); + i++; + + startMarkerCombo = Gtk::manage(new MarkerComboBox("marker-start", SP_MARKER_LOC_START)); + startMarkerCombo->set_tooltip_text(_("Start Markers are drawn on the first node of a path or shape")); + startMarkerConn = startMarkerCombo->signal_changed().connect( + sigc::bind<MarkerComboBox *, StrokeStyle *, SPMarkerLoc>( + sigc::ptr_fun(&StrokeStyle::markerSelectCB), startMarkerCombo, this, SP_MARKER_LOC_START)); + startMarkerCombo->show(); + + hb->pack_start(*startMarkerCombo, true, true, 0); + + midMarkerCombo = Gtk::manage(new MarkerComboBox("marker-mid", SP_MARKER_LOC_MID)); + midMarkerCombo->set_tooltip_text(_("Mid Markers are drawn on every node of a path or shape except the first and last nodes")); + midMarkerConn = midMarkerCombo->signal_changed().connect( + sigc::bind<MarkerComboBox *, StrokeStyle *, SPMarkerLoc>( + sigc::ptr_fun(&StrokeStyle::markerSelectCB), midMarkerCombo, this, SP_MARKER_LOC_MID)); + midMarkerCombo->show(); + + hb->pack_start(*midMarkerCombo, true, true, 0); + + endMarkerCombo = Gtk::manage(new MarkerComboBox("marker-end", SP_MARKER_LOC_END)); + endMarkerCombo->set_tooltip_text(_("End Markers are drawn on the last node of a path or shape")); + endMarkerConn = endMarkerCombo->signal_changed().connect( + sigc::bind<MarkerComboBox *, StrokeStyle *, SPMarkerLoc>( + sigc::ptr_fun(&StrokeStyle::markerSelectCB), endMarkerCombo, this, SP_MARKER_LOC_END)); + endMarkerCombo->show(); + + hb->pack_start(*endMarkerCombo, true, true, 0); + + i++; + + /* Join type */ + // TRANSLATORS: The line join style specifies the shape to be used at the + // corners of paths. It can be "miter", "round" or "bevel". + spw_label(table, _("Join:"), 0, i, nullptr); + + hb = spw_hbox(table, 3, 1, i); + + Gtk::RadioButtonGroup joinGrp; + + joinRound = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-round"), + hb, STROKE_STYLE_BUTTON_JOIN, "round"); + + // TRANSLATORS: Round join: joining lines with a rounded corner. + // For an example, draw a triangle with a large stroke width and modify the + // "Join" option (in the Fill and Stroke dialog). + joinRound->set_tooltip_text(_("Round join")); + + joinBevel = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-bevel"), + hb, STROKE_STYLE_BUTTON_JOIN, "bevel"); + + // TRANSLATORS: Bevel join: joining lines with a blunted (flattened) corner. + // For an example, draw a triangle with a large stroke width and modify the + // "Join" option (in the Fill and Stroke dialog). + joinBevel->set_tooltip_text(_("Bevel join")); + + joinMiter = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-miter"), + hb, STROKE_STYLE_BUTTON_JOIN, "miter"); + + // TRANSLATORS: Miter join: joining lines with a sharp (pointed) corner. + // For an example, draw a triangle with a large stroke width and modify the + // "Join" option (in the Fill and Stroke dialog). + joinMiter->set_tooltip_text(_("Miter join")); + + /* Miterlimit */ + // TRANSLATORS: Miter limit: only for "miter join", this limits the length + // of the sharp "spike" when the lines connect at too sharp an angle. + // When two line segments meet at a sharp angle, a miter join results in a + // spike that extends well beyond the connection point. The purpose of the + // miter limit is to cut off such spikes (i.e. convert them into bevels) + // when they become too long. + //spw_label(t, _("Miter _limit:"), 0, i); + miterLimitAdj = new Glib::RefPtr<Gtk::Adjustment>(Gtk::Adjustment::create(4.0, 0.0, 100.0, 0.1, 10.0, 0.0)); + miterLimitSpin = new Inkscape::UI::Widget::SpinButton(*miterLimitAdj, 0.1, 2); + miterLimitSpin->set_tooltip_text(_("Maximum length of the miter (in units of stroke width)")); + miterLimitSpin->show(); + sp_dialog_defocus_on_enter_cpp(miterLimitSpin); + + hb->pack_start(*miterLimitSpin, false, false, 0); + (*miterLimitAdj)->signal_value_changed().connect(sigc::mem_fun(*this, &StrokeStyle::miterLimitChangedCB)); + i++; + + /* Cap type */ + // TRANSLATORS: cap type specifies the shape for the ends of lines + //spw_label(t, _("_Cap:"), 0, i); + spw_label(table, _("Cap:"), 0, i, nullptr); + + hb = spw_hbox(table, 3, 1, i); + + Gtk::RadioButtonGroup capGrp; + + capButt = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-butt"), + hb, STROKE_STYLE_BUTTON_CAP, "butt"); + + // TRANSLATORS: Butt cap: the line shape does not extend beyond the end point + // of the line; the ends of the line are square + capButt->set_tooltip_text(_("Butt cap")); + + capRound = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-round"), + hb, STROKE_STYLE_BUTTON_CAP, "round"); + + // TRANSLATORS: Round cap: the line shape extends beyond the end point of the + // line; the ends of the line are rounded + capRound->set_tooltip_text(_("Round cap")); + + capSquare = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-square"), + hb, STROKE_STYLE_BUTTON_CAP, "square"); + + // TRANSLATORS: Square cap: the line shape extends beyond the end point of the + // line; the ends of the line are square + capSquare->set_tooltip_text(_("Square cap")); + + i++; + + /* Paint order */ + // TRANSLATORS: Paint order determines the order the 'fill', 'stroke', and 'markers are painted. + spw_label(table, _("Order:"), 0, i, nullptr); + + hb = spw_hbox(table, 4, 1, i); + + Gtk::RadioButtonGroup paintOrderGrp; + + paintOrderFSM = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-fsm"), + hb, STROKE_STYLE_BUTTON_ORDER, "normal"); + paintOrderFSM->set_tooltip_text(_("Fill, Stroke, Markers")); + + paintOrderSFM = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-sfm"), + hb, STROKE_STYLE_BUTTON_ORDER, "stroke fill markers"); + paintOrderSFM->set_tooltip_text(_("Stroke, Fill, Markers")); + + paintOrderFMS = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-fms"), + hb, STROKE_STYLE_BUTTON_ORDER, "fill markers stroke"); + paintOrderFMS->set_tooltip_text(_("Fill, Markers, Stroke")); + + i++; + + hb = spw_hbox(table, 4, 1, i); + + paintOrderMFS = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-mfs"), + hb, STROKE_STYLE_BUTTON_ORDER, "markers fill stroke"); + paintOrderMFS->set_tooltip_text(_("Markers, Fill, Stroke")); + + paintOrderSMF = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-smf"), + hb, STROKE_STYLE_BUTTON_ORDER, "stroke markers fill"); + paintOrderSMF->set_tooltip_text(_("Stroke, Markers, Fill")); + + paintOrderMSF = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-msf"), + hb, STROKE_STYLE_BUTTON_ORDER, "markers stroke fill"); + paintOrderMSF->set_tooltip_text(_("Markers, Stroke, Fill")); + + i++; +} + +StrokeStyle::~StrokeStyle() +{ + selectModifiedConn.disconnect(); + selectChangedConn.disconnect(); +} + +void StrokeStyle::setDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + + if (this->desktop) { + selectModifiedConn.disconnect(); + selectChangedConn.disconnect(); + _document_replaced_connection.disconnect(); + } + this->desktop = desktop; + + if (!desktop) { + return; + } + + if (desktop->selection) { + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &StrokeStyle::selectionChangedCB))); + selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &StrokeStyle::selectionModifiedCB))); + } + + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(this, &StrokeStyle::_handleDocumentReplaced)); + + _handleDocumentReplaced(nullptr, desktop->getDocument()); + + updateLine(); + } +} + +void StrokeStyle::_handleDocumentReplaced(SPDesktop *, SPDocument *document) +{ + for (MarkerComboBox *combo : { startMarkerCombo, midMarkerCombo, endMarkerCombo }) { + combo->setDocument(document); + } +} + + +/** + * Helper function for creating stroke-style radio buttons. + * + * \param[in] grp The Gtk::RadioButtonGroup in which to add the button + * \param[in] icon The icon for the button + * \param[in] hb The Gtk::Box container in which to add the button + * \param[in] button_type The type (join/cap) for the button + * \param[in] stroke_style The style attribute to associate with the button + * + * \details After instantiating the button, it is added to a container box and + * a handler for the toggle event is connected. + */ +StrokeStyle::StrokeStyleButton * +StrokeStyle::makeRadioButton(Gtk::RadioButtonGroup &grp, + char const *icon, + Gtk::HBox *hb, + StrokeStyleButtonType button_type, + gchar const *stroke_style) +{ + g_assert(icon != nullptr); + g_assert(hb != nullptr); + + StrokeStyleButton *tb = new StrokeStyleButton(grp, icon, button_type, stroke_style); + + hb->pack_start(*tb, false, false, 0); + set_data(icon, tb); + + tb->signal_toggled().connect(sigc::bind<StrokeStyleButton *, StrokeStyle *>( + sigc::ptr_fun(&StrokeStyle::buttonToggledCB), tb, this)); + + return tb; +} + +bool StrokeStyle::shouldMarkersBeUpdated() +{ + return startMarkerCombo->update() || midMarkerCombo->update() || + endMarkerCombo->update(); +} + +/** + * Handles when user selects one of the markers from the marker combobox. + * Gets the marker uri string and applies it to all selected + * items in the current desktop. + */ +void StrokeStyle::markerSelectCB(MarkerComboBox *marker_combo, StrokeStyle *spw, SPMarkerLoc const /*which*/) +{ + if (spw->update || spw->shouldMarkersBeUpdated()) { + return; + } + + spw->update = true; + + SPDocument *document = spw->desktop->getDocument(); + if (!document) { + return; + } + + /* Get Marker */ + gchar const *marker = marker_combo->get_active_marker_uri(); + + + SPCSSAttr *css = sp_repr_css_attr_new(); + gchar const *combo_id = marker_combo->get_id(); + sp_repr_css_set_property(css, combo_id, marker); + + // Also update the marker combobox, so the document's markers + // show up at the top of the combobox +// sp_stroke_style_line_update( SP_WIDGET(spw), desktop ? desktop->getSelection() : NULL); + //spw->updateMarkerHist(which); + + Inkscape::Selection *selection = spw->desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (!SP_IS_SHAPE(item) || SP_IS_RECT(item)) { // can't set marker to rect, until it's converted to using <path> + continue; + } + Inkscape::XML::Node *selrepr = item->getRepr(); + if (selrepr) { + sp_repr_css_change_recursive(selrepr, css, "style"); + SPObject *markerObj = getMarkerObj(marker, document); + spw->setMarkerColor(markerObj, marker_combo->get_loc(), item); + } + + item->requestModified(SP_OBJECT_MODIFIED_FLAG); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, _("Set markers")); + } + + sp_repr_css_attr_unref(css); + css = nullptr; + + spw->update = false; +}; + +void StrokeStyle::updateMarkerHist(SPMarkerLoc const which) +{ + switch (which) { + case SP_MARKER_LOC_START: + startMarkerConn.block(); + startMarkerCombo->set_active_history(); + startMarkerConn.unblock(); + break; + + case SP_MARKER_LOC_MID: + midMarkerConn.block(); + midMarkerCombo->set_active_history(); + midMarkerConn.unblock(); + break; + + case SP_MARKER_LOC_END: + endMarkerConn.block(); + endMarkerCombo->set_active_history(); + endMarkerConn.unblock(); + break; + default: + g_assert_not_reached(); + } +} + +/** + * Callback for when UnitMenu widget is modified. + * Triggers update action. + */ +void StrokeStyle::unitChangedCB() +{ + Inkscape::Util::Unit const *new_unit = unitSelector->getUnit(); + if (new_unit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) { + widthSpin->set_value(100); + } + widthSpin->set_value(Inkscape::Util::Quantity::convert(widthSpin->get_value(), _old_unit, new_unit)); + _old_unit = new_unit; +} + +/** + * Callback for when stroke style widget is modified. + * Triggers update action. + */ +void +StrokeStyle::selectionModifiedCB(guint flags) +{ + // We care deeply about only updating when the style is updated + // if we update on other flags, we slow inkscape down when dragging + if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) { + updateLine(); + } +} + +/** + * Callback for when stroke style widget is changed. + * Triggers update action. + */ +void +StrokeStyle::selectionChangedCB() +{ + updateLine(); +} + +/* + * Fork marker if necessary and set the referencing items url to the new marker + * Return the new marker + */ +SPObject * +StrokeStyle::forkMarker(SPObject *marker, int loc, SPItem *item) +{ + if (!item || !marker) { + return nullptr; + } + + gchar const *marker_id = SPMarkerNames[loc].key; + + /* + * Optimization when all the references to this marker are from this item + * then we can reuse it and don't need to fork + */ + Glib::ustring urlId = Glib::ustring::format("url(#", marker->getRepr()->attribute("id"), ")"); + unsigned int refs = 0; + for (int i = SP_MARKER_LOC_START; i < SP_MARKER_LOC_QTY; i++) { + if (item->style->marker_ptrs[i]->set && + !strcmp(urlId.c_str(), item->style->marker_ptrs[i]->value())) { + refs++; + } + } + if (marker->hrefcount <= refs) { + return marker; + } + + marker = sp_marker_fork_if_necessary(marker); + + // Update the items url to new marker + Inkscape::XML::Node *mark_repr = marker->getRepr(); + SPCSSAttr *css_item = sp_repr_css_attr_new(); + sp_repr_css_set_property(css_item, marker_id, g_strconcat("url(#", mark_repr->attribute("id"), ")", NULL)); + sp_repr_css_change_recursive(item->getRepr(), css_item, "style"); + + sp_repr_css_attr_unref(css_item); + css_item = nullptr; + + return marker; +} + +/** + * Change the color of the marker to match the color of the item. + * Marker stroke color is set to item stroke color. + * Fill color : + * 1. If the item has fill, use that for the marker fill, + * 2. If the marker has same fill and stroke assume its solid, use item stroke for both fill and stroke the line stroke + * 3. If the marker has fill color, use the marker fill color + * + */ +void +StrokeStyle::setMarkerColor(SPObject *marker, int loc, SPItem *item) +{ + + if (!item || !marker) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gboolean colorStock = prefs->getBool("/options/markers/colorStockMarkers", true); + gboolean colorCustom = prefs->getBool("/options/markers/colorCustomMarkers", false); + const gchar *stock = marker->getRepr()->attribute("inkscape:isstock"); + gboolean isStock = (stock && !strcmp(stock,"true")); + + if (isStock ? !colorStock : !colorCustom) { + return; + } + + // Check if we need to fork this marker + marker = forkMarker(marker, loc, item); + + Inkscape::XML::Node *repr = marker->getRepr()->firstChild(); + if (!repr) { + return; + }; + + // Current line style + SPCSSAttr *css_item = sp_css_attr_from_object(item, SP_STYLE_FLAG_ALWAYS); + const char *lstroke = getItemColorForMarker(item, FOR_STROKE, loc); + const char *lstroke_opacity = sp_repr_css_property(css_item, "stroke-opacity", "1"); + const char *lfill = getItemColorForMarker(item, FOR_FILL, loc); + const char *lfill_opacity = sp_repr_css_property(css_item, "fill-opacity", "1"); + + // Current marker style + SPCSSAttr *css_marker = sp_css_attr_from_object(marker->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"); + + // Create new marker style with the lines stroke + SPCSSAttr *css = sp_repr_css_attr_new(); + + sp_repr_css_set_property(css, "stroke", lstroke); + sp_repr_css_set_property(css, "stroke-opacity", lstroke_opacity); + + if (strcmp(lfill, "none") ) { + // 1. If the line has fill, use that for the marker fill + sp_repr_css_set_property(css, "fill", lfill); + sp_repr_css_set_property(css, "fill-opacity", lfill_opacity); + } + else if (mfill && mstroke && !strcmp(mfill, mstroke) && mfill[0] == '#' && strcmp(mfill, "#ffffff")) { + // 2. If the marker has same fill and stroke assume its solid. use line stroke for both fill and stroke the line stroke + sp_repr_css_set_property(css, "fill", lstroke); + sp_repr_css_set_property(css, "fill-opacity", lstroke_opacity); + } + else if (mfill && mfill[0] == '#' && strcmp(mfill, "#000000")) { + // 3. If the marker has fill color, use the marker fill color + sp_repr_css_set_property(css, "fill", mfill); + //sp_repr_css_set_property(css, "fill-opacity", mfill_opacity); + } + + sp_repr_css_change_recursive(marker->firstChild()->getRepr(), css, "style"); + + // Tell the combos to update its image cache of this marker + gchar const *mid = marker->getRepr()->attribute("id"); + startMarkerCombo->update_marker_image(mid); + midMarkerCombo->update_marker_image(mid); + endMarkerCombo->update_marker_image(mid); + + sp_repr_css_attr_unref(css); + css = nullptr; + + +} + +/* + * Get the fill or stroke color of the item + * If its a gradient, then return first or last stop color + */ +const char * +StrokeStyle::getItemColorForMarker(SPItem *item, Inkscape::PaintTarget fill_or_stroke, int loc) +{ + SPCSSAttr *css_item = sp_css_attr_from_object(item, SP_STYLE_FLAG_ALWAYS); + const char *color; + if (fill_or_stroke == FOR_FILL) + color = sp_repr_css_property(css_item, "fill", "none"); + else + color = sp_repr_css_property(css_item, "stroke", "none"); + + if (!strncmp (color, "url(", 4)) { + // If the item has a gradient use the first stop color for the marker + + SPGradient *grad = getGradient(item, fill_or_stroke); + if (grad) { + SPGradient *vector = grad->getVector(FALSE); + SPStop *stop = vector->getFirstStop(); + if (loc == SP_MARKER_LOC_END) { + stop = sp_last_stop(vector); + } + if (stop) { + guint32 const c1 = stop->get_rgba32(); + gchar c[64]; + sp_svg_write_color(c, sizeof(c), c1); + color = g_strdup(c); + //lstroke_opacity = Glib::ustring::format(stop->opacity).c_str(); + } + } + } + return color; +} +/** + * Sets selector widgets' dash style from an SPStyle object. + */ +void +StrokeStyle::setDashSelectorFromStyle(Inkscape::UI::Widget::DashSelector *dsel, SPStyle *style) +{ + if (!style->stroke_dasharray.values.empty()) { + double d[64]; + size_t len = MIN(style->stroke_dasharray.values.size(), 64); + /* Set dash */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gboolean scale = prefs->getBool("/options/dash/scale", true); + double scaledash = 1.0; + if (scale) { + scaledash = style->stroke_width.computed; + } + for (unsigned i = 0; i < len; i++) { + if (style->stroke_width.computed != 0) + d[i] = style->stroke_dasharray.values[i].value / scaledash; + else + d[i] = style->stroke_dasharray.values[i].value; // is there a better thing to do for stroke_width==0? + } + dsel->set_dash(len, d, + style->stroke_width.computed != 0 ? style->stroke_dashoffset.value / scaledash + : style->stroke_dashoffset.value); + } else { + dsel->set_dash(0, nullptr, 0.0); + } +} + +/** + * Sets the join type for a line, and updates the stroke style widget's buttons + */ +void +StrokeStyle::setJoinType (unsigned const jointype) +{ + Gtk::RadioButton *tb = nullptr; + switch (jointype) { + case SP_STROKE_LINEJOIN_MITER: + tb = joinMiter; + break; + case SP_STROKE_LINEJOIN_ROUND: + tb = joinRound; + break; + case SP_STROKE_LINEJOIN_BEVEL: + tb = joinBevel; + break; + default: + // Should not happen + std::cerr << "StrokeStyle::setJoinType(): Invalid value: " << jointype << std::endl; + tb = joinMiter; + break; + } + setJoinButtons(tb); +} + +/** + * Sets the cap type for a line, and updates the stroke style widget's buttons + */ +void +StrokeStyle::setCapType (unsigned const captype) +{ + Gtk::RadioButton *tb = nullptr; + switch (captype) { + case SP_STROKE_LINECAP_BUTT: + tb = capButt; + break; + case SP_STROKE_LINECAP_ROUND: + tb = capRound; + break; + case SP_STROKE_LINECAP_SQUARE: + tb = capSquare; + break; + default: + // Should not happen + std::cerr << "StrokeStyle::setCapType(): Invalid value: " << captype << std::endl; + tb = capButt; + break; + } + setCapButtons(tb); +} + +/** + * Sets the cap type for a line, and updates the stroke style widget's buttons + */ +void +StrokeStyle::setPaintOrder (gchar const *paint_order) +{ + Gtk::RadioButton *tb = paintOrderFSM; + + SPIPaintOrder temp; + temp.read( paint_order ); + + if (temp.layer[0] != SP_CSS_PAINT_ORDER_NORMAL) { + + if (temp.layer[0] == SP_CSS_PAINT_ORDER_FILL) { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) { + tb = paintOrderFSM; + } else { + tb = paintOrderFMS; + } + } else if (temp.layer[0] == SP_CSS_PAINT_ORDER_STROKE) { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_FILL) { + tb = paintOrderSFM; + } else { + tb = paintOrderSMF; + } + } else { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) { + tb = paintOrderMSF; + } else { + tb = paintOrderMFS; + } + } + + } + setPaintOrderButtons(tb); +} + +/** + * Callback for when stroke style widget is updated, including markers, cap type, + * join type, etc. + */ +void +StrokeStyle::updateLine() +{ + if (update) { + return; + } + + update = true; + + Inkscape::Selection *sel = desktop ? desktop->getSelection() : nullptr; + + FillOrStroke kind = GPOINTER_TO_INT(get_data("kind")) ? FILL : STROKE; + + // create temporary style + SPStyle query(SP_ACTIVE_DOCUMENT); + // query into it + int result_sw = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_STROKEWIDTH); + int result_ml = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_STROKEMITERLIMIT); + int result_cap = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_STROKECAP); + int result_join = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_STROKEJOIN); + + int result_order = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_PAINTORDER); + + SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL); + + if (!sel || sel->isEmpty()) { + // Nothing selected, grey-out all controls in the stroke-style dialog + table->set_sensitive(false); + + update = false; + + return; + } else { + table->set_sensitive(true); + + if (result_sw == QUERY_STYLE_MULTIPLE_AVERAGED) { + unitSelector->setUnit("%"); + } else { + // same width, or only one object; no sense to keep percent, switch to absolute + Inkscape::Util::Unit const *tempunit = unitSelector->getUnit(); + if (tempunit->type != Inkscape::Util::UNIT_TYPE_LINEAR) { + unitSelector->setUnit(SP_ACTIVE_DESKTOP->getNamedView()->display_units->abbr); + } + } + + Inkscape::Util::Unit const *unit = unitSelector->getUnit(); + + if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) { + double avgwidth = Inkscape::Util::Quantity::convert(query.stroke_width.computed, "px", unit); + (*widthAdj)->set_value(avgwidth); + } else { + (*widthAdj)->set_value(100); + } + + // if none of the selected objects has a stroke, than quite some controls should be disabled + // The markers might still be shown though, so these will not be disabled + bool enabled = (result_sw != QUERY_STYLE_NOTHING) && !targPaint.isNoneSet(); + /* No objects stroked, set insensitive */ + joinMiter->set_sensitive(enabled); + joinRound->set_sensitive(enabled); + joinBevel->set_sensitive(enabled); + + miterLimitSpin->set_sensitive(enabled); + + capButt->set_sensitive(enabled); + capRound->set_sensitive(enabled); + capSquare->set_sensitive(enabled); + + dashSelector->set_sensitive(enabled); + } + + if (result_ml != QUERY_STYLE_NOTHING) + (*miterLimitAdj)->set_value(query.stroke_miterlimit.value); // TODO: reflect averagedness? + + using Inkscape::is_query_style_updateable; + if (! is_query_style_updateable(result_join)) { + setJoinType(query.stroke_linejoin.value); + } else { + setJoinButtons(nullptr); + } + + if (! is_query_style_updateable(result_cap)) { + setCapType (query.stroke_linecap.value); + } else { + setCapButtons(nullptr); + } + + if (! is_query_style_updateable(result_order)) { + setPaintOrder (query.paint_order.value); + } else { + setPaintOrder (nullptr); + } + + std::vector<SPItem*> const objects(sel->items().begin(), sel->items().end()); + if (objects.size()) { + SPObject *const object = objects[0]; + SPStyle *const style = object->style; + /* Markers */ + updateAllMarkers(objects, true); // FIXME: make this desktop query too + + /* Dash */ + setDashSelectorFromStyle(dashSelector, style); // FIXME: make this desktop query too + } + table->set_sensitive(true); + + update = false; +} + +/** + * Sets a line's dash properties in a CSS style object. + */ +void +StrokeStyle::setScaledDash(SPCSSAttr *css, + int ndash, double *dash, double offset, + double scale) +{ + if (ndash > 0) { + Inkscape::CSSOStringStream osarray; + for (int i = 0; i < ndash; i++) { + osarray << dash[i] * scale; + if (i < (ndash - 1)) { + osarray << ","; + } + } + sp_repr_css_set_property(css, "stroke-dasharray", osarray.str().c_str()); + + Inkscape::CSSOStringStream osoffset; + osoffset << offset * scale; + sp_repr_css_set_property(css, "stroke-dashoffset", osoffset.str().c_str()); + } else { + sp_repr_css_set_property(css, "stroke-dasharray", "none"); + sp_repr_css_set_property(css, "stroke-dashoffset", nullptr); + } +} + +static inline double calcScaleLineWidth(const double width_typed, SPItem *const item, Inkscape::Util::Unit const *const unit) +{ + if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) { + return Inkscape::Util::Quantity::convert(width_typed, unit, "px"); + } else { // percentage + const gdouble old_w = item->style->stroke_width.computed; + return old_w * width_typed / 100; + } +} + +/** + * Sets line properties like width, dashes, markers, etc. on all currently selected items. + */ +void +StrokeStyle::scaleLine() +{ + if (!desktop) { + return; + } + + if (update) { + return; + } + + update = true; + + SPDocument *document = desktop->getDocument(); + Inkscape::Selection *selection = desktop->getSelection(); + auto items= selection->items(); + + /* TODO: Create some standardized method */ + SPCSSAttr *css = sp_repr_css_attr_new(); + + if (!items.empty()) { + double width_typed = (*widthAdj)->get_value(); + double const miterlimit = (*miterLimitAdj)->get_value(); + + Inkscape::Util::Unit const *const unit = unitSelector->getUnit(); + + double *dash, offset; + int ndash; + dashSelector->get_dash(&ndash, &dash, &offset); + + for(auto i=items.begin();i!=items.end();++i){ + /* Set stroke width */ + const double width = calcScaleLineWidth(width_typed, (*i), unit); + + { + Inkscape::CSSOStringStream os_width; + os_width << width; + sp_repr_css_set_property(css, "stroke-width", os_width.str().c_str()); + } + + { + Inkscape::CSSOStringStream os_ml; + os_ml << miterlimit; + sp_repr_css_set_property(css, "stroke-miterlimit", os_ml.str().c_str()); + } + + /* Set dash */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gboolean scale = prefs->getBool("/options/dash/scale", true); + if (scale) { + setScaledDash(css, ndash, dash, offset, width); + } + else { + setScaledDash(css, ndash, dash, offset, document->getDocumentScale()[0]); + } + sp_desktop_apply_css_recursive ((*i), css, true); + } + + g_free(dash); + + if (unit->type != Inkscape::Util::UNIT_TYPE_LINEAR) { + // reset to 100 percent + (*widthAdj)->set_value(100.0); + } + + } + + // we have already changed the items, so set style without changing selection + // FIXME: move the above stroke-setting stuff, including percentages, to desktop-style + sp_desktop_set_style (desktop, css, false); + + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, + _("Set stroke style")); + + update = false; +} + +/** + * Callback for when the stroke style's width changes. + * Causes all line styles to be applied to all selected items. + */ +void +StrokeStyle::widthChangedCB() +{ + if (update) { + return; + } + + scaleLine(); +} + +/** + * Callback for when the stroke style's miterlimit changes. + * Causes all line styles to be applied to all selected items. + */ +void +StrokeStyle::miterLimitChangedCB() +{ + if (update) { + return; + } + + scaleLine(); +} + +/** + * Callback for when the stroke style's dash changes. + * Causes all line styles to be applied to all selected items. + */ + +void +StrokeStyle::lineDashChangedCB() +{ + if (update) { + return; + } + + scaleLine(); +} + +/** + * This routine handles toggle events for buttons in the stroke style dialog. + * + * When activated, this routine gets the data for the various widgets, and then + * calls the respective routines to update css properties, etc. + * + */ +void StrokeStyle::buttonToggledCB(StrokeStyleButton *tb, StrokeStyle *spw) +{ + if (spw->update) { + return; + } + + if (tb->get_active()) { + if (tb->get_button_type() == STROKE_STYLE_BUTTON_JOIN) { + spw->miterLimitSpin->set_sensitive(!strcmp(tb->get_stroke_style(), "miter")); + } + + /* TODO: Create some standardized method */ + SPCSSAttr *css = sp_repr_css_attr_new(); + + switch (tb->get_button_type()) { + case STROKE_STYLE_BUTTON_JOIN: + sp_repr_css_set_property(css, "stroke-linejoin", tb->get_stroke_style()); + sp_desktop_set_style (spw->desktop, css); + spw->setJoinButtons(tb); + break; + case STROKE_STYLE_BUTTON_CAP: + sp_repr_css_set_property(css, "stroke-linecap", tb->get_stroke_style()); + sp_desktop_set_style (spw->desktop, css); + spw->setCapButtons(tb); + break; + case STROKE_STYLE_BUTTON_ORDER: + sp_repr_css_set_property(css, "paint-order", tb->get_stroke_style()); + sp_desktop_set_style (spw->desktop, css); + //spw->setPaintButtons(tb); + } + + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(spw->desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, _("Set stroke style")); + } +} + +/** + * Updates the join style toggle buttons + */ +void +StrokeStyle::setJoinButtons(Gtk::ToggleButton *active) +{ + joinMiter->set_active(active == joinMiter); + miterLimitSpin->set_sensitive(active == joinMiter); + joinRound->set_active(active == joinRound); + joinBevel->set_active(active == joinBevel); +} + +/** + * Updates the cap style toggle buttons + */ +void +StrokeStyle::setCapButtons(Gtk::ToggleButton *active) +{ + capButt->set_active(active == capButt); + capRound->set_active(active == capRound); + capSquare->set_active(active == capSquare); +} + + +/** + * Updates the paint order style toggle buttons + */ +void +StrokeStyle::setPaintOrderButtons(Gtk::ToggleButton *active) +{ + paintOrderFSM->set_active(active == paintOrderFSM); + paintOrderSFM->set_active(active == paintOrderSFM); + paintOrderFMS->set_active(active == paintOrderFMS); + paintOrderMFS->set_active(active == paintOrderMFS); + paintOrderSMF->set_active(active == paintOrderSMF); + paintOrderMSF->set_active(active == paintOrderMSF); +} + + +/** + * Recursively builds a simple list from an arbitrarily complex selection + * of items and grouped items + */ +static void buildGroupedItemList(SPObject *element, std::vector<SPObject*> &simple_list) +{ + if (SP_IS_GROUP(element)) { + for (SPObject *i = element->firstChild(); i; i = i->getNext()) { + buildGroupedItemList(i, simple_list); + } + } else { + simple_list.push_back(element); + } +} + + +/** + * Updates the marker combobox to highlight the appropriate marker and scroll to + * that marker. + */ +void +StrokeStyle::updateAllMarkers(std::vector<SPItem*> const &objects, bool skip_undo) +{ + struct { MarkerComboBox *key; int loc; } const keyloc[] = { + { startMarkerCombo, SP_MARKER_LOC_START }, + { midMarkerCombo, SP_MARKER_LOC_MID }, + { endMarkerCombo, SP_MARKER_LOC_END } + }; + + bool all_texts = true; + + auto simplified_list = std::vector<SPObject *>(); + for (SPItem *item : objects) { + buildGroupedItemList(item, simplified_list); + } + + for (SPObject *object : simplified_list) { + if (!SP_IS_TEXT(object)) { + all_texts = false; + break; + } + } + + // We show markers of the last object in the list only + // FIXME: use the first in the list that has the marker of each type, if any + + // -1 means prefs haven't been queried yet + int update = -1; + + for (auto const &markertype : keyloc) { + // For all three marker types, + + // find the corresponding combobox item + MarkerComboBox *combo = markertype.key; + + // Quit if we're in update state + if (combo->update()) { + return; + } + + // Per SVG spec, text objects cannot have markers; disable combobox if only texts are selected + combo->set_sensitive(!all_texts); + + SPObject *marker = nullptr; + + if (!all_texts) { + for (SPObject *object : simplified_list) { + char const *value = object->style->marker_ptrs[markertype.loc]->value(); + + // If the object has this type of markers, + if (value == nullptr) + continue; + + // Extract the name of the marker that the object uses + marker = getMarkerObj(value, object->document); + + // Set the marker color + if (update < 0) { + // query prefs (only once) + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + update = prefs->getBool("/options/markers/colorUpdateMarkers", true) ? 1 : 0; + } + + if (update > 0) { + setMarkerColor(marker, markertype.loc, SP_ITEM(object)); + + if (!skip_undo) { + SPDocument *document = desktop->getDocument(); + DocumentUndo::maybeDone(document, "UaM", SP_VERB_DIALOG_FILL_STROKE, + _("Set marker color")); + } + } + } + } + + // Scroll the combobox to that marker + combo->set_current(marker); + } + +} + + + +} // 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 : diff --git a/src/widgets/stroke-style.h b/src/widgets/stroke-style.h new file mode 100644 index 0000000..d11c1ae --- /dev/null +++ b/src/widgets/stroke-style.h @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Widgets used in the stroke style dialog. + */ +/* Author: + * Lauris Kaplinski <lauris@ximian.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +// WHOA! talk about header bloat! + +#ifndef SEEN_DIALOGS_STROKE_STYLE_H +#define SEEN_DIALOGS_STROKE_STYLE_H + +#include <glibmm/i18n.h> +#include <gtkmm/grid.h> +#include <gtkmm/radiobutton.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "fill-n-stroke-factory.h" +#include "fill-style.h" // to get sp_fill_style_widget_set_desktop +#include "gradient-chemistry.h" + +#include "inkscape.h" +#include "io/sys.h" +#include "path-prefix.h" +#include "preferences.h" +#include "selection.h" +#include "verbs.h" + +#include "display/canvas-bpath.h" // for SP_STROKE_LINEJOIN_* +#include "display/drawing.h" + +#include "helper/stock-items.h" + +#include "style.h" + +#include "svg/css-ostringstream.h" + +#include "ui/cache/svg_preview_cache.h" +#include "ui/dialog-events.h" +#include "ui/icon-names.h" +#include "ui/widget/spinbutton.h" + +#include "widgets/paint-selector.h" +#include "widgets/spw-utilities.h" +#include "widgets/stroke-marker-selector.h" + +#include "xml/repr.h" + +namespace Gtk { +class Widget; +class Container; +} + +namespace Inkscape { + namespace Util { + class Unit; + } + namespace UI { + namespace Widget { + class DashSelector; + class UnitMenu; + } + } +} + +struct { gchar const *key; gint value; } const SPMarkerNames[] = { + {"marker-all", SP_MARKER_LOC}, + {"marker-start", SP_MARKER_LOC_START}, + {"marker-mid", SP_MARKER_LOC_MID}, + {"marker-end", SP_MARKER_LOC_END}, + {"", SP_MARKER_LOC_QTY}, + {nullptr, -1} +}; + +/** + * Creates an instance of a paint style widget. + */ +Gtk::Widget *sp_stroke_style_paint_widget_new(); + +/** + * Creates an instance of a line style widget. + */ +Gtk::Widget *sp_stroke_style_line_widget_new(); + +/** + * Switches a line or paint style widget to track the given desktop. + */ +void sp_stroke_style_widget_set_desktop(Gtk::Widget *widget, SPDesktop *desktop); + +SPObject *getMarkerObj(gchar const *n, SPDocument *doc); + +namespace Inkscape { +class StrokeStyleButton; + +class StrokeStyle : public Gtk::Box +{ +public: + StrokeStyle(); + ~StrokeStyle() override; + void setDesktop(SPDesktop *desktop); + +private: + /** List of valid types for the stroke-style radio-button widget */ + enum StrokeStyleButtonType { + STROKE_STYLE_BUTTON_JOIN, ///< A button to set the line-join style + STROKE_STYLE_BUTTON_CAP, ///< A button to set the line-cap style + STROKE_STYLE_BUTTON_ORDER ///< A button to set the paint-order style + }; + + /** + * A custom radio-button for setting the stroke style. It can be configured + * to set either the join or cap style by setting the button_type field. + */ + class StrokeStyleButton : public Gtk::RadioButton { + public: + StrokeStyleButton(Gtk::RadioButtonGroup &grp, + char const *icon, + StrokeStyleButtonType button_type, + gchar const *stroke_style); + + /** Get the type (line/cap) of the stroke-style button */ + inline StrokeStyleButtonType get_button_type() {return button_type;} + + /** Get the stroke style attribute associated with the button */ + inline gchar const * get_stroke_style() {return stroke_style;} + + private: + StrokeStyleButtonType button_type; ///< The type (line/cap) of the button + gchar const *stroke_style; ///< The stroke style associated with the button + }; + + void updateLine(); + void updateAllMarkers(std::vector<SPItem*> const &objects, bool skip_undo = false); + void updateMarkerHist(SPMarkerLoc const which); + void setDashSelectorFromStyle(Inkscape::UI::Widget::DashSelector *dsel, SPStyle *style); + void setJoinType (unsigned const jointype); + void setCapType (unsigned const captype); + void setPaintOrder (gchar const *paint_order); + void setJoinButtons(Gtk::ToggleButton *active); + void setCapButtons(Gtk::ToggleButton *active); + void setPaintOrderButtons(Gtk::ToggleButton *active); + void scaleLine(); + void setScaledDash(SPCSSAttr *css, int ndash, double *dash, double offset, double scale); + void setMarkerColor(SPObject *marker, int loc, SPItem *item); + SPObject *forkMarker(SPObject *marker, int loc, SPItem *item); + const char *getItemColorForMarker(SPItem *item, Inkscape::PaintTarget fill_or_stroke, int loc); + + StrokeStyleButton * makeRadioButton(Gtk::RadioButtonGroup &grp, + char const *icon, + Gtk::HBox *hb, + StrokeStyleButtonType button_type, + gchar const *stroke_style); + + // Callback functions + void selectionModifiedCB(guint flags); + void selectionChangedCB(); + void widthChangedCB(); + void miterLimitChangedCB(); + void lineDashChangedCB(); + void unitChangedCB(); + bool shouldMarkersBeUpdated(); + static void markerSelectCB(MarkerComboBox *marker_combo, StrokeStyle *spw, SPMarkerLoc const which); + static void buttonToggledCB(StrokeStyleButton *tb, StrokeStyle *spw); + + + MarkerComboBox *startMarkerCombo; + MarkerComboBox *midMarkerCombo; + MarkerComboBox *endMarkerCombo; + Gtk::Grid *table; + Glib::RefPtr<Gtk::Adjustment> *widthAdj; + Glib::RefPtr<Gtk::Adjustment> *miterLimitAdj; + Inkscape::UI::Widget::SpinButton *miterLimitSpin; + Inkscape::UI::Widget::SpinButton *widthSpin; + Inkscape::UI::Widget::UnitMenu *unitSelector; + StrokeStyleButton *joinMiter; + StrokeStyleButton *joinRound; + StrokeStyleButton *joinBevel; + StrokeStyleButton *capButt; + StrokeStyleButton *capRound; + StrokeStyleButton *capSquare; + StrokeStyleButton *paintOrderFSM; + StrokeStyleButton *paintOrderSFM; + StrokeStyleButton *paintOrderFMS; + StrokeStyleButton *paintOrderMFS; + StrokeStyleButton *paintOrderSMF; + StrokeStyleButton *paintOrderMSF; + Inkscape::UI::Widget::DashSelector *dashSelector; + + gboolean update; + SPDesktop *desktop; + sigc::connection selectChangedConn; + sigc::connection selectModifiedConn; + sigc::connection startMarkerConn; + sigc::connection midMarkerConn; + sigc::connection endMarkerConn; + sigc::connection unitChangedConn; + + Inkscape::Util::Unit const *_old_unit; + + void _handleDocumentReplaced(SPDesktop *, SPDocument *); + sigc::connection _document_replaced_connection; +}; + +} // namespace Inkscape + +#endif // SEEN_DIALOGS_STROKE_STYLE_H + +/* + 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 : diff --git a/src/widgets/style-utils.h b/src/widgets/style-utils.h new file mode 100644 index 0000000..1557218 --- /dev/null +++ b/src/widgets/style-utils.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Common utility functions for manipulating style. + *//* + * Authors: + * see git history + * Shlomi Fish <shlomif@cpan.org> + * + * Copyright (C) 2016 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_STYLE_UTILS_H +#define SEEN_DIALOGS_STYLE_UTILS_H + +#include "desktop-style.h" + +namespace Inkscape { + inline bool is_query_style_updateable(const int style) { + return (style == QUERY_STYLE_MULTIPLE_DIFFERENT || style == QUERY_STYLE_NOTHING); + } +} // namespace Inkscape + +#endif // SEEN_DIALOGS_STYLE_UTILS_H + +/* + 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 : diff --git a/src/widgets/swatch-selector.cpp b/src/widgets/swatch-selector.cpp new file mode 100644 index 0000000..aeead1c --- /dev/null +++ b/src/widgets/swatch-selector.cpp @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "swatch-selector.h" + +#include <glibmm/i18n.h> + +#include "document-undo.h" +#include "document.h" +#include "gradient-chemistry.h" +#include "gradient-selector.h" +#include "verbs.h" + +#include "object/sp-stop.h" + +#include "svg/css-ostringstream.h" +#include "svg/svg-color.h" + +#include "ui/widget/color-notebook.h" + +#include "xml/node.h" + +namespace Inkscape +{ +namespace Widgets +{ + +SwatchSelector::SwatchSelector() : + Gtk::VBox(), + _gsel(nullptr), + _updating_color(false) +{ + using Inkscape::UI::Widget::ColorNotebook; + + GtkWidget *gsel = sp_gradient_selector_new(); + _gsel = SP_GRADIENT_SELECTOR(gsel); + g_object_set_data( G_OBJECT(gobj()), "base", this ); + _gsel->setMode(SPGradientSelector::MODE_SWATCH); + + gtk_widget_show(gsel); + + pack_start(*Gtk::manage(Glib::wrap(gsel))); + + Gtk::Widget *color_selector = Gtk::manage(new ColorNotebook(_selected_color)); + color_selector->show(); + pack_start(*color_selector); + + //_selected_color.signal_grabbed.connect(sigc::mem_fun(this, &SwatchSelector::_grabbedCb)); + _selected_color.signal_dragged.connect(sigc::mem_fun(this, &SwatchSelector::_changedCb)); + _selected_color.signal_released.connect(sigc::mem_fun(this, &SwatchSelector::_changedCb)); + // signal_changed doesn't get called if updating shape with colour. + //_selected_color.signal_changed.connect(sigc::mem_fun(this, &SwatchSelector::_changedCb)); +} + +SwatchSelector::~SwatchSelector() +{ + _gsel = nullptr; +} + +SPGradientSelector *SwatchSelector::getGradientSelector() +{ + return _gsel; +} + +void SwatchSelector::_changedCb() +{ + if (_updating_color) { + return; + } + // TODO might have to block cycles + + if (_gsel && _gsel->getVector()) { + SPGradient *gradient = _gsel->getVector(); + SPGradient *ngr = sp_gradient_ensure_vector_normalized(gradient); + if (ngr != gradient) { + /* Our master gradient has changed */ + // TODO replace with proper - sp_gradient_vector_widget_load_gradient(GTK_WIDGET(swsel->_gsel), ngr); + } + + ngr->ensureVector(); + + + SPStop* stop = ngr->getFirstStop(); + if (stop) { + SPColor color = _selected_color.color(); + gfloat alpha = _selected_color.alpha(); + guint32 rgb = color.toRGBA32( 0x00 ); + + // TODO replace with generic shared code that also handles icc-color + Inkscape::CSSOStringStream os; + gchar c[64]; + sp_svg_write_color(c, sizeof(c), rgb); + os << "stop-color:" << c << ";stop-opacity:" << static_cast<gdouble>(alpha) <<";"; + stop->setAttribute("style", os.str()); + + DocumentUndo::done(ngr->document, SP_VERB_CONTEXT_GRADIENT, + _("Change swatch color")); + } + } +} + +void SwatchSelector::connectGrabbedHandler( GCallback handler, void *data ) +{ + GObject* obj = G_OBJECT(_gsel); + g_signal_connect( obj, "grabbed", handler, data ); +} + +void SwatchSelector::connectDraggedHandler( GCallback handler, void *data ) +{ + GObject* obj = G_OBJECT(_gsel); + g_signal_connect( obj, "dragged", handler, data ); +} + +void SwatchSelector::connectReleasedHandler( GCallback handler, void *data ) +{ + GObject* obj = G_OBJECT(_gsel); + g_signal_connect( obj, "released", handler, data ); +} + +void SwatchSelector::connectchangedHandler( GCallback handler, void *data ) +{ + GObject* obj = G_OBJECT(_gsel); + g_signal_connect( obj, "changed", handler, data ); +} + +void SwatchSelector::setVector(SPDocument */*doc*/, SPGradient *vector) +{ + //GtkVBox * box = gobj(); + _gsel->setVector((vector) ? vector->document : nullptr, vector); + + if ( vector && vector->isSolid() ) { + SPStop* stop = vector->getFirstStop(); + + guint32 const colorVal = stop->get_rgba32(); + _updating_color = true; + _selected_color.setValue(colorVal); + _updating_color = false; + // gtk_widget_show_all( GTK_WIDGET(_csel) ); + } else { + //gtk_widget_hide( GTK_WIDGET(_csel) ); + } + +/* +*/ +} + +} // namespace Widgets +} // 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 : diff --git a/src/widgets/swatch-selector.h b/src/widgets/swatch-selector.h new file mode 100644 index 0000000..88e7ad6 --- /dev/null +++ b/src/widgets/swatch-selector.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_SWATCH_SELECTOR_H +#define SEEN_SP_SWATCH_SELECTOR_H + +#include <gtkmm/box.h> +#include "ui/selected-color.h" + +class SPDocument; +class SPGradient; +struct SPColorSelector; +struct SPGradientSelector; + +namespace Inkscape +{ +namespace Widgets +{ + +class SwatchSelector : public Gtk::VBox +{ +public: + SwatchSelector(); + ~SwatchSelector() override; + + void connectGrabbedHandler( GCallback handler, void *data ); + void connectDraggedHandler( GCallback handler, void *data ); + void connectReleasedHandler( GCallback handler, void *data ); + void connectchangedHandler( GCallback handler, void *data ); + + void setVector(SPDocument *doc, SPGradient *vector); + + SPGradientSelector *getGradientSelector(); + +private: + void _grabbedCb(); + void _draggedCb(); + void _releasedCb(); + void _changedCb(); + + SPGradientSelector *_gsel; + Inkscape::UI::SelectedColor _selected_color; + bool _updating_color; +}; + + +} // namespace Widgets +} // namespace Inkscape + +#endif // SEEN_SP_SWATCH_SELECTOR_H + +/* + 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 : + diff --git a/src/widgets/toolbox.cpp b/src/widgets/toolbox.cpp new file mode 100644 index 0000000..6248dec --- /dev/null +++ b/src/widgets/toolbox.cpp @@ -0,0 +1,842 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** + * @file + * Inkscape toolbar definitions and general utility functions. + * Each tool should have its own xxx-toolbar implementation file + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * Jabiertxo Arraiza <jabier.arraiza@marker.es> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2015 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include <gtkmm/action.h> +#include <gtkmm/actiongroup.h> +#include <gtkmm/toolitem.h> +#include <glibmm/i18n.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "inkscape.h" +#include "verbs.h" + +#include "ink-action.h" + +#include "helper/action.h" +#include "helper/verb-action.h" + +#include "include/gtkmm_version.h" + +#include "io/resource.h" + +#include "object/sp-namedview.h" + +#include "ui/icon-names.h" +#include "ui/tools-switch.h" +#include "ui/uxmanager.h" +#include "ui/widget/button.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/style-swatch.h" +#include "ui/widget/unit-tracker.h" + +#include "widgets/spinbutton-events.h" +#include "widgets/spw-utilities.h" +#include "widgets/widget-sizes.h" + +#include "xml/attribute-record.h" +#include "xml/node-event-vector.h" + +#include "ui/toolbar/arc-toolbar.h" +#include "ui/toolbar/box3d-toolbar.h" +#include "ui/toolbar/calligraphy-toolbar.h" +#include "ui/toolbar/connector-toolbar.h" +#include "ui/toolbar/dropper-toolbar.h" +#include "ui/toolbar/eraser-toolbar.h" +#include "ui/toolbar/gradient-toolbar.h" +#include "ui/toolbar/lpe-toolbar.h" +#include "ui/toolbar/mesh-toolbar.h" +#include "ui/toolbar/measure-toolbar.h" +#include "ui/toolbar/node-toolbar.h" +#include "ui/toolbar/rect-toolbar.h" +#include "ui/toolbar/paintbucket-toolbar.h" +#include "ui/toolbar/pencil-toolbar.h" +#include "ui/toolbar/select-toolbar.h" +#include "ui/toolbar/snap-toolbar.h" +#include "ui/toolbar/spray-toolbar.h" +#include "ui/toolbar/spiral-toolbar.h" +#include "ui/toolbar/star-toolbar.h" +#include "ui/toolbar/tweak-toolbar.h" +#include "ui/toolbar/text-toolbar.h" +#include "ui/toolbar/zoom-toolbar.h" + +#include "toolbox.h" + +#include "ui/tools/tool-base.h" + +//#define DEBUG_TEXT + +using Inkscape::UI::UXManager; +using Inkscape::DocumentUndo; +using Inkscape::UI::ToolboxFactory; +using Inkscape::UI::Tools::ToolBase; + +using Inkscape::IO::Resource::get_filename; +using Inkscape::IO::Resource::UIS; + +typedef void (*SetupFunction)(GtkWidget *toolbox, SPDesktop *desktop); +typedef void (*UpdateFunction)(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox); + +enum BarId { + BAR_TOOL = 0, + BAR_AUX, + BAR_COMMANDS, + BAR_SNAP, +}; + +#define BAR_ID_KEY "BarIdValue" +#define HANDLE_POS_MARK "x-inkscape-pos" + +GtkIconSize ToolboxFactory::prefToSize( Glib::ustring const &path, int base ) { + static GtkIconSize sizeChoices[] = { + GTK_ICON_SIZE_LARGE_TOOLBAR, + GTK_ICON_SIZE_SMALL_TOOLBAR, + GTK_ICON_SIZE_DND, + GTK_ICON_SIZE_DIALOG + }; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int index = prefs->getIntLimited( path, base, 0, G_N_ELEMENTS(sizeChoices) ); + return sizeChoices[index]; +} + +Gtk::IconSize ToolboxFactory::prefToSize_mm(Glib::ustring const &path, int base) +{ + static Gtk::IconSize sizeChoices[] = { Gtk::ICON_SIZE_LARGE_TOOLBAR, Gtk::ICON_SIZE_SMALL_TOOLBAR, + Gtk::ICON_SIZE_DND, Gtk::ICON_SIZE_DIALOG }; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int index = prefs->getIntLimited(path, base, 0, G_N_ELEMENTS(sizeChoices)); + return sizeChoices[index]; +} + +static struct { + gchar const *type_name; + gchar const *data_name; + sp_verb_t verb; + sp_verb_t doubleclick_verb; +} const tools[] = { + { "/tools/select", "select_tool", SP_VERB_CONTEXT_SELECT, SP_VERB_CONTEXT_SELECT_PREFS}, + { "/tools/nodes", "node_tool", SP_VERB_CONTEXT_NODE, SP_VERB_CONTEXT_NODE_PREFS }, + { "/tools/tweak", "tweak_tool", SP_VERB_CONTEXT_TWEAK, SP_VERB_CONTEXT_TWEAK_PREFS }, + { "/tools/spray", "spray_tool", SP_VERB_CONTEXT_SPRAY, SP_VERB_CONTEXT_SPRAY_PREFS }, + { "/tools/zoom", "zoom_tool", SP_VERB_CONTEXT_ZOOM, SP_VERB_CONTEXT_ZOOM_PREFS }, + { "/tools/measure", "measure_tool", SP_VERB_CONTEXT_MEASURE, SP_VERB_CONTEXT_MEASURE_PREFS }, + { "/tools/shapes/rect", "rect_tool", SP_VERB_CONTEXT_RECT, SP_VERB_CONTEXT_RECT_PREFS }, + { "/tools/shapes/3dbox", "3dbox_tool", SP_VERB_CONTEXT_3DBOX, SP_VERB_CONTEXT_3DBOX_PREFS }, + { "/tools/shapes/arc", "arc_tool", SP_VERB_CONTEXT_ARC, SP_VERB_CONTEXT_ARC_PREFS }, + { "/tools/shapes/star", "star_tool", SP_VERB_CONTEXT_STAR, SP_VERB_CONTEXT_STAR_PREFS }, + { "/tools/shapes/spiral", "spiral_tool", SP_VERB_CONTEXT_SPIRAL, SP_VERB_CONTEXT_SPIRAL_PREFS }, + { "/tools/freehand/pencil", "pencil_tool", SP_VERB_CONTEXT_PENCIL, SP_VERB_CONTEXT_PENCIL_PREFS }, + { "/tools/freehand/pen", "pen_tool", SP_VERB_CONTEXT_PEN, SP_VERB_CONTEXT_PEN_PREFS }, + { "/tools/calligraphic", "dyna_draw_tool", SP_VERB_CONTEXT_CALLIGRAPHIC, SP_VERB_CONTEXT_CALLIGRAPHIC_PREFS }, + { "/tools/lpetool", "lpetool_tool", SP_VERB_CONTEXT_LPETOOL, SP_VERB_CONTEXT_LPETOOL_PREFS }, + { "/tools/eraser", "eraser_tool", SP_VERB_CONTEXT_ERASER, SP_VERB_CONTEXT_ERASER_PREFS }, + { "/tools/paintbucket", "paintbucket_tool", SP_VERB_CONTEXT_PAINTBUCKET, SP_VERB_CONTEXT_PAINTBUCKET_PREFS }, + { "/tools/text", "text_tool", SP_VERB_CONTEXT_TEXT, SP_VERB_CONTEXT_TEXT_PREFS }, + { "/tools/connector","connector_tool", SP_VERB_CONTEXT_CONNECTOR, SP_VERB_CONTEXT_CONNECTOR_PREFS }, + { "/tools/gradient", "gradient_tool", SP_VERB_CONTEXT_GRADIENT, SP_VERB_CONTEXT_GRADIENT_PREFS }, + { "/tools/mesh", "mesh_tool", SP_VERB_CONTEXT_MESH, SP_VERB_CONTEXT_MESH_PREFS }, + { "/tools/dropper", "dropper_tool", SP_VERB_CONTEXT_DROPPER, SP_VERB_CONTEXT_DROPPER_PREFS }, + { nullptr, nullptr, 0, 0 } +}; + +static struct { + gchar const *type_name; + gchar const *data_name; + GtkWidget *(*create_func)(SPDesktop *desktop); + gchar const *ui_name; + gint swatch_verb_id; + gchar const *swatch_tool; + gchar const *swatch_tip; +} const aux_toolboxes[] = { + { "/tools/select", "select_toolbox", Inkscape::UI::Toolbar::SelectToolbar::create, "SelectToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/nodes", "node_toolbox", Inkscape::UI::Toolbar::NodeToolbar::create, "NodeToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/tweak", "tweak_toolbox", Inkscape::UI::Toolbar::TweakToolbar::create, "TweakToolbar", + SP_VERB_CONTEXT_TWEAK_PREFS, "/tools/tweak", N_("Color/opacity used for color tweaking")}, + { "/tools/spray", "spray_toolbox", Inkscape::UI::Toolbar::SprayToolbar::create, "SprayToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/zoom", "zoom_toolbox", Inkscape::UI::Toolbar::ZoomToolbar::create, "ZoomToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + // If you change MeasureToolbar here, change it also in desktop-widget.cpp + { "/tools/measure", "measure_toolbox", Inkscape::UI::Toolbar::MeasureToolbar::create, "MeasureToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/shapes/star", "star_toolbox", Inkscape::UI::Toolbar::StarToolbar::create, "StarToolbar", + SP_VERB_CONTEXT_STAR_PREFS, "/tools/shapes/star", N_("Style of new stars")}, + { "/tools/shapes/rect", "rect_toolbox", Inkscape::UI::Toolbar::RectToolbar::create, "RectToolbar", + SP_VERB_CONTEXT_RECT_PREFS, "/tools/shapes/rect", N_("Style of new rectangles")}, + { "/tools/shapes/3dbox", "3dbox_toolbox", Inkscape::UI::Toolbar::Box3DToolbar::create, "3DBoxToolbar", + SP_VERB_CONTEXT_3DBOX_PREFS, "/tools/shapes/3dbox", N_("Style of new 3D boxes")}, + { "/tools/shapes/arc", "arc_toolbox", Inkscape::UI::Toolbar::ArcToolbar::create, "ArcToolbar", + SP_VERB_CONTEXT_ARC_PREFS, "/tools/shapes/arc", N_("Style of new ellipses")}, + { "/tools/shapes/spiral", "spiral_toolbox", Inkscape::UI::Toolbar::SpiralToolbar::create, "SpiralToolbar", + SP_VERB_CONTEXT_SPIRAL_PREFS, "/tools/shapes/spiral", N_("Style of new spirals")}, + { "/tools/freehand/pencil", "pencil_toolbox", Inkscape::UI::Toolbar::PencilToolbar::create_pencil, "PencilToolbar", + SP_VERB_CONTEXT_PENCIL_PREFS, "/tools/freehand/pencil", N_("Style of new paths created by Pencil")}, + { "/tools/freehand/pen", "pen_toolbox", Inkscape::UI::Toolbar::PencilToolbar::create_pen, "PenToolbar", + SP_VERB_CONTEXT_PEN_PREFS, "/tools/freehand/pen", N_("Style of new paths created by Pen")}, + { "/tools/calligraphic", "calligraphy_toolbox", Inkscape::UI::Toolbar::CalligraphyToolbar::create, "CalligraphyToolbar", + SP_VERB_CONTEXT_CALLIGRAPHIC_PREFS, "/tools/calligraphic", N_("Style of new calligraphic strokes")}, + { "/tools/eraser", "eraser_toolbox", Inkscape::UI::Toolbar::EraserToolbar::create, "EraserToolbar", + SP_VERB_CONTEXT_ERASER_PREFS, "/tools/eraser", _("TBD")}, + { "/tools/lpetool", "lpetool_toolbox", Inkscape::UI::Toolbar::LPEToolbar::create, "LPEToolToolbar", + SP_VERB_CONTEXT_LPETOOL_PREFS, "/tools/lpetool", _("TBD")}, + // If you change TextToolbar here, change it also in desktop-widget.cpp + { "/tools/text", "text_toolbox", Inkscape::UI::Toolbar::TextToolbar::create, "TextToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/dropper", "dropper_toolbox", Inkscape::UI::Toolbar::DropperToolbar::create, "DropperToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/connector", "connector_toolbox", Inkscape::UI::Toolbar::ConnectorToolbar::create, "ConnectorToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/gradient", "gradient_toolbox", Inkscape::UI::Toolbar::GradientToolbar::create, "GradientToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/mesh", "mesh_toolbox", Inkscape::UI::Toolbar::MeshToolbar::create, "MeshToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/paintbucket", "paintbucket_toolbox", Inkscape::UI::Toolbar::PaintbucketToolbar::create, "PaintbucketToolbar", + SP_VERB_CONTEXT_PAINTBUCKET_PREFS, "/tools/paintbucket", N_("Style of Paint Bucket fill objects")}, + { nullptr, nullptr, nullptr, nullptr, + SP_VERB_INVALID, nullptr, nullptr } +}; + + +static Glib::RefPtr<Gtk::ActionGroup> create_or_fetch_actions( SPDesktop* desktop ); + +static void setup_snap_toolbox(GtkWidget *toolbox, SPDesktop *desktop); + +static void setup_tool_toolbox(GtkWidget *toolbox, SPDesktop *desktop); +static void update_tool_toolbox(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox); + +static void setup_aux_toolbox(GtkWidget *toolbox, SPDesktop *desktop); +static void update_aux_toolbox(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox); + +static void setup_commands_toolbox(GtkWidget *toolbox, SPDesktop *desktop); +static void update_commands_toolbox(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox); + +static void trigger_sp_action( GtkAction* /*act*/, gpointer user_data ) +{ + SPAction* targetAction = SP_ACTION(user_data); + if ( targetAction ) { + sp_action_perform( targetAction, nullptr ); + } +} + +static GtkAction* create_action_for_verb( Inkscape::Verb* verb, Inkscape::UI::View::View* view, GtkIconSize size ) +{ + GtkAction* act = nullptr; + + SPAction* targetAction = verb->get_action(Inkscape::ActionContext(view)); + InkAction* inky = ink_action_new( verb->get_id(), _(verb->get_name()), verb->get_tip(), verb->get_image(), size ); + act = GTK_ACTION(inky); + gtk_action_set_sensitive( act, targetAction->sensitive ); + + g_signal_connect( G_OBJECT(inky), "activate", G_CALLBACK(trigger_sp_action), targetAction ); + + // FIXME: memory leak: this is not unrefed anywhere + g_object_ref(G_OBJECT(targetAction)); + g_object_set_data_full(G_OBJECT(inky), "SPAction", (void*) targetAction, (GDestroyNotify) &g_object_unref); + targetAction->signal_set_sensitive.connect( + sigc::bind<0>( + sigc::ptr_fun(>k_action_set_sensitive), + GTK_ACTION(inky))); + + return act; +} + +static std::map<SPDesktop*, Glib::RefPtr<Gtk::ActionGroup> > groups; + +static void desktopDestructHandler(SPDesktop *desktop) +{ + std::map<SPDesktop*, Glib::RefPtr<Gtk::ActionGroup> >::iterator it = groups.find(desktop); + if (it != groups.end()) + { + groups.erase(it); + } +} + +static Glib::RefPtr<Gtk::ActionGroup> create_or_fetch_actions( SPDesktop* desktop ) +{ + Inkscape::UI::View::View *view = desktop; + gint verbsToUse[] = { + // disabled until we have icons for them: + //find + //SP_VERB_EDIT_TILE, + //SP_VERB_EDIT_UNTILE, + SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + SP_VERB_DIALOG_DISPLAY, + SP_VERB_DIALOG_FILL_STROKE, + SP_VERB_DIALOG_NAMEDVIEW, + SP_VERB_DIALOG_TEXT, + SP_VERB_DIALOG_XML_EDITOR, + SP_VERB_DIALOG_SELECTORS, + SP_VERB_DIALOG_LAYERS, + SP_VERB_EDIT_CLONE, + SP_VERB_EDIT_COPY, + SP_VERB_EDIT_CUT, + SP_VERB_EDIT_DUPLICATE, + SP_VERB_EDIT_PASTE, + SP_VERB_EDIT_REDO, + SP_VERB_EDIT_UNDO, + SP_VERB_EDIT_UNLINK_CLONE, + //SP_VERB_FILE_EXPORT, + SP_VERB_DIALOG_EXPORT, + SP_VERB_FILE_IMPORT, + SP_VERB_FILE_NEW, + SP_VERB_FILE_OPEN, + SP_VERB_FILE_PRINT, + SP_VERB_FILE_SAVE, + SP_VERB_OBJECT_TO_CURVE, + SP_VERB_SELECTION_GROUP, + SP_VERB_SELECTION_OUTLINE, + SP_VERB_SELECTION_UNGROUP, + SP_VERB_ZOOM_1_1, + SP_VERB_ZOOM_1_2, + SP_VERB_ZOOM_2_1, + SP_VERB_ZOOM_DRAWING, + SP_VERB_ZOOM_IN, + SP_VERB_ZOOM_NEXT, + SP_VERB_ZOOM_OUT, + SP_VERB_ZOOM_PAGE, + SP_VERB_ZOOM_PAGE_WIDTH, + SP_VERB_ZOOM_PREV, + SP_VERB_ZOOM_SELECTION, + SP_VERB_ZOOM_CENTER_PAGE + }; + + GtkIconSize toolboxSize = ToolboxFactory::prefToSize("/toolbox/small"); + Glib::RefPtr<Gtk::ActionGroup> mainActions; + if (desktop == nullptr) + { + return mainActions; + } + + if ( groups.find(desktop) != groups.end() ) { + mainActions = groups[desktop]; + } + + if ( !mainActions ) { + mainActions = Gtk::ActionGroup::create("main"); + groups[desktop] = mainActions; + desktop->connectDestroy(&desktopDestructHandler); + } + + for (int i : verbsToUse) { + Inkscape::Verb* verb = Inkscape::Verb::get(i); + if ( verb ) { + if (!mainActions->get_action(verb->get_id())) { + GtkAction* act = create_action_for_verb( verb, view, toolboxSize ); + mainActions->add(Glib::wrap(act)); + } + } + } + + if ( !mainActions->get_action("ToolZoom") ) { + for ( guint i = 0; i < G_N_ELEMENTS(tools) && tools[i].type_name; i++ ) { + Glib::RefPtr<VerbAction> va = VerbAction::create(Inkscape::Verb::get(tools[i].verb), Inkscape::Verb::get(tools[i].doubleclick_verb), view); + if ( va ) { + mainActions->add(va); + if ( i == 0 ) { + va->set_active(true); + } + } else { + // This creates a blank action using the data_name, this can replace + // tools that have been disabled by compile time options. + Glib::RefPtr<Gtk::Action> act = Gtk::Action::create(Glib::ustring(tools[i].data_name)); + act->set_sensitive(false); + mainActions->add(act); + } + } + } + + return mainActions; +} + + +static GtkWidget* toolboxNewCommon( GtkWidget* tb, BarId id, GtkPositionType /*handlePos*/ ) +{ + g_object_set_data(G_OBJECT(tb), "desktop", nullptr); + + gtk_widget_set_sensitive(tb, FALSE); + + GtkWidget *hb = gtk_event_box_new(); // A simple, neutral container. + gtk_widget_set_name(hb, "ToolboxCommon"); + + gtk_container_add(GTK_CONTAINER(hb), tb); + gtk_widget_show(GTK_WIDGET(tb)); + + sigc::connection* conn = new sigc::connection; + g_object_set_data(G_OBJECT(hb), "event_context_connection", conn); + + gpointer val = GINT_TO_POINTER(id); + g_object_set_data(G_OBJECT(hb), BAR_ID_KEY, val); + + return hb; +} + +GtkWidget *ToolboxFactory::createToolToolbox() +{ + auto tb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_name(tb, "ToolToolbox"); + gtk_box_set_homogeneous(GTK_BOX(tb), FALSE); + + return toolboxNewCommon( tb, BAR_TOOL, GTK_POS_TOP ); +} + +GtkWidget *ToolboxFactory::createAuxToolbox() +{ + auto tb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_name(tb, "AuxToolbox"); + gtk_box_set_homogeneous(GTK_BOX(tb), FALSE); + + return toolboxNewCommon( tb, BAR_AUX, GTK_POS_LEFT ); +} + +//#################################### +//# Commands Bar +//#################################### + +GtkWidget *ToolboxFactory::createCommandsToolbox() +{ + auto tb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_name(tb, "CommandsToolbox"); + gtk_box_set_homogeneous(GTK_BOX(tb), FALSE); + + return toolboxNewCommon( tb, BAR_COMMANDS, GTK_POS_LEFT ); +} + +GtkWidget *ToolboxFactory::createSnapToolbox() +{ + auto tb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_name(tb, "SnapToolbox"); + gtk_box_set_homogeneous(GTK_BOX(tb), FALSE); + + return toolboxNewCommon( tb, BAR_SNAP, GTK_POS_LEFT ); +} + +static GtkWidget* createCustomSlider( GtkAdjustment *adjustment, gdouble climbRate, guint digits, Inkscape::UI::Widget::UnitTracker *unit_tracker) +{ + auto adj = Glib::wrap(adjustment, true); + auto inkSpinner = new Inkscape::UI::Widget::SpinButton(adj, climbRate, digits); + inkSpinner->addUnitTracker(unit_tracker); + inkSpinner = Gtk::manage( inkSpinner ); + GtkWidget *widget = GTK_WIDGET( inkSpinner->gobj() ); + return widget; +} + +void ToolboxFactory::setToolboxDesktop(GtkWidget *toolbox, SPDesktop *desktop) +{ + sigc::connection *conn = static_cast<sigc::connection*>(g_object_get_data(G_OBJECT(toolbox), + "event_context_connection")); + + BarId id = static_cast<BarId>( GPOINTER_TO_INT(g_object_get_data(G_OBJECT(toolbox), BAR_ID_KEY)) ); + + SetupFunction setup_func = nullptr; + UpdateFunction update_func = nullptr; + + switch (id) { + case BAR_TOOL: + setup_func = setup_tool_toolbox; + update_func = update_tool_toolbox; + break; + + case BAR_AUX: + toolbox = gtk_bin_get_child(GTK_BIN(toolbox)); + setup_func = setup_aux_toolbox; + update_func = update_aux_toolbox; + break; + + case BAR_COMMANDS: + setup_func = setup_commands_toolbox; + update_func = update_commands_toolbox; + break; + + case BAR_SNAP: + setup_func = setup_snap_toolbox; + update_func = updateSnapToolbox; + break; + default: + g_warning("Unexpected toolbox id encountered."); + } + + gpointer ptr = g_object_get_data(G_OBJECT(toolbox), "desktop"); + SPDesktop *old_desktop = static_cast<SPDesktop*>(ptr); + + if (old_desktop) { + std::vector<Gtk::Widget*> children = Glib::wrap(GTK_CONTAINER(toolbox))->get_children(); + for ( auto i:children ) { + gtk_container_remove( GTK_CONTAINER(toolbox), i->gobj() ); + } + } + + g_object_set_data(G_OBJECT(toolbox), "desktop", (gpointer)desktop); + + if (desktop && setup_func && update_func) { + gtk_widget_set_sensitive(toolbox, TRUE); + setup_func(toolbox, desktop); + update_func(desktop, desktop->event_context, toolbox); + *conn = desktop->connectEventContextChanged(sigc::bind (sigc::ptr_fun(update_func), toolbox)); + } else { + gtk_widget_set_sensitive(toolbox, FALSE); + } + +} // end of sp_toolbox_set_desktop() + + +static void setupToolboxCommon( GtkWidget *toolbox, + SPDesktop *desktop, + gchar const *ui_file, + gchar const* toolbarName, + gchar const* sizePref ) +{ + Glib::RefPtr<Gtk::ActionGroup> mainActions = create_or_fetch_actions( desktop ); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + GtkUIManager* mgr = gtk_ui_manager_new(); + GError* err = nullptr; + + GtkOrientation orientation = GTK_ORIENTATION_HORIZONTAL; + + gtk_ui_manager_insert_action_group( mgr, mainActions->gobj(), 0 ); + + Glib::ustring filename = get_filename(UIS, ui_file); + gtk_ui_manager_add_ui_from_file( mgr, filename.c_str(), &err ); + if(err) { + g_warning("Failed to load %s: %s", filename.c_str(), err->message); + g_error_free(err); + return; + } + + GtkWidget* toolBar = gtk_ui_manager_get_widget( mgr, toolbarName ); + if ( prefs->getBool("/toolbox/icononly", true) ) { + gtk_toolbar_set_style( GTK_TOOLBAR(toolBar), GTK_TOOLBAR_ICONS ); + } + + GtkIconSize toolboxSize = ToolboxFactory::prefToSize(sizePref); + gtk_toolbar_set_icon_size( GTK_TOOLBAR(toolBar), static_cast<GtkIconSize>(toolboxSize) ); + + GtkPositionType pos = static_cast<GtkPositionType>(GPOINTER_TO_INT(g_object_get_data( G_OBJECT(toolbox), HANDLE_POS_MARK ))); + orientation = ((pos == GTK_POS_LEFT) || (pos == GTK_POS_RIGHT)) ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL; + gtk_orientable_set_orientation (GTK_ORIENTABLE(toolBar), orientation); + gtk_toolbar_set_show_arrow(GTK_TOOLBAR(toolBar), TRUE); + + g_object_set_data(G_OBJECT(toolBar), "desktop", nullptr); + + GtkWidget* child = gtk_bin_get_child(GTK_BIN(toolbox)); + if ( child ) { + gtk_container_remove( GTK_CONTAINER(toolbox), child ); + } + + gtk_container_add( GTK_CONTAINER(toolbox), toolBar ); +} + +#define noDUMP_DETAILS 1 + +void ToolboxFactory::setOrientation(GtkWidget* toolbox, GtkOrientation orientation) +{ +#if DUMP_DETAILS + g_message("Set orientation for %p to be %d", toolbox, orientation); + GType type = G_OBJECT_TYPE(toolbox); + g_message(" [%s]", g_type_name(type)); + g_message(" %p", g_object_get_data(G_OBJECT(toolbox), BAR_ID_KEY)); +#endif + + GtkPositionType pos = (orientation == GTK_ORIENTATION_HORIZONTAL) ? GTK_POS_LEFT : GTK_POS_TOP; + + if (GTK_IS_BIN(toolbox)) { +#if DUMP_DETAILS + g_message(" is a BIN"); +#endif // DUMP_DETAILS + GtkWidget* child = gtk_bin_get_child(GTK_BIN(toolbox)); + if (child) { +#if DUMP_DETAILS + GType type2 = G_OBJECT_TYPE(child); + g_message(" child [%s]", g_type_name(type2)); +#endif // DUMP_DETAILS + + if (GTK_IS_BOX(child)) { +#if DUMP_DETAILS + g_message(" is a BOX"); +#endif // DUMP_DETAILS + + std::vector<Gtk::Widget*> children = Glib::wrap(GTK_CONTAINER(child))->get_children(); + if (!children.empty()) { + for (auto curr:children) { + GtkWidget* child2 = curr->gobj(); +#if DUMP_DETAILS + GType type3 = G_OBJECT_TYPE(child2); + g_message(" child2 [%s]", g_type_name(type3)); +#endif // DUMP_DETAILS + + if (GTK_IS_CONTAINER(child2)) { + std::vector<Gtk::Widget*> children2 = Glib::wrap(GTK_CONTAINER(child2))->get_children(); + if (!children2.empty()) { + for (auto curr2:children2) { + GtkWidget* child3 = curr2->gobj(); +#if DUMP_DETAILS + GType type4 = G_OBJECT_TYPE(child3); + g_message(" child3 [%s]", g_type_name(type4)); +#endif // DUMP_DETAILS + if (GTK_IS_TOOLBAR(child3)) { + GtkToolbar* childBar = GTK_TOOLBAR(child3); + gtk_orientable_set_orientation(GTK_ORIENTABLE(childBar), orientation); + } + } + } + } + + + if (GTK_IS_TOOLBAR(child2)) { + GtkToolbar* childBar = GTK_TOOLBAR(child2); + gtk_orientable_set_orientation(GTK_ORIENTABLE(childBar), orientation); + } else { + g_message("need to add dynamic switch"); + } + } + } else { + // The call is being made before the toolbox proper has been setup. + g_object_set_data(G_OBJECT(toolbox), HANDLE_POS_MARK, GINT_TO_POINTER(pos)); + } + } else if (GTK_IS_TOOLBAR(child)) { + GtkToolbar* toolbar = GTK_TOOLBAR(child); + gtk_orientable_set_orientation( GTK_ORIENTABLE(toolbar), orientation ); + } + } + } +} + +void setup_tool_toolbox(GtkWidget *toolbox, SPDesktop *desktop) +{ + setupToolboxCommon(toolbox, desktop, "toolbar-tool.ui", "/ui/ToolToolbar", "/toolbox/tools/small"); +} + +void update_tool_toolbox( SPDesktop *desktop, ToolBase *eventcontext, GtkWidget * /*toolbox*/ ) +{ + gchar const *const tname = ( eventcontext + ? eventcontext->getPrefsPath().c_str() //g_type_name(G_OBJECT_TYPE(eventcontext)) + : nullptr ); + Glib::RefPtr<Gtk::ActionGroup> mainActions = create_or_fetch_actions( desktop ); + + for (int i = 0 ; tools[i].type_name ; i++ ) { + Glib::RefPtr<Gtk::Action> act = mainActions->get_action( Inkscape::Verb::get(tools[i].verb)->get_id() ); + if ( act ) { + bool setActive = tname && !strcmp(tname, tools[i].type_name); + Glib::RefPtr<VerbAction> verbAct = Glib::RefPtr<VerbAction>::cast_dynamic(act); + if ( verbAct ) { + verbAct->set_active(setActive); + } + } + } +} + +/** + * \brief Generate the auxiliary toolbox + * + * \details This is the one that appears below the main menu, and contains + * tool-specific toolbars. Each toolbar is created here, using + * its "create" method. + * + * The actual method used for each toolbar is specified in the + * "aux_toolboxes" array, defined above. + */ +void setup_aux_toolbox(GtkWidget *toolbox, SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Loop through all the toolboxes and create them using either + // their "create" methods. + for (int i = 0 ; aux_toolboxes[i].type_name ; i++ ) { + if (aux_toolboxes[i].create_func) { + GtkWidget *sub_toolbox = aux_toolboxes[i].create_func(desktop); + gtk_widget_set_name( sub_toolbox, "SubToolBox" ); + + auto holder = gtk_grid_new(); + gtk_grid_attach(GTK_GRID(holder), sub_toolbox, 0, 0, 1, 1); + + // This part is just for styling + if ( prefs->getBool( "/toolbox/icononly", true) ) { + gtk_toolbar_set_style( GTK_TOOLBAR(sub_toolbox), GTK_TOOLBAR_ICONS ); + } + + GtkIconSize toolboxSize = ToolboxFactory::prefToSize("/toolbox/small"); + gtk_toolbar_set_icon_size( GTK_TOOLBAR(sub_toolbox), static_cast<GtkIconSize>(toolboxSize) ); + gtk_widget_set_hexpand(sub_toolbox, TRUE); + + // Add a swatch widget if one was specified + if ( aux_toolboxes[i].swatch_verb_id != SP_VERB_INVALID ) { + auto swatch = new Inkscape::UI::Widget::StyleSwatch( nullptr, _(aux_toolboxes[i].swatch_tip) ); + swatch->setDesktop( desktop ); + swatch->setClickVerb( aux_toolboxes[i].swatch_verb_id ); + swatch->setWatchedTool( aux_toolboxes[i].swatch_tool, true ); + swatch->set_margin_start(AUX_BETWEEN_BUTTON_GROUPS); + swatch->set_margin_end(AUX_BETWEEN_BUTTON_GROUPS); + swatch->set_margin_top(AUX_SPACING); + swatch->set_margin_bottom(AUX_SPACING); + + auto swatch_ = GTK_WIDGET( swatch->gobj() ); + gtk_grid_attach( GTK_GRID(holder), swatch_, 1, 0, 1, 1); + } + + // Add the new toolbar into the toolbox (i.e., make it the visible toolbar) + // and also store a pointer to it inside the toolbox. This allows the + // active toolbar to be changed. + gtk_container_add(GTK_CONTAINER(toolbox), holder); + gtk_widget_set_name( holder, aux_toolboxes[i].ui_name ); + + // TODO: We could make the toolbox a custom subclass of GtkEventBox + // so that we can store a list of toolbars, rather than using + // GObject data + g_object_set_data(G_OBJECT(toolbox), aux_toolboxes[i].data_name, holder); + gtk_widget_show(sub_toolbox); + gtk_widget_show(holder); + } else if (aux_toolboxes[i].swatch_verb_id != SP_VERB_NONE) { + g_warning("Could not create toolbox %s", aux_toolboxes[i].ui_name); + } + } +} + +void update_aux_toolbox(SPDesktop * /*desktop*/, ToolBase *eventcontext, GtkWidget *toolbox) +{ + gchar const *tname = ( eventcontext + ? eventcontext->getPrefsPath().c_str() //g_type_name(G_OBJECT_TYPE(eventcontext)) + : nullptr ); + for (int i = 0 ; aux_toolboxes[i].type_name ; i++ ) { + GtkWidget *sub_toolbox = GTK_WIDGET(g_object_get_data(G_OBJECT(toolbox), aux_toolboxes[i].data_name)); + if (tname && !strcmp(tname, aux_toolboxes[i].type_name)) { + gtk_widget_show_now(sub_toolbox); + g_object_set_data(G_OBJECT(toolbox), "shows", sub_toolbox); + } else { + gtk_widget_hide(sub_toolbox); + } + //FIX issue #Inkscape686 + GtkAllocation allocation; + gtk_widget_get_allocation(sub_toolbox, &allocation); + gtk_widget_size_allocate(sub_toolbox, &allocation); + } + //FIX issue #Inkscape125 + GtkAllocation allocation; + gtk_widget_get_allocation(toolbox, &allocation); + gtk_widget_size_allocate(toolbox, &allocation); +} + +void setup_commands_toolbox(GtkWidget *toolbox, SPDesktop *desktop) +{ + setupToolboxCommon(toolbox, desktop, "toolbar-commands.ui", "/ui/CommandsToolbar", "/toolbox/small"); +} + +void update_commands_toolbox(SPDesktop * /*desktop*/, ToolBase * /*eventcontext*/, GtkWidget * /*toolbox*/) +{ +} + +void setup_snap_toolbox(GtkWidget *toolbox, SPDesktop *desktop) +{ + Glib::ustring sizePref("/toolbox/secondary"); + auto toolBar = Inkscape::UI::Toolbar::SnapToolbar::create(desktop); + auto prefs = Inkscape::Preferences::get(); + + if ( prefs->getBool("/toolbox/icononly", true) ) { + gtk_toolbar_set_style( GTK_TOOLBAR(toolBar), GTK_TOOLBAR_ICONS ); + } + + GtkIconSize toolboxSize = ToolboxFactory::prefToSize(sizePref.c_str()); + gtk_toolbar_set_icon_size( GTK_TOOLBAR(toolBar), static_cast<GtkIconSize>(toolboxSize) ); + + GtkPositionType pos = static_cast<GtkPositionType>(GPOINTER_TO_INT(g_object_get_data( G_OBJECT(toolbox), HANDLE_POS_MARK ))); + auto orientation = ((pos == GTK_POS_LEFT) || (pos == GTK_POS_RIGHT)) ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL; + gtk_orientable_set_orientation (GTK_ORIENTABLE(toolBar), orientation); + gtk_toolbar_set_show_arrow(GTK_TOOLBAR(toolBar), TRUE); + + GtkWidget* child = gtk_bin_get_child(GTK_BIN(toolbox)); + if ( child ) { + gtk_container_remove( GTK_CONTAINER(toolbox), child ); + } + + gtk_container_add( GTK_CONTAINER(toolbox), toolBar ); +} + +Glib::ustring ToolboxFactory::getToolboxName(GtkWidget* toolbox) +{ + Glib::ustring name; + BarId id = static_cast<BarId>( GPOINTER_TO_INT(g_object_get_data(G_OBJECT(toolbox), BAR_ID_KEY)) ); + switch(id) { + case BAR_TOOL: + name = "ToolToolbar"; + break; + case BAR_AUX: + name = "AuxToolbar"; + break; + case BAR_COMMANDS: + name = "CommandsToolbar"; + break; + case BAR_SNAP: + name = "SnapToolbar"; + break; + } + + return name; +} + +void ToolboxFactory::updateSnapToolbox(SPDesktop *desktop, ToolBase * /*eventcontext*/, GtkWidget *toolbox) +{ + auto tb = dynamic_cast<Inkscape::UI::Toolbar::SnapToolbar*>(Glib::wrap(GTK_TOOLBAR(gtk_bin_get_child(GTK_BIN(toolbox))))); + + if (!tb) { + std::cerr << "Can't get snap toolbar" << std::endl; + return; + } + + Inkscape::UI::Toolbar::SnapToolbar::update(tb); +} + +void ToolboxFactory::showAuxToolbox(GtkWidget *toolbox_toplevel) +{ + gtk_widget_show(toolbox_toplevel); + GtkWidget *toolbox = gtk_bin_get_child(GTK_BIN(toolbox_toplevel)); + + GtkWidget *shown_toolbox = GTK_WIDGET(g_object_get_data(G_OBJECT(toolbox), "shows")); + if (!shown_toolbox) { + return; + } + gtk_widget_show(toolbox); +} + +#define MODE_LABEL_WIDTH 70 + + +/* + 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 : diff --git a/src/widgets/toolbox.h b/src/widgets/toolbox.h new file mode 100644 index 0000000..3a1bb4c --- /dev/null +++ b/src/widgets/toolbox.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_TOOLBOX_H +#define SEEN_TOOLBOX_H + +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/ustring.h> + +#include "preferences.h" + +#define TOOLBAR_SLIDER_HINT "compact" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ToolBase; + +} +} +} + +namespace Inkscape { +namespace UI { + +namespace Widget { + class UnitTracker; +} + +/** + * Main toolbox source. + */ +class ToolboxFactory +{ +public: + static void setToolboxDesktop(GtkWidget *toolbox, SPDesktop *desktop); + static void setOrientation(GtkWidget* toolbox, GtkOrientation orientation); + static void showAuxToolbox(GtkWidget* toolbox); + + static GtkWidget *createToolToolbox(); + static GtkWidget *createAuxToolbox(); + static GtkWidget *createCommandsToolbox(); + static GtkWidget *createSnapToolbox(); + + + static Glib::ustring getToolboxName(GtkWidget* toolbox); + + static void updateSnapToolbox(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *eventcontext, GtkWidget *toolbox); + + static GtkIconSize prefToSize(Glib::ustring const &path, int base = 0 ); + static Gtk::IconSize prefToSize_mm(Glib::ustring const &path, int base = 0); + + ToolboxFactory() = delete; +}; + + + +} // namespace UI +} // namespace Inkscape + +#endif /* !SEEN_TOOLBOX_H */ + +/* + 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 : diff --git a/src/widgets/widget-sizes.h b/src/widgets/widget-sizes.h new file mode 100644 index 0000000..8dd8c6c --- /dev/null +++ b/src/widgets/widget-sizes.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2016 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +// #define TOOL_BUTTON_SIZE 28 + +// GTK uses 24 for icon sizes by default. Spacing adjust to keep the +// toolbar the same as other GTK applications. If we want that, use +// these defines instead: +//#define AUX_BUTTON_SIZE 24 +//#define AUX_SPACING 2 + +// #define AUX_BUTTON_SIZE 20 +#define AUX_SPACING 3 + +#define AUX_BETWEEN_BUTTON_GROUPS 7 +#define AUX_BETWEEN_SPINBUTTONS 0 +#define AUX_SPINBUTTON_WIDTH 62 +#define AUX_SPINBUTTON_WIDTH_SMALL 56 +#define AUX_SPINBUTTON_HEIGHT 20 +#define AUX_OPTION_MENU_WIDTH 55 +#define AUX_OPTION_MENU_HEIGHT 20 +#define AUX_MENU_ITEM_WIDTH 32 +#define AUX_MENU_ITEM_HEIGHT 18 + +#define SPIN_STEP 0.1 +#define SPIN_PAGE_STEP 5.0 + +#define BOTTOM_BAR_HEIGHT 20 +#define BOTTOM_BUTTON_SIZE 14 + +#define STATUS_BAR_FONT_SIZE 10000 + +#define STATUS_ZOOM_WIDTH 57 +#define STATUS_ROTATION_WIDTH 57 + +#define SELECTED_STYLE_SB_WIDTH 48 +#define SELECTED_STYLE_WIDTH 190 +#define STYLE_SWATCH_WIDTH 135 + +#define STATUS_LAYER_FONT_SIZE 7700 + +/* + 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 : |