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 | 1937 | ||||
-rw-r--r-- | src/widgets/desktop-widget.h | 266 | ||||
-rw-r--r-- | src/widgets/paintdef.cpp | 190 | ||||
-rw-r--r-- | src/widgets/paintdef.h | 91 | ||||
-rw-r--r-- | src/widgets/sp-attribute-widget.cpp | 297 | ||||
-rw-r--r-- | src/widgets/sp-attribute-widget.h | 172 | ||||
-rw-r--r-- | src/widgets/sp-xmlview-tree.cpp | 936 | ||||
-rw-r--r-- | src/widgets/sp-xmlview-tree.h | 80 | ||||
-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 | 641 | ||||
-rw-r--r-- | src/widgets/toolbox.h | 80 | ||||
-rw-r--r-- | src/widgets/widget-sizes.h | 38 |
16 files changed, 5072 insertions, 0 deletions
diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt new file mode 100644 index 0000000..3701fc5 --- /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 + paintdef.cpp + sp-attribute-widget.cpp + sp-xmlview-tree.cpp + spw-utilities.cpp + toolbox.cpp + + # ------- + # Headers + desktop-widget.h + paintdef.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..edb4200 --- /dev/null +++ b/src/widgets/desktop-widget.cpp @@ -0,0 +1,1937 @@ +// 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-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 "object/sp-grid.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 "widgets/paintdef.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), + _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); + } + 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()); + } + private: + CMSPrefWatcher &_pw; + }; + + DisplayProfileWatcher _dpw; + + void _setCmsSensitive(bool value); + + std::list<SPDesktopWidget*> _widget_list; + EgeColorProfTracker *_tracker; + + friend class DisplayProfileWatcher; +}; + +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::_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(); + + 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); + + _tb_snap_pos = prefs->createObserver("/toolbox/simplesnap", sigc::mem_fun(*this, &SPDesktopWidget::repack_snaptoolbar)); + repack_snaptoolbar(); + + 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); + Inkscape::UI::set_icon_sizes(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")); + + _ds_sticky_zoom = prefs->createObserver("/options/stickyzoom/value", [=]() { sticky_zoom_updated(); }); + sticky_zoom_updated(); + + /* 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); + Inkscape::UI::set_icon_sizes(snap_toolbox, size); + Inkscape::UI::set_icon_sizes(commands_toolbox, size); + Inkscape::UI::set_icon_sizes(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()); +} + +/** + * 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_("enhance thin lines"); + } 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; +} + +void SPDesktopWidget::showNotice(Glib::ustring const &msg, unsigned timeout) +{ + _canvas_grid->showNotice(msg, timeout); +} + +/** + * 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(); + Gtk::Container *window = get_toplevel(); + if (window) { + bool dark = INKSCAPE.themecontext->isCurrentThemeDark(dynamic_cast<Gtk::Container *>(window)); + prefs->setBool("/theme/darkTheme", dark); + INKSCAPE.themecontext->getChangeThemeSignal().emit(); + INKSCAPE.themecontext->add_gtk_css(true); + } +} + +/* 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->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(); + setMessage(Inkscape::NORMAL_MESSAGE, down ? _("Locked all guides") : _("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); + 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); +} + +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(); + repack_snaptoolbar(); + + 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; + + // 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); +} + +/** + * Choose where to pack the snap toolbar. + */ +void SPDesktopWidget::repack_snaptoolbar() +{ + Inkscape::Preferences* prefs = Inkscape::Preferences::get(); + bool is_perm = prefs->getInt("/toolbox/simplesnap", 1) == 2; + auto& aux = *Glib::wrap(aux_toolbox); + auto& snap = *Glib::wrap(snap_toolbox); + + // Only remove from the parent if the status has changed + auto parent = snap.get_parent(); + if (parent && ((is_perm && parent != _hbox) || (!is_perm && parent != _top_toolbars))) { + parent->remove(snap); + } + + // Only repack if there's no parent widget now. + if (!snap.get_parent()) { + if (is_perm) { + ToolboxFactory::setOrientation(snap_toolbox, GTK_ORIENTATION_VERTICAL); + _hbox->pack_end(snap, false, true); + } else { + ToolboxFactory::setOrientation(snap_toolbox, GTK_ORIENTATION_HORIZONTAL); + _top_toolbars->attach(snap, 1, 0, 1, 2); + } + } + + // Always reset the various constraints, even if not repacked. + if (is_perm) { + snap.set_valign(Gtk::ALIGN_START); + } else { + // 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_width(aux) = 2; + _top_toolbars->child_property_height(snap) = 1; + 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); + } + } +} + +void +SPDesktopWidget::update_rulers() +{ + _canvas_grid->UpdateRulers(); +} + + +void SPDesktopWidget::namedviewModified(SPObject *obj, guint flags) +{ + auto nv = cast<SPNamedView>(obj); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + _dt2r = 1. / nv->display_units->factor; + + _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())); + + 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) { + auto image = cast<SPImage>(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::sticky_zoom_updated() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _canvas_grid->GetStickyZoom()->set_active(prefs->getBool("/options/stickyzoom/value", false)); +} + +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 (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) +{ + auto origin = horiz ? Inkscape::UI::Tools::DelayedSnapEvent::GUIDE_HRULER + : Inkscape::UI::Tools::DelayedSnapEvent::GUIDE_VRULER; + desktop->event_context->snap_delay_handler(widget->gobj(), this, event, origin); + + 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); + } + + _active_guide.reset(); + 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(); + SPGrid * grid = desktop->namedview->getFirstEnabledGrid(); + if (grid) { + if (grid->getType() == GridType::AXONOMETRIC ) { + auto angle_x = Geom::rad_from_deg(grid->getAngleX()); + auto angle_z = Geom::rad_from_deg(grid->getAngleZ()); + if (event->state & GDK_CONTROL_MASK) { + // guidelines normal to gridlines + normal_bl_to_tr = Geom::Point::polar(-angle_x, 1.0); + normal_tr_to_bl = Geom::Point::polar(angle_z, 1.0); + } else { + normal_bl_to_tr = Geom::rot90(Geom::Point::polar(angle_z, 1.0)); + normal_tr_to_bl = Geom::rot90(Geom::Point::polar(-angle_x, 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 = make_canvasitem<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(); +} + +Gio::ActionMap* SPDesktopWidget::get_action_map() { + return window; +} + +/* + 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..70f4613 --- /dev/null +++ b/src/widgets/desktop-widget.h @@ -0,0 +1,266 @@ +// 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 <cstddef> +#include <2geom/point.h> +#include <sigc++/connection.h> +#include <gtkmm.h> + +#include "message.h" +#include "preferences.h" +#include "ui/view/view-widget.h" +#include "display/control/canvas-item-ptr.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; } + + Gio::ActionMap* get_action_map(); + + 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; + + CanvasItemPtr<Inkscape::CanvasItemGuideLine> _active_guide; ///< 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: + 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); + 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 enableInteraction(); + void disableInteraction(); + void updateTitle(gchar const *uri); + bool onFocusInEvent(GdkEventFocus *); + Inkscape::UI::Dialog::DialogContainer *getDialogContainer(); + void showNotice(Glib::ustring const &msg, unsigned timeout = 0); + + 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 repack_snaptoolbar(); + + 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(); + void sticky_zoom_updated(); + + 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_snap_pos; + Inkscape::PrefObserver _tb_icon_sizes1; + Inkscape::PrefObserver _tb_icon_sizes2; + Inkscape::PrefObserver _tb_visible_buttons; + Inkscape::PrefObserver _ds_sticky_zoom; + + 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/paintdef.cpp b/src/widgets/paintdef.cpp new file mode 100644 index 0000000..a3fba3f --- /dev/null +++ b/src/widgets/paintdef.cpp @@ -0,0 +1,190 @@ +// 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 "paintdef.h" + +#include <memory> +#include <algorithm> +#include <cstdint> +#include <cstring> +#include <cstdio> +#include <glibmm/i18n.h> +#include <glibmm/stringutils.h> +#include <glibmm/regex.h> + +static char const *mimeOSWB_COLOR = "application/x-oswb-color"; +static char const *mimeX_COLOR = "application/x-color"; +static char const *mimeTEXT = "text/plain"; + +PaintDef::PaintDef() + : description(_("none")) + , type(NONE) + , rgb({0, 0, 0}) +{ +} + +PaintDef::PaintDef(std::array<unsigned, 3> const &rgb, std::string description) + : description(std::move(description)) + , type(RGB) + , rgb(rgb) +{ +} + +std::string PaintDef::get_color_id() const +{ + if (type == NONE) { + return "none"; + } + if (!description.empty() && description[0] != '#') { + auto name = Glib::ustring(std::move(description)); + // Convert description to ascii, strip out symbols, remove duplicate dashes and prefixes + static auto const reg1 = Glib::Regex::create("[^[:alnum:]]"); + name = reg1->replace(name, 0, "-", static_cast<Glib::RegexMatchFlags>(0)); + static auto const reg2 = Glib::Regex::create("-{2,}"); + name = reg2->replace(name, 0, "-", static_cast<Glib::RegexMatchFlags>(0)); + static auto const reg3 = Glib::Regex::create("(^-|-$)"); + name = reg3->replace(name, 0, "", static_cast<Glib::RegexMatchFlags>(0)); + // Move important numbers from the start where they are invalid xml, to the end. + static auto const reg4 = Glib::Regex::create("^(\\d+)(-?)([^\\d]*)"); + name = reg4->replace(name, 0, "\\3\\2\\1", static_cast<Glib::RegexMatchFlags>(0)); + return name.lowercase(); + } + auto [r, g, b] = rgb; + char buf[12]; + std::snprintf(buf, 12, "rgb%02x%02x%02x", r, g, b); + return std::string(buf); +} + +std::vector<std::string> const &PaintDef::getMIMETypes() +{ + static std::vector<std::string> mimetypes = {mimeOSWB_COLOR, mimeX_COLOR, mimeTEXT}; + return mimetypes; +} + +std::pair<std::vector<char>, int> PaintDef::getMIMEData(std::string const &type) const +{ + auto from_data = [] (void const *p, int len) { + std::vector<char> v(len); + std::memcpy(v.data(), p, len); + return v; + }; + + auto [r, g, b] = rgb; + + if (type == mimeTEXT) { + std::array<char, 8> tmp; + std::snprintf(tmp.data(), 8, "#%02x%02x%02x", r, g, b); + return std::make_pair(from_data(tmp.data(), 8), 8); + } else if (type == mimeX_COLOR) { + std::array<uint16_t, 4> tmp = {(uint16_t)((r << 8) | r), (uint16_t)((g << 8) | g), (uint16_t)((b << 8) | b), 0xffff}; + return std::make_pair(from_data(tmp.data(), 8), 16); + } else if (type == mimeOSWB_COLOR) { + std::string tmp("<paint>"); + switch (get_type()) { + case PaintDef::NONE: + tmp += "<nocolor/>"; + break; + default: + tmp += std::string("<color name=\"") + description + "\">"; + tmp += "<sRGB r=\""; + tmp += Glib::Ascii::dtostr(r / 255.0); + tmp += "\" g=\""; + tmp += Glib::Ascii::dtostr(g / 255.0); + tmp += "\" b=\""; + tmp += Glib::Ascii::dtostr(b / 255.0); + tmp += "\"/>"; + tmp += "</color>"; + } + tmp += "</paint>"; + return std::make_pair(from_data(tmp.c_str(), tmp.size()), 8); + } else { + return {{}, 0}; + } +} + +bool PaintDef::fromMIMEData(std::string const &type_str, char const *data, int len) +{ + if (type_str == mimeTEXT) { + // unused + } else if (type_str == mimeX_COLOR) { + // unused + } else if (type_str == mimeOSWB_COLOR) { + std::string xml(data, len); + if (xml.find("<nocolor/>") != std::string::npos) { + type = PaintDef::NONE; + rgb = {0, 0, 0}; + return true; + } else if (auto pos = xml.find("<sRGB"); pos != std::string::npos) { + std::string srgb = xml.substr(pos, xml.find(">", pos)); + type = PaintDef::RGB; + if (auto numPos = srgb.find("r="); numPos != std::string::npos) { + double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3)); + rgb[0] = static_cast<int>(255 * dbl); + } + if (auto numPos = srgb.find("g="); numPos != std::string::npos) { + double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3)); + rgb[1] = static_cast<int>(255 * dbl); + } + if (auto numPos = srgb.find("b="); numPos != std::string::npos) { + double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3)); + rgb[2] = static_cast<int>(255 * dbl); + } + if (auto pos = xml.find("<color "); pos != std::string::npos) { + std::string colorTag = xml.substr(pos, xml.find(">", pos)); + if (auto namePos = colorTag.find("name="); namePos != std::string::npos) { + char quote = colorTag[namePos + 5]; + auto endPos = colorTag.find(quote, namePos + 6); + description = colorTag.substr(namePos + 6, endPos - (namePos + 6)); + } + } + return true; + } + } + + return false; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/paintdef.h b/src/widgets/paintdef.h new file mode 100644 index 0000000..f66ae03 --- /dev/null +++ b/src/widgets/paintdef.h @@ -0,0 +1,91 @@ +// 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 INKSCAPE_WIDGETS_PAINTDEF_H +#define INKSCAPE_WIDGETS_PAINTDEF_H + +#include <string> +#include <vector> +#include <array> +#include <utility> + +/** + * Pure data representation of a color definition. + */ +class PaintDef +{ +public: + enum ColorType + { + NONE, + RGB + }; + + /// Create a color of type NONE + PaintDef(); + + /// Create a color of type RGB + PaintDef(std::array<unsigned, 3> const &rgb, std::string description); + + std::string get_color_id() const; + + std::string const &get_description() const { return description; } + ColorType get_type() const { return type; } + std::array<unsigned, 3> const &get_rgb() const { return rgb; } + + static std::vector<std::string> const &getMIMETypes(); + std::pair<std::vector<char>, int> getMIMEData(std::string const &type) const; + bool fromMIMEData(std::string const &type, char const *data, int len); + +protected: + std::string description; + ColorType type; + std::array<unsigned, 3> rgb; +}; + +#endif // INKSCAPE_WIDGETS_PAINTDEF_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/sp-attribute-widget.cpp b/src/widgets/sp-attribute-widget.cpp new file mode 100644 index 0000000..90ca6ea --- /dev/null +++ b/src/widgets/sp-attribute-widget.cpp @@ -0,0 +1,297 @@ +// 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 2 + + +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 || !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(false); + ll->set_margin_end(XPAD); + ll->set_margin_top(YPAD); + ll->set_margin_bottom(YPAD); + 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(false); + ee->set_margin_start(XPAD); + ee->set_margin_top(YPAD); + ee->set_margin_bottom(YPAD); + 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) +{ + 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..891db0a --- /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/bin.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::Bin { +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..a64e25d --- /dev/null +++ b/src/widgets/sp-xmlview-tree.cpp @@ -0,0 +1,936 @@ +// 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 "sp-xmlview-tree.h" + +#include <cstring> +#include <glibmm/markup.h> +#include <glibmm/property.h> +#include <glibmm/ustring.h> +#include <gmodule.h> +#include <gtkmm/cellrenderer.h> +#include <gtkmm/cellrenderertext.h> +#include <memory> +#include <string> + +#include "ui/syntax.h" +#include "xml/node-observer.h" +#include "xml/node.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; + std::unique_ptr<Inkscape::XML::NodeObserver> observer; + + 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_MARKUP_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 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 bool get_first_child(NodeData *data, GtkTreeIter *child_iter); +static void remove_dummy_rows(GtkTreeStore *store, GtkTreeIter *iter); +static void sp_remove_newlines_and_tabs(std::string &val, size_t const maxlen = 200); + +namespace { + +static auto null_to_empty(char const *str) +{ + return str ? str : ""; +} + +class ElementNodeObserver final : public Inkscape::XML::NodeObserver +{ + NodeData *_nodedata; + +public: + ElementNodeObserver(NodeData *nodedata) + : _nodedata(nodedata) + {} + + void notifyChildAdded(Inkscape::XML::Node&, Inkscape::XML::Node &child_, Inkscape::XML::Node *ref) override + { + GtkTreeIter before; + Inkscape::XML::Node *child = &child_; + + if (_nodedata->tree->blocked) return; + + if (!ref_to_sibling (_nodedata, ref, &before)) { + return; + } + + GtkTreeIter data_iter; + tree_ref_to_iter(_nodedata->tree, &data_iter, _nodedata->rowref); + + if (!_nodedata->expanded) { + auto model = GTK_TREE_MODEL(_nodedata->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(_nodedata->tree, &data_iter, &before, child); + } + + void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark key_, Inkscape::Util::ptr_shared, + Inkscape::Util::ptr_shared) override + { + auto const key = g_quark_to_string(key_); + if (std::strcmp(key, "id") != 0 && std::strcmp(key, "inkscape:label") != 0) + return; + elementAttrOrNameChangedUpdate(&node); + } + + void notifyElementNameChanged(Inkscape::XML::Node &node, GQuark, GQuark) override + { + elementAttrOrNameChangedUpdate(&node); + } + + void notifyChildOrderChanged(Inkscape::XML::Node &, Inkscape::XML::Node &child, Inkscape::XML::Node *, + Inkscape::XML::Node *newref) override + { + GtkTreeIter before, node; + + if (_nodedata->tree->blocked) + return; + + ref_to_sibling(_nodedata, newref, &before); + repr_to_child(_nodedata, &child, &node); + + if (gtk_tree_store_iter_is_valid(_nodedata->tree->store, &before)) { + gtk_tree_store_move_before(_nodedata->tree->store, &node, &before); + } else { + repr_to_child(_nodedata, newref, &before); + gtk_tree_store_move_after(_nodedata->tree->store, &node, &before); + } + } + + void notifyChildRemoved(Inkscape::XML::Node &repr, Inkscape::XML::Node &child, Inkscape::XML::Node *) override + { + if (_nodedata->tree->blocked) + return; + + GtkTreeIter iter; + if (repr_to_child(_nodedata, &child, &iter)) { + delete sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(_nodedata->tree->store), &iter); + gtk_tree_store_remove(_nodedata->tree->store, &iter); + } else if (!repr.firstChild() && get_first_child(_nodedata, &iter)) { + // remove dummy when all children gone + remove_dummy_rows(_nodedata->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(_nodedata->tree))); +#endif + } + + void elementAttrOrNameChangedUpdate(Inkscape::XML::Node *repr) + { + if (_nodedata->tree->blocked) { + return; + } + + auto node_name = Glib::ustring(null_to_empty(repr->name())); + auto pos = node_name.find("svg:"); + if (pos != Glib::ustring::npos) { + // Do not decorate element names the with default namespace "svg"; it is just visual noise. + node_name.erase(pos, 4); + } + + // Build a plain-text and a markup-enabled representation of the node. + auto &formatter = *_nodedata->tree->formatter; + auto display_text = Glib::ustring::compose("<%1", node_name); + formatter.openTag(node_name.c_str()); + + if (auto id_value = repr->attribute("id")) { + display_text += Glib::ustring::compose(" id=\"%1\"", id_value); + formatter.addAttribute("id", id_value); + } + if (auto label_value = repr->attribute("inkscape:label")) { + display_text += Glib::ustring::compose(" inkscape:label=\"%1\"", label_value); + formatter.addAttribute("inkscape:label", label_value); + } + display_text += ">"; + auto markup = formatter.finishTag(); + + GtkTreeIter iter; + if (tree_ref_to_iter(_nodedata->tree, &iter, _nodedata->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_TEXT_COL, display_text.c_str(), -1); + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_MARKUP_COL, markup.c_str(), -1); + } + } +}; + +class TextNodeObserver final : public Inkscape::XML::NodeObserver +{ + NodeData *_nodedata; + +public: + TextNodeObserver(NodeData *nodedata) + : _nodedata(nodedata) + {} + + void notifyContentChanged(Inkscape::XML::Node &, Inkscape::Util::ptr_shared, + Inkscape::Util::ptr_shared new_content) override + { + if (_nodedata->tree->blocked) + return; + + auto text_content = std::string("\"").append(null_to_empty(new_content.pointer())).append("\""); + sp_remove_newlines_and_tabs(text_content); + + auto &formatter = *_nodedata->tree->formatter; + auto markup = formatter.formatContent(text_content.c_str(), false); + + GtkTreeIter iter; + if (tree_ref_to_iter(_nodedata->tree, &iter, _nodedata->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_TEXT_COL, text_content.c_str(), -1); + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_MARKUP_COL, markup.c_str(), -1); + } + } +}; + +class CommentNodeObserver final : public Inkscape::XML::NodeObserver +{ + NodeData *_nodedata; + +public: + CommentNodeObserver(NodeData *nodedata) + : _nodedata(nodedata) + {} + void notifyContentChanged(Inkscape::XML::Node &, Inkscape::Util::ptr_shared, + Inkscape::Util::ptr_shared new_content) override + { + if (_nodedata->tree->blocked) + return; + + auto comment = std::string("<!--").append(null_to_empty(new_content.pointer())).append("-->"); + sp_remove_newlines_and_tabs(comment); + + auto &formatter = *_nodedata->tree->formatter; + auto markup = formatter.formatComment(comment.c_str(), false); + + GtkTreeIter iter; + if (tree_ref_to_iter(_nodedata->tree, &iter, _nodedata->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_TEXT_COL, comment.c_str(), -1); + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_MARKUP_COL, markup.c_str(), -1); + } + } +}; + +class PINodeObserver final : public Inkscape::XML::NodeObserver +{ + NodeData *_nodedata; + +public: + PINodeObserver(NodeData *nodedata) + : _nodedata(nodedata) + {} + void notifyContentChanged(Inkscape::XML::Node &repr, Inkscape::Util::ptr_shared, + Inkscape::Util::ptr_shared new_content) override + { + if (_nodedata->tree->blocked) + return; + + auto processing_instr = std::string("<?").append(repr.name()).append(" ").append(null_to_empty(new_content.pointer())).append("?>"); + sp_remove_newlines_and_tabs(processing_instr); + + auto &formatter = *_nodedata->tree->formatter; + auto markup = formatter.formatProlog(processing_instr.c_str()); + + GtkTreeIter iter; + if (tree_ref_to_iter(_nodedata->tree, &iter, _nodedata->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_TEXT_COL, processing_instr.c_str(), -1); + gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_MARKUP_COL, markup.c_str(), -1); + } + } +}; + +} // namespace + +/** + * 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; + ElementNodeObserver e(data); + data->repr->synthesizeEvents(e); + } + + return false; +} + +/** Node name renderer for XML tree. + * It knows to use markup, but falls back to plain text for selected nodes. + */ +class NodeRenderer : public Gtk::CellRendererText { +public: + NodeRenderer(): + Glib::ObjectBase(typeid(CellRendererText)), + Gtk::CellRendererText(), + _property_plain_text(*this, "plain", "-") {} + + // "text" and "markup" properties from CellRendererText are in use for marked up text; + // we need a separate property for plain text (placeholder_text could be hijacked potentially) + Glib::Property<Glib::ustring> _property_plain_text; + + void render_vfunc(const Cairo::RefPtr<Cairo::Context>& ctx, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags) override { + if (flags & Gtk::CELL_RENDERER_SELECTED) { + // use plain text instead of marked up text to render selected nodes, so they are legible + property_text() = _property_plain_text.get_value(); + } + Gtk::CellRendererText::render_vfunc(ctx, widget, background_area, cell_area, flags); + } +}; + +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)); + tree->_tree_move = new sigc::signal<void ()>(); + + 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); + + auto r = new NodeRenderer(); + tree->renderer = r; + + GtkCellRenderer* renderer = r->Gtk::CellRenderer::gobj(); + GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("", renderer, + "markup", STORE_MARKUP_COL, // marked up text for decorated color output + "plain", STORE_TEXT_COL, // plain text for searching and rendering selected nodes + 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); + + tree->formatter = new Inkscape::UI::Syntax::XMLFormatter(); + + 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; +} + +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); + + delete tree->renderer; + tree->renderer = nullptr; + delete tree->formatter; + tree->formatter = nullptr; + delete tree->_tree_move; + tree->_tree_move = nullptr; + + 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) +{ + 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) { + data->observer = std::make_unique<TextNodeObserver>(data); + } else if (repr->type() == Inkscape::XML::NodeType::COMMENT_NODE) { + data->observer = std::make_unique<CommentNodeObserver>(data); + } else if (repr->type() == Inkscape::XML::NodeType::PI_NODE) { + data->observer = std::make_unique<PINodeObserver>(data); + } else if (repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE) { + data->observer = std::make_unique<ElementNodeObserver>(data); + } + + if (data->observer) { + /* cheat a little to get the text updated on nodes without id */ + if (repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE && repr->attribute("id") == nullptr) { + data->observer->notifyAttributeChanged(*repr, g_quark_from_static_string("id"), + Inkscape::Util::ptr_shared(), Inkscape::Util::ptr_shared()); + } + repr->addObserver(*data->observer); + repr->synthesizeEvents(*data->observer); + } +} + +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) { + if (observer) { + repr->removeObserver(*observer); + } + Inkscape::GC::release(repr); + } + gtk_tree_row_reference_free(rowref); +} + +/** + * 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) +{ + 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); + } + } +} + +/* + * 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 + tree->_tree_move->emit(); + } +} + +/* + * 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, G_TYPE_STRING); + + 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..b9b2037 --- /dev/null +++ b/src/widgets/sp-xmlview-tree.h @@ -0,0 +1,80 @@ +// 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> +#include <gtkmm/cellrenderertext.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; +namespace Inkscape::UI::Syntax { class XMLFormatter; } + +class SPXMLViewTree +{ +public: + GtkTreeView tree; + GtkTreeStore *store; + Inkscape::XML::Node * repr; + gint blocked; + Gtk::CellRendererText* renderer; + Inkscape::UI::Syntax::XMLFormatter* formatter; + + sigc::connection connectTreeMove(const sigc::slot<void ()> &slot) + { + return _tree_move->connect(slot); + } +// private: Make private and not-pointer when refactoring to C++ + sigc::signal<void ()> *_tree_move; +}; + +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..a351223 --- /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..3847618 --- /dev/null +++ b/src/widgets/toolbox.cpp @@ -0,0 +1,641 @@ +// 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/util.h" +#include "ui/builder-utils.h" +#include "ui/widget/style-swatch.h" +#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/booleans-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; +} + +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/booleans", "Booleans", Inkscape::UI::Toolbar::BooleansToolbar::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) +{ + Gtk::Widget* toolbar = nullptr; + + auto builder = Inkscape::UI::create_builder("toolbar-tool.ui"); + builder->get_widget("tool-toolbar", toolbar); + if (!toolbar) { + std::cerr << "InkscapeWindow: Failed to load tool toolbar!" << std::endl; + } + + _attachHandlers(builder, window); + + return toolboxNewCommon( GTK_WIDGET(toolbar->gobj()), BAR_TOOL, GTK_POS_LEFT ); +} + +/** + * @brief Create a context menu for a tool button. + * @param tool_name The tool name (parameter to the tool-switch action) + * @param win The Inkscape window which will display the preferences dialog. + */ +Gtk::Menu *ToolboxFactory::_getContextMenu(Glib::ustring tool_name, InkscapeWindow *win) +{ + auto menu = new Gtk::Menu(); + auto gio_menu = Gio::Menu::create(); + auto action_group = Gio::SimpleActionGroup::create(); + menu->insert_action_group("ctx", action_group); + action_group->add_action("open-tool-preferences", sigc::bind<Glib::ustring, InkscapeWindow *>( + sigc::ptr_fun(&tool_preferences), tool_name, win)); + + auto menu_item = Gio::MenuItem::create(_("Open tool preferences"), "ctx.open-tool-preferences"); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getInt("/theme/menuIcons", true)) { + auto _icon = Gio::Icon::create("preferences-system"); + menu_item->set_icon(_icon); + } + + gio_menu->append_item(menu_item); + menu->bind_model(gio_menu, true); + menu->show(); + return menu; +} + +/** + * @brief Attach handlers to all tool buttons, so that double-clicking on a tool + * in the toolbar opens up that tool's preferences, and a right click opens a + * context menu with the same functionality. + * @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::_attachHandlers(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()); + + auto menu = _getContextMenu(tool_name, win); + menu->attach_to_widget(*radio); + + 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; + } + if (ev->button == 3) { + menu->popup_at_pointer(reinterpret_cast<GdkEvent *>(ev)); + } + 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); + + Gtk::Toolbar* toolbar = nullptr; + + auto builder = Inkscape::UI::create_builder("toolbar-commands.ui"); + 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_homogeneous(false); + + bool simple_snap = true; + Gtk::Toolbar* toolbar = nullptr; + + auto builder = Inkscape::UI::create_builder("toolbar-snap.ui"); + 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); + Inkscape::UI::set_icon_sizes(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..c95250b --- /dev/null +++ b/src/widgets/toolbox.h @@ -0,0 +1,80 @@ +// 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.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); + + 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 Gtk::Menu *_getContextMenu(Glib::ustring tool_name, InkscapeWindow *window); + static void _attachHandlers(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 : |