diff options
Diffstat (limited to 'src/widgets')
-rw-r--r-- | src/widgets/CMakeLists.txt | 22 | ||||
-rw-r--r-- | src/widgets/README | 10 | ||||
-rw-r--r-- | src/widgets/desktop-widget.cpp | 2012 | ||||
-rw-r--r-- | src/widgets/desktop-widget.h | 265 | ||||
-rw-r--r-- | src/widgets/ege-paint-def.cpp | 316 | ||||
-rw-r--r-- | src/widgets/ege-paint-def.h | 112 | ||||
-rw-r--r-- | src/widgets/mappings.xml | 332 | ||||
-rw-r--r-- | src/widgets/sp-attribute-widget.cpp | 301 | ||||
-rw-r--r-- | src/widgets/sp-attribute-widget.h | 172 | ||||
-rw-r--r-- | src/widgets/sp-xmlview-tree.cpp | 870 | ||||
-rw-r--r-- | src/widgets/sp-xmlview-tree.h | 68 | ||||
-rw-r--r-- | src/widgets/spw-utilities.cpp | 216 | ||||
-rw-r--r-- | src/widgets/spw-utilities.h | 61 | ||||
-rw-r--r-- | src/widgets/style-utils.h | 35 | ||||
-rw-r--r-- | src/widgets/toolbox.cpp | 640 | ||||
-rw-r--r-- | src/widgets/toolbox.h | 79 | ||||
-rw-r--r-- | src/widgets/widget-sizes.h | 38 |
17 files changed, 5549 insertions, 0 deletions
diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt new file mode 100644 index 0000000..0855b5a --- /dev/null +++ b/src/widgets/CMakeLists.txt @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(widgets_SRC + desktop-widget.cpp + ege-paint-def.cpp + sp-attribute-widget.cpp + sp-xmlview-tree.cpp + spw-utilities.cpp + toolbox.cpp + + # ------- + # Headers + desktop-widget.h + ege-paint-def.h + sp-attribute-widget.h + sp-xmlview-tree.h + spw-utilities.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..7cfa223 --- /dev/null +++ b/src/widgets/desktop-widget.cpp @@ -0,0 +1,2012 @@ +// 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 "enums.h" +#include "file.h" +#include "inkscape-application.h" +#include "inkscape-window.h" +#include "inkscape-version.h" + +#include "display/control/canvas-axonomgrid.h" +#include "display/control/canvas-item-drawing.h" +#include "display/control/canvas-item-guideline.h" + +#include "extension/db.h" + +#include "object/sp-image.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" + +#include "ui/shortcuts.h" +#include "ui/dialog/swatches.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/monitor.h" // Monitor aspect ratio +#include "ui/dialog/dialog-container.h" +#include "ui/dialog/dialog-multipaned.h" +#include "ui/dialog/dialog-window.h" +#include "ui/tools/box3d-tool.h" +#include "ui/tools/text-tool.h" +#include "ui/util.h" +#include "ui/widget/canvas.h" +#include "ui/widget/canvas-grid.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/ink-ruler.h" +#include "ui/widget/layer-selector.h" +#include "ui/widget/page-selector.h" +#include "ui/widget/selected-style.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" +#include "ui/themes.h" + +#include "util/units.h" + +// We're in the "widgets" directory, so no need to explicitly prefix these: +#include "spw-utilities.h" +#include "toolbox.h" +#include "widget-sizes.h" + +#include "ui/widget/color-palette.h" +#include "ui/widget/preview.h" +#include "ui/dialog/color-item.h" +#include "widgets/ege-paint-def.h" + +using Inkscape::DocumentUndo; +using Inkscape::UI::Dialog::DialogContainer; +using Inkscape::UI::Dialog::DialogMultipaned; +using Inkscape::UI::Dialog::DialogWindow; +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::UI::ToolboxFactory; +using Inkscape::Util::unit_table; + + +//--------------------------------------------------------------------- +/* SPDesktopWidget */ + +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; +}; + +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 ); +} + +void CMSPrefWatcher::_refreshAll() +{ + for (auto & it : _widget_list) { + it->requestCanvasUpdate(); + } +} + +void CMSPrefWatcher::_setCmsSensitive(bool enabled) +{ + for ( auto dtw : _widget_list ) { + auto cms_adj = dtw->get_canvas_grid()->GetCmsAdjust(); + if ( cms_adj->get_sensitive() != enabled ) { + dtw->cms_adjust_set_sensitive(enabled); + } + } +} + +static CMSPrefWatcher* watcher = nullptr; + +SPDesktopWidget::SPDesktopWidget(InkscapeWindow* inkscape_window) + : window (inkscape_window) +{ + auto *const dtw = this; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + /* Main table */ + dtw->_vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + dtw->_vbox->set_name("DesktopMainTable"); + dtw->add(*dtw->_vbox); + + /* Status bar */ + dtw->_statusbar = Gtk::manage(new Gtk::Box()); + dtw->_statusbar->set_name("DesktopStatusBar"); + dtw->_vbox->pack_end(*dtw->_statusbar, false, true); + + /* Swatch Bar */ + dtw->_panels = Gtk::manage(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->_tbbox = Gtk::manage(new Gtk::Paned(Gtk::ORIENTATION_HORIZONTAL)); + dtw->_tbbox->set_name("ToolboxCanvasPaned"); + dtw->_hbox->pack_start(*dtw->_tbbox, true, true); + + dtw->_vbox->pack_end(*dtw->_hbox, true, true); + + dtw->_top_toolbars = Gtk::make_managed<Gtk::Grid>(); + dtw->_top_toolbars->set_name("TopToolbars"); + dtw->_vbox->pack_end(*dtw->_top_toolbars, false, true); + + /* Toolboxes */ + dtw->aux_toolbox = ToolboxFactory::createAuxToolbox(); + + dtw->snap_toolbox = ToolboxFactory::createSnapToolbox(); + ToolboxFactory::setOrientation(dtw->snap_toolbox, GTK_ORIENTATION_HORIZONTAL); + dtw->_top_toolbars->attach(*Glib::wrap(dtw->snap_toolbox), 1, 0, 1, 2); + + dtw->commands_toolbox = ToolboxFactory::createCommandsToolbox(); + auto cmd = Glib::wrap(dtw->commands_toolbox); + dtw->_top_toolbars->attach(*cmd, 0, 0); + + dtw->_top_toolbars->attach(*Glib::wrap(dtw->aux_toolbox), 0, 1); + + dtw->tool_toolbox = ToolboxFactory::createToolToolbox(inkscape_window); + ToolboxFactory::setOrientation( dtw->tool_toolbox, GTK_ORIENTATION_VERTICAL ); + dtw->_tbbox->pack1(*Glib::wrap(dtw->tool_toolbox), false, true); + auto tbox_width = prefs->getEntry("/toolbox/tools/width"); + if (tbox_width.isValid()) { + _tbbox->set_position(tbox_width.getIntLimited(32, 8, 500)); + } + + auto set_visible_buttons = [=](GtkWidget* tb) { + int buttons_before_separator = 0; + Gtk::Widget* last_sep = nullptr; + Gtk::FlowBox* last_box = nullptr; + sp_traverse_widget_tree(Glib::wrap(tb), [&](Gtk::Widget* widget) { + if (auto flowbox = dynamic_cast<Gtk::FlowBox*>(widget)) { + flowbox->show(); + flowbox->set_no_show_all(); + flowbox->set_max_children_per_line(1); + last_box = flowbox; + } + else if (auto btn = dynamic_cast<Gtk::Button*>(widget)) { + auto name = sp_get_action_target(widget); + auto show = prefs->getBool(ToolboxFactory::get_tool_visible_buttons_path(name), true); + auto parent = btn->get_parent(); + if (show) { + parent->show(); + ++buttons_before_separator; + // keep the max_children up to date improves display. + last_box->set_max_children_per_line(buttons_before_separator); + last_sep = nullptr; + } + else { + parent->hide(); + } + } + else if (auto sep = dynamic_cast<Gtk::Separator*>(widget)) { + if (buttons_before_separator <= 0) { + sep->hide(); + } + else { + sep->show(); + buttons_before_separator = 0; + last_sep = sep; + } + } + return false; + }); + if (last_sep) { + // hide trailing separator + last_sep->hide(); + } + }; + auto set_toolbar_prefs = [=]() { + int min = ToolboxFactory::min_pixel_size; + int max = ToolboxFactory::max_pixel_size; + int s = prefs->getIntLimited(ToolboxFactory::tools_icon_size, min, min, max); + ToolboxFactory::set_icon_size(tool_toolbox, s); + }; + + // watch for changes + _tb_icon_sizes1 = prefs->createObserver(ToolboxFactory::tools_icon_size, [=]() { set_toolbar_prefs(); }); + _tb_icon_sizes2 = prefs->createObserver(ToolboxFactory::ctrlbars_icon_size, [=]() { apply_ctrlbar_settings(); }); + _tb_visible_buttons = prefs->createObserver(ToolboxFactory::tools_visible_buttons, [=]() { set_visible_buttons(tool_toolbox); }); + + // restore preferences + set_toolbar_prefs(); + apply_ctrlbar_settings(); + set_visible_buttons(tool_toolbox); + + /* Canvas Grid (canvas, rulers, scrollbars, etc.) */ + // desktop widgets owns it + _canvas_grid = new Inkscape::UI::Widget::CanvasGrid(this); + + /* Canvas */ + dtw->_canvas = _canvas_grid->GetCanvas(); + + dtw->_canvas->set_cms_active(prefs->getBool("/options/displayprofile/enable")); + + /* Dialog Container */ + _container = Gtk::manage(new DialogContainer(inkscape_window)); + _columns = _container->get_columns(); + _columns->set_dropzone_sizes(2, -1); + dtw->_tbbox->pack2(*_container, true, true); + + _canvas_grid->set_hexpand(true); + _canvas_grid->set_vexpand(true); + _columns->append(_canvas_grid); + + // --------------- Status Tool Bar ------------------// + + // Selected Style (Fill/Stroke/Opacity) + dtw->_selected_style = Gtk::manage(new Inkscape::UI::Widget::SelectedStyle(true)); + dtw->_statusbar->pack_start(*dtw->_selected_style, false, false); + _selected_style->show_all(); + _selected_style->set_no_show_all(); + + // Layer Selector + _layer_selector = Gtk::manage(new Inkscape::UI::Widget::LayerSelector(nullptr)); + // separate layer selector buttons from status text + auto vseparator = Gtk::make_managed<Gtk::Separator>(Gtk::ORIENTATION_VERTICAL); + vseparator->set_margin_end(6); + vseparator->set_margin_top(6); + vseparator->set_margin_bottom(6); + _layer_selector->pack_end(*vseparator); + _layer_selector->show_all(); + _layer_selector->set_no_show_all(); + dtw->_statusbar->pack_start(*_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); + + dtw->_zoom_status_box = Gtk::make_managed<Gtk::Box>(); + // 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 Inkscape::UI::Widget::SpinButton(zoom_adj)); + + dtw->_zoom_status->set_defocus_widget(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)); + 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); + + dtw->_rotation_status_box = Gtk::make_managed<Gtk::Box>(); + dtw->_rotation_status_box->set_margin_start(10); + // Rotate status spinbutton --------------- + auto rotation_adj = Gtk::Adjustment::create(0, -360.0, 360.0, 1.0); + + dtw->_rotation_status = Gtk::manage(new Inkscape::UI::Widget::SpinButton(rotation_adj)); + + // FIXME: This is a bit of a hack, to avoid the ExpressionEvaluator struggling to parse the + // degree symbol. It would be better to improve ExpressionEvaluator so it copes + dtw->_rotation_status->set_dont_evaluate(true); + + dtw->_rotation_status->set_defocus_widget(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_output_connection = dtw->_rotation_status->signal_output().connect(sigc::mem_fun(dtw, &SPDesktopWidget::rotation_output)); + 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 "); + + // TRANSLATORS: Abbreviation for canvas zoom level + auto label_z = Gtk::manage(new Gtk::Label(C_("canvas", "Z:"))); + label_z->set_name("ZLabel"); + // TRANSLATORS: Abbreviation for canvas rotation + auto label_r = Gtk::manage(new Gtk::Label(C_("canvas", "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->show_all(); + dtw->_coord_status->set_no_show_all(); + + dtw->_zoom_status_box->pack_start(*label_z, true, true); + dtw->_zoom_status_box->pack_end(*dtw->_zoom_status, true, true); + dtw->_zoom_status_box->show_all(); + + dtw->_rotation_status_box->pack_start(*label_r, true, true); + dtw->_rotation_status_box->pack_end(*dtw->_rotation_status, true, true); + dtw->_rotation_status_box->show_all(); + dtw->_rotation_status_box->set_no_show_all(); + + dtw->_statusbar->pack_end(*dtw->_rotation_status_box, false, false); + dtw->_statusbar->pack_end(*dtw->_zoom_status_box, false, false); + dtw->_statusbar->pack_end(*dtw->_coord_status, false, false); + + update_statusbar_visibility(); + + _statusbar_preferences_observer = prefs->createObserver("/statusbar/visibility", [=]() { + update_statusbar_visibility(); + }); + + // --------------- Color Management ---------------- // + dtw->_tracker = ege_color_prof_tracker_new(GTK_WIDGET(_layer_selector->gobj())); + bool fromDisplay = prefs->getBool( "/options/displayprofile/from_display"); + if ( fromDisplay ) { + auto id = Inkscape::CMSSystem::getDisplayId(0); + dtw->_canvas->set_cms_key(id); + dtw->cms_adjust_set_sensitive(!id.empty()); + } + g_signal_connect( G_OBJECT(dtw->_tracker), "changed", G_CALLBACK(SPDesktopWidget::color_profile_event), dtw ); + + // ------------------ Finish Up -------------------- // + dtw->_vbox->show_all(); + dtw->_canvas_grid->ShowCommandPalette(false); + + dtw->_canvas->grab_focus(); +} + +void SPDesktopWidget::apply_ctrlbar_settings() { + Inkscape::Preferences* prefs = Inkscape::Preferences::get(); + int min = ToolboxFactory::min_pixel_size; + int max = ToolboxFactory::max_pixel_size; + int size = prefs->getIntLimited(ToolboxFactory::ctrlbars_icon_size, min, min, max); + ToolboxFactory::set_icon_size(snap_toolbox, size); + ToolboxFactory::set_icon_size(commands_toolbox, size); + ToolboxFactory::set_icon_size(aux_toolbox, size); +} + +void SPDesktopWidget::update_statusbar_visibility() { + auto prefs = Inkscape::Preferences::get(); + Glib::ustring path("/statusbar/visibility/"); + + _coord_status->set_visible(prefs->getBool(path + "coordinates", true)); + _rotation_status_box->set_visible(prefs->getBool(path + "rotation", true)); + _layer_selector->set_visible(prefs->getBool(path + "layer", true)); + _selected_style->set_visible(prefs->getBool(path + "style", true)); +} + +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 = _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); +} + +/** + * Called before SPDesktopWidget destruction. + * (Might be called more than once) + */ +void +SPDesktopWidget::on_unrealize() +{ + auto dtw = this; + + if (_tbbox) { + Inkscape::Preferences::get()->setInt("/toolbox/tools/width", _tbbox->get_position()); + } + + if (dtw->desktop) { + if ( watcher ) { + watcher->remove(dtw); + } + + for (auto &conn : dtw->_connections) { + conn.disconnect(); + } + + // Canvas + dtw->_canvas->set_drawing(nullptr); // Ensures deactivation + dtw->_canvas->set_desktop(nullptr); // Todo: Remove desktop dependency. + + // Zoom + dtw->_zoom_status_input_connection.disconnect(); + dtw->_zoom_status_output_connection.disconnect(); + g_signal_handlers_disconnect_by_data(G_OBJECT(dtw->_zoom_status->gobj()), dtw->_zoom_status->gobj()); + 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_by_data(G_OBJECT(dtw->_rotation_status->gobj()), dtw->_rotation_status->gobj()); + dtw->_rotation_status_value_changed_connection.disconnect(); + dtw->_rotation_status_populate_popup_connection.disconnect(); + + dtw->_panels->setDesktop(nullptr); + + delete _container; // will unrealize dtw->_canvas + + _layer_selector->setDesktop(nullptr); + 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; + } + + parent_type::on_unrealize(); +} + +SPDesktopWidget::~SPDesktopWidget() { + delete _canvas_grid; +} + +/** + * 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(); + auto namedview = doc->getNamedView(); + + std::string Name; + if (doc->isModifiedSinceSave()) { + Name += "*"; + } + + Name += uri; + + if (namedview->viewcount > 1) { + Name += ": "; + Name += std::to_string(namedview->viewcount); + } + Name += " ("; + + auto render_mode = desktop->getCanvas()->get_render_mode(); + auto color_mode = desktop->getCanvas()->get_color_mode(); + + if (render_mode == Inkscape::RenderMode::OUTLINE) { + Name += N_("outline"); + } else if (render_mode == Inkscape::RenderMode::NO_FILTERS) { + Name += N_("no filters"); + } else if (render_mode == Inkscape::RenderMode::VISIBLE_HAIRLINES) { + Name += N_("visible hairlines"); + } else if (render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) { + Name += N_("outline overlay"); + } + + if (color_mode != Inkscape::ColorMode::NORMAL && + render_mode != Inkscape::RenderMode::NORMAL) { + Name += ", "; + } + + if (color_mode == Inkscape::ColorMode::GRAYSCALE) { + Name += N_("grayscale"); + } else if (color_mode == 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); + } +} + +DialogContainer *SPDesktopWidget::getDialogContainer() +{ + return _container; +} + +/** + * Resize handler, keeps the desktop centered. + */ +void SPDesktopWidget::on_size_allocate(Gtk::Allocation &allocation) +{ + // This function is called a lot during mouse move events without + // resizing the widget. Desktop position/zoom must not be updated + // for these trivial invocations. + if (allocation == get_allocation()) { + parent_type::on_size_allocate(allocation); + return; + } + + Geom::Rect const d_canvas = _canvas->get_area_world(); + + parent_type::on_size_allocate(allocation); + + if (d_canvas.hasZeroArea()) { + return; + } + + Geom::Point const midpoint_dt = desktop->w2d(d_canvas.midpoint()); + double zoom = desktop->current_zoom(); + + if (_canvas_grid->GetStickyZoom()->get_active()) { + /* Calculate adjusted zoom */ + double oldshortside = d_canvas.minExtent(); + double newshortside = _canvas->get_area_world().minExtent(); + zoom *= newshortside / oldshortside; + } + + desktop->zoom_absolute(midpoint_dt, zoom, false); +} + +/** + * Callback to realize desktop widget. + */ +void SPDesktopWidget::on_realize() +{ + SPDesktopWidget *dtw = this; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + parent_type::on_realize(); + + 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::Container *window = get_toplevel(); + if (settings && window) { + g_object_get(settings, "gtk-theme-name", >kThemeName, nullptr); + g_object_get(settings, "gtk-application-prefer-dark-theme", >kApplicationPreferDarkTheme, nullptr); + bool dark = INKSCAPE.themecontext->isCurrentThemeDark(dynamic_cast<Gtk::Container *>(window)); + if (dark) { + prefs->setBool("/theme/darkTheme", true); + window->get_style_context()->add_class("dark"); + window->get_style_context()->remove_class("bright"); + } else { + prefs->setBool("/theme/darkTheme", false); + 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.themecontext->getChangeThemeSignal().emit(); + } +} + +/* 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 + dtw->_canvas->grab_focus(); + } + + if ((event->type == GDK_BUTTON_PRESS) && (event->button.button == 3)) { + if (event->button.state & GDK_SHIFT_MASK) { + dtw->desktop->getCanvasDrawing()->set_sticky(true); + } else { + dtw->desktop->getCanvasDrawing()->set_sticky(false); + } + } + + { + // 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->get_current_canvas_item()) { + return (gint)sp_desktop_root_handler (event, dtw->desktop); + } + } + + return FALSE; +} + +void +SPDesktopWidget::color_profile_event(EgeColorProfTracker */*tracker*/, SPDesktopWidget *dtw) +{ + // Handle profile changes + GdkWindow *window = dtw->get_window()->gobj(); + + // 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 ); + dtw->_canvas->set_cms_key(id); + dtw->requestCanvasUpdate(); + dtw->cms_adjust_set_sensitive(!id.empty()); +} + +void +SPDesktopWidget::update_guides_lock() +{ + bool down = _canvas_grid->GetGuideLock()->get_active(); + auto nv = desktop->getNamedView(); + bool lock = nv->getLockGuides(); + + if (down != lock) { + nv->toggleLockGuides(); + if (down) { + setMessage (Inkscape::NORMAL_MESSAGE, _("Locked all guides")); + } else { + setMessage (Inkscape::NORMAL_MESSAGE, _("Unlocked all guides")); + } + } +} + +void +SPDesktopWidget::cms_adjust_toggled() +{ + auto _cms_adjust = _canvas_grid->GetCmsAdjust(); + + bool down = _cms_adjust->get_active(); + if ( down != _canvas->get_cms_active() ) { + _canvas->set_cms_active(down); + desktop->redrawDesktop(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/displayprofile/enable", down); + if (down) { + setMessage (Inkscape::NORMAL_MESSAGE, _("Color-managed display is <b>enabled</b> in this window")); + } else { + setMessage (Inkscape::NORMAL_MESSAGE, _("Color-managed display is <b>disabled</b> in this window")); + } + } +} + +void +SPDesktopWidget::cms_adjust_set_sensitive(bool enabled) +{ + _canvas_grid->GetCmsAdjust()->set_sensitive(enabled); +} + +/** + * \pre this->desktop->main != 0 + */ +void +SPDesktopWidget::requestCanvasUpdate() { + // ^^ also this->desktop != 0 + g_return_if_fail(this->desktop != nullptr); + desktop->getCanvas()->queue_draw(); +} + +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) { + this->set_sensitive(); + } +} + +void +SPDesktopWidget::disableInteraction() +{ + if (_interaction_disabled_counter == 0) { + this->set_sensitive(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); + // The get_positon is very unreliable (see Gtk docs) and will often return zero. + if (!x && !y) { + if (Glib::RefPtr<Gdk::Window> w = window->get_window()) { + Gdk::Rectangle rect; + w->get_frame_extents(rect); + x = rect.get_x(); + y = rect.get_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); + } +} + +/** + * \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()); + 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 == PREFS_DIALOGS_WINDOWS_AGGRESSIVE) + // without this, a transient window not always emerges on top + gtk_window_present (w); + } +} + +void +SPDesktopWidget::presentWindow() +{ + if (window) + window->present(); +} + +bool SPDesktopWidget::showInfoDialog( Glib::ustring const &message ) +{ + bool result = false; + 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->gobj()))); + 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->gobj()))); + if (GTK_IS_WINDOW(topw)) { + if (desktop->is_maximized()) { + gtk_window_unmaximize(topw); + } else { + gtk_window_maximize(topw); + } + } +} + +void +SPDesktopWidget::fullscreen() +{ + GtkWindow *topw = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(_canvas->gobj()))); + if (GTK_IS_WINDOW(topw)) { + if (desktop->is_fullscreen()) { + gtk_window_unfullscreen(topw); + // widget layout is triggered by the resulting window_state_event + } else { + 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. + * Also move command toolbar to top or side as required. + */ +void SPDesktopWidget::layoutWidgets() +{ + SPDesktopWidget *dtw = this; + Glib::ustring pref_root; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (desktop && desktop->is_focusMode()) { + pref_root = "/focus/"; + } else if (desktop && 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(); + } + + _canvas_grid->ShowScrollbars(prefs->getBool(pref_root + "scrollbars/state", true)); + _canvas_grid->ShowRulers( prefs->getBool(pref_root + "rulers/state", true)); + + // Move command toolbar as required. + + // If interface_mode unset, use screen aspect ratio. Needs to be synced with "canvas-interface-mode" action. + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_primary(); + double const width = monitor_geometry.get_width(); + double const height = monitor_geometry.get_height(); + bool widescreen = (height > 0 && width/height > 1.65); + widescreen = prefs->getInt(pref_root + "task/taskset", widescreen ? 2 : 0) == 2; // legacy + widescreen = prefs->getBool(pref_root + "interface_mode", widescreen); + + auto commands_toolbox_cpp = dynamic_cast<Gtk::Bin *>(Glib::wrap(commands_toolbox)); + if (commands_toolbox_cpp) { + + // Unlink command toolbar. + commands_toolbox_cpp->reference(); // So toolbox is not deleted. + auto parent = commands_toolbox_cpp->get_parent(); + parent->remove(*commands_toolbox_cpp); + + auto orientation = Gtk::ORIENTATION_HORIZONTAL; + auto orientation_c = GTK_ORIENTATION_HORIZONTAL; + // Link command toolbar back. + if (!widescreen) { + _top_toolbars->attach(*commands_toolbox_cpp, 0, 0); // Always first + gtk_box_set_child_packing(_vbox->gobj(), commands_toolbox, false, true, 0, GTK_PACK_START); // expand, fill, padding, pack_type + orientation = Gtk::ORIENTATION_HORIZONTAL; + orientation_c = GTK_ORIENTATION_HORIZONTAL; + commands_toolbox_cpp->set_hexpand(true); + } else { + _hbox->add(*commands_toolbox_cpp); + gtk_box_set_child_packing(_hbox->gobj(), commands_toolbox, false, true, 0, GTK_PACK_START); // expand, fill, padding, pack_type + orientation = Gtk::ORIENTATION_VERTICAL; + orientation_c = GTK_ORIENTATION_VERTICAL; + commands_toolbox_cpp->set_hexpand(false); + } + commands_toolbox_cpp->unreference(); + + auto box = dynamic_cast<Gtk::Box *>(commands_toolbox_cpp->get_child()); + if (box) { + box->set_orientation(orientation); + for (auto child : box->get_children()) { + if (auto toolbar = dynamic_cast<Gtk::Toolbar *>(child)) { + gtk_orientable_set_orientation(GTK_ORIENTABLE(toolbar->gobj()), orientation_c); + //toolbar->set_orientation(orientation); // Missing in C++ interface! + } + } + } + } else { + std::cerr << "SPDesktopWidget::layoutWidgets(): Wrong widget type for command toolbar!" << std::endl; + } + + // Temporary for Gtk3: Gtk toolbar resets icon sizes, so reapply them. + // TODO: remove this call in Gtk4 after Gtk::Toolbar is eliminated. + apply_ctrlbar_settings(); + + auto& snap = *Glib::wrap(snap_toolbox); + auto& aux = *Glib::wrap(aux_toolbox); + + // This ensures that the Snap toolbox is on the top and only takes the needed space. + if (_top_toolbars->get_children().size() == 3 && gtk_widget_get_visible(commands_toolbox)) { + _top_toolbars->child_property_height(snap) = 1; + _top_toolbars->child_property_width(aux) = 2; + snap.set_valign(Gtk::ALIGN_START); + } + else { + _top_toolbars->child_property_width(aux) = 1; + _top_toolbars->child_property_height(snap) = 2; + snap.set_valign(Gtk::ALIGN_CENTER); + } + + Inkscape::UI::resize_widget_children(_top_toolbars); +} + +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; + auto thing = sp_search_by_name_recursive(Glib::wrap(aux_toolbox), id); + + // The toolbutton could be a few different types so try casting to + // each of them. + // TODO: This will be simpler in Gtk+ 4 when ToolItems have gone + auto toggle_button = dynamic_cast<Gtk::ToggleButton *>(thing); + auto toggle_tool_button = dynamic_cast<Gtk::ToggleToolButton *>(thing); + + if ( !thing ) { + //g_message( "Unable to locate item for {%s}", id ); + } else if (toggle_button) { + isActive = toggle_button->get_active(); + } else if (toggle_tool_button) { + isActive = toggle_tool_button->get_active(); + } else { + //g_message( "Item for {%s} is of an unsupported type", id ); + } + + return isActive; +} + +SPDesktopWidget::SPDesktopWidget(InkscapeWindow *inkscape_window, SPDocument *document) + : SPDesktopWidget(inkscape_window) +{ + set_name("SPDesktopWidget"); + + SPDesktopWidget *dtw = this; + + SPNamedView *namedview = document->getNamedView(); + + dtw->_dt2r = 1. / namedview->display_units->factor; + + dtw->_ruler_origin = Geom::Point(0,0); //namedview->gridorigin; Why was the grid origin used here? + + // This section seems backwards! + dtw->desktop = new SPDesktop(); + dtw->desktop->init (namedview, dtw->_canvas, this); + dtw->_canvas->set_desktop(desktop); + 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->_canvas_grid->UpdateRulers(); + + dtw->setView(dtw->desktop); + + /* Listen on namedview modification */ + dtw->modified_connection = namedview->connectModified(sigc::mem_fun(*dtw, &SPDesktopWidget::namedviewModified)); + + _layer_selector->setDesktop(dtw->desktop); + + // We never want a page widget if there's no desktop. + _page_selector = Gtk::manage(new Inkscape::UI::Widget::PageSelector(desktop)); + _statusbar->pack_end(*_page_selector, false, false); + + ToolboxFactory::setToolboxDesktop(dtw->aux_toolbox, dtw->desktop); + + dtw->layoutWidgets(); + + dtw->_panels->setDesktop(dtw->desktop); +} + + +void +SPDesktopWidget::update_rulers() +{ + _canvas_grid->UpdateRulers(); +} + + +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? + + _canvas_grid->GetVRuler()->set_unit(nv->getDisplayUnit()); + _canvas_grid->GetHRuler()->set_unit(nv->getDisplayUnit()); + _canvas_grid->GetVRuler()->set_tooltip_text(gettext(nv->display_units->name_plural.c_str())); + _canvas_grid->GetHRuler()->set_tooltip_text(gettext(nv->display_units->name_plural.c_str())); + _canvas_grid->UpdateRulers(); + + /* This loops through all the grandchildren of aux toolbox, + * and for each that it finds, it performs an sp_search_by_name_recursive(), + * looking for widgets named "unit-tracker" (this is used by + * all toolboxes to refer to the unit selector). The default document units + * is then selected within these unit selectors. + * + * 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 (auto container = dynamic_cast<Gtk::Container *>(i)) { + std::vector<Gtk::Widget*> grch = container->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" || name == "CalligraphicToolbar" ) + continue; + + auto tracker = dynamic_cast<Inkscape::UI::Widget::ComboToolItem*>(sp_search_by_name_recursive(j, "unit-tracker")); + + if (tracker) { // it's null when inkscape is first opened + if (auto ptr = static_cast<UnitTracker*>(tracker->get_data(Glib::Quark("unit-tracker")))) { + ptr->setActiveUnit(nv->display_units); + } + } + } // grandchildren + } // if child is a container + } // children + } // if aux_toolbox is a container + } +} + +void +SPDesktopWidget::on_adjustment_value_changed() +{ + if (update) + return; + + update = true; + + // Do not call canvas->scrollTo directly... messes up 'offset'. + desktop->scroll_absolute( Geom::Point(_canvas_grid->GetHAdj()->get_value(), + _canvas_grid->GetVAdj()->get_value()), false); + + update = false; +} + +/* 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) +{ + double new_typed = g_strtod (_zoom_status->get_text().c_str(), nullptr); + *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() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double const zoom_factor = pow (2, _zoom_status->get_value()); + + // Zoom around center of window + Geom::Rect const d_canvas = _canvas->get_area_world(); + Geom::Point midpoint = desktop->w2d(d_canvas.midpoint()); + + _zoom_status_value_changed_connection.block(); + if(prefs->getDouble("/options/zoomcorrection/shown", true)) { + desktop->zoom_realworld(midpoint, zoom_factor); + } else { + desktop->zoom_absolute(midpoint, zoom_factor); + } + _zoom_status_value_changed_connection.unblock(); + _zoom_status->defocus(); +} + +void +SPDesktopWidget::zoom_menu_handler(double factor) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if(prefs->getDouble("/options/zoomcorrection/shown", true)) { + desktop->zoom_realworld(desktop->current_center(), factor); + } else { + desktop->zoom_absolute(desktop->current_center(), factor, false); + } +} + + + +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([=]() { desktop->getDocument()->getPageManager().zoomToSelectedPage(desktop); }); + 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([=]() { desktop->getDocument()->getPageManager().centerToSelectedPage(desktop); }); + menu->append(*item_center_page); + + menu->show_all(); +} + + +void +SPDesktopWidget::sticky_zoom_toggled() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/stickyzoom/value", _canvas_grid->GetStickyZoom()->get_active()); +} + + +void +SPDesktopWidget::update_zoom() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // It's very important that the value used in set_value is the same as the one + // set as it otherwise creates an infinate loop between the spin button and update_zoom + double correction = 1.0; + if(prefs->getDouble("/options/zoomcorrection/shown", true)) { + correction = prefs->getDouble("/options/zoomcorrection/value", 1.0); + } + _zoom_status_value_changed_connection.block(); + _zoom_status->set_value(log(desktop->current_zoom() / correction) / log(2)); + _zoom_status->queue_draw(); + _zoom_status_value_changed_connection.unblock(); +} + + +// ---------------------- Rotation ------------------------ + +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 = _canvas->get_area_world(); + _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(); + + _rotation_status->defocus(); +} + +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_command_palette() { + // TODO: Turn into action and remove this function. + _canvas_grid->ToggleCommandPalette(); +} + +void +SPDesktopWidget::toggle_rulers() +{ + // TODO: Turn into action and remove this function. + _canvas_grid->ToggleRulers(); +} + +void +SPDesktopWidget::toggle_scrollbars() +{ + // TODO: Turn into action and remove this function. + _canvas_grid->ToggleScrollbars(); +} + +bool +SPDesktopWidget::get_color_prof_adj_enabled() const +{ + auto _cms_adjust = _canvas_grid->GetCmsAdjust(); + return _cms_adjust->get_sensitive() && _cms_adjust->get_active(); +} + +void +SPDesktopWidget::toggle_color_prof_adj() +{ + auto _cms_adjust = _canvas_grid->GetCmsAdjust(); + if (_cms_adjust->get_sensitive()) { + bool active = _cms_adjust->get_active(); + _cms_adjust->set_active(!active); + } +} + +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 = true; + + /* The desktop region we always show unconditionally */ + SPDocument *doc = desktop->doc(); + + auto deskarea = doc->preferredBounds(); + deskarea->expandBy(doc->getDimensions()); // Double size + + /* The total size of pages should be added unconditionally */ + deskarea->unionWith(doc->getPageManager().getDesktopRect()); + + if (Inkscape::Preferences::get()->getInt("/tools/bounding_box") == 0) { + deskarea->unionWith(doc->getRoot()->desktopVisualBounds()); + } else { + deskarea->unionWith(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->get_area_world(); + + /* Viewbox is always included into scrollable region */ + carea = Geom::unify(carea, viewbox); + + auto _hadj = _canvas_grid->GetHAdj(); + auto _vadj = _canvas_grid->GetVAdj(); + 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 = false; +} + +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(widget), horiz); + break; + case GDK_MOTION_NOTIFY: + dtw->on_ruler_box_motion_notify_event(&event->motion, Glib::wrap(widget), horiz); + break; + case GDK_BUTTON_RELEASE: + dtw->on_ruler_box_button_release_event(&event->button, Glib::wrap(widget), horiz); + break; + default: + break; + } + + return FALSE; +} + +bool +SPDesktopWidget::on_ruler_box_motion_notify_event(GdkEventMotion *event, Gtk::Widget *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->gobj())); + + 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 event_w(_canvas->canvas_to_world(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->setShowGuides(true); + } + + Geom::Point normal = _normal; + if (!(event->state & GDK_SHIFT_MASK)) { + ruler_snap_new_guide(desktop, event_dt, normal); + } + _active_guide->set_normal(normal); + _active_guide->set_origin(event_dt); + + desktop->set_coordinate_status(event_dt); + } + + return false; +} + +// End guide creation or toggle guides on/off. +bool +SPDesktopWidget::on_ruler_box_button_release_event(GdkEventButton *event, Gtk::Widget *widget, bool horiz) +{ + int wx, wy; + + GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas->gobj())); + + 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) { + desktop->event_context->discard_delayed_snap_event(); + + auto seat = gdk_device_get_seat(event->device); + gdk_seat_ungrab(seat); + + Geom::Point const event_w(_canvas->canvas_to_world(event_win)); + Geom::Point event_dt(desktop->w2d(event_w)); + + Geom::Point normal = _normal; + if (!(event->state & GDK_SHIFT_MASK)) { + ruler_snap_new_guide(desktop, event_dt, normal); + } + + delete _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; + } + repr->setAttributePoint("position", Geom::Point( newx, newy )); + repr->setAttributePoint("orientation", normal); + desktop->namedview->appendChild(repr); + Inkscape::GC::release(repr); + DocumentUndo::done(desktop->getDocument(), _("Create guide"), ""); + } + desktop->set_coordinate_status(event_dt); + + if (!_ruler_dragged) { + // Ruler click (without drag) toggle the guide visibility on and off + desktop->namedview->toggleShowGuides(); + } + + _ruler_clicked = false; + _ruler_dragged = false; + } + + return false; +} + +// Start guide creation by dragging from ruler. +bool +SPDesktopWidget::on_ruler_box_button_press_event(GdkEventButton *event, Gtk::Widget *widget, bool horiz) +{ + if (_ruler_clicked) // event triggered on a double click: do no process the click + return false; + + int wx, wy; + + GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas->gobj())); + + 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(_canvas->canvas_to_world(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 = new Inkscape::CanvasItemGuideLine(desktop->getCanvasGuides(), Glib::ustring(), event_dt, _normal); + _active_guide->set_stroke(desktop->namedview->guidehicolor); + + // Ruler grabs all events until button release. + 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; +} + +void +SPDesktopWidget::ruler_snap_new_guide(SPDesktop *desktop, Geom::Point &event_dt, Geom::Point &normal) +{ + desktop->getCanvas()->grab_focus(); + 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.isTargetSnappable(Inkscape::SNAPTARGET_PATH_PERPENDICULAR); + bool pref_tang = m.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_PATH_TANGENTIAL); + m.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_PATH_PERPENDICULAR, false); + m.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_PATH_TANGENTIAL, 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.setTargetSnappable(Inkscape::SNAPTARGET_PATH_PERPENDICULAR, pref_perp); + m.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_PATH_TANGENTIAL, 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..347d8c7 --- /dev/null +++ b/src/widgets/desktop-widget.h @@ -0,0 +1,265 @@ +// 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 "preferences.h" +#include "ui/view/view-widget.h" +#include "preferences.h" + +#include <cstddef> +#include <sigc++/connection.h> +#include <2geom/point.h> + +// forward declaration +typedef struct _EgeColorProfTracker EgeColorProfTracker; + +class InkscapeWindow; +struct SPCanvasItem; +class SPDocument; +class SPDesktop; +struct SPDesktopWidget; +class SPObject; + +namespace Inkscape { + class CanvasItemGuideLine; +namespace UI { +namespace Dialog { +class DialogContainer; +class DialogMultipaned; +class SwatchesPanel; +} // namespace Dialog + +namespace Widget { + class Button; + class Canvas; + class CanvasGrid; + class LayerSelector; + class PageSelector; + class SelectedStyle; + class SpinButton; + class Ruler; +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#define SP_DESKTOP_WIDGET(o) dynamic_cast<SPDesktopWidget*>(o) +#define SP_IS_DESKTOP_WIDGET(o) bool(dynamic_cast<SPDesktopWidget const *>(o)) + +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); + +/// A GtkEventBox on an SPDesktop. +class SPDesktopWidget : public SPViewWidget { + using parent_type = SPViewWidget; + + SPDesktopWidget(InkscapeWindow *inkscape_window); + +public: + SPDesktopWidget(InkscapeWindow *inkscape_window, SPDocument *document); + ~SPDesktopWidget() override; + + Inkscape::UI::Widget::CanvasGrid *get_canvas_grid() { return _canvas_grid; } // Temp, I hope! + Inkscape::UI::Widget::Canvas *get_canvas() { return _canvas; } + + void on_size_allocate(Gtk::Allocation &) override; + void on_realize() override; + void on_unrealize() override; + + sigc::connection modified_connection; + + SPDesktop *desktop = nullptr; + + InkscapeWindow *window = nullptr; + Gtk::MenuBar *_menubar; +private: + // Flags for ruler event handling + bool _ruler_clicked = false; ///< True if the ruler has been clicked + bool _ruler_dragged = false; ///< True if a drag on the ruler is occurring + + bool update = false; + + Inkscape::CanvasItemGuideLine *_active_guide = nullptr; ///< The guide being handled during a ruler event + Geom::Point _normal; ///< Normal to the guide currently being handled during ruler event + int _xp = 0; ///< x coordinate for start of drag + int _yp = 0; ///< y coordinate for start of drag + + // The root vbox of the window layout. + Gtk::Box *_vbox; + + Gtk::Paned *_tbbox; + Gtk::Box *_hbox; + Inkscape::UI::Dialog::DialogContainer *_container = nullptr; + Inkscape::UI::Dialog::DialogMultipaned *_columns; + Gtk::Grid* _top_toolbars; + + Gtk::Box *_statusbar; + + Inkscape::UI::Dialog::SwatchesPanel *_panels; + + Glib::RefPtr<Gtk::Adjustment> _hadj; + Glib::RefPtr<Gtk::Adjustment> _vadj; + + Gtk::Grid *_coord_status; + + Gtk::Label *_select_status; + Gtk::Label *_coord_status_x; + Gtk::Label *_coord_status_y; + + Gtk::Box* _zoom_status_box; + Inkscape::UI::Widget::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::Box* _rotation_status_box; + Inkscape::UI::Widget::SpinButton *_rotation_status = nullptr; + 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::SelectedStyle *_selected_style; + + /** A grid for display the canvas, rulers, and scrollbars. */ + Inkscape::UI::Widget::CanvasGrid *_canvas_grid; + + unsigned int _interaction_disabled_counter = 0; + +public: + Geom::Point _ruler_origin; + double _dt2r; + +private: + Inkscape::UI::Widget::Canvas *_canvas = nullptr; + std::vector<sigc::connection> _connections; + Inkscape::PrefObserver _statusbar_preferences_observer; + Inkscape::UI::Widget::LayerSelector* _layer_selector; + Inkscape::UI::Widget::PageSelector* _page_selector; + +public: + EgeColorProfTracker* _tracker; + + void setMessage(Inkscape::MessageType type, gchar const *message); + Geom::Point window_get_pointer(); + 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 setCoordinateStatus(Geom::Point p); + void requestCanvasUpdate(); + void requestCanvasUpdateAndWait(); + void enableInteraction(); + void disableInteraction(); + void updateTitle(gchar const *uri); + bool onFocusInEvent(GdkEventFocus *); + Inkscape::UI::Dialog::DialogContainer *getDialogContainer(); + + Gtk::MenuBar *menubar() { return _menubar; } + + void updateNamedview(); + void update_guides_lock(); + + // Canvas Grid Widget + void cms_adjust_set_sensitive(bool enabled); + bool get_color_prof_adj_enabled() const; + void toggle_color_prof_adj(); + void update_zoom(); + void update_rotation(); + void update_rulers(); + + void iconify(); + void maximize(); + void fullscreen(); + static gint ruler_event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw, bool horiz); + + void layoutWidgets(); + void toggle_scrollbars(); + void update_scrollbars(double scale); + void toggle_command_palette(); + void toggle_rulers(); + void sticky_zoom_toggled(); + + Gtk::Widget *get_tool_toolbox() const { return Glib::wrap(tool_toolbox); } +private: + GtkWidget *tool_toolbox; + GtkWidget *aux_toolbox; + GtkWidget *commands_toolbox; + GtkWidget *snap_toolbox; + Inkscape::PrefObserver _tb_icon_sizes1; + Inkscape::PrefObserver _tb_icon_sizes2; + Inkscape::PrefObserver _tb_visible_buttons; + + void namedviewModified(SPObject *obj, guint flags); + 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); + bool rotation_output(); + void rotation_value_changed(); + void rotation_populate_popup(Gtk::Menu *menu); + //void canvas_tbl_size_allocate(Gtk::Allocation &allocation); + void update_statusbar_visibility(); + void apply_ctrlbar_settings(); + +public: + void cms_adjust_toggled(); +private: + static void color_profile_event(EgeColorProfTracker *tracker, SPDesktopWidget *dtw); + static void ruler_snap_new_guide(SPDesktop *desktop, Geom::Point &event_dt, Geom::Point &normal); + static gint event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw); + +public: // Move to CanvasGrid + bool on_ruler_box_button_press_event(GdkEventButton *event, Gtk::Widget *widget, bool horiz); + bool on_ruler_box_button_release_event(GdkEventButton *event, Gtk::Widget *widget, bool horiz); + bool on_ruler_box_motion_notify_event(GdkEventMotion *event, Gtk::Widget *widget, bool horiz); + void on_adjustment_value_changed(); +}; + +#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/mappings.xml b/src/widgets/mappings.xml new file mode 100644 index 0000000..c399361 --- /dev/null +++ b/src/widgets/mappings.xml @@ -0,0 +1,332 @@ +<?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='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'/> + + <!-- 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/sp-attribute-widget.cpp b/src/widgets/sp-attribute-widget.cpp new file mode 100644 index 0000000..2c8322b --- /dev/null +++ b/src/widgets/sp-attribute-widget.cpp @@ -0,0 +1,301 @@ +// 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 "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, _("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-xmlview-tree.cpp b/src/widgets/sp-xmlview-tree.cpp new file mode 100644 index 0000000..8bfaed6 --- /dev/null +++ b/src/widgets/sp-xmlview-tree.cpp @@ -0,0 +1,870 @@ +// 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 <gmodule.h> + +#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, nullptr); + 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::NodeType::TEXT_NODE ) { + vec = &text_repr_events; + } else if ( repr->type() == Inkscape::XML::NodeType::COMMENT_NODE ) { + vec = &comment_repr_events; + } else if ( repr->type() == Inkscape::XML::NodeType::PI_NODE ) { + vec = &pi_repr_events; + } else if ( repr->type() == Inkscape::XML::NodeType::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::NodeType::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); + } +} + +/** + * Truncate `val` to `maxlen` unicode characters and replace newlines and tabs + * with placeholder symbols. The string is modified in place. + * @param[in,out] val String in UTF-8 encoding + */ +static void sp_remove_newlines_and_tabs(std::string &val, size_t const maxlen = 200) +{ + if (g_utf8_strlen(val.data(), maxlen * 2) > maxlen) { + size_t newlen = g_utf8_offset_to_pointer(val.data(), maxlen - 3) - val.data(); + val.resize(newlen); + val.append("…"); + } + + struct + { + const char *query; + const char *replacement; + } replacements[] = { + {"\r\n", "⏎"}, + {"\n", "⏎"}, + {"\t", "⇥"}, + }; + + for (auto const &item : replacements) { + for (size_t pos = 0; (pos = val.find(item.query, pos)) != std::string::npos;) { + val.replace(pos, strlen(item.query), item.replacement); + } + } +} + +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; + + auto nolinecontent = std::string("\"").append(new_content).append("\""); + 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); + } +} + +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; + + auto nolinecontent = std::string("<!--").append(new_content).append("-->"); + 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); + } +} + +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; + + auto nolinecontent = std::string("<?").append(repr->name()).append(" ").append(new_content).append("?>"); + 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); + } +} + +/* + * 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 happened 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::NodeType::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..36ea43f --- /dev/null +++ b/src/widgets/sp-xmlview-tree.h @@ -0,0 +1,68 @@ +// 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> + +namespace Inkscape::XML { +class Node; +} + +/** + * 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/spw-utilities.cpp b/src/widgets/spw-utilities.cpp new file mode 100644 index 0000000..e9de66b --- /dev/null +++ b/src/widgets/spw-utilities.cpp @@ -0,0 +1,216 @@ +// 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::Box * spw_hbox(Gtk::Grid * table, int width, int col, int row) +{ + /* Create a new hbox with a 4-pixel spacing between children */ + Gtk::Box *hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 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; +} + +/** + * This function traverses a tree of widgets descending into bins and containers. + * It stops and returns a pointer to the first child widget for which 'eval' evaluates to true. + * If 'eval' never returns true then this function visits all widgets and returns nullptr. + * + * \param[in] widget The widget to start traversal from - top of the tree + * \param[in] eval The callback invoked for each visited widget + * + * \return The widget for which 'eval' returned true, or nullptr otherwise. + * Note: it could be a starting widget too. + */ +Gtk::Widget* sp_traverse_widget_tree(Gtk::Widget* widget, const std::function<bool (Gtk::Widget*)>& eval) { + if (!widget) return nullptr; + + if (eval(widget)) return widget; + + if (auto bin = dynamic_cast<Gtk::Bin*>(widget)) { + return sp_traverse_widget_tree(bin->get_child(), eval); + } + else if (auto container = dynamic_cast<Gtk::Container*>(widget)) { + auto children = container->get_children(); + for (auto child : children) { + if (auto found = sp_traverse_widget_tree(child, eval)) { + return found; + } + } + } + + return nullptr; +} + +/** + * This function traverses a tree of widgets searching for first focusable widget. + * + * \param[in] widget The widget to start traversal from - top of the tree + * + * \return The first focusable widget or nullptr if none are focusable. + */ +Gtk::Widget* sp_find_focusable_widget(Gtk::Widget* widget) { + return sp_traverse_widget_tree(widget, [](Gtk::Widget* w) { return w->get_can_focus(); }); +} + + +Glib::ustring sp_get_action_target(Gtk::Widget* widget) { + Glib::ustring target; + + if (widget && GTK_IS_ACTIONABLE(widget->gobj())) { + auto variant = gtk_actionable_get_action_target_value(GTK_ACTIONABLE(widget->gobj())); + auto type = variant ? g_variant_get_type_string(variant) : nullptr; + if (type && strcmp(type, "s") == 0) { + target = g_variant_get_string(variant, nullptr); + } + } + + return target; +} +/* + 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..fc37792 --- /dev/null +++ b/src/widgets/spw-utilities.h @@ -0,0 +1,61 @@ +// 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. +*/ + +#include <glib.h> +#include <glibmm/ustring.h> +#include <gtk/gtk.h> +#include <functional> + +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::Box * 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); + +Gtk::Widget* sp_traverse_widget_tree(Gtk::Widget* widget, const std::function<bool (Gtk::Widget*)>& eval); + +Gtk::Widget* sp_find_focusable_widget(Gtk::Widget* widget); + +// get string action target, if available +Glib::ustring sp_get_action_target(Gtk::Widget* widget); + +#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/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/toolbox.cpp b/src/widgets/toolbox.cpp new file mode 100644 index 0000000..f449e33 --- /dev/null +++ b/src/widgets/toolbox.cpp @@ -0,0 +1,640 @@ +// 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 "toolbox.h" + +#include <gtkmm.h> +#include <glibmm/i18n.h> + +#include "actions/actions-canvas-snapping.h" +#include "actions/actions-tools.h" +#include "io/resource.h" +#include "ui/widget/style-swatch.h" +#include "widgets/spw-utilities.h" // sp_traverse_widget_tree() +#include "widgets/widget-sizes.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/marker-toolbar.h" +#include "ui/toolbar/page-toolbar.h" +#include "ui/toolbar/paintbucket-toolbar.h" +#include "ui/toolbar/pencil-toolbar.h" +#include "ui/toolbar/select-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 "ui/tools/tool-base.h" + +//#define DEBUG_TEXT + +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" + +int ToolboxFactory::prefToPixelSize(Glib::ustring const& path) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int size = prefs->getIntLimited(path, 16, 16, 48); + return size; +} + +void ToolboxFactory::set_icon_size(GtkWidget* toolbox, int pixel_size) { + sp_traverse_widget_tree(Glib::wrap(toolbox), [=](Gtk::Widget* widget) { + if (auto ico = dynamic_cast<Gtk::Image*>(widget)) { + ico->set_from_icon_name(ico->get_icon_name(), static_cast<Gtk::IconSize>(Gtk::ICON_SIZE_BUTTON)); + ico->set_pixel_size(pixel_size); + } + return false; + }); +} + +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; + Glib::ustring const tool_name; + GtkWidget *(*create_func)(SPDesktop *desktop); + gchar const *swatch_tip; +} const aux_toolboxes[] = { + // If you change the tool_name for Measure or Text here, change it also in desktop-widget.cpp. + // clang-format off + { "/tools/select", "Select", Inkscape::UI::Toolbar::SelectToolbar::create, nullptr}, + { "/tools/nodes", "Node", Inkscape::UI::Toolbar::NodeToolbar::create, nullptr}, + { "/tools/marker", "Marker", Inkscape::UI::Toolbar::MarkerToolbar::create, nullptr}, + { "/tools/shapes/rect", "Rect", Inkscape::UI::Toolbar::RectToolbar::create, N_("Style of new rectangles")}, + { "/tools/shapes/arc", "Arc", Inkscape::UI::Toolbar::ArcToolbar::create, N_("Style of new ellipses")}, + { "/tools/shapes/star", "Star", Inkscape::UI::Toolbar::StarToolbar::create, N_("Style of new stars")}, + { "/tools/shapes/3dbox", "3DBox", Inkscape::UI::Toolbar::Box3DToolbar::create, N_("Style of new 3D boxes")}, + { "/tools/shapes/spiral", "Spiral", Inkscape::UI::Toolbar::SpiralToolbar::create, N_("Style of new spirals")}, + { "/tools/freehand/pencil", "Pencil", Inkscape::UI::Toolbar::PencilToolbar::create_pencil, N_("Style of new paths created by Pencil")}, + { "/tools/freehand/pen", "Pen", Inkscape::UI::Toolbar::PencilToolbar::create_pen, N_("Style of new paths created by Pen")}, + { "/tools/calligraphic", "Calligraphic", Inkscape::UI::Toolbar::CalligraphyToolbar::create, N_("Style of new calligraphic strokes")}, + { "/tools/text", "Text", Inkscape::UI::Toolbar::TextToolbar::create, nullptr}, + { "/tools/gradient", "Gradient", Inkscape::UI::Toolbar::GradientToolbar::create, nullptr}, + { "/tools/mesh", "Mesh", Inkscape::UI::Toolbar::MeshToolbar::create, nullptr}, + { "/tools/zoom", "Zoom", Inkscape::UI::Toolbar::ZoomToolbar::create, nullptr}, + { "/tools/measure", "Measure", Inkscape::UI::Toolbar::MeasureToolbar::create, nullptr}, + { "/tools/dropper", "Dropper", Inkscape::UI::Toolbar::DropperToolbar::create, nullptr}, + { "/tools/tweak", "Tweak", Inkscape::UI::Toolbar::TweakToolbar::create, N_("Color/opacity used for color tweaking")}, + { "/tools/spray", "Spray", Inkscape::UI::Toolbar::SprayToolbar::create, nullptr}, + { "/tools/connector", "Connector", Inkscape::UI::Toolbar::ConnectorToolbar::create, nullptr}, + { "/tools/pages", "Pages", Inkscape::UI::Toolbar::PageToolbar::create, nullptr}, + { "/tools/paintbucket", "Paintbucket", Inkscape::UI::Toolbar::PaintbucketToolbar::create, N_("Style of Paint Bucket fill objects")}, + { "/tools/eraser", "Eraser", Inkscape::UI::Toolbar::EraserToolbar::create, _("TBD")}, + { "/tools/lpetool", "LPETool", Inkscape::UI::Toolbar::LPEToolbar::create, _("TBD")}, + { nullptr, "", nullptr, nullptr } + // clang-format on +}; + + +static void setup_aux_toolbox(GtkWidget *toolbox, SPDesktop *desktop); +static void update_aux_toolbox(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox); + +static GtkWidget* toolboxNewCommon( GtkWidget* tb, BarId id, GtkPositionType /*handlePos*/ ) +{ + g_object_set_data(G_OBJECT(tb), "desktop", nullptr); + + gtk_widget_set_sensitive(tb, TRUE); + + 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(InkscapeWindow *window) +{ + Glib::ustring tool_toolbar_builder_file = get_filename(UIS, "toolbar-tool.ui"); + auto builder = Gtk::Builder::create(); + try + { + builder->add_from_file(tool_toolbar_builder_file); + } + catch (const Glib::Error& ex) + { + std::cerr << "ToolboxFactor::createToolToolbox: " << tool_toolbar_builder_file << " file not read! " << ex.what().raw() << std::endl; + } + + Gtk::Widget* toolbar = nullptr; + builder->get_widget("tool-toolbar", toolbar); + if (!toolbar) { + std::cerr << "InkscapeWindow: Failed to load tool toolbar!" << std::endl; + } + + _attachDoubleClickHandlers(builder, window); + + return toolboxNewCommon( GTK_WIDGET(toolbar->gobj()), BAR_TOOL, GTK_POS_LEFT ); +} + +/** + * @brief Attach double click handlers to all tool buttons, so that double-clicking on a tool + * in the toolbar opens up that tool's preferences. + * @param builder The builder that contains a loaded UI structure containing RadioButton's. + * @param win The Inkscape window which will display the preferences dialog. + */ +void ToolboxFactory::_attachDoubleClickHandlers(Glib::RefPtr<Gtk::Builder> builder, InkscapeWindow *win) +{ + for (auto &object : builder->get_objects()) { + if (auto radio = dynamic_cast<Gtk::RadioButton *>(object.get())) { + + Glib::VariantBase action_target; + radio->get_property("action-target", action_target); + if (!action_target.is_of_type(Glib::VARIANT_TYPE_STRING)) { + continue; + } + + auto tool_name = Glib::ustring((gchar const *)action_target.get_data()); + radio->signal_button_press_event().connect([=](GdkEventButton *ev) -> bool { + // Open tool preferences upon double click + if (ev->type == GDK_2BUTTON_PRESS && ev->button == 1) { + tool_preferences(tool_name, win); + return true; + } + return false; + }); + } + } +} + +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 = new Gtk::Box(); + tb->set_name("CommandsToolbox"); + tb->set_orientation(Gtk::ORIENTATION_VERTICAL); + tb->set_homogeneous(false); + + Glib::ustring commands_toolbar_builder_file = get_filename(UIS, "toolbar-commands.ui"); + auto builder = Gtk::Builder::create(); + try + { + builder->add_from_file(commands_toolbar_builder_file); + } + catch (const Glib::Error& ex) + { + std::cerr << "ToolboxFactor::createCommandsToolbox: " << commands_toolbar_builder_file << " file not read! " << ex.what().raw() << std::endl; + } + + Gtk::Toolbar* toolbar = nullptr; + builder->get_widget("commands-toolbar", toolbar); + if (!toolbar) { + std::cerr << "ToolboxFactory: Failed to load commands toolbar!" << std::endl; + } else { + tb->pack_start(*toolbar, false, false); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if ( prefs->getBool("/toolbox/icononly", true) ) { + toolbar->set_toolbar_style( Gtk::TOOLBAR_ICONS ); + } + } + + return toolboxNewCommon(GTK_WIDGET(tb->gobj()), BAR_COMMANDS, GTK_POS_LEFT); +} + +int show_popover(void* button) { + auto btn = static_cast<Gtk::MenuButton*>(button); + btn->get_popover()->show(); + return false; +} + +class SnapBar : public Gtk::Box { +public: + SnapBar() = default; + ~SnapBar() override = default; + + Inkscape::PrefObserver _observer; +}; + +GtkWidget *ToolboxFactory::createSnapToolbox() +{ + auto tb = new SnapBar(); + tb->set_name("SnapToolbox"); + tb->set_orientation(Gtk::ORIENTATION_VERTICAL); + tb->set_homogeneous(false); + + Glib::ustring snap_toolbar_builder_file = get_filename(UIS, "toolbar-snap.ui"); + auto builder = Gtk::Builder::create(); + try + { + builder->add_from_file(snap_toolbar_builder_file); + } + catch (const Glib::Error& ex) + { + std::cerr << "ToolboxFactor::createSnapToolbox: " << snap_toolbar_builder_file << " file not read! " << ex.what().raw() << std::endl; + } + + bool simple_snap = true; + Gtk::Toolbar* toolbar = nullptr; + builder->get_widget("snap-toolbar", toolbar); + if (!toolbar) { + std::cerr << "InkscapeWindow: Failed to load snap toolbar!" << std::endl; + } else { + tb->pack_start(*toolbar, false, false); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if ( prefs->getBool("/toolbox/icononly", true) ) { + toolbar->set_toolbar_style( Gtk::TOOLBAR_ICONS ); + } + simple_snap = prefs->getBool("/toolbox/simplesnap", simple_snap); + } + + Gtk::ToolItem* item_simple = nullptr; + Gtk::ToolItem* item_advanced = nullptr; + Gtk::MenuButton* btn_simple = nullptr; + Gtk::MenuButton* btn_advanced = nullptr; + Gtk::LinkButton* simple = nullptr; + Gtk::LinkButton* advanced = nullptr; + builder->get_widget("simple-link", simple); + builder->get_widget("advanced-link", advanced); + builder->get_widget("tool-item-advanced", item_advanced); + builder->get_widget("tool-item-simple", item_simple); + builder->get_widget("btn-simple", btn_simple); + builder->get_widget("btn-advanced", btn_advanced); + if (simple && advanced && item_simple && item_advanced && btn_simple && btn_advanced) { + // keep only one popup button visible + if (simple_snap) { + item_simple->show(); + item_advanced->hide(); + } + else { + item_advanced->show(); + item_simple->hide(); + } + + // Watch snap bar preferences; + Inkscape::Preferences* prefs = Inkscape::Preferences::get(); + tb->_observer = prefs->createObserver(ToolboxFactory::snap_bar_simple, [=](const Preferences::Entry& entry) { + if (entry.getBool(true)) { + item_advanced->hide(); + item_simple->show(); + // adjust snapping options when transitioning to simple scheme, since most are hidden + transition_to_simple_snapping(); + } + else { + item_simple->hide(); + item_advanced->show(); + } + }); + + // switch to simple mode + simple->signal_activate_link().connect([=](){ + g_timeout_add(250, &show_popover, btn_simple); + Inkscape::Preferences::get()->setBool(ToolboxFactory::snap_bar_simple, true); + return true; + }, false); + + // switch to advanced mode + advanced->signal_activate_link().connect([=](){ + g_timeout_add(250, &show_popover, btn_advanced); + Inkscape::Preferences::get()->setBool(ToolboxFactory::snap_bar_simple, false); + return true; + }, false); + } + + return toolboxNewCommon(GTK_WIDGET(tb->gobj()), BAR_SNAP, GTK_POS_LEFT); +} + +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 = nullptr; // setup_tool_toolbox; + update_func = nullptr; // 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 = nullptr; // setup_commands_toolbox; + update_func = nullptr; // update_commands_toolbox; + break; + + case BAR_SNAP: + setup_func = nullptr; + update_func = nullptr; + 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, TRUE); + } + +} // end of sp_toolbox_set_desktop() + + +#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 ); + } + } + } +} + +/** + * \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); + // center items vertically/horizontally to prevent stretching; + // all buttons will look uniform across toolbars if their original size is preserved + if (auto* tb = dynamic_cast<Gtk::Container*>(Glib::wrap(sub_toolbox))) { + for (auto&& item : tb->get_children()) { + if (dynamic_cast<Gtk::Button*>(item) || + dynamic_cast<Gtk::SpinButton*>(item) || + dynamic_cast<Gtk::ToolButton*>(item)) { + item->set_valign(Gtk::ALIGN_CENTER); + item->set_halign(Gtk::ALIGN_CENTER); + } + } + } + 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 ); + } + + int pixel_size = ToolboxFactory::prefToPixelSize(ToolboxFactory::ctrlbars_icon_size); + ToolboxFactory::set_icon_size(sub_toolbox, pixel_size); + gtk_widget_set_hexpand(sub_toolbox, TRUE); + + // Add a swatch widget if swatch tooltip is defined. + if ( aux_toolboxes[i].swatch_tip) { + auto swatch = new Inkscape::UI::Widget::StyleSwatch( nullptr, _(aux_toolboxes[i].swatch_tip) ); + swatch->setDesktop( desktop ); + swatch->setToolName(aux_toolboxes[i].tool_name); + // swatch->setClickVerb( aux_toolboxes[i].swatch_verb_id ); + swatch->setWatchedTool( aux_toolboxes[i].type_name, 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); + Glib::ustring ui_name = aux_toolboxes[i].tool_name + "Toolbar"; // If you change "Toolbar" here, change it also in desktop-widget.cpp. + gtk_widget_set_name( holder, ui_name.c_str() ); + + // 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].tool_name.c_str(), holder); + gtk_widget_show(sub_toolbox); + gtk_widget_show(holder); + } else if (aux_toolboxes[i].swatch_tip) { + g_warning("Could not create toolbox %s", aux_toolboxes[i].tool_name.c_str()); + } + } +} + +void update_aux_toolbox(SPDesktop * /*desktop*/, ToolBase *eventcontext, GtkWidget *toolbox) +{ + 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].tool_name.c_str())); + if (eventcontext && eventcontext->getPrefsPath() == 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 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); +} + +Glib::ustring ToolboxFactory::get_tool_visible_buttons_path(const Glib::ustring& button_action_name) { + return Glib::ustring(ToolboxFactory::tools_visible_buttons) + "/show" + button_action_name; +} + +/* + 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..40258a9 --- /dev/null +++ b/src/widgets/toolbox.h @@ -0,0 +1,79 @@ +// 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 <gtk/gtk.h> +#include <gtkmm/builder.h> +#include <gtkmm/enums.h> + +#include "preferences.h" + +class InkscapeWindow; +class SPDesktop; + +namespace Inkscape { +namespace UI { + +/** + * 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(InkscapeWindow *window); + static GtkWidget *createAuxToolbox(); + static GtkWidget *createCommandsToolbox(); + static GtkWidget *createSnapToolbox(); + + static int prefToPixelSize(Glib::ustring const& path); + static Gtk::IconSize prefToSize_mm(Glib::ustring const &path, int base = 0); + + static void set_icon_size(GtkWidget* toolbox, int pixel_size); + ToolboxFactory() = delete; + + static constexpr const char* tools_icon_size = "/toolbox/tools/iconsize"; + static constexpr const char* tools_visible_buttons = "/toolbox/tools/buttons"; + static constexpr const char* ctrlbars_icon_size = "/toolbox/controlbars/iconsize"; + static constexpr const char* snap_bar_simple = "/toolbox/simplesnap"; + static constexpr const int min_pixel_size = 16; + static constexpr const int max_pixel_size = 48; + static Glib::ustring get_tool_visible_buttons_path(const Glib::ustring& button_action_name); + +private: + static void _attachDoubleClickHandlers(Glib::RefPtr<Gtk::Builder> builder, InkscapeWindow *window); +}; + + + +} // 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..e70b071 --- /dev/null +++ b/src/widgets/widget-sizes.h @@ -0,0 +1,38 @@ +// 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 SPIN_STEP 0.1 +#define SPIN_PAGE_STEP 5.0 + +#define STATUS_ZOOM_WIDTH 57 +#define STATUS_ROTATION_WIDTH 57 + +/* + 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 : |