diff options
Diffstat (limited to 'src/ui/dialog')
132 files changed, 61587 insertions, 0 deletions
diff --git a/src/ui/dialog/README.md b/src/ui/dialog/README.md new file mode 100644 index 0000000..17217d2 --- /dev/null +++ b/src/ui/dialog/README.md @@ -0,0 +1,46 @@ +# Dialog System + +Author: vanntile + +The dialog system used for dialog-type widgets is made out of the classes +defined in the following header files (each explained later on): + +- dialog-container.h +- dialog-multipaned.h +- dialog-notebook.h +- dialog-window.h +- dialog-base.h + +This is a Gtk::Notebook based dialog manager with a GIMP style multipane. +Dialogs can live only inside DialogNotebooks, as pages in the inner +Gtk::Notebook, with their own tabs. A DialogNotebook itself is one of the +children of a DialogMultipaned. There can be several levels of DialogMultipaned, +but the top parent is a DialogContainer, which manages the existence of such +dialogs. + +DialogMultipaned is a paned-type container which supports resizing the children +by dragging the separator "handle" widgets. More than this, it supports the +addition of new children (in DialogNotebook) by drag-and-dropping them at the +extremeties of a multipane, where you can find dropzones (left and right for +horizontal ones, or top and bottom for vertical ones). + +Dialogs can also live independently in their own DialogWindow. In this floating +state, they track the last active window, while in the attached (docked) state, +they track the InkscapeWindow they are in. You can drag tabs from DialogNotebook +to move a DialogBase (or child class instance) between windows. In Wayland, +if you drag the tab to an invalid position, it will create automatically a +DialogWindow to live in. + +DialogContainers are instantiated with a horizontal DialogMultipaned. + +## Initialisation + +The initial DialogContainer is created inside a DesktopWidget, then inserting +toolbars and an empty vertical DialogMultipaned where new dialogs will be added. + +## Adding a new dialog + +In order to add a new dialog to a window, you have to call the method +`new_dialog(const Glib::ustring& dialog_type)` on the container inside +the desktop of the window. Allowed dialog types can be found in the +dialog-container.cpp file. diff --git a/src/ui/dialog/about.cpp b/src/ui/dialog/about.cpp new file mode 100644 index 0000000..89c3551 --- /dev/null +++ b/src/ui/dialog/about.cpp @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for the about screen + * + * Copyright (C) Martin Owens 2019 <doctormo@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "about.h" + +#include <algorithm> +#include <fstream> +#include <random> +#include <regex> +#include <streambuf> +#include <string> + +#include "document.h" +#include "inkscape-version-info.h" +#include "io/resource.h" +#include "ui/util.h" +#include "ui/view/svg-view-widget.h" +#include "util/units.h" + +using namespace Inkscape::IO; +using namespace Inkscape::UI::View; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static Gtk::Window *window = nullptr; +static Gtk::Notebook *tabs = nullptr; + +void close_about_screen() { + window->hide(); +} +bool show_copy_button(Gtk::Button *button, Gtk::Label *label) { + reveal_widget(button, true); + reveal_widget(label, false); + return false; +} +void copy_version(Gtk::Button *button, Gtk::Label *label) { + auto clipboard = Gtk::Clipboard::get(); + clipboard->set_text(Inkscape::inkscape_version()); + if (label) { + reveal_widget(button, false); + reveal_widget(label, true); + Glib::signal_timeout().connect_seconds( + sigc::bind(sigc::ptr_fun(&show_copy_button), button, label), 2); + } +} +void copy_debug_info(Gtk::Button *button, Gtk::Label *label) { + auto clipboard = Gtk::Clipboard::get(); + clipboard->set_text(Inkscape::debug_info()); + if (label) { + reveal_widget(button, false); + reveal_widget(label, true); + Glib::signal_timeout().connect_seconds( + sigc::bind(sigc::ptr_fun(&show_copy_button), button, label), 2); + } +} + +void AboutDialog::show_about() { + + if(!window) { + // Load glade file here + Glib::ustring gladefile = Resource::get_filename(Resource::UIS, "inkscape-about.glade"); + Glib::RefPtr<Gtk::Builder> builder; + try { + builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_error("Glade file loading failed for about screen dialog"); + return; + } + builder->get_widget("about-screen-window", window); + builder->get_widget("tabs", tabs); + if(!tabs || !window) { + g_error("Window or tabs in glade file are missing or do not have the right ids."); + return; + } + // Automatic signal handling (requires -rdynamic compile flag) + //gtk_builder_connect_signals(builder->gobj(), NULL); + + // When automatic handling fails + Gtk::Button *version; + Gtk::Label *label; + builder->get_widget("version", version); + builder->get_widget("version-copied", label); + if(version) { + version->set_label(Inkscape::inkscape_version()); + version->signal_clicked().connect( + sigc::bind(sigc::ptr_fun(©_version), version, label)); + } + + Gtk::Button *debug_info; + Gtk::Label *label2; + builder->get_widget("debug_info", debug_info); + builder->get_widget("debug-info-copied", label2); + if (debug_info) { + debug_info->signal_clicked().connect( + sigc::bind(sigc::ptr_fun(©_debug_info), version, label2)); + } + + // Render the about screen image via inkscape SPDocument + auto filename = Resource::get_filename(Resource::SCREENS, "about.svg", true, false); + SPDocument *doc = SPDocument::createNewDoc(filename.c_str(), TRUE); + + // Bind glade's container to our SVGViewWidget class + if(doc) { + //SVGViewWidget *viewer; + //builder->get_widget_derived("image-container", viewer, doc); + //Gtk::manage(viewer); + auto viewer = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc)); + double width = doc->getWidth().value("px"); + double height = doc->getHeight().value("px"); + viewer->setResize(width, height); + + Gtk::AspectFrame *splash_widget; + builder->get_widget("aspect-frame", splash_widget); + splash_widget->unset_label(); + splash_widget->set_shadow_type(Gtk::SHADOW_NONE); + splash_widget->property_ratio() = width / height; + splash_widget->add(*viewer); + splash_widget->show_all(); + } else { + g_error("Error loading about screen SVG."); + } + + Gtk::TextView *authors; + builder->get_widget("credits-authors", authors); + std::random_device rd; + std::mt19937 g(rd()); + + if(authors) { + std::ifstream fn(Resource::get_filename(Resource::DOCS, "AUTHORS")); + std::vector<std::string> authors_data; + std::string line; + while (getline(fn, line)) { + authors_data.push_back(line); + } + std::shuffle(std::begin(authors_data), std::end(authors_data), g); + std::string str = ""; + for (auto author : authors_data) { + str += author + "\n"; + } + authors->get_buffer()->set_text(str.c_str()); + } + + Gtk::TextView *translators; + builder->get_widget("credits-translators", translators); + if(translators) { + std::ifstream fn(Resource::get_filename(Resource::DOCS, "TRANSLATORS")); + std::vector<std::string> translators_data; + std::string line; + while (getline(fn, line)) { + translators_data.push_back(line); + } + std::string str = ""; + std::regex e("(.*?)(<.*|)"); + std::shuffle(std::begin(translators_data), std::end(translators_data), g); + for (auto translator : translators_data) { + str += std::regex_replace(translator, e, "$1") + "\n"; + } + translators->get_buffer()->set_text(str.c_str()); + } + + Gtk::Label *license; + builder->get_widget("license-text", license); + if(license) { + std::ifstream fn(Resource::get_filename(Resource::DOCS, "LICENSE")); + std::string str((std::istreambuf_iterator<char>(fn)), + std::istreambuf_iterator<char>()); + license->set_markup(str.c_str()); + } + } + if(window) { + window->show(); + tabs->set_current_page(0); + } else { + g_error("About screen window couldn't be loaded. Missing window id in glade file."); + } +} + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/about.h b/src/ui/dialog/about.h new file mode 100644 index 0000000..b6d7923 --- /dev/null +++ b/src/ui/dialog/about.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for the about screen + * + * Copyright (C) Martin Owens 2019 <doctormo@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef ABOUTDIALOG_H +#define ABOUTDIALOG_H + +#include <gtkmm.h> +#include <gtkmm/builder.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class AboutDialog { + + public: + static void show_about(); + + private: +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // ABOUTDIALOG_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/ui/dialog/align-and-distribute.cpp b/src/ui/dialog/align-and-distribute.cpp new file mode 100644 index 0000000..cc6c58b --- /dev/null +++ b/src/ui/dialog/align-and-distribute.cpp @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Align and Distribute widget + */ +/* Authors: + * Tavmjong Bah + * + * Based on dialog by: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Aubanel MONNIER <aubi@libertysurf.fr> + * Frank Felfe <innerspace@iname.com> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2021 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "align-and-distribute.h" // widget + +#include <iostream> + +#include <giomm.h> + +#include "desktop.h" // Tool switching. +#include "inkscape-window.h" // Activate window action. +#include "actions/actions-tools.h" // Tool switching. +#include "io/resource.h" +#include "ui/dialog/dialog-base.h" // Tool switching. + +namespace Inkscape { +namespace UI { +namespace Dialog { + +using Inkscape::IO::Resource::get_filename; +using Inkscape::IO::Resource::UIS; + +AlignAndDistribute::AlignAndDistribute(Inkscape::UI::Dialog::DialogBase* dlg) + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + Glib::ustring builder_file = get_filename(UIS, "align-and-distribute.ui"); + auto builder = Gtk::Builder::create(); + try + { + builder->add_from_file(builder_file); + } + catch (const Glib::Error& ex) + { + std::cerr << "AlignAndDistribute::AlignAndDistribute: " << builder_file << " file not read! " << ex.what().raw() << std::endl; + } + + builder->get_widget("align-and-distribute-box", align_and_distribute_box); + if (!align_and_distribute_box) { + std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (box)!" << std::endl; + } else { + add(*align_and_distribute_box); + } + + builder->get_widget("align-and-distribute-object", align_and_distribute_object); + if (!align_and_distribute_object) { + std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (object)!" << std::endl; + } else { + align_and_distribute_object->show(); + } + + builder->get_widget("align-and-distribute-node", align_and_distribute_node); + if (!align_and_distribute_node) { + std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (node)!" << std::endl; + } else { + align_and_distribute_node->hide(); + } + + // ------------ Object Align ------------- + + builder->get_widget("align-relative-object", align_relative_object); + if (!align_relative_object) { + std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (combobox)!" << std::endl; + } else { + std::string align_to = prefs->getString("/dialogs/align/objects-align-to", "selection"); + align_relative_object->set_active_id(align_to); + align_relative_object->signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_align_relative_object_changed)); + } + + builder->get_widget("align-move-as-group", align_move_as_group); + if (!align_move_as_group) { + std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (group button)!" << std::endl; + } else { + bool sel_as_group = prefs->getBool("/dialogs/align/sel-as-groups"); + align_move_as_group->set_active(sel_as_group); + + align_move_as_group->signal_clicked().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_align_as_group_clicked)); + } + + // clang-format off + std::vector<std::pair<std::string, std::string>> align_buttons = { + {"align-horizontal-right-to-anchor", "right anchor" }, + {"align-horizontal-left", "left" }, + {"align-horizontal-center", "hcenter" }, + {"align-horizontal-right", "right" }, + {"align-horizontal-left-to-anchor", "left anchor" }, + {"align-horizontal-baseline", "horizontal" }, + {"align-vertical-bottom-to-anchor", "bottom anchor" }, + {"align-vertical-top", "top" }, + {"align-vertical-center", "vcenter" }, + {"align-vertical-bottom", "bottom" }, + {"align-vertical-top-to-anchor", "top anchor" }, + {"align-vertical-baseline", "vertical" } + }; + // clang-format on + + for (auto align_button: align_buttons) { + Gtk::Button* button; + builder->get_widget(align_button.first, button); + if (!button) { + std::cerr << "AlignAndDistribute::AlignAndDisribute: failed to get button: " + << align_button.first << " " << align_button.second << std::endl; + } else { + button->signal_button_press_event().connect( + sigc::bind<std::string>(sigc::mem_fun(*this, &AlignAndDistribute::on_align_button_press_event), align_button.second), false); + } + } + + + // ------------ Remove overlap ------------- + + builder->get_widget("remove-overlap-button", remove_overlap_button); + if (!remove_overlap_button) { + std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget!" << std::endl; + } else { + remove_overlap_button->signal_button_press_event().connect( + sigc::mem_fun(*this, &AlignAndDistribute::on_remove_overlap_button_press_event), false); // false => run first. + } + + builder->get_widget("remove-overlap-hgap", remove_overlap_hgap); + if (!remove_overlap_hgap) { + std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget!" << std::endl; + } + + builder->get_widget("remove-overlap-vgap", remove_overlap_vgap); + if (!remove_overlap_vgap) { + std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget!" << std::endl; + } + + // ------------ Node Align ------------- + + builder->get_widget("align-relative-node", align_relative_node); + if (!align_relative_node) { + std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (combobox)!" << std::endl; + } else { + std::string align_nodes_to = prefs->getString("/dialogs/align/nodes-align-to", "first"); + align_relative_node->set_active_id(align_nodes_to); + align_relative_node->signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_align_relative_node_changed)); + } + + std::vector<std::pair<std::string, std::string>> align_node_buttons = { + {"align-node-horizontal", "horizontal"}, + {"align-node-vertical", "vertical" } + }; + + for (auto align_button: align_node_buttons) { + Gtk::Button* button; + builder->get_widget(align_button.first, button); + if (!button) { + std::cerr << "AlignAndDistribute::AlignAndDisribute: failed to get button: " + << align_button.first << " " << align_button.second << std::endl; + } else { + button->signal_button_press_event().connect( + sigc::bind<std::string>(sigc::mem_fun(*this, &AlignAndDistribute::on_align_node_button_press_event), align_button.second), false); + } + } + + + // ------------ Set initial values ------------ + + // Normal or node alignment? + auto desktop = dlg->getDesktop(); + if (desktop) { + desktop_changed(desktop); + } +} + +void +AlignAndDistribute::desktop_changed(SPDesktop* desktop) +{ + tool_connection.disconnect(); + if (desktop) { + tool_connection = + desktop->connectEventContextChanged(sigc::mem_fun(*this, &AlignAndDistribute::tool_changed_callback)); + tool_changed(desktop); + } +} + +void +AlignAndDistribute::tool_changed(SPDesktop* desktop) +{ + bool node = get_active_tool(desktop) == "Node"; + if (node) { + align_and_distribute_object->hide(); + align_and_distribute_node->show(); + } else { + align_and_distribute_object->show(); + align_and_distribute_node->hide(); + } +} + +void +AlignAndDistribute::tool_changed_callback(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + tool_changed(desktop); +} + + +void +AlignAndDistribute::on_align_as_group_clicked() +{ + bool state = align_move_as_group->get_active(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/align/sel-as-groups", state); +} + +void +AlignAndDistribute::on_align_relative_object_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/dialogs/align/objects-align-to", align_relative_object->get_active_id()); +} + +void +AlignAndDistribute::on_align_relative_node_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/dialogs/align/nodes-align-to", align_relative_node->get_active_id()); +} + +bool +AlignAndDistribute::on_align_button_press_event(GdkEventButton* button_event, const std::string& align_to) +{ + Glib::ustring argument = align_to; + + argument += " " + align_relative_object->get_active_id(); + + if (align_move_as_group->get_active()) { + argument += " group"; + } + + auto variant = Glib::Variant<Glib::ustring>::create(argument); + auto app = Gio::Application::get_default(); + + if (align_to.find("vertical") != Glib::ustring::npos or align_to.find("horizontal") != Glib::ustring::npos) { + app->activate_action("object-align-text", variant); + } else { + app->activate_action("object-align", variant); + } + + return true; +} + +bool +AlignAndDistribute::on_remove_overlap_button_press_event(GdkEventButton* button_event) +{ + double hgap = remove_overlap_hgap->get_value(); + double vgap = remove_overlap_vgap->get_value(); + + auto variant = Glib::Variant<std::tuple<double, double>>::create(std::tuple<double, double>(hgap, vgap)); + auto app = Gio::Application::get_default(); + app->activate_action("object-remove-overlaps", variant); + return true; +} + +bool +AlignAndDistribute::on_align_node_button_press_event(GdkEventButton* button_event, const std::string& direction) +{ + Glib::ustring argument = align_relative_node->get_active_id(); + + auto variant = Glib::Variant<Glib::ustring>::create(argument); + InkscapeWindow* win = InkscapeApplication::instance()->get_active_window(); + if (!win) { + return true; + } + if (direction == "horizontal") { + win->activate_action("node-align-horizontal", variant); + } else { + win->activate_action("node-align-vertical", variant); + } + + return true; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/align-and-distribute.h b/src/ui/dialog/align-and-distribute.h new file mode 100644 index 0000000..15cfdd9 --- /dev/null +++ b/src/ui/dialog/align-and-distribute.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Align and Distribute widget + */ +/* Authors: + * Tavmjong Bah + * + * Based on dialog by: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Aubanel MONNIER <aubi@libertysurf.fr> + * Frank Felfe <innerspace@iname.com> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2021 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_ALIGN_AND_DISTRIBUTE_H +#define INKSCAPE_UI_WIDGET_ALIGN_AND_DISTRIBUTE_H + +#include <sigc++/connection.h> +#include <gtkmm.h> + +class SPDesktop; + +namespace Inkscape { +namespace UI { + +namespace Tools { +class ToolBase; +} + +namespace Dialog { +class DialogBase; + +class AlignAndDistribute : public Gtk::Box +{ +public: + AlignAndDistribute(Inkscape::UI::Dialog::DialogBase* dlg); + ~AlignAndDistribute() override = default; + + void desktop_changed(SPDesktop* desktop); + void tool_changed(SPDesktop* desktop); // Need to show different widgets for node vs. other tools. + void tool_changed_callback(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + +private: + + // ********* Widgets ********** // + + Gtk::Box* align_and_distribute_box = nullptr; + Gtk::Box* align_and_distribute_object = nullptr; // Hidden when node tool active. + Gtk::Box* align_and_distribute_node = nullptr; // Visible when node tool active. + + // Align + Gtk::ToggleButton* align_move_as_group = nullptr; + Gtk::ComboBox* align_relative_object = nullptr; + Gtk::ComboBox* align_relative_node = nullptr; + + // Remove overlap + Gtk::Button* remove_overlap_button = nullptr; + Gtk::SpinButton* remove_overlap_hgap = nullptr; + Gtk::SpinButton* remove_overlap_vgap = nullptr; + + + // ********* Signal handlers ********** // + + void on_align_as_group_clicked(); + void on_align_relative_object_changed(); + void on_align_relative_node_changed(); + + bool on_align_button_press_event(GdkEventButton* button_event, const std::string& align_to); + bool on_remove_overlap_button_press_event(GdkEventButton* button_event); + bool on_align_node_button_press_event(GdkEventButton* button_event, const std::string& align_to); + + sigc::connection tool_connection; + +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_ALIGN_AND_DISTRIBUTE_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/ui/dialog/arrange-tab.h b/src/ui/dialog/arrange-tab.h new file mode 100644 index 0000000..6f464b9 --- /dev/null +++ b/src/ui/dialog/arrange-tab.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @brief Arrange tools base class + */ +/* Authors: + * * Declara Denis + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_ARRANGE_TAB_H +#define INKSCAPE_UI_DIALOG_ARRANGE_TAB_H + +#include <gtkmm/box.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * This interface should be implemented by each arrange mode. + * The class is a Gtk::VBox and will be displayed as a tab in + * the dialog + */ +class ArrangeTab : public Gtk::Box +{ +public: + ArrangeTab() : Gtk::Box(Gtk::ORIENTATION_VERTICAL) {} + ~ArrangeTab() override = default; + + /** + * Do the actual work! This method is invoked to actually arrange the + * selection + */ + virtual void arrange() = 0; +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + + +#endif /* INKSCAPE_UI_DIALOG_ARRANGE_TAB_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/ui/dialog/attrdialog.cpp b/src/ui/dialog/attrdialog.cpp new file mode 100644 index 0000000..4d792b5 --- /dev/null +++ b/src/ui/dialog/attrdialog.cpp @@ -0,0 +1,711 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for XML attributes + */ +/* Authors: + * Martin Owens + * + * Copyright (C) Martin Owens 2018 <doctormo@gmail.com> + * + * Released under GNU GPLv2 or later, read the file 'COPYING' for more information + */ + +#include "attrdialog.h" + +#include "selection.h" +#include "document-undo.h" +#include "message-context.h" +#include "message-stack.h" +#include "style.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/widget/iconrenderer.h" + +#include "xml/node-event-vector.h" +#include "xml/attribute-record.h" + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +/** + * Return true if `node` is a text or comment node + */ +static bool is_text_or_comment_node(Inkscape::XML::Node const &node) +{ + switch (node.type()) { + case Inkscape::XML::NodeType::TEXT_NODE: + case Inkscape::XML::NodeType::COMMENT_NODE: + return true; + default: + return false; + } +} + +static void on_attr_changed (Inkscape::XML::Node * repr, + const gchar * name, + const gchar * /*old_value*/, + const gchar * new_value, + bool /*is_interactive*/, + gpointer data) +{ + ATTR_DIALOG(data)->onAttrChanged(repr, name, new_value); +} + +static void on_content_changed (Inkscape::XML::Node * repr, + gchar const * oldcontent, + gchar const * newcontent, + gpointer data) +{ + auto self = ATTR_DIALOG(data); + auto buffer = self->_content_tv->get_buffer(); + if (!buffer->get_modified()) { + const char *c = repr->content(); + buffer->set_text(c ? c : ""); + } + buffer->set_modified(false); +} + +Inkscape::XML::NodeEventVector _repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + on_attr_changed, + on_content_changed, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static gboolean key_callback(GtkWidget *widget, GdkEventKey *event, AttrDialog *attrdialog); +/** + * Constructor + * A treeview whose each row corresponds to an XML attribute of a selected node + * New attribute can be added by clicking '+' at bottom of the attr pane. '-' + */ +AttrDialog::AttrDialog() + : DialogBase("/dialogs/attr", "AttrDialog") + , _repr(nullptr) + , _mainBox(Gtk::ORIENTATION_VERTICAL) + , status_box(Gtk::ORIENTATION_HORIZONTAL) +{ + set_size_request(20, 15); + _mainBox.pack_start(_scrolledWindow, Gtk::PACK_EXPAND_WIDGET); + + // For text and comment nodes + _content_tv = Gtk::manage(new Gtk::TextView()); + _content_tv->show(); + _content_tv->set_wrap_mode(Gtk::WrapMode::WRAP_CHAR); + _content_tv->set_monospace(true); + _content_tv->set_border_width(4); + _content_tv->set_buffer(Gtk::TextBuffer::create()); + _content_tv->get_buffer()->signal_end_user_action().connect([this]() { + if (_repr) { + _repr->setContent(_content_tv->get_buffer()->get_text().c_str()); + setUndo(_("Type text")); + } + }); + _content_sw = Gtk::manage(new Gtk::ScrolledWindow()); + _content_sw->hide(); + _content_sw->set_no_show_all(); + _content_sw->add(*_content_tv); + _mainBox.pack_start(*_content_sw); + + // For element nodes + _treeView.set_headers_visible(true); + _treeView.set_hover_selection(true); + _treeView.set_activate_on_single_click(true); + _treeView.set_can_focus(false); + _scrolledWindow.add(_treeView); + _scrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + + _store = Gtk::ListStore::create(_attrColumns); + _treeView.set_model(_store); + + Inkscape::UI::Widget::IconRenderer * addRenderer = manage(new Inkscape::UI::Widget::IconRenderer()); + addRenderer->add_icon("edit-delete"); + + _treeView.append_column("", *addRenderer); + Gtk::TreeViewColumn *col = _treeView.get_column(0); + if (col) { + auto add_icon = Gtk::manage(sp_get_icon_image("list-add", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + col->set_clickable(true); + col->set_widget(*add_icon); + add_icon->set_tooltip_text(_("Add a new attribute")); + add_icon->show(); + auto button = add_icon->get_parent()->get_parent()->get_parent(); + // Assign the button event so that create happens BEFORE delete. If this code + // isn't in this exact way, the onAttrDelete is called when the header lines are pressed. + button->signal_button_release_event().connect(sigc::mem_fun(*this, &AttrDialog::onAttrCreate), false); + } + addRenderer->signal_activated().connect(sigc::mem_fun(*this, &AttrDialog::onAttrDelete)); + _treeView.signal_key_press_event().connect(sigc::mem_fun(*this, &AttrDialog::onKeyPressed)); + _treeView.set_search_column(-1); + + _nameRenderer = Gtk::manage(new Gtk::CellRendererText()); + _nameRenderer->property_editable() = true; + _nameRenderer->property_placeholder_text().set_value(_("Attribute Name")); + _nameRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::nameEdited)); + _nameRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startNameEdit)); + _treeView.append_column(_("Name"), *_nameRenderer); + _nameCol = _treeView.get_column(1); + if (_nameCol) { + _nameCol->set_resizable(true); + _nameCol->add_attribute(_nameRenderer->property_text(), _attrColumns._attributeName); + } + status.set_halign(Gtk::ALIGN_START); + status.set_valign(Gtk::ALIGN_CENTER); + status.set_size_request(1, -1); + status.set_markup(""); + status.set_line_wrap(true); + status.get_style_context()->add_class("inksmall"); + status_box.pack_start(status, TRUE, TRUE, 0); + pack_end(status_box, false, false, 2); + + _message_stack = std::make_shared<Inkscape::MessageStack>(); + _message_context = std::unique_ptr<Inkscape::MessageContext>(new Inkscape::MessageContext(_message_stack)); + _message_changed_connection = + _message_stack->connectChanged(sigc::bind(sigc::ptr_fun(_set_status_message), GTK_WIDGET(status.gobj()))); + + _valueRenderer = Gtk::manage(new Gtk::CellRendererText()); + _valueRenderer->property_editable() = true; + _valueRenderer->property_placeholder_text().set_value(_("Attribute Value")); + _valueRenderer->property_ellipsize().set_value(Pango::ELLIPSIZE_END); + _valueRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::valueEdited)); + _valueRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startValueEdit)); + _treeView.append_column(_("Value"), *_valueRenderer); + _valueCol = _treeView.get_column(2); + if (_valueCol) { + _valueCol->add_attribute(_valueRenderer->property_text(), _attrColumns._attributeValueRender); + } + _popover = Gtk::manage(new Gtk::Popover()); + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + _textview = Gtk::manage(new Gtk::TextView()); + _textview->set_wrap_mode(Gtk::WrapMode::WRAP_CHAR); + _textview->set_editable(true); + _textview->set_monospace(true); + _textview->set_border_width(6); + _textview->signal_map().connect(sigc::mem_fun(*this, &AttrDialog::textViewMap)); + Glib::RefPtr<Gtk::TextBuffer> textbuffer = Gtk::TextBuffer::create(); + textbuffer->set_text(""); + _textview->set_buffer(textbuffer); + _scrolled_text_view.add(*_textview); + _scrolled_text_view.set_max_content_height(450); + _scrolled_text_view.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _scrolled_text_view.set_propagate_natural_width(true); + Gtk::Label *helpreturn = Gtk::manage(new Gtk::Label(_("Shift+Return for a new line"))); + helpreturn->get_style_context()->add_class("inksmall"); + Gtk::Button *apply = Gtk::manage(new Gtk::Button()); + Gtk::Image *icon = Gtk::manage(sp_get_icon_image("on-outline", 26)); + apply->set_relief(Gtk::RELIEF_NONE); + icon->show(); + apply->add(*icon); + apply->signal_clicked().connect(sigc::mem_fun(*this, &AttrDialog::valueEditedPop)); + Gtk::Button *cancel = Gtk::manage(new Gtk::Button()); + icon = Gtk::manage(sp_get_icon_image("off-outline", 26)); + cancel->set_relief(Gtk::RELIEF_NONE); + icon->show(); + cancel->add(*icon); + cancel->signal_clicked().connect(sigc::mem_fun(*this, &AttrDialog::valueCanceledPop)); + hbox->pack_end(*apply, Gtk::PACK_SHRINK, 3); + hbox->pack_end(*cancel, Gtk::PACK_SHRINK, 3); + hbox->pack_end(*helpreturn, Gtk::PACK_SHRINK, 3); + vbox->pack_start(_scrolled_text_view, Gtk::PACK_EXPAND_WIDGET, 3); + vbox->pack_start(*hbox, Gtk::PACK_EXPAND_WIDGET, 3); + _popover->add(*vbox); + _popover->show(); + _popover->set_relative_to(_treeView); + _popover->set_position(Gtk::PositionType::POS_BOTTOM); + _popover->signal_closed().connect(sigc::mem_fun(*this, &AttrDialog::popClosed)); + _popover->get_style_context()->add_class("attrpop"); + attr_reset_context(0); + pack_start(_mainBox, Gtk::PACK_EXPAND_WIDGET); + // I couldn't get the signal go well not using C way signals + g_signal_connect(GTK_WIDGET(_popover->gobj()), "key-press-event", G_CALLBACK(key_callback), this); + _popover->hide(); + _updating = false; +} + +void AttrDialog::textViewMap() +{ + auto vscroll = _scrolled_text_view.get_vadjustment(); + int height = vscroll->get_upper() + 12; // padding 6+6 + if (height < 450) { + _scrolled_text_view.set_min_content_height(height); + vscroll->set_value(vscroll->get_lower()); + } else { + _scrolled_text_view.set_min_content_height(450); + } +} + +gboolean sp_show_pop_map(gpointer data) +{ + AttrDialog *attrdialog = reinterpret_cast<AttrDialog *>(data); + attrdialog->textViewMap(); + return FALSE; +} + +static gboolean key_callback(GtkWidget *widget, GdkEventKey *event, AttrDialog *attrdialog) +{ + switch (event->keyval) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + if (attrdialog->_popover->is_visible()) { + if (!(event->state & GDK_SHIFT_MASK)) { + attrdialog->valueEditedPop(); + attrdialog->_popover->hide(); + return true; + } else { + g_timeout_add(50, &sp_show_pop_map, attrdialog); + } + } + } break; + } + return false; +} + +/** + * Prepare value string suitable for display in a Gtk::CellRendererText + * + * Value is truncated at the first new line character (if any) and a visual indicator and ellipsis is added. + * Overall length is limited as well to prevent performance degradation for very long values. + * + * @param value Raw attribute value as UTF-8 encoded string + * @return Single-line string with fixed maximum length + */ +static Glib::ustring prepare_rendervalue(const char *value) +{ + constexpr int MAX_LENGTH = 500; // maximum length of string before it's truncated for performance reasons + // ~400 characters fit horizontally on a WQHD display, so 500 should be plenty + + Glib::ustring renderval; + + // truncate to MAX_LENGTH + if (g_utf8_strlen(value, -1) > MAX_LENGTH) { + renderval = Glib::ustring(value, MAX_LENGTH) + "…"; + } else { + renderval = value; + } + + // truncate at first newline (if present) and add a visual indicator + auto ind = renderval.find('\n'); + if (ind != Glib::ustring::npos) { + renderval.replace(ind, Glib::ustring::npos, " ⏎ …"); + } + + return renderval; +} + + +/** + * @brief AttrDialog::~AttrDialog + * Class destructor + */ +AttrDialog::~AttrDialog() +{ + _message_changed_connection.disconnect(); + _message_context = nullptr; + _message_stack = nullptr; +} + +void AttrDialog::startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path) +{ + Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell); + entry->signal_key_press_event().connect(sigc::bind(sigc::mem_fun(*this, &AttrDialog::onNameKeyPressed), entry)); +} + + +gboolean sp_show_attr_pop(gpointer data) +{ + AttrDialog *attrdialog = reinterpret_cast<AttrDialog *>(data); + attrdialog->_popover->show_all(); + + return FALSE; +} + +gboolean sp_close_entry(gpointer data) +{ + Gtk::CellEditable *cell = reinterpret_cast<Gtk::CellEditable *>(data); + if (cell) { + cell->property_editing_canceled() = true; + cell->remove_widget(); + } + return FALSE; +} + +void AttrDialog::startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path) +{ + Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell); + int width = 0; + int height = 0; + int colwidth = _valueCol->get_width(); + _textview->set_size_request(510, -1); + _popover->set_size_request(520, -1); + valuepath = path; + entry->get_layout()->get_pixel_size(width, height); + Gtk::TreeIter iter = *_store->get_iter(path); + Gtk::TreeModel::Row row = *iter; + if (row && this->_repr) { + Glib::ustring name = row[_attrColumns._attributeName]; + if (row[_attrColumns._attributeValue] != row[_attrColumns._attributeValueRender] || colwidth - 10 < width) { + valueediting = entry->get_text(); + Gdk::Rectangle rect; + _treeView.get_cell_area((Gtk::TreeModel::Path)iter, *_valueCol, rect); + if (_popover->get_position() == Gtk::PositionType::POS_BOTTOM) { + rect.set_y(rect.get_y() + 20); + } + _popover->set_pointing_to(rect); + Glib::RefPtr<Gtk::TextBuffer> textbuffer = Gtk::TextBuffer::create(); + textbuffer->set_text(row[_attrColumns._attributeValue]); + _textview->set_buffer(textbuffer); + g_timeout_add(50, &sp_close_entry, cell); + g_timeout_add(50, &sp_show_attr_pop, this); + } else { + entry->signal_key_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &AttrDialog::onValueKeyPressed), entry)); + } + } +} + +void AttrDialog::popClosed() +{ + Glib::RefPtr<Gtk::TextBuffer> textbuffer = Gtk::TextBuffer::create(); + textbuffer->set_text(""); + _textview->set_buffer(textbuffer); + _scrolled_text_view.set_min_content_height(20); +} + +/** + * @brief AttrDialog::setRepr + * Set the internal xml object that I'm working on right now. + */ +void AttrDialog::setRepr(Inkscape::XML::Node * repr) +{ + if ( repr == _repr ) return; + if (_repr) { + _store->clear(); + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + _repr = repr; + if (repr) { + Inkscape::GC::anchor(_repr); + _repr->addListener(&_repr_events, this); + _repr->synthesizeEvents(&_repr_events, this); + + // show either attributes or content + bool show_content = is_text_or_comment_node(*_repr); + _scrolledWindow.set_visible(!show_content); + _content_sw->set_visible(show_content); + } +} + +void AttrDialog::setUndo(Glib::ustring const &event_description) +{ + DocumentUndo::done(getDocument(), event_description, INKSCAPE_ICON("dialog-xml-editor")); +} + +void AttrDialog::_set_status_message(Inkscape::MessageType /*type*/, const gchar *message, GtkWidget *widget) +{ + if (widget) { + gtk_label_set_markup(GTK_LABEL(widget), message ? message : ""); + } +} + +/** + * Sets the AttrDialog status bar, depending on which attr is selected. + */ +void AttrDialog::attr_reset_context(gint attr) +{ + if (attr == 0) { + _message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> attribute to edit.")); + } else { + const gchar *name = g_quark_to_string(attr); + _message_context->setF( + Inkscape::NORMAL_MESSAGE, + _("Attribute <b>%s</b> selected. Press <b>Ctrl+Enter</b> when done editing to commit changes."), name); + } +} + +/** + * @brief AttrDialog::onAttrChanged + * This is called when the XML has an updated attribute + */ +void AttrDialog::onAttrChanged(Inkscape::XML::Node *repr, const gchar * name, const gchar * new_value) +{ + if (_updating) { + return; + } + Glib::ustring renderval; + if (new_value) { + renderval = prepare_rendervalue(new_value); + } + for(auto iter: this->_store->children()) + { + Gtk::TreeModel::Row row = *iter; + Glib::ustring col_name = row[_attrColumns._attributeName]; + if(name == col_name) { + if(new_value) { + row[_attrColumns._attributeValue] = new_value; + row[_attrColumns._attributeValueRender] = renderval; + new_value = nullptr; // Don't make a new one + } else { + _store->erase(iter); + } + break; + } + } + if (new_value) { + Gtk::TreeModel::Row row = *(_store->prepend()); + row[_attrColumns._attributeName] = name; + row[_attrColumns._attributeValue] = new_value; + row[_attrColumns._attributeValueRender] = renderval; + } +} + +/** + * @brief AttrDialog::onAttrCreate + * This function is a slot to signal_clicked for '+' button panel. + */ +bool AttrDialog::onAttrCreate(GdkEventButton *event) +{ + if(event->type == GDK_BUTTON_RELEASE && event->button == 1 && this->_repr) { + Gtk::TreeIter iter = _store->prepend(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter; + _treeView.set_cursor(path, *_nameCol, true); + grab_focus(); + return true; + } + return false; +} + +/** + * @brief AttrDialog::onAttrDelete + * @param event + * @return true + * Delete the attribute from the xml + */ +void AttrDialog::onAttrDelete(Glib::ustring path) +{ + Gtk::TreeModel::Row row = *_store->get_iter(path); + if (row) { + Glib::ustring name = row[_attrColumns._attributeName]; + { + this->_store->erase(row); + this->_repr->removeAttribute(name); + this->setUndo(_("Delete attribute")); + } + } +} + +/** + * @brief AttrDialog::onKeyPressed + * @param event + * @return true + * Delete or create elements based on key presses + */ +bool AttrDialog::onKeyPressed(GdkEventKey *event) +{ + bool ret = false; + if(this->_repr) { + auto selection = this->_treeView.get_selection(); + Gtk::TreeModel::Row row = *(selection->get_selected()); + Gtk::TreeIter iter = *(selection->get_selected()); + switch (event->keyval) + { + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: { + // Create new attribute (repeat code, fold into above event!) + Glib::ustring name = row[_attrColumns._attributeName]; + { + this->_store->erase(row); + this->_repr->removeAttribute(name); + this->setUndo(_("Delete attribute")); + } + ret = true; + } break; + case GDK_KEY_plus: + case GDK_KEY_Insert: + { + // Create new attribute (repeat code, fold into above event!) + Gtk::TreeIter iter = this->_store->prepend(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter; + this->_treeView.set_cursor(path, *this->_nameCol, true); + grab_focus(); + ret = true; + } break; + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + if (_popover->is_visible()) { + if (!(event->state & GDK_SHIFT_MASK)) { + valueEditedPop(); + _popover->hide(); + ret = true; + } + } + } break; + } + } + return ret; +} + +bool AttrDialog::onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onNameKeyPressed"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + entry->editing_done(); + ret = true; + break; + } + return ret; +} + + +bool AttrDialog::onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onValueKeyPressed"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + entry->editing_done(); + ret = true; + break; + } + return ret; +} + +gboolean sp_attrdialog_store_move_to_next(gpointer data) +{ + AttrDialog *attrdialog = reinterpret_cast<AttrDialog *>(data); + auto selection = attrdialog->_treeView.get_selection(); + Gtk::TreeIter iter = *(selection->get_selected()); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter; + Gtk::TreeViewColumn *focus_column; + attrdialog->_treeView.get_cursor(path, focus_column); + if (path == attrdialog->_modelpath && focus_column == attrdialog->_treeView.get_column(1)) { + attrdialog->_treeView.set_cursor(attrdialog->_modelpath, *attrdialog->_valueCol, true); + } + return FALSE; +} + +/** + * + * + * @brief AttrDialog::nameEdited + * @param event + * @return + * Called when the name is edited in the TreeView editable column + */ +void AttrDialog::nameEdited (const Glib::ustring& path, const Glib::ustring& name) +{ + Gtk::TreeIter iter = *_store->get_iter(path); + _modelpath = (Gtk::TreeModel::Path)iter; + Gtk::TreeModel::Row row = *iter; + if(row && this->_repr) { + Glib::ustring old_name = row[_attrColumns._attributeName]; + if (old_name == name) { + g_timeout_add(50, &sp_attrdialog_store_move_to_next, this); + grab_focus(); + return; + } + // Do not allow empty name (this would delete the attribute) + if (name.empty()) { + return; + } + // Do not allow duplicate names + const auto children = _store->children(); + for (const auto &child : children) { + if (name == child[_attrColumns._attributeName]) { + return; + } + } + if(std::any_of(name.begin(), name.end(), isspace)) { + return; + } + // Copy old value and remove old name + Glib::ustring value; + if (!old_name.empty()) { + value = row[_attrColumns._attributeValue]; + _updating = true; + _repr->removeAttribute(old_name); + _updating = false; + } + + // Do the actual renaming and set new value + row[_attrColumns._attributeName] = name; + grab_focus(); + _updating = true; + _repr->setAttributeOrRemoveIfEmpty(name, value); // use char * overload (allows empty attribute values) + _updating = false; + g_timeout_add(50, &sp_attrdialog_store_move_to_next, this); + this->setUndo(_("Rename attribute")); + } +} + +void AttrDialog::valueEditedPop() +{ + Glib::ustring value = _textview->get_buffer()->get_text(); + valueEdited(valuepath, value); + valueediting = ""; + _popover->hide(); +} + +void AttrDialog::valueCanceledPop() +{ + if (!valueediting.empty()) { + Glib::RefPtr<Gtk::TextBuffer> textbuffer = Gtk::TextBuffer::create(); + textbuffer->set_text(valueediting); + _textview->set_buffer(textbuffer); + } + _popover->hide(); +} + +/** + * @brief AttrDialog::valueEdited + * @param event + * @return + * Called when the value is edited in the TreeView editable column + */ +void AttrDialog::valueEdited (const Glib::ustring& path, const Glib::ustring& value) +{ + auto selection = getSelection(); + if (!selection) + return; + + Gtk::TreeModel::Row row = *_store->get_iter(path); + if(row && this->_repr) { + Glib::ustring name = row[_attrColumns._attributeName]; + Glib::ustring old_value = row[_attrColumns._attributeValue]; + if (old_value == value) { + return; + } + if(name.empty()) return; + { + _repr->setAttributeOrRemoveIfEmpty(name, value); + } + if(!value.empty()) { + row[_attrColumns._attributeValue] = value; + Glib::ustring renderval = prepare_rendervalue(value.c_str()); + row[_attrColumns._attributeValueRender] = renderval; + } + SPObject *obj = nullptr; + if (selection->objects().size() == 1) { + obj = selection->objects().back(); + + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + this->setUndo(_("Change attribute value")); + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape diff --git a/src/ui/dialog/attrdialog.h b/src/ui/dialog/attrdialog.h new file mode 100644 index 0000000..2d4a6a5 --- /dev/null +++ b/src/ui/dialog/attrdialog.h @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for XML attributes based on Gtk TreeView + */ +/* Authors: + * Martin Owens + * + * Copyright (C) Martin Owens 2018 <doctormo@gmail.com> + * + * Released under GNU GPLv2 or later, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_DIALOGS_ATTRDIALOG_H +#define SEEN_UI_DIALOGS_ATTRDIALOG_H + +#include <gtkmm/dialog.h> +#include <gtkmm/liststore.h> +#include <gtkmm/popover.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/textview.h> +#include <gtkmm/treeview.h> + +#include "desktop.h" +#include "message.h" +#include "ui/dialog/dialog-base.h" + +#define ATTR_DIALOG(obj) (dynamic_cast<Inkscape::UI::Dialog::AttrDialog*>((Inkscape::UI::Dialog::AttrDialog*)obj)) + +namespace Inkscape { +class MessageStack; +class MessageContext; +namespace UI { +namespace Dialog { + +/** + * @brief The AttrDialog class + * This dialog allows to add, delete and modify XML attributes created in the + * xml editor. + */ +class AttrDialog : public DialogBase +{ +public: + AttrDialog(); + ~AttrDialog() override; + + static AttrDialog &getInstance() { return *new AttrDialog(); } + + // Data structure + class AttrColumns : public Gtk::TreeModel::ColumnRecord { + public: + AttrColumns() { + add(_attributeName); + add(_attributeValue); + add(_attributeValueRender); + } + Gtk::TreeModelColumn<Glib::ustring> _attributeName; + Gtk::TreeModelColumn<Glib::ustring> _attributeValue; + Gtk::TreeModelColumn<Glib::ustring> _attributeValueRender; + }; + AttrColumns _attrColumns; + + // TreeView + Gtk::TreeView _treeView; + Glib::RefPtr<Gtk::ListStore> _store; + Gtk::CellRendererText *_nameRenderer; + Gtk::CellRendererText *_valueRenderer; + Gtk::TreeViewColumn *_nameCol; + Gtk::TreeViewColumn *_valueCol; + Gtk::TreeModel::Path _modelpath; + Gtk::Popover *_popover; + Gtk::TextView *_textview; + Glib::ustring valuepath; + Glib::ustring valueediting; + + // Text/comment nodes + Gtk::TextView *_content_tv; + Gtk::ScrolledWindow *_content_sw; + + /** + * Status bar + */ + std::shared_ptr<Inkscape::MessageStack> _message_stack; + std::unique_ptr<Inkscape::MessageContext> _message_context; + + // Widgets + Gtk::Box _mainBox; + Gtk::ScrolledWindow _scrolledWindow; + Gtk::ScrolledWindow _scrolled_text_view; + Gtk::Button _buttonAddAttribute; + // Variables - Inkscape + Inkscape::XML::Node* _repr; + Gtk::Box status_box; + Gtk::Label status; + bool _updating; + + // Helper functions + void setRepr(Inkscape::XML::Node * repr); + void setUndo(Glib::ustring const &event_description); + /** + * Sets the XML status bar, depending on which attr is selected. + */ + void attr_reset_context(gint attr); + static void _set_status_message(Inkscape::MessageType type, const gchar *message, GtkWidget *dialog); + + /** + * Signal handlers + */ + sigc::connection _message_changed_connection; + void onAttrChanged(Inkscape::XML::Node *repr, const gchar * name, const gchar * new_value); + bool onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry); + bool onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry); + void onAttrDelete(Glib::ustring path); + bool onAttrCreate(GdkEventButton *event); + bool onKeyPressed(GdkEventKey *event); + void popClosed(); + void startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path); + void startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path); + void nameEdited(const Glib::ustring &path, const Glib::ustring &name); + void valueEdited(const Glib::ustring &path, const Glib::ustring &value); + void textViewMap(); + void valueCanceledPop(); + void valueEditedPop(); +}; + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // ATTRDIALOG_H diff --git a/src/ui/dialog/calligraphic-profile-rename.cpp b/src/ui/dialog/calligraphic-profile-rename.cpp new file mode 100644 index 0000000..604ac7f --- /dev/null +++ b/src/ui/dialog/calligraphic-profile-rename.cpp @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for naming calligraphic profiles. + * + * @note This file is in the wrong directory because of link order issues - + * it is required by widgets/toolbox.cpp, and libspwidgets.a comes after + * libinkdialogs.a in the current link order. + */ +/* Author: + * Aubanel MONNIER + * + * Copyright (C) 2007 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "calligraphic-profile-rename.h" +#include <glibmm/i18n.h> +#include <gtkmm/grid.h> + +#include "desktop.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +CalligraphicProfileRename::CalligraphicProfileRename() : + _layout_table(Gtk::manage(new Gtk::Grid())), + _applied(false) +{ + set_title(_("Edit profile")); + + auto mainVBox = get_content_area(); + _layout_table->set_column_spacing(4); + _layout_table->set_row_spacing(4); + + _profile_name_entry.set_activates_default(true); + + _profile_name_label.set_label(_("Profile name:")); + _profile_name_label.set_halign(Gtk::ALIGN_END); + _profile_name_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table->attach(_profile_name_label, 0, 0, 1, 1); + + _profile_name_entry.set_hexpand(); + _layout_table->attach(_profile_name_entry, 1, 0, 1, 1); + + mainVBox->pack_start(*_layout_table, false, false, 4); + // Buttons + _close_button.set_use_underline(); + _close_button.set_label(_("_Cancel")); + _close_button.set_can_default(); + + _delete_button.set_use_underline(true); + _delete_button.set_label(_("_Delete")); + _delete_button.set_can_default(); + _delete_button.set_visible(false); + + _apply_button.set_use_underline(true); + _apply_button.set_label(_("_Save")); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &CalligraphicProfileRename::_close)); + _delete_button.signal_clicked() + .connect(sigc::mem_fun(*this, &CalligraphicProfileRename::_delete)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &CalligraphicProfileRename::_apply)); + + signal_delete_event().connect( sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &CalligraphicProfileRename::_close)), true ) ); + + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_delete_button, Gtk::RESPONSE_DELETE_EVENT); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); +} + +void CalligraphicProfileRename::_apply() +{ + _profile_name = _profile_name_entry.get_text(); + _applied = true; + _deleted = false; + _close(); +} + +void CalligraphicProfileRename::_delete() +{ + _profile_name = _profile_name_entry.get_text(); + _applied = true; + _deleted = true; + _close(); +} + +void CalligraphicProfileRename::_close() +{ + this->Gtk::Dialog::hide(); +} + +void CalligraphicProfileRename::show(SPDesktop *desktop, const Glib::ustring profile_name) +{ + CalligraphicProfileRename &dial = instance(); + dial._applied=false; + dial._deleted=false; + dial.set_modal(true); + + dial._profile_name = profile_name; + dial._profile_name_entry.set_text(profile_name); + + if (profile_name.empty()) { + dial.set_title(_("Add profile")); + dial._delete_button.set_visible(false); + + } else { + dial.set_title(_("Edit profile")); + dial._delete_button.set_visible(true); + } + + desktop->setWindowTransient (dial.gobj()); + dial.property_destroy_with_parent() = true; + // dial.Gtk::Dialog::show(); + //dial.present(); + dial.run(); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/calligraphic-profile-rename.h b/src/ui/dialog/calligraphic-profile-rename.h new file mode 100644 index 0000000..195275b --- /dev/null +++ b/src/ui/dialog/calligraphic-profile-rename.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for naming calligraphic profiles + */ +/* Author: + * Aubanel MONNIER + * + * Copyright (C) 2007 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_CALLIGRAPHIC_PROFILE_H +#define INKSCAPE_DIALOG_CALLIGRAPHIC_PROFILE_H + +#include <gtkmm/dialog.h> +#include <gtkmm/entry.h> +#include <gtkmm/label.h> + +namespace Gtk { +class Grid; +} + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class CalligraphicProfileRename : public Gtk::Dialog { +public: + CalligraphicProfileRename(); + ~CalligraphicProfileRename() override = default; + Glib::ustring getName() const { + return "CalligraphicProfileRename"; + } + + static void show(SPDesktop *desktop, const Glib::ustring profile_name); + static bool applied() { + return instance()._applied; + } + static bool deleted() { + return instance()._deleted; + } + static Glib::ustring getProfileName() { + return instance()._profile_name; + } + +protected: + void _close(); + void _apply(); + void _delete(); + + Gtk::Label _profile_name_label; + Gtk::Entry _profile_name_entry; + Gtk::Grid* _layout_table; + + Gtk::Button _close_button; + Gtk::Button _delete_button; + Gtk::Button _apply_button; + Glib::ustring _profile_name; + bool _applied; + bool _deleted; +private: + static CalligraphicProfileRename &instance() { + static CalligraphicProfileRename instance_; + return instance_; + } + CalligraphicProfileRename(CalligraphicProfileRename const &) = delete; // no copy + CalligraphicProfileRename &operator=(CalligraphicProfileRename const &) = delete; // no assign +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_DIALOG_CALLIGRAPHIC_PROFILE_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/ui/dialog/clonetiler.cpp b/src/ui/dialog/clonetiler.cpp new file mode 100644 index 0000000..9769018 --- /dev/null +++ b/src/ui/dialog/clonetiler.cpp @@ -0,0 +1,2819 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** + * @file + * Clone tiling dialog + */ +/* Authors: + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <goejendaagh@zonnet.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * Romain de Bossoreille + * + * Copyright (C) 2004-2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "clonetiler.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/adjustment.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/combobox.h> +#include <gtkmm/grid.h> +#include <gtkmm/liststore.h> +#include <gtkmm/notebook.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/sizegroup.h> + +#include <2geom/transforms.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "message-stack.h" + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing.h" + +#include "ui/icon-loader.h" + +#include "object/algorithms/unclump.h" + +#include "object/sp-item.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" +#include "object/sp-use.h" + +#include "ui/icon-names.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/unit-menu.h" + +#include "svg/svg-color.h" +#include "svg/svg.h" + +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { + +namespace Widget { +/** + * Simple extension of Gtk::CheckButton, which adds a flag + * to indicate whether the box should be unticked when reset + */ +class CheckButtonInternal : public Gtk::CheckButton { + private: + bool _uncheckable = false; + public: + CheckButtonInternal() = default; + + CheckButtonInternal(const Glib::ustring &label) + : Gtk::CheckButton(label) + {} + + void set_uncheckable(const bool val = true) { _uncheckable = val; } + bool get_uncheckable() const { return _uncheckable; } +}; +} + +namespace Dialog { + +#define SB_MARGIN 1 +#define VB_MARGIN 4 + +static Glib::ustring const prefs_path = "/dialogs/clonetiler/"; + +static Inkscape::Drawing *trace_drawing = nullptr; +static unsigned trace_visionkey; +static gdouble trace_zoom; +static SPDocument *trace_doc = nullptr; + +CloneTiler::CloneTiler() + : DialogBase("/dialogs/clonetiler/", "CloneTiler") + , table_row_labels(nullptr) +{ + set_spacing(0); + + { + auto prefs = Inkscape::Preferences::get(); + + auto mainbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); + mainbox->set_homogeneous(false); + mainbox->set_border_width(6); + + pack_start(*mainbox, true, true, 0); + + nb = Gtk::manage(new Gtk::Notebook()); + mainbox->pack_start(*nb, false, false, 0); + + + // Symmetry + { + auto vb = new_tab(nb, _("_Symmetry")); + + /* TRANSLATORS: For the following 17 symmetry groups, see + * http://www.bib.ulb.ac.be/coursmath/doc/17.htm (visual examples); + * http://www.clarku.edu/~djoyce/wallpaper/seventeen.html (English vocabulary); or + * http://membres.lycos.fr/villemingerard/Geometri/Sym1D.htm (French vocabulary). + */ + struct SymGroups { + gint group; + Glib::ustring label; + } const sym_groups[] = { + // TRANSLATORS: "translation" means "shift" / "displacement" here. + {TILE_P1, _("<b>P1</b>: simple translation")}, + {TILE_P2, _("<b>P2</b>: 180° rotation")}, + {TILE_PM, _("<b>PM</b>: reflection")}, + // TRANSLATORS: "glide reflection" is a reflection and a translation combined. + // For more info, see http://mathforum.org/sum95/suzanne/symsusan.html + {TILE_PG, _("<b>PG</b>: glide reflection")}, + {TILE_CM, _("<b>CM</b>: reflection + glide reflection")}, + {TILE_PMM, _("<b>PMM</b>: reflection + reflection")}, + {TILE_PMG, _("<b>PMG</b>: reflection + 180° rotation")}, + {TILE_PGG, _("<b>PGG</b>: glide reflection + 180° rotation")}, + {TILE_CMM, _("<b>CMM</b>: reflection + reflection + 180° rotation")}, + {TILE_P4, _("<b>P4</b>: 90° rotation")}, + {TILE_P4M, _("<b>P4M</b>: 90° rotation + 45° reflection")}, + {TILE_P4G, _("<b>P4G</b>: 90° rotation + 90° reflection")}, + {TILE_P3, _("<b>P3</b>: 120° rotation")}, + {TILE_P31M, _("<b>P31M</b>: reflection + 120° rotation, dense")}, + {TILE_P3M1, _("<b>P3M1</b>: reflection + 120° rotation, sparse")}, + {TILE_P6, _("<b>P6</b>: 60° rotation")}, + {TILE_P6M, _("<b>P6M</b>: reflection + 60° rotation")}, + }; + + gint current = prefs->getInt(prefs_path + "symmetrygroup", 0); + + // Add a new combo box widget with the list of symmetry groups to the vbox + auto combo = Gtk::manage(new Gtk::ComboBoxText()); + combo->set_tooltip_text(_("Select one of the 17 symmetry groups for the tiling")); + + // Hack to add markup support + auto cell_list = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(combo->gobj())); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo->gobj()), + GTK_CELL_RENDERER(cell_list->data), + "markup", 0, nullptr); + + for (const auto & sg : sym_groups) { + // Add the description of the symgroup to a new row + combo->append(sg.label); + } + + vb->pack_start(*combo, false, false, SB_MARGIN); + + combo->set_active(current); + combo->signal_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::symgroup_changed), combo)); + } + + table_row_labels = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + + // Shift + { + auto vb = new_tab(nb, _("S_hift")); + + auto table = table_x_y_rand (3); + vb->pack_start(*table, false, false, 0); + + // X + { + auto l = Gtk::manage(new Gtk::Label("")); + // TRANSLATORS: "shift" means: the tiles will be shifted (offset) horizontally by this amount + // xgettext:no-c-format + l->set_markup(_("<b>Shift X:</b>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Horizontal shift per row (in % of tile width)"), "shiftx_per_j", + -10000, 10000, "%"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Horizontal shift per column (in % of tile width)"), "shiftx_per_i", + -10000, 10000, "%"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the horizontal shift by this percentage"), "shiftx_rand", + 0, 1000, "%"); + table_attach (table, l, 0, 2, 4); + } + + // Y + { + auto l = Gtk::manage(new Gtk::Label("")); + // TRANSLATORS: "shift" means: the tiles will be shifted (offset) vertically by this amount + // xgettext:no-c-format + l->set_markup(_("<b>Shift Y:</b>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Vertical shift per row (in % of tile height)"), "shifty_per_j", + -10000, 10000, "%"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Vertical shift per column (in % of tile height)"), "shifty_per_i", + -10000, 10000, "%"); + table_attach (table, l, 0, 3, 3); + } + + { + auto l = spinbox ( + _("Randomize the vertical shift by this percentage"), "shifty_rand", + 0, 1000, "%"); + table_attach (table, l, 0, 3, 4); + } + + // Exponent + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<b>Exponent:</b>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = spinbox ( + _("Whether rows are spaced evenly (1), converge (<1) or diverge (>1)"), "shifty_exp", + 0, 10, "", true); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = spinbox ( + _("Whether columns are spaced evenly (1), converge (<1) or diverge (>1)"), "shiftx_exp", + 0, 10, "", true); + table_attach (table, l, 0, 4, 3); + } + + { // alternates + auto l = Gtk::manage(new Gtk::Label("")); + // TRANSLATORS: "Alternate" is a verb here + l->set_markup(_("<small>Alternate:</small>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 5, 1); + } + + { + auto l = checkbox (_("Alternate the sign of shifts for each row"), "shifty_alternate"); + table_attach (table, l, 0, 5, 2); + } + + { + auto l = checkbox (_("Alternate the sign of shifts for each column"), "shiftx_alternate"); + table_attach (table, l, 0, 5, 3); + } + + { // Cumulate + auto l = Gtk::manage(new Gtk::Label("")); + // TRANSLATORS: "Cumulate" is a verb here + l->set_markup(_("<small>Cumulate:</small>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 6, 1); + } + + { + auto l = checkbox (_("Cumulate the shifts for each row"), "shifty_cumulate"); + table_attach (table, l, 0, 6, 2); + } + + { + auto l = checkbox (_("Cumulate the shifts for each column"), "shiftx_cumulate"); + table_attach (table, l, 0, 6, 3); + } + + { // Exclude tile width and height in shift + auto l = Gtk::manage(new Gtk::Label("")); + // TRANSLATORS: "Cumulate" is a verb here + l->set_markup(_("<small>Exclude tile:</small>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 7, 1); + } + + { + auto l = checkbox (_("Exclude tile height in shift"), "shifty_excludeh"); + table_attach (table, l, 0, 7, 2); + } + + { + auto l = checkbox (_("Exclude tile width in shift"), "shiftx_excludew"); + table_attach (table, l, 0, 7, 3); + } + + } + + + // Scale + { + auto vb = new_tab(nb, _("Sc_ale")); + + auto table = table_x_y_rand(2); + vb->pack_start(*table, false, false, 0); + + // X + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<b>Scale X:</b>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Horizontal scale per row (in % of tile width)"), "scalex_per_j", + -100, 1000, "%"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Horizontal scale per column (in % of tile width)"), "scalex_per_i", + -100, 1000, "%"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the horizontal scale by this percentage"), "scalex_rand", + 0, 1000, "%"); + table_attach (table, l, 0, 2, 4); + } + + // Y + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<b>Scale Y:</b>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Vertical scale per row (in % of tile height)"), "scaley_per_j", + -100, 1000, "%"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Vertical scale per column (in % of tile height)"), "scaley_per_i", + -100, 1000, "%"); + table_attach (table, l, 0, 3, 3); + } + + { + auto l = spinbox (_("Randomize the vertical scale by this percentage"), "scaley_rand", + 0, 1000, "%"); + table_attach (table, l, 0, 3, 4); + } + + // Exponent + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<b>Exponent:</b>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = spinbox (_("Whether row scaling is uniform (1), converge (<1) or diverge (>1)"), "scaley_exp", + 0, 10, "", true); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = spinbox (_("Whether column scaling is uniform (1), converge (<1) or diverge (>1)"), "scalex_exp", + 0, 10, "", true); + table_attach (table, l, 0, 4, 3); + } + + // Logarithmic (as in logarithmic spiral) + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<b>Base:</b>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 5, 1); + } + + { + auto l = spinbox (_("Base for a logarithmic spiral: not used (0), converge (<1), or diverge (>1)"), "scaley_log", + 0, 10, "", false); + table_attach (table, l, 0, 5, 2); + } + + { + auto l = spinbox (_("Base for a logarithmic spiral: not used (0), converge (<1), or diverge (>1)"), "scalex_log", + 0, 10, "", false); + table_attach (table, l, 0, 5, 3); + } + + { // alternates + auto l = Gtk::manage(new Gtk::Label("")); + // TRANSLATORS: "Alternate" is a verb here + l->set_markup(_("<small>Alternate:</small>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 6, 1); + } + + { + auto l = checkbox (_("Alternate the sign of scales for each row"), "scaley_alternate"); + table_attach (table, l, 0, 6, 2); + } + + { + auto l = checkbox (_("Alternate the sign of scales for each column"), "scalex_alternate"); + table_attach (table, l, 0, 6, 3); + } + + { // Cumulate + auto l = Gtk::manage(new Gtk::Label("")); + // TRANSLATORS: "Cumulate" is a verb here + l->set_markup(_("<small>Cumulate:</small>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 7, 1); + } + + { + auto l = checkbox (_("Cumulate the scales for each row"), "scaley_cumulate"); + table_attach (table, l, 0, 7, 2); + } + + { + auto l = checkbox (_("Cumulate the scales for each column"), "scalex_cumulate"); + table_attach (table, l, 0, 7, 3); + } + + } + + + // Rotation + { + auto vb = new_tab(nb, _("_Rotation")); + + auto table = table_x_y_rand (1); + vb->pack_start(*table, false, false, 0); + + // Angle + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<b>Angle:</b>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Rotate tiles by this angle for each row"), "rotate_per_j", + -180, 180, "°"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Rotate tiles by this angle for each column"), "rotate_per_i", + -180, 180, "°"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the rotation angle by this percentage"), "rotate_rand", + 0, 100, "%"); + table_attach (table, l, 0, 2, 4); + } + + { // alternates + auto l = Gtk::manage(new Gtk::Label("")); + // TRANSLATORS: "Alternate" is a verb here + l->set_markup(_("<small>Alternate:</small>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = checkbox (_("Alternate the rotation direction for each row"), "rotate_alternatej"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = checkbox (_("Alternate the rotation direction for each column"), "rotate_alternatei"); + table_attach (table, l, 0, 3, 3); + } + + { // Cumulate + auto l = Gtk::manage(new Gtk::Label("")); + // TRANSLATORS: "Cumulate" is a verb here + l->set_markup(_("<small>Cumulate:</small>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = checkbox (_("Cumulate the rotation for each row"), "rotate_cumulatej"); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = checkbox (_("Cumulate the rotation for each column"), "rotate_cumulatei"); + table_attach (table, l, 0, 4, 3); + } + + } + + + // Blur and opacity + { + auto vb = new_tab(nb, _("_Blur & opacity")); + + auto table = table_x_y_rand(1); + vb->pack_start(*table, false, false, 0); + + + // Blur + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<b>Blur:</b>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox (_("Blur tiles by this percentage for each row"), "blur_per_j", + 0, 100, "%"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox (_("Blur tiles by this percentage for each column"), "blur_per_i", + 0, 100, "%"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the tile blur by this percentage"), "blur_rand", + 0, 100, "%"); + table_attach (table, l, 0, 2, 4); + } + + { // alternates + auto l = Gtk::manage(new Gtk::Label("")); + // TRANSLATORS: "Alternate" is a verb here + l->set_markup(_("<small>Alternate:</small>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = checkbox (_("Alternate the sign of blur change for each row"), "blur_alternatej"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = checkbox (_("Alternate the sign of blur change for each column"), "blur_alternatei"); + table_attach (table, l, 0, 3, 3); + } + + + + // Dissolve + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<b>Opacity:</b>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = spinbox (_("Decrease tile opacity by this percentage for each row"), "opacity_per_j", + 0, 100, "%"); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = spinbox (_("Decrease tile opacity by this percentage for each column"), "opacity_per_i", + 0, 100, "%"); + table_attach (table, l, 0, 4, 3); + } + + { + auto l = spinbox (_("Randomize the tile opacity by this percentage"), "opacity_rand", + 0, 100, "%"); + table_attach (table, l, 0, 4, 4); + } + + { // alternates + auto l = Gtk::manage(new Gtk::Label("")); + // TRANSLATORS: "Alternate" is a verb here + l->set_markup(_("<small>Alternate:</small>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 5, 1); + } + + { + auto l = checkbox (_("Alternate the sign of opacity change for each row"), "opacity_alternatej"); + table_attach (table, l, 0, 5, 2); + } + + { + auto l = checkbox (_("Alternate the sign of opacity change for each column"), "opacity_alternatei"); + table_attach (table, l, 0, 5, 3); + } + } + + + // Color + { + auto vb = new_tab(nb, _("Co_lor")); + + { + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + hb->set_homogeneous(false); + + auto l = Gtk::manage(new Gtk::Label(_("Initial color: "))); + hb->pack_start(*l, false, false, 0); + + guint32 rgba = 0x000000ff | sp_svg_read_color (prefs->getString(prefs_path + "initial_color").data(), 0x000000ff); + color_picker = new Inkscape::UI::Widget::ColorPicker (*new Glib::ustring(_("Initial color of tiled clones")), *new Glib::ustring(_("Initial color for clones (works only if the original has unset fill or stroke or on spray tool in copy mode)")), rgba, false); + color_changed_connection = color_picker->connectChanged(sigc::mem_fun(*this, &CloneTiler::on_picker_color_changed)); + + hb->pack_start(*color_picker, false, false, 0); + + vb->pack_start(*hb, false, false, 0); + } + + + auto table = table_x_y_rand(3); + vb->pack_start(*table, false, false, 0); + + // Hue + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<b>H:</b>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox (_("Change the tile hue by this percentage for each row"), "hue_per_j", + -100, 100, "%"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox (_("Change the tile hue by this percentage for each column"), "hue_per_i", + -100, 100, "%"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the tile hue by this percentage"), "hue_rand", + 0, 100, "%"); + table_attach (table, l, 0, 2, 4); + } + + + // Saturation + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<b>S:</b>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = spinbox (_("Change the color saturation by this percentage for each row"), "saturation_per_j", + -100, 100, "%"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = spinbox (_("Change the color saturation by this percentage for each column"), "saturation_per_i", + -100, 100, "%"); + table_attach (table, l, 0, 3, 3); + } + + { + auto l = spinbox (_("Randomize the color saturation by this percentage"), "saturation_rand", + 0, 100, "%"); + table_attach (table, l, 0, 3, 4); + } + + // Lightness + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<b>L:</b>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = spinbox (_("Change the color lightness by this percentage for each row"), "lightness_per_j", + -100, 100, "%"); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = spinbox (_("Change the color lightness by this percentage for each column"), "lightness_per_i", + -100, 100, "%"); + table_attach (table, l, 0, 4, 3); + } + + { + auto l = spinbox (_("Randomize the color lightness by this percentage"), "lightness_rand", + 0, 100, "%"); + table_attach (table, l, 0, 4, 4); + } + + + { // alternates + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<small>Alternate:</small>")); + l->set_xalign(0.0); + table_row_labels->add_widget(*l); + table_attach (table, l, 1, 5, 1); + } + + { + auto l = checkbox (_("Alternate the sign of color changes for each row"), "color_alternatej"); + table_attach (table, l, 0, 5, 2); + } + + { + auto l = checkbox (_("Alternate the sign of color changes for each column"), "color_alternatei"); + table_attach (table, l, 0, 5, 3); + } + + } + + // Trace + { + auto vb = new_tab(nb, _("_Trace")); + { + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + hb->set_border_width(4); + hb->set_homogeneous(false); + vb->pack_start(*hb, false, false, 0); + + _b = Gtk::manage(new UI::Widget::CheckButtonInternal(_("Trace the drawing under the clones/sprayed items"))); + _b->set_uncheckable(); + bool old = prefs->getBool(prefs_path + "dotrace"); + _b->set_active(old); + _b->set_tooltip_text(_("For each clone/sprayed item, pick a value from the drawing in its location and apply it")); + hb->pack_start(*_b, false, false, 0); + _b->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::do_pick_toggled)); + } + + { + auto vvb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 0)); + vvb->set_homogeneous(false); + vb->pack_start(*vvb, false, false, 0); + _dotrace = vvb; + + { + auto frame = Gtk::manage(new Gtk::Frame(_("1. Pick from the drawing:"))); + frame->set_shadow_type(Gtk::SHADOW_NONE); + vvb->pack_start(*frame, false, false, 0); + + auto table = Gtk::manage(new Gtk::Grid()); + table->set_row_spacing(4); + table->set_column_spacing(6); + table->set_border_width(4); + frame->add(*table); + + Gtk::RadioButtonGroup rb_group; + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Color"))); + radio->set_tooltip_text(_("Pick the visible color and opacity")); + table_attach(table, radio, 0.0, 1, 1); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_COLOR)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_COLOR); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Opacity"))); + radio->set_tooltip_text(_("Pick the total accumulated opacity")); + table_attach (table, radio, 0.0, 2, 1); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_OPACITY)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_OPACITY); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("R"))); + radio->set_tooltip_text(_("Pick the Red component of the color")); + table_attach (table, radio, 0.0, 1, 2); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_R)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_R); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("G"))); + radio->set_tooltip_text(_("Pick the Green component of the color")); + table_attach (table, radio, 0.0, 2, 2); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_G)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_G); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("B"))); + radio->set_tooltip_text(_("Pick the Blue component of the color")); + table_attach (table, radio, 0.0, 3, 2); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_B)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_B); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color hue", "H"))); + radio->set_tooltip_text(_("Pick the hue of the color")); + table_attach (table, radio, 0.0, 1, 3); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_H)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_H); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color saturation", "S"))); + radio->set_tooltip_text(_("Pick the saturation of the color")); + table_attach (table, radio, 0.0, 2, 3); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_S)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_S); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color lightness", "L"))); + radio->set_tooltip_text(_("Pick the lightness of the color")); + table_attach (table, radio, 0.0, 3, 3); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_L)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_L); + } + + } + + { + auto frame = Gtk::manage(new Gtk::Frame(_("2. Tweak the picked value:"))); + frame->set_shadow_type(Gtk::SHADOW_NONE); + vvb->pack_start(*frame, false, false, VB_MARGIN); + + auto table = Gtk::manage(new Gtk::Grid()); + table->set_row_spacing(4); + table->set_column_spacing(6); + table->set_border_width(4); + frame->add(*table); + + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("Gamma-correct:")); + table_attach (table, l, 1.0, 1, 1); + } + { + auto l = spinbox (_("Shift the mid-range of the picked value upwards (>0) or downwards (<0)"), "gamma_picked", + -10, 10, ""); + table_attach (table, l, 0.0, 1, 2); + } + + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("Randomize:")); + table_attach (table, l, 1.0, 1, 3); + } + { + auto l = spinbox (_("Randomize the picked value by this percentage"), "rand_picked", + 0, 100, "%"); + table_attach (table, l, 0.0, 1, 4); + } + + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("Invert:")); + table_attach (table, l, 1.0, 2, 1); + } + { + auto l = checkbox (_("Invert the picked value"), "invert_picked"); + table_attach (table, l, 0.0, 2, 2); + } + } + + { + auto frame = Gtk::manage(new Gtk::Frame(_("3. Apply the value to the clones':"))); + frame->set_shadow_type(Gtk::SHADOW_NONE); + vvb->pack_start(*frame, false, false, 0); + + auto table = Gtk::manage(new Gtk::Grid()); + table->set_row_spacing(4); + table->set_column_spacing(6); + table->set_border_width(4); + frame->add(*table); + + { + auto b = Gtk::manage(new Gtk::CheckButton(_("Presence"))); + bool old = prefs->getBool(prefs_path + "pick_to_presence", true); + b->set_active(old); + b->set_tooltip_text(_("Each clone is created with the probability determined by the picked value in that point")); + table_attach (table, b, 0.0, 1, 1); + b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_presence")); + } + + { + auto b = Gtk::manage(new Gtk::CheckButton(_("Size"))); + bool old = prefs->getBool(prefs_path + "pick_to_size"); + b->set_active(old); + b->set_tooltip_text(_("Each clone's size is determined by the picked value in that point")); + table_attach (table, b, 0.0, 2, 1); + b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_size")); + } + + { + auto b = Gtk::manage(new Gtk::CheckButton(_("Color"))); + bool old = prefs->getBool(prefs_path + "pick_to_color", false); + b->set_active(old); + b->set_tooltip_text(_("Each clone is painted by the picked color (the original must have unset fill or stroke)")); + table_attach (table, b, 0.0, 1, 2); + b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_color")); + } + + { + auto b = Gtk::manage(new Gtk::CheckButton(_("Opacity"))); + bool old = prefs->getBool(prefs_path + "pick_to_opacity", false); + b->set_active(old); + b->set_tooltip_text(_("Each clone's opacity is determined by the picked value in that point")); + table_attach (table, b, 0.0, 2, 2); + b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_opacity")); + } + } + vvb->set_sensitive(prefs->getBool(prefs_path + "dotrace")); + } + } + + { + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + hb->set_homogeneous(false); + mainbox->pack_start(*hb, false, false, 0); + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("Apply to tiled clones:")); + hb->pack_start(*l, false, false, 0); + } + // Rows/columns, width/height + { + auto table = Gtk::manage(new Gtk::Grid()); + table->set_row_spacing(4); + table->set_column_spacing(6); + + table->set_border_width(VB_MARGIN); + mainbox->pack_start(*table, false, false, 0); + + { + { + auto a = Gtk::Adjustment::create(0.0, 1, 500, 1, 10, 0); + int value = prefs->getInt(prefs_path + "jmax", 2); + a->set_value (value); + + auto sb = new Inkscape::UI::Widget::SpinButton(a, 1.0, 0); + sb->set_tooltip_text (_("How many rows in the tiling")); + sb->set_width_chars (7); + sb->set_name("row"); + table_attach(table, sb, 0.0f, 1, 2); + _rowscols.push_back(sb); + + a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::xy_changed), a, "jmax")); + } + + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup("×"); + table_attach(table, l, 0.0f, 1, 3); + _rowscols.push_back(l); + } + + { + auto a = Gtk::Adjustment::create(0.0, 1, 500, 1, 10, 0); + int value = prefs->getInt(prefs_path + "imax", 2); + a->set_value (value); + + auto sb = new Inkscape::UI::Widget::SpinButton(a, 1.0, 0); + sb->set_tooltip_text (_("How many columns in the tiling")); + sb->set_width_chars (7); + table_attach(table, sb, 0.0f, 1, 4); + _rowscols.push_back(sb); + + a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::xy_changed), a, "imax")); + } + } + + { + // unitmenu + unit_menu = new Inkscape::UI::Widget::UnitMenu(); + unit_menu->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR); + unit_menu->setUnit(SP_ACTIVE_DESKTOP->getNamedView()->display_units->abbr); + unitChangedConn = unit_menu->signal_changed().connect(sigc::mem_fun(*this, &CloneTiler::unit_changed)); + + { + // Width spinbutton + fill_width = Gtk::Adjustment::create(0.0, -1e6, 1e6, 1.0, 10.0, 0); + + double value = prefs->getDouble(prefs_path + "fillwidth", 50.0); + Inkscape::Util::Unit const *unit = unit_menu->getUnit(); + gdouble const units = Inkscape::Util::Quantity::convert(value, "px", unit); + fill_width->set_value (units); + + auto e = new Inkscape::UI::Widget::SpinButton(fill_width, 1.0, 2); + e->set_tooltip_text (_("Width of the rectangle to be filled")); + e->set_width_chars (7); + e->set_digits (4); + table_attach(table, e, 0.0f, 2, 2); + _widthheight.push_back(e); + fill_width->signal_value_changed().connect(sigc::mem_fun(*this, &CloneTiler::fill_width_changed)); + } + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup("×"); + table_attach(table, l, 0.0f, 2, 3); + _widthheight.push_back(l); + } + + { + // Height spinbutton + fill_height = Gtk::Adjustment::create(0.0, -1e6, 1e6, 1.0, 10.0, 0); + + double value = prefs->getDouble(prefs_path + "fillheight", 50.0); + Inkscape::Util::Unit const *unit = unit_menu->getUnit(); + gdouble const units = Inkscape::Util::Quantity::convert(value, "px", unit); + fill_height->set_value (units); + + auto e = new Inkscape::UI::Widget::SpinButton(fill_height, 1.0, 2); + e->set_tooltip_text (_("Height of the rectangle to be filled")); + e->set_width_chars (7); + e->set_digits (4); + table_attach(table, e, 0.0f, 2, 4); + _widthheight.push_back(e); + fill_height->signal_value_changed().connect(sigc::mem_fun(*this, &CloneTiler::fill_height_changed)); + } + + table_attach(table, unit_menu, 0.0f, 2, 5); + _widthheight.push_back(unit_menu); + } + + // Switch + Gtk::RadioButtonGroup rb_group; + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Rows, columns: "))); + radio->set_tooltip_text(_("Create the specified number of rows and columns")); + table_attach(table, radio, 0.0, 1, 1); + + if (!prefs->getBool(prefs_path + "fillrect")) { + radio->set_active(true); + switch_to_create(); + } + radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_create)); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Width, height: "))); + radio->set_tooltip_text(_("Fill the specified width and height with the tiling")); + table_attach(table, radio, 0.0, 2, 1); + + if (prefs->getBool(prefs_path + "fillrect")) { + radio->set_active(true); + switch_to_fill(); + } + radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_fill)); + } + } + + + // Use saved pos + { + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + mainbox->pack_start(*hb, false, false, 0); + + _cb_keep_bbox = Gtk::manage(new UI::Widget::CheckButtonInternal(_("Use saved size and position of the tile"))); + auto keepbbox = prefs->getBool(prefs_path + "keepbbox", true); + _cb_keep_bbox->set_active(keepbbox); + _cb_keep_bbox->set_tooltip_text(_("Pretend that the size and position of the tile are the same " + "as the last time you tiled it (if any), instead of using the " + "current size")); + hb->pack_start(*_cb_keep_bbox, false, false, 0); + _cb_keep_bbox->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::keep_bbox_toggled)); + } + + // Statusbar + { + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + hb->set_homogeneous(false); + mainbox->pack_end(*hb, false, false, 0); + auto l = Gtk::manage(new Gtk::Label("")); + _status = l; + hb->pack_start(*l, false, false, 0); + } + + // Buttons + { + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + hb->set_homogeneous(false); + mainbox->pack_start(*hb, false, false, 0); + + { + auto b = Gtk::manage(new Gtk::Button()); + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup_with_mnemonic(_(" <b>_Create</b> ")); + b->add(*l); + b->set_tooltip_text(_("Create and tile the clones of the selection")); + b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::apply)); + hb->pack_end(*b, false, false, 0); + } + + { // buttons which are enabled only when there are tiled clones + auto sb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4)); + sb->set_homogeneous(false); + hb->pack_end(*sb, false, false, 0); + _buttons_on_tiles = sb; + { + // TRANSLATORS: if a group of objects are "clumped" together, then they + // are unevenly spread in the given amount of space - as shown in the + // diagrams on the left in the following screenshot: + // http://www.inkscape.org/screenshots/gallery/inkscape-0.42-CVS-tiles-unclump.png + // So unclumping is the process of spreading a number of objects out more evenly. + auto b = Gtk::manage(new Gtk::Button(_(" _Unclump "), true)); + b->set_tooltip_text(_("Spread out clones to reduce clumping; can be applied repeatedly")); + b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::unclump)); + sb->pack_end(*b, false, false, 0); + } + + { + auto b = Gtk::manage(new Gtk::Button(_(" Re_move "), true)); + b->set_tooltip_text(_("Remove existing tiled clones of the selected object (siblings only)")); + b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::on_remove_button_clicked)); + sb->pack_end(*b, false, false, 0); + } + + // connect to global selection changed signal (so we can change desktops) and + // external_change (so we're not fooled by undo) + selectChangedConn = INKSCAPE.signal_selection_changed.connect(sigc::mem_fun(*this, &CloneTiler::change_selection)); + externChangedConn = INKSCAPE.signal_external_change.connect(sigc::mem_fun(*this, &CloneTiler::external_change)); + + // update now + change_selection(SP_ACTIVE_DESKTOP->getSelection()); + } + + { + auto b = Gtk::manage(new Gtk::Button(_(" R_eset "), true)); + // TRANSLATORS: "change" is a noun here + b->set_tooltip_text(_("Reset all shifts, scales, rotates, opacity and color changes in the dialog to zero")); + b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::reset)); + hb->pack_start(*b, false, false, 0); + } + } + + mainbox->show_all(); + } + + show_all(); +} + +CloneTiler::~CloneTiler () +{ + selectChangedConn.disconnect(); + externChangedConn.disconnect(); + color_changed_connection.disconnect(); +} + +void CloneTiler::on_picker_color_changed(guint rgba) +{ + static bool is_updating = false; + if (is_updating || !SP_ACTIVE_DESKTOP) + return; + + is_updating = true; + + gchar c[32]; + sp_svg_write_color(c, sizeof(c), rgba); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(prefs_path + "initial_color", c); + + is_updating = false; +} + +void CloneTiler::change_selection(Inkscape::Selection *selection) +{ + if (selection->isEmpty()) { + _buttons_on_tiles->set_sensitive(false); + _status->set_markup(_("<small>Nothing selected.</small>")); + return; + } + + if (boost::distance(selection->items()) > 1) { + _buttons_on_tiles->set_sensitive(false); + _status->set_markup(_("<small>More than one object selected.</small>")); + return; + } + + guint n = number_of_clones(selection->singleItem()); + if (n > 0) { + _buttons_on_tiles->set_sensitive(true); + gchar *sta = g_strdup_printf (_("<small>Object has <b>%d</b> tiled clones.</small>"), n); + _status->set_markup(sta); + g_free (sta); + } else { + _buttons_on_tiles->set_sensitive(false); + _status->set_markup(_("<small>Object has no tiled clones.</small>")); + } +} + +void CloneTiler::external_change() +{ + change_selection(SP_ACTIVE_DESKTOP->getSelection()); +} + +Geom::Affine CloneTiler::get_transform( + // symmetry group + int type, + + // row, column + int i, int j, + + // center, width, height of the tile + double cx, double cy, + double w, double h, + + // values from the dialog: + // Shift + double shiftx_per_i, double shifty_per_i, + double shiftx_per_j, double shifty_per_j, + double shiftx_rand, double shifty_rand, + double shiftx_exp, double shifty_exp, + int shiftx_alternate, int shifty_alternate, + int shiftx_cumulate, int shifty_cumulate, + int shiftx_excludew, int shifty_excludeh, + + // Scale + double scalex_per_i, double scaley_per_i, + double scalex_per_j, double scaley_per_j, + double scalex_rand, double scaley_rand, + double scalex_exp, double scaley_exp, + double scalex_log, double scaley_log, + int scalex_alternate, int scaley_alternate, + int scalex_cumulate, int scaley_cumulate, + + // Rotation + double rotate_per_i, double rotate_per_j, + double rotate_rand, + int rotate_alternatei, int rotate_alternatej, + int rotate_cumulatei, int rotate_cumulatej + ) +{ + + // Shift (in units of tile width or height) ------------- + double delta_shifti = 0.0; + double delta_shiftj = 0.0; + + if( shiftx_alternate ) { + delta_shifti = (double)(i%2); + } else { + if( shiftx_cumulate ) { // Should the delta shifts be cumulative (i.e. 1, 1+2, 1+2+3, ...) + delta_shifti = (double)(i*i); + } else { + delta_shifti = (double)i; + } + } + + if( shifty_alternate ) { + delta_shiftj = (double)(j%2); + } else { + if( shifty_cumulate ) { + delta_shiftj = (double)(j*j); + } else { + delta_shiftj = (double)j; + } + } + + // Random shift, only calculate if non-zero. + double delta_shiftx_rand = 0.0; + double delta_shifty_rand = 0.0; + if( shiftx_rand != 0.0 ) delta_shiftx_rand = shiftx_rand * g_random_double_range (-1, 1); + if( shifty_rand != 0.0 ) delta_shifty_rand = shifty_rand * g_random_double_range (-1, 1); + + + // Delta shift (units of tile width/height) + double di = shiftx_per_i * delta_shifti + shiftx_per_j * delta_shiftj + delta_shiftx_rand; + double dj = shifty_per_i * delta_shifti + shifty_per_j * delta_shiftj + delta_shifty_rand; + + // Shift in actual x and y, used below + double dx = w * di; + double dy = h * dj; + + double shifti = di; + double shiftj = dj; + + // Include tile width and height in shift if required + if( !shiftx_excludew ) shifti += i; + if( !shifty_excludeh ) shiftj += j; + + // Add exponential shift if necessary + double shifti_sign = (shifti > 0.0) ? 1.0 : -1.0; + shifti = shifti_sign * pow(fabs(shifti), shiftx_exp); + double shiftj_sign = (shiftj > 0.0) ? 1.0 : -1.0; + shiftj = shiftj_sign * pow(fabs(shiftj), shifty_exp); + + // Final shift + Geom::Affine rect_translate (Geom::Translate (w * shifti, h * shiftj)); + + // Rotation (in degrees) ------------ + double delta_rotationi = 0.0; + double delta_rotationj = 0.0; + + if( rotate_alternatei ) { + delta_rotationi = (double)(i%2); + } else { + if( rotate_cumulatei ) { + delta_rotationi = (double)(i*i + i)/2.0; + } else { + delta_rotationi = (double)i; + } + } + + if( rotate_alternatej ) { + delta_rotationj = (double)(j%2); + } else { + if( rotate_cumulatej ) { + delta_rotationj = (double)(j*j + j)/2.0; + } else { + delta_rotationj = (double)j; + } + } + + double delta_rotate_rand = 0.0; + if( rotate_rand != 0.0 ) delta_rotate_rand = rotate_rand * 180.0 * g_random_double_range (-1, 1); + + double dr = rotate_per_i * delta_rotationi + rotate_per_j * delta_rotationj + delta_rotate_rand; + + // Scale (times the original) ----------- + double delta_scalei = 0.0; + double delta_scalej = 0.0; + + if( scalex_alternate ) { + delta_scalei = (double)(i%2); + } else { + if( scalex_cumulate ) { // Should the delta scales be cumulative (i.e. 1, 1+2, 1+2+3, ...) + delta_scalei = (double)(i*i + i)/2.0; + } else { + delta_scalei = (double)i; + } + } + + if( scaley_alternate ) { + delta_scalej = (double)(j%2); + } else { + if( scaley_cumulate ) { + delta_scalej = (double)(j*j + j)/2.0; + } else { + delta_scalej = (double)j; + } + } + + // Random scale, only calculate if non-zero. + double delta_scalex_rand = 0.0; + double delta_scaley_rand = 0.0; + if( scalex_rand != 0.0 ) delta_scalex_rand = scalex_rand * g_random_double_range (-1, 1); + if( scaley_rand != 0.0 ) delta_scaley_rand = scaley_rand * g_random_double_range (-1, 1); + // But if random factors are same, scale x and y proportionally + if( scalex_rand == scaley_rand ) delta_scalex_rand = delta_scaley_rand; + + // Total delta scale + double scalex = 1.0 + scalex_per_i * delta_scalei + scalex_per_j * delta_scalej + delta_scalex_rand; + double scaley = 1.0 + scaley_per_i * delta_scalei + scaley_per_j * delta_scalej + delta_scaley_rand; + + if( scalex < 0.0 ) scalex = 0.0; + if( scaley < 0.0 ) scaley = 0.0; + + // Add exponential scale if necessary + if ( scalex_exp != 1.0 ) scalex = pow( scalex, scalex_exp ); + if ( scaley_exp != 1.0 ) scaley = pow( scaley, scaley_exp ); + + // Add logarithmic factor if necessary + if ( scalex_log > 0.0 ) scalex = pow( scalex_log, scalex - 1.0 ); + if ( scaley_log > 0.0 ) scaley = pow( scaley_log, scaley - 1.0 ); + // Alternative using rotation angle + //if ( scalex_log != 1.0 ) scalex *= pow( scalex_log, M_PI*dr/180 ); + //if ( scaley_log != 1.0 ) scaley *= pow( scaley_log, M_PI*dr/180 ); + + + // Calculate transformation matrices, translating back to "center of tile" (rotation center) before transforming + Geom::Affine drot_c = Geom::Translate(-cx, -cy) * Geom::Rotate (M_PI*dr/180) * Geom::Translate(cx, cy); + + Geom::Affine dscale_c = Geom::Translate(-cx, -cy) * Geom::Scale (scalex, scaley) * Geom::Translate(cx, cy); + + Geom::Affine d_s_r = dscale_c * drot_c; + + Geom::Affine rotate_180_c = Geom::Translate(-cx, -cy) * Geom::Rotate (M_PI) * Geom::Translate(cx, cy); + + Geom::Affine rotate_90_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-M_PI/2) * Geom::Translate(cx, cy); + Geom::Affine rotate_m90_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( M_PI/2) * Geom::Translate(cx, cy); + + Geom::Affine rotate_120_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-2*M_PI/3) * Geom::Translate(cx, cy); + Geom::Affine rotate_m120_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( 2*M_PI/3) * Geom::Translate(cx, cy); + + Geom::Affine rotate_60_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-M_PI/3) * Geom::Translate(cx, cy); + Geom::Affine rotate_m60_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( M_PI/3) * Geom::Translate(cx, cy); + + Geom::Affine flip_x = Geom::Translate(-cx, -cy) * Geom::Scale (-1, 1) * Geom::Translate(cx, cy); + Geom::Affine flip_y = Geom::Translate(-cx, -cy) * Geom::Scale (1, -1) * Geom::Translate(cx, cy); + + + // Create tile with required symmetry + const double cos60 = cos(M_PI/3); + const double sin60 = sin(M_PI/3); + const double cos30 = cos(M_PI/6); + const double sin30 = sin(M_PI/6); + + switch (type) { + + case TILE_P1: + return d_s_r * rect_translate; + break; + + case TILE_P2: + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * rotate_180_c * rect_translate; + } + break; + + case TILE_PM: + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + break; + + case TILE_PG: + if (j % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + break; + + case TILE_CM: + if ((i + j) % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + break; + + case TILE_PMM: + if (j % 2 == 0) { + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + } else { + if (i % 2 == 0) { + return d_s_r * flip_y * rect_translate; + } else { + return d_s_r * flip_x * flip_y * rect_translate; + } + } + break; + + case TILE_PMG: + if (j % 2 == 0) { + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * rotate_180_c * rect_translate; + } + } else { + if (i % 2 == 0) { + return d_s_r * flip_y * rect_translate; + } else { + return d_s_r * rotate_180_c * flip_y * rect_translate; + } + } + break; + + case TILE_PGG: + if (j % 2 == 0) { + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_y * rect_translate; + } + } else { + if (i % 2 == 0) { + return d_s_r * rotate_180_c * rect_translate; + } else { + return d_s_r * rotate_180_c * flip_y * rect_translate; + } + } + break; + + case TILE_CMM: + if (j % 4 == 0) { + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + } else if (j % 4 == 1) { + if (i % 2 == 0) { + return d_s_r * flip_y * rect_translate; + } else { + return d_s_r * flip_x * flip_y * rect_translate; + } + } else if (j % 4 == 2) { + if (i % 2 == 1) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + } else { + if (i % 2 == 1) { + return d_s_r * flip_y * rect_translate; + } else { + return d_s_r * flip_x * flip_y * rect_translate; + } + } + break; + + case TILE_P4: + { + Geom::Affine ori (Geom::Translate ((w + h) * pow((i/2), shiftx_exp) + dx, (h + w) * pow((j/2), shifty_exp) + dy)); + Geom::Affine dia1 (Geom::Translate (w/2 + h/2, -h/2 + w/2)); + Geom::Affine dia2 (Geom::Translate (-w/2 + h/2, h/2 + w/2)); + if (j % 2 == 0) { + if (i % 2 == 0) { + return d_s_r * ori; + } else { + return d_s_r * rotate_m90_c * dia1 * ori; + } + } else { + if (i % 2 == 0) { + return d_s_r * rotate_90_c * dia2 * ori; + } else { + return d_s_r * rotate_180_c * dia1 * dia2 * ori; + } + } + } + break; + + case TILE_P4M: + { + double max = MAX(w, h); + Geom::Affine ori (Geom::Translate ((max + max) * pow((i/4), shiftx_exp) + dx, (max + max) * pow((j/2), shifty_exp) + dy)); + Geom::Affine dia1 (Geom::Translate ( w/2 - h/2, h/2 - w/2)); + Geom::Affine dia2 (Geom::Translate (-h/2 + w/2, w/2 - h/2)); + if (j % 2 == 0) { + if (i % 4 == 0) { + return d_s_r * ori; + } else if (i % 4 == 1) { + return d_s_r * flip_y * rotate_m90_c * dia1 * ori; + } else if (i % 4 == 2) { + return d_s_r * rotate_m90_c * dia1 * Geom::Translate (h, 0) * ori; + } else if (i % 4 == 3) { + return d_s_r * flip_x * Geom::Translate (w, 0) * ori; + } + } else { + if (i % 4 == 0) { + return d_s_r * flip_y * Geom::Translate(0, h) * ori; + } else if (i % 4 == 1) { + return d_s_r * rotate_90_c * dia2 * Geom::Translate(0, h) * ori; + } else if (i % 4 == 2) { + return d_s_r * flip_y * rotate_90_c * dia2 * Geom::Translate(h, 0) * Geom::Translate(0, h) * ori; + } else if (i % 4 == 3) { + return d_s_r * flip_y * flip_x * Geom::Translate(w, 0) * Geom::Translate(0, h) * ori; + } + } + } + break; + + case TILE_P4G: + { + double max = MAX(w, h); + Geom::Affine ori (Geom::Translate ((max + max) * pow((i/4), shiftx_exp) + dx, (max + max) * pow(j, shifty_exp) + dy)); + Geom::Affine dia1 (Geom::Translate ( w/2 + h/2, h/2 - w/2)); + Geom::Affine dia2 (Geom::Translate (-h/2 + w/2, w/2 + h/2)); + if (((i/4) + j) % 2 == 0) { + if (i % 4 == 0) { + return d_s_r * ori; + } else if (i % 4 == 1) { + return d_s_r * rotate_m90_c * dia1 * ori; + } else if (i % 4 == 2) { + return d_s_r * rotate_90_c * dia2 * ori; + } else if (i % 4 == 3) { + return d_s_r * rotate_180_c * dia1 * dia2 * ori; + } + } else { + if (i % 4 == 0) { + return d_s_r * flip_y * Geom::Translate (0, h) * ori; + } else if (i % 4 == 1) { + return d_s_r * flip_y * rotate_m90_c * dia1 * Geom::Translate (-h, 0) * ori; + } else if (i % 4 == 2) { + return d_s_r * flip_y * rotate_90_c * dia2 * Geom::Translate (h, 0) * ori; + } else if (i % 4 == 3) { + return d_s_r * flip_x * Geom::Translate (w, 0) * ori; + } + } + } + break; + + case TILE_P3: + { + double width; + double height; + Geom::Affine dia1; + Geom::Affine dia2; + if (w > h) { + width = w + w * cos60; + height = 2 * w * sin60; + dia1 = Geom::Affine (Geom::Translate (w/2 + w/2 * cos60, -(w/2 * sin60))); + dia2 = dia1 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60))); + } else { + width = h * cos (M_PI/6); + height = h; + dia1 = Geom::Affine (Geom::Translate (h/2 * cos30, -(h/2 * sin30))); + dia2 = dia1 * Geom::Affine (Geom::Translate (0, h/2)); + } + Geom::Affine ori (Geom::Translate (width * pow((2*(i/3) + j%2), shiftx_exp) + dx, (height/2) * pow(j, shifty_exp) + dy)); + if (i % 3 == 0) { + return d_s_r * ori; + } else if (i % 3 == 1) { + return d_s_r * rotate_m120_c * dia1 * ori; + } else if (i % 3 == 2) { + return d_s_r * rotate_120_c * dia2 * ori; + } + } + break; + + case TILE_P31M: + { + Geom::Affine ori; + Geom::Affine dia1; + Geom::Affine dia2; + Geom::Affine dia3; + Geom::Affine dia4; + if (w > h) { + ori = Geom::Affine(Geom::Translate (w * pow((i/6) + 0.5*(j%2), shiftx_exp) + dx, (w * cos30) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (0, h/2) * Geom::Translate (w/2, 0) * Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, -h/2 * sin30) ); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60 - h/2 * sin30))); + dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); + } else { + ori = Geom::Affine (Geom::Translate (2*h * cos30 * pow((i/6 + 0.5*(j%2)), shiftx_exp) + dx, (2*h - h * sin30) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (0, -h/2) * Geom::Translate (h/2 * cos30, h/2 * sin30)); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (0, h/2)); + dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); + } + if (i % 6 == 0) { + return d_s_r * ori; + } else if (i % 6 == 1) { + return d_s_r * flip_y * rotate_m120_c * dia1 * ori; + } else if (i % 6 == 2) { + return d_s_r * rotate_m120_c * dia2 * ori; + } else if (i % 6 == 3) { + return d_s_r * flip_y * rotate_120_c * dia3 * ori; + } else if (i % 6 == 4) { + return d_s_r * rotate_120_c * dia4 * ori; + } else if (i % 6 == 5) { + return d_s_r * flip_y * Geom::Translate(0, h) * ori; + } + } + break; + + case TILE_P3M1: + { + double width; + double height; + Geom::Affine dia1; + Geom::Affine dia2; + Geom::Affine dia3; + Geom::Affine dia4; + if (w > h) { + width = w + w * cos60; + height = 2 * w * sin60; + dia1 = Geom::Affine (Geom::Translate (0, h/2) * Geom::Translate (w/2, 0) * Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, -h/2 * sin30) ); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60 - h/2 * sin30))); + dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); + } else { + width = 2 * h * cos (M_PI/6); + height = 2 * h; + dia1 = Geom::Affine (Geom::Translate (0, -h/2) * Geom::Translate (h/2 * cos30, h/2 * sin30)); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (0, h/2)); + dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); + } + Geom::Affine ori (Geom::Translate (width * pow((2*(i/6) + j%2), shiftx_exp) + dx, (height/2) * pow(j, shifty_exp) + dy)); + if (i % 6 == 0) { + return d_s_r * ori; + } else if (i % 6 == 1) { + return d_s_r * flip_y * rotate_m120_c * dia1 * ori; + } else if (i % 6 == 2) { + return d_s_r * rotate_m120_c * dia2 * ori; + } else if (i % 6 == 3) { + return d_s_r * flip_y * rotate_120_c * dia3 * ori; + } else if (i % 6 == 4) { + return d_s_r * rotate_120_c * dia4 * ori; + } else if (i % 6 == 5) { + return d_s_r * flip_y * Geom::Translate(0, h) * ori; + } + } + break; + + case TILE_P6: + { + Geom::Affine ori; + Geom::Affine dia1; + Geom::Affine dia2; + Geom::Affine dia3; + Geom::Affine dia4; + Geom::Affine dia5; + if (w > h) { + ori = Geom::Affine(Geom::Translate (w * pow((2*(i/6) + (j%2)), shiftx_exp) + dx, (2*w * sin60) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60)); + dia2 = dia1 * Geom::Affine (Geom::Translate (w/2, 0)); + dia3 = dia2 * Geom::Affine (Geom::Translate (w/2 * cos60, w/2 * sin60)); + dia4 = dia3 * Geom::Affine (Geom::Translate (-w/2 * cos60, w/2 * sin60)); + dia5 = dia4 * Geom::Affine (Geom::Translate (-w/2, 0)); + } else { + ori = Geom::Affine(Geom::Translate (2*h * cos30 * pow((i/6 + 0.5*(j%2)), shiftx_exp) + dx, (h + h * sin30) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (-w/2, -h/2) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (w/2 * cos60, w/2 * sin60)); + dia2 = dia1 * Geom::Affine (Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2 * cos60, w/2 * sin60)); + dia3 = dia2 * Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2, h/2)); + dia4 = dia3 * dia1.inverse(); + dia5 = dia3 * dia2.inverse(); + } + if (i % 6 == 0) { + return d_s_r * ori; + } else if (i % 6 == 1) { + return d_s_r * rotate_m60_c * dia1 * ori; + } else if (i % 6 == 2) { + return d_s_r * rotate_m120_c * dia2 * ori; + } else if (i % 6 == 3) { + return d_s_r * rotate_180_c * dia3 * ori; + } else if (i % 6 == 4) { + return d_s_r * rotate_120_c * dia4 * ori; + } else if (i % 6 == 5) { + return d_s_r * rotate_60_c * dia5 * ori; + } + } + break; + + case TILE_P6M: + { + + Geom::Affine ori; + Geom::Affine dia1, dia2, dia3, dia4, dia5, dia6, dia7, dia8, dia9, dia10; + if (w > h) { + ori = Geom::Affine(Geom::Translate (w * pow((2*(i/12) + (j%2)), shiftx_exp) + dx, (2*w * sin60) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (w/2, h/2) * Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, h/2 * sin30)); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, -h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (-h/2 * cos30, h/2 * sin30) * Geom::Translate (w * cos60, 0) * Geom::Translate (-h/2 * cos30, -h/2 * sin30)); + dia4 = dia3 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia5 = dia4 * Geom::Affine (Geom::Translate (-h/2 * cos30, -h/2 * sin30) * Geom::Translate (-w/2 * cos60, w/2 * sin60) * Geom::Translate (w/2, -h/2)); + dia6 = dia5 * Geom::Affine (Geom::Translate (0, h)); + dia7 = dia6 * dia1.inverse(); + dia8 = dia6 * dia2.inverse(); + dia9 = dia6 * dia3.inverse(); + dia10 = dia6 * dia4.inverse(); + } else { + ori = Geom::Affine(Geom::Translate (4*h * cos30 * pow((i/12 + 0.5*(j%2)), shiftx_exp) + dx, (2*h + 2*h * sin30) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (-w/2, -h/2) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (w/2 * cos60, w/2 * sin60)); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, -h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (h * cos30, 0) * Geom::Translate (-w/2 * cos60, w/2 * sin60)); + dia4 = dia3 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia5 = dia4 * Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2, h/2)); + dia6 = dia5 * Geom::Affine (Geom::Translate (0, h)); + dia7 = dia6 * dia1.inverse(); + dia8 = dia6 * dia2.inverse(); + dia9 = dia6 * dia3.inverse(); + dia10 = dia6 * dia4.inverse(); + } + if (i % 12 == 0) { + return d_s_r * ori; + } else if (i % 12 == 1) { + return d_s_r * flip_y * rotate_m60_c * dia1 * ori; + } else if (i % 12 == 2) { + return d_s_r * rotate_m60_c * dia2 * ori; + } else if (i % 12 == 3) { + return d_s_r * flip_y * rotate_m120_c * dia3 * ori; + } else if (i % 12 == 4) { + return d_s_r * rotate_m120_c * dia4 * ori; + } else if (i % 12 == 5) { + return d_s_r * flip_x * dia5 * ori; + } else if (i % 12 == 6) { + return d_s_r * flip_x * flip_y * dia6 * ori; + } else if (i % 12 == 7) { + return d_s_r * flip_y * rotate_120_c * dia7 * ori; + } else if (i % 12 == 8) { + return d_s_r * rotate_120_c * dia8 * ori; + } else if (i % 12 == 9) { + return d_s_r * flip_y * rotate_60_c * dia9 * ori; + } else if (i % 12 == 10) { + return d_s_r * rotate_60_c * dia10 * ori; + } else if (i % 12 == 11) { + return d_s_r * flip_y * Geom::Translate (0, h) * ori; + } + } + break; + + default: + break; + } + + return Geom::identity(); +} + +bool CloneTiler::is_a_clone_of(SPObject *tile, SPObject *obj) +{ + bool result = false; + char *id_href = nullptr; + + if (obj) { + Inkscape::XML::Node *obj_repr = obj->getRepr(); + id_href = g_strdup_printf("#%s", obj_repr->attribute("id")); + } + + if (dynamic_cast<SPUse *>(tile) && + tile->getRepr()->attribute("xlink:href") && + (!id_href || !strcmp(id_href, tile->getRepr()->attribute("xlink:href"))) && + tile->getRepr()->attribute("inkscape:tiled-clone-of") && + (!id_href || !strcmp(id_href, tile->getRepr()->attribute("inkscape:tiled-clone-of")))) + { + result = true; + } else { + result = false; + } + if (id_href) { + g_free(id_href); + id_href = nullptr; + } + return result; +} + +void CloneTiler::trace_hide_tiled_clones_recursively(SPObject *from) +{ + if (!trace_drawing) + return; + + for (auto& o: from->children) { + SPItem *item = dynamic_cast<SPItem *>(&o); + if (item && is_a_clone_of(&o, nullptr)) { + item->invoke_hide(trace_visionkey); // FIXME: hide each tiled clone's original too! + } + trace_hide_tiled_clones_recursively (&o); + } +} + +void CloneTiler::trace_setup(SPDocument *doc, gdouble zoom, SPItem *original) +{ + trace_drawing = new Inkscape::Drawing(); + /* Create ArenaItem and set transform */ + trace_visionkey = SPItem::display_key_new(1); + trace_doc = doc; + trace_drawing->setRoot(trace_doc->getRoot()->invoke_show(*trace_drawing, trace_visionkey, SP_ITEM_SHOW_DISPLAY)); + + // hide the (current) original and any tiled clones, we only want to pick the background + original->invoke_hide(trace_visionkey); + trace_hide_tiled_clones_recursively(trace_doc->getRoot()); + + trace_doc->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + trace_doc->ensureUpToDate(); + + trace_zoom = zoom; +} + +guint32 CloneTiler::trace_pick(Geom::Rect box) +{ + if (!trace_drawing) { + return 0; + } + + trace_drawing->root()->setTransform(Geom::Scale(trace_zoom)); + trace_drawing->update(); + + /* Item integer bbox in points */ + Geom::IntRect ibox = (box * Geom::Scale(trace_zoom)).roundOutwards(); + + /* Find visible area */ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, ibox.width(), ibox.height()); + Inkscape::DrawingContext dc(s, ibox.min()); + /* Render */ + trace_drawing->render(dc, ibox); + double R = 0, G = 0, B = 0, A = 0; + ink_cairo_surface_average_color(s, R, G, B, A); + cairo_surface_destroy(s); + + return SP_RGBA32_F_COMPOSE (R, G, B, A); +} + +void CloneTiler::trace_finish() +{ + if (trace_doc) { + trace_doc->getRoot()->invoke_hide(trace_visionkey); + delete trace_drawing; + trace_doc = nullptr; + trace_drawing = nullptr; + } +} + +void CloneTiler::unclump() +{ + auto selection = getSelection(); + if (!selection) + return; + + // check if something is selected + if (selection->isEmpty() || boost::distance(selection->items()) > 1) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>one object</b> whose tiled clones to unclump.")); + return; + } + + auto obj = selection->singleItem(); + auto parent = obj->parent; + + std::vector<SPItem*> to_unclump; // not including the original + + for (auto& child: parent->children) { + if (is_a_clone_of (&child, obj)) { + to_unclump.push_back((SPItem*)&child); + } + } + + getDocument()->ensureUpToDate(); + reverse(to_unclump.begin(),to_unclump.end()); + ::unclump (to_unclump); + + DocumentUndo::done(getDocument(), _("Unclump tiled clones"), INKSCAPE_ICON("dialog-tile-clones")); +} + +guint CloneTiler::number_of_clones(SPObject *obj) +{ + SPObject *parent = obj->parent; + + guint n = 0; + + for (auto& child: parent->children) { + if (is_a_clone_of (&child, obj)) { + n ++; + } + } + + return n; +} + +void CloneTiler::remove(bool do_undo/* = true*/) +{ + auto selection = getSelection(); + if (!selection) + return; + + // check if something is selected + if (selection->isEmpty() || boost::distance(selection->items()) > 1) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>one object</b> whose tiled clones to remove.")); + return; + } + + SPObject *obj = selection->singleItem(); + SPObject *parent = obj->parent; + +// remove old tiling + std::vector<SPObject *> to_delete; + for (auto& child: parent->children) { + if (is_a_clone_of (&child, obj)) { + to_delete.push_back(&child); + } + } + for (auto obj:to_delete) { + g_assert(obj != nullptr); + obj->deleteObject(); + } + + change_selection (selection); + + if (do_undo) { + DocumentUndo::done(getDocument(), _("Delete tiled clones"), INKSCAPE_ICON("dialog-tile-clones")); + } +} + +Geom::Rect CloneTiler::transform_rect(Geom::Rect const &r, Geom::Affine const &m) +{ + using Geom::X; + using Geom::Y; + Geom::Point const p1 = r.corner(1) * m; + Geom::Point const p2 = r.corner(2) * m; + Geom::Point const p3 = r.corner(3) * m; + Geom::Point const p4 = r.corner(4) * m; + return Geom::Rect( + Geom::Point( + std::min(std::min(p1[X], p2[X]), std::min(p3[X], p4[X])), + std::min(std::min(p1[Y], p2[Y]), std::min(p3[Y], p4[Y]))), + Geom::Point( + std::max(std::max(p1[X], p2[X]), std::max(p3[X], p4[X])), + std::max(std::max(p1[Y], p2[Y]), std::max(p3[Y], p4[Y])))); +} + +/** +Randomizes \a val by \a rand, with 0 < val < 1 and all values (including 0, 1) having the same +probability of being displaced. + */ +double CloneTiler::randomize01(double val, double rand) +{ + double base = MIN (val - rand, 1 - 2*rand); + if (base < 0) { + base = 0; + } + val = base + g_random_double_range (0, MIN (2 * rand, 1 - base)); + return CLAMP(val, 0, 1); // this should be unnecessary with the above provisions, but just in case... +} + + +void CloneTiler::apply() +{ + auto desktop = getDesktop(); + auto selection = getSelection(); + if (!selection) + return; + + // check if something is selected + if (selection->isEmpty()) { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select an <b>object</b> to clone.")); + return; + } + + // Check if more than one object is selected. + if (boost::distance(selection->items()) > 1) { + desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("If you want to clone several objects, <b>group</b> them and <b>clone the group</b>.")); + return; + } + + // set "busy" cursor + desktop->setWaitingCursor(); + + // set statusbar text + _status->set_markup(_("<small>Creating tiled clones...</small>")); + _status->queue_draw(); + + SPObject *obj = selection->singleItem(); + if (!obj) { + // Should never happen (empty selection checked above). + std::cerr << "CloneTiler::clonetile_apply(): No object in single item selection!!!" << std::endl; + return; + } + Inkscape::XML::Node *obj_repr = obj->getRepr(); + const char *id_href = g_strdup_printf("#%s", obj_repr->attribute("id")); + SPObject *parent = obj->parent; + + remove(false); + + Geom::Scale scale = getDocument()->getDocumentScale().inverse(); + double scale_units = scale[Geom::X]; // Use just x direction.... + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double shiftx_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_per_i", 0, -10000, 10000); + double shifty_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_per_i", 0, -10000, 10000); + double shiftx_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_per_j", 0, -10000, 10000); + double shifty_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_per_j", 0, -10000, 10000); + double shiftx_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_rand", 0, 0, 1000); + double shifty_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_rand", 0, 0, 1000); + double shiftx_exp = prefs->getDoubleLimited(prefs_path + "shiftx_exp", 1, 0, 10); + double shifty_exp = prefs->getDoubleLimited(prefs_path + "shifty_exp", 1, 0, 10); + bool shiftx_alternate = prefs->getBool(prefs_path + "shiftx_alternate"); + bool shifty_alternate = prefs->getBool(prefs_path + "shifty_alternate"); + bool shiftx_cumulate = prefs->getBool(prefs_path + "shiftx_cumulate"); + bool shifty_cumulate = prefs->getBool(prefs_path + "shifty_cumulate"); + bool shiftx_excludew = prefs->getBool(prefs_path + "shiftx_excludew"); + bool shifty_excludeh = prefs->getBool(prefs_path + "shifty_excludeh"); + + double scalex_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_per_i", 0, -100, 1000); + double scaley_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_per_i", 0, -100, 1000); + double scalex_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_per_j", 0, -100, 1000); + double scaley_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_per_j", 0, -100, 1000); + double scalex_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_rand", 0, 0, 1000); + double scaley_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_rand", 0, 0, 1000); + double scalex_exp = prefs->getDoubleLimited(prefs_path + "scalex_exp", 1, 0, 10); + double scaley_exp = prefs->getDoubleLimited(prefs_path + "scaley_exp", 1, 0, 10); + double scalex_log = prefs->getDoubleLimited(prefs_path + "scalex_log", 0, 0, 10); + double scaley_log = prefs->getDoubleLimited(prefs_path + "scaley_log", 0, 0, 10); + bool scalex_alternate = prefs->getBool(prefs_path + "scalex_alternate"); + bool scaley_alternate = prefs->getBool(prefs_path + "scaley_alternate"); + bool scalex_cumulate = prefs->getBool(prefs_path + "scalex_cumulate"); + bool scaley_cumulate = prefs->getBool(prefs_path + "scaley_cumulate"); + + double rotate_per_i = prefs->getDoubleLimited(prefs_path + "rotate_per_i", 0, -180, 180); + double rotate_per_j = prefs->getDoubleLimited(prefs_path + "rotate_per_j", 0, -180, 180); + double rotate_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "rotate_rand", 0, 0, 100); + bool rotate_alternatei = prefs->getBool(prefs_path + "rotate_alternatei"); + bool rotate_alternatej = prefs->getBool(prefs_path + "rotate_alternatej"); + bool rotate_cumulatei = prefs->getBool(prefs_path + "rotate_cumulatei"); + bool rotate_cumulatej = prefs->getBool(prefs_path + "rotate_cumulatej"); + + double blur_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_per_i", 0, 0, 100); + double blur_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_per_j", 0, 0, 100); + bool blur_alternatei = prefs->getBool(prefs_path + "blur_alternatei"); + bool blur_alternatej = prefs->getBool(prefs_path + "blur_alternatej"); + double blur_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_rand", 0, 0, 100); + + double opacity_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_per_i", 0, 0, 100); + double opacity_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_per_j", 0, 0, 100); + bool opacity_alternatei = prefs->getBool(prefs_path + "opacity_alternatei"); + bool opacity_alternatej = prefs->getBool(prefs_path + "opacity_alternatej"); + double opacity_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_rand", 0, 0, 100); + + Glib::ustring initial_color = prefs->getString(prefs_path + "initial_color"); + double hue_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_per_j", 0, -100, 100); + double hue_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_per_i", 0, -100, 100); + double hue_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_rand", 0, 0, 100); + double saturation_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_per_j", 0, -100, 100); + double saturation_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_per_i", 0, -100, 100); + double saturation_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_rand", 0, 0, 100); + double lightness_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_per_j", 0, -100, 100); + double lightness_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_per_i", 0, -100, 100); + double lightness_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_rand", 0, 0, 100); + bool color_alternatej = prefs->getBool(prefs_path + "color_alternatej"); + bool color_alternatei = prefs->getBool(prefs_path + "color_alternatei"); + + int type = prefs->getInt(prefs_path + "symmetrygroup", 0); + bool keepbbox = prefs->getBool(prefs_path + "keepbbox", true); + int imax = prefs->getInt(prefs_path + "imax", 2); + int jmax = prefs->getInt(prefs_path + "jmax", 2); + + bool fillrect = prefs->getBool(prefs_path + "fillrect"); + double fillwidth = scale_units*prefs->getDoubleLimited(prefs_path + "fillwidth", 50, 0, 1e6); + double fillheight = scale_units*prefs->getDoubleLimited(prefs_path + "fillheight", 50, 0, 1e6); + + bool dotrace = prefs->getBool(prefs_path + "dotrace"); + int pick = prefs->getInt(prefs_path + "pick"); + bool pick_to_presence = prefs->getBool(prefs_path + "pick_to_presence"); + bool pick_to_size = prefs->getBool(prefs_path + "pick_to_size"); + bool pick_to_color = prefs->getBool(prefs_path + "pick_to_color"); + bool pick_to_opacity = prefs->getBool(prefs_path + "pick_to_opacity"); + double rand_picked = 0.01 * prefs->getDoubleLimited(prefs_path + "rand_picked", 0, 0, 100); + bool invert_picked = prefs->getBool(prefs_path + "invert_picked"); + double gamma_picked = prefs->getDoubleLimited(prefs_path + "gamma_picked", 0, -10, 10); + + SPItem *item = dynamic_cast<SPItem *>(obj); + if (dotrace) { + trace_setup(getDocument(), 1.0, item); + } + + Geom::Point center; + double w = 0; + double h = 0; + double x0 = 0; + double y0 = 0; + + if (keepbbox && + obj_repr->attribute("inkscape:tile-w") && + obj_repr->attribute("inkscape:tile-h") && + obj_repr->attribute("inkscape:tile-x0") && + obj_repr->attribute("inkscape:tile-y0") && + obj_repr->attribute("inkscape:tile-cx") && + obj_repr->attribute("inkscape:tile-cy")) { + + double cx = obj_repr->getAttributeDouble("inkscape:tile-cx", 0); + double cy = obj_repr->getAttributeDouble("inkscape:tile-cy", 0); + center = Geom::Point (cx, cy); + + w = obj_repr->getAttributeDouble("inkscape:tile-w", w); + h = obj_repr->getAttributeDouble("inkscape:tile-h", h); + x0 = obj_repr->getAttributeDouble("inkscape:tile-x0", x0); + y0 = obj_repr->getAttributeDouble("inkscape:tile-y0", y0); + } else { + bool prefs_bbox = prefs->getBool("/tools/bounding_box", false); + SPItem::BBoxType bbox_type = ( !prefs_bbox ? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX ); + Geom::OptRect r = item->documentBounds(bbox_type); + if (r) { + w = scale_units*r->dimensions()[Geom::X]; + h = scale_units*r->dimensions()[Geom::Y]; + x0 = scale_units*r->min()[Geom::X]; + y0 = scale_units*r->min()[Geom::Y]; + center = scale_units*desktop->dt2doc(item->getCenter()); + + obj_repr->setAttributeSvgDouble("inkscape:tile-cx", center[Geom::X]); + obj_repr->setAttributeSvgDouble("inkscape:tile-cy", center[Geom::Y]); + obj_repr->setAttributeSvgDouble("inkscape:tile-w", w); + obj_repr->setAttributeSvgDouble("inkscape:tile-h", h); + obj_repr->setAttributeSvgDouble("inkscape:tile-x0", x0); + obj_repr->setAttributeSvgDouble("inkscape:tile-y0", y0); + } else { + center = Geom::Point(0, 0); + w = h = 0; + x0 = y0 = 0; + } + } + + Geom::Point cur(0, 0); + Geom::Rect bbox_original (Geom::Point (x0, y0), Geom::Point (x0 + w, y0 + h)); + double perimeter_original = (w + h)/4; + + // The integers i and j are reserved for tile column and row. + // The doubles x and y are used for coordinates + for (int i = 0; + fillrect? + (fabs(cur[Geom::X]) < fillwidth && i < 200) // prevent "freezing" with too large fillrect, arbitrarily limit rows + : (i < imax); + i ++) { + for (int j = 0; + fillrect? + (fabs(cur[Geom::Y]) < fillheight && j < 200) // prevent "freezing" with too large fillrect, arbitrarily limit cols + : (j < jmax); + j ++) { + + // Note: We create a clone at 0,0 too, right over the original, in case our clones are colored + + // Get transform from symmetry, shift, scale, rotation + Geom::Affine orig_t = get_transform (type, i, j, center[Geom::X], center[Geom::Y], w, h, + shiftx_per_i, shifty_per_i, + shiftx_per_j, shifty_per_j, + shiftx_rand, shifty_rand, + shiftx_exp, shifty_exp, + shiftx_alternate, shifty_alternate, + shiftx_cumulate, shifty_cumulate, + shiftx_excludew, shifty_excludeh, + scalex_per_i, scaley_per_i, + scalex_per_j, scaley_per_j, + scalex_rand, scaley_rand, + scalex_exp, scaley_exp, + scalex_log, scaley_log, + scalex_alternate, scaley_alternate, + scalex_cumulate, scaley_cumulate, + rotate_per_i, rotate_per_j, + rotate_rand, + rotate_alternatei, rotate_alternatej, + rotate_cumulatei, rotate_cumulatej ); + Geom::Affine parent_transform = (((SPItem*)item->parent)->i2doc_affine())*(item->document->getRoot()->c2p.inverse()); + Geom::Affine t = parent_transform*orig_t*parent_transform.inverse(); + cur = center * t - center; + if (fillrect) { + if ((cur[Geom::X] > fillwidth) || (cur[Geom::Y] > fillheight)) { // off limits + continue; + } + } + + gchar color_string[32]; *color_string = 0; + + // Color tab + if (!initial_color.empty()) { + guint32 rgba = sp_svg_read_color (initial_color.data(), 0x000000ff); + float hsl[3]; + SPColor::rgb_to_hsl_floatv (hsl, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba)); + + double eff_i = (color_alternatei? (i%2) : (i)); + double eff_j = (color_alternatej? (j%2) : (j)); + + hsl[0] += hue_per_i * eff_i + hue_per_j * eff_j + hue_rand * g_random_double_range (-1, 1); + double notused; + hsl[0] = modf( hsl[0], ¬used ); // Restrict to 0-1 + hsl[1] += saturation_per_i * eff_i + saturation_per_j * eff_j + saturation_rand * g_random_double_range (-1, 1); + hsl[1] = CLAMP (hsl[1], 0, 1); + hsl[2] += lightness_per_i * eff_i + lightness_per_j * eff_j + lightness_rand * g_random_double_range (-1, 1); + hsl[2] = CLAMP (hsl[2], 0, 1); + + float rgb[3]; + SPColor::hsl_to_rgb_floatv (rgb, hsl[0], hsl[1], hsl[2]); + sp_svg_write_color(color_string, sizeof(color_string), SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1.0)); + } + + // Blur + double blur = 0.0; + { + int eff_i = (blur_alternatei? (i%2) : (i)); + int eff_j = (blur_alternatej? (j%2) : (j)); + blur = (blur_per_i * eff_i + blur_per_j * eff_j + blur_rand * g_random_double_range (-1, 1)); + blur = CLAMP (blur, 0, 1); + } + + // Opacity + double opacity = 1.0; + { + int eff_i = (opacity_alternatei? (i%2) : (i)); + int eff_j = (opacity_alternatej? (j%2) : (j)); + opacity = 1 - (opacity_per_i * eff_i + opacity_per_j * eff_j + opacity_rand * g_random_double_range (-1, 1)); + opacity = CLAMP (opacity, 0, 1); + } + + // Trace tab + if (dotrace) { + Geom::Rect bbox_t = transform_rect (bbox_original, t*Geom::Scale(1.0/scale_units)); + + guint32 rgba = trace_pick (bbox_t); + float r = SP_RGBA32_R_F(rgba); + float g = SP_RGBA32_G_F(rgba); + float b = SP_RGBA32_B_F(rgba); + float a = SP_RGBA32_A_F(rgba); + + float hsl[3]; + SPColor::rgb_to_hsl_floatv (hsl, r, g, b); + + gdouble val = 0; + switch (pick) { + case PICK_COLOR: + val = 1 - hsl[2]; // inverse lightness; to match other picks where black = max + break; + case PICK_OPACITY: + val = a; + break; + case PICK_R: + val = r; + break; + case PICK_G: + val = g; + break; + case PICK_B: + val = b; + break; + case PICK_H: + val = hsl[0]; + break; + case PICK_S: + val = hsl[1]; + break; + case PICK_L: + val = 1 - hsl[2]; + break; + default: + break; + } + + if (rand_picked > 0) { + val = randomize01 (val, rand_picked); + r = randomize01 (r, rand_picked); + g = randomize01 (g, rand_picked); + b = randomize01 (b, rand_picked); + } + + if (gamma_picked != 0) { + double power; + if (gamma_picked > 0) + power = 1/(1 + fabs(gamma_picked)); + else + power = 1 + fabs(gamma_picked); + + val = pow (val, power); + r = pow (r, power); + g = pow (g, power); + b = pow (b, power); + } + + if (invert_picked) { + val = 1 - val; + r = 1 - r; + g = 1 - g; + b = 1 - b; + } + + val = CLAMP (val, 0, 1); + r = CLAMP (r, 0, 1); + g = CLAMP (g, 0, 1); + b = CLAMP (b, 0, 1); + + // recompose tweaked color + rgba = SP_RGBA32_F_COMPOSE(r, g, b, a); + + if (pick_to_presence) { + if (g_random_double_range (0, 1) > val) { + continue; // skip! + } + } + if (pick_to_size) { + t = parent_transform * Geom::Translate(-center[Geom::X], -center[Geom::Y]) + * Geom::Scale (val, val) * Geom::Translate(center[Geom::X], center[Geom::Y]) + * parent_transform.inverse() * t; + } + if (pick_to_opacity) { + opacity *= val; + } + if (pick_to_color) { + sp_svg_write_color(color_string, sizeof(color_string), rgba); + } + } + + if (opacity < 1e-6) { // invisibly transparent, skip + continue; + } + + if (fabs(t[0]) + fabs (t[1]) + fabs(t[2]) + fabs(t[3]) < 1e-6) { // too small, skip + continue; + } + + // Create the clone + Inkscape::XML::Node *clone = obj_repr->document()->createElement("svg:use"); + clone->setAttribute("x", "0"); + clone->setAttribute("y", "0"); + clone->setAttribute("inkscape:tiled-clone-of", id_href); + clone->setAttribute("xlink:href", id_href); + + Geom::Point new_center; + bool center_set = false; + if (obj_repr->attribute("inkscape:transform-center-x") || obj_repr->attribute("inkscape:transform-center-y")) { + new_center = scale_units*desktop->dt2doc(item->getCenter()) * orig_t; + center_set = true; + } + + clone->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(t)); + + if (opacity < 1.0) { + clone->setAttributeCssDouble("opacity", opacity); + } + + if (*color_string) { + clone->setAttribute("fill", color_string); + clone->setAttribute("stroke", color_string); + } + + // add the new clone to the top of the original's parent + parent->getRepr()->appendChild(clone); + + if (blur > 0.0) { + SPObject *clone_object = desktop->getDocument()->getObjectByRepr(clone); + SPItem *item = dynamic_cast<SPItem *>(clone_object); + double radius = blur * perimeter_original * t.descrim(); + // this is necessary for all newly added clones to have correct bboxes, + // otherwise filters won't work: + desktop->getDocument()->ensureUpToDate(); + SPFilter *constructed = new_filter_gaussian_blur(desktop->getDocument(), radius, t.descrim()); + constructed->update_filter_region(item); + sp_style_set_property_url (clone_object, "filter", constructed, false); + } + + if (center_set) { + SPObject *clone_object = desktop->getDocument()->getObjectByRepr(clone); + SPItem *item = dynamic_cast<SPItem *>(clone_object); + if (clone_object && item) { + clone_object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + item->setCenter(desktop->doc2dt(new_center)); + clone_object->updateRepr(); + } + } + + Inkscape::GC::release(clone); + } + cur[Geom::Y] = 0; + } + + if (dotrace) { + trace_finish (); + } + + change_selection(selection); + + desktop->clearWaitingCursor(); + DocumentUndo::done(getDocument(), _("Create tiled clones"), INKSCAPE_ICON("dialog-tile-clones")); +} + +Gtk::Box * CloneTiler::new_tab(Gtk::Notebook *nb, const gchar *label) +{ + auto l = Gtk::manage(new Gtk::Label(label, true)); + auto vb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, VB_MARGIN)); + vb->set_homogeneous(false); + vb->set_border_width(VB_MARGIN); + nb->append_page(*vb, *l); + return vb; +} + +void CloneTiler::checkbox_toggled(Gtk::ToggleButton *tb, + const Glib::ustring &attr) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + attr, tb->get_active()); +} + +Gtk::Widget * CloneTiler::checkbox(const char *tip, + const Glib::ustring &attr) +{ + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + auto b = Gtk::manage(new UI::Widget::CheckButtonInternal()); + b->set_tooltip_text(tip); + + auto const prefs = Inkscape::Preferences::get(); + auto const value = prefs->getBool(prefs_path + attr); + b->set_active(value); + + hb->pack_start(*b, false, true); + b->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::checkbox_toggled), b, attr)); + + b->set_uncheckable(); + + return hb; +} + +void CloneTiler::value_changed(Glib::RefPtr<Gtk::Adjustment> &adj, + Glib::ustring const &pref) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble(prefs_path + pref, adj->get_value()); +} + +Gtk::Widget * CloneTiler::spinbox(const char *tip, + const Glib::ustring &attr, + double lower, + double upper, + const gchar *suffix, + bool exponent/* = false*/) +{ + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + + { + // Parameters for adjustment + auto const initial_value = (exponent ? 1.0 : 0.0); + auto const step_increment = (exponent ? 0.01 : 0.1); + auto const page_increment = (exponent ? 0.05 : 0.4); + + auto a = Gtk::Adjustment::create(initial_value, + lower, + upper, + step_increment, + page_increment); + + auto const climb_rate = (exponent ? 0.01 : 0.1); + auto const digits = (exponent ? 2 : 1); + + auto sb = new Inkscape::UI::Widget::SpinButton(a, climb_rate, digits); + + sb->set_tooltip_text (tip); + sb->set_width_chars (5); + sb->set_digits(3); + hb->pack_start(*sb, false, false, SB_MARGIN); + + auto prefs = Inkscape::Preferences::get(); + auto value = prefs->getDoubleLimited(prefs_path + attr, exponent? 1.0 : 0.0, lower, upper); + a->set_value (value); + a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::value_changed), a, attr)); + + if (exponent) { + sb->set_oneable(); + } else { + sb->set_zeroable(); + } + } + + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(suffix); + hb->pack_start(*l); + } + + return hb; +} + +void CloneTiler::symgroup_changed(Gtk::ComboBox *cb) +{ + auto prefs = Inkscape::Preferences::get(); + auto group_new = cb->get_active_row_number(); + prefs->setInt(prefs_path + "symmetrygroup", group_new); +} + +void CloneTiler::xy_changed(Glib::RefPtr<Gtk::Adjustment> &adj, Glib::ustring const &pref) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt(prefs_path + pref, (int) floor(adj->get_value() + 0.5)); +} + +void CloneTiler::keep_bbox_toggled() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + "keepbbox", _cb_keep_bbox->get_active()); +} + +void CloneTiler::pick_to(Gtk::ToggleButton *tb, Glib::ustring const &pref) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + pref, tb->get_active()); +} + + +void CloneTiler::reset_recursive(Gtk::Widget *w) +{ + if (w) { + auto sb = dynamic_cast<Inkscape::UI::Widget::SpinButton *>(w); + auto tb = dynamic_cast<Inkscape::UI::Widget::CheckButtonInternal *>(w); + + { + if (sb && sb->get_zeroable()) { // spinbutton + auto a = sb->get_adjustment(); + a->set_value(0); + } + } + { + if (sb && sb->get_oneable()) { // spinbutton + auto a = sb->get_adjustment(); + a->set_value(1); + } + } + { + if (tb && tb->get_uncheckable()) { // checkbox + tb->set_active(false); + } + } + } + + auto container = dynamic_cast<Gtk::Container *>(w); + + if (container) { + auto c = container->get_children(); + for (auto i : c) { + reset_recursive(i); + } + } +} + +void CloneTiler::reset() +{ + reset_recursive(this); +} + +void CloneTiler::table_attach(Gtk::Grid *table, Gtk::Widget *widget, float align, int row, int col) +{ + widget->set_halign(Gtk::ALIGN_FILL); + widget->set_valign(Gtk::ALIGN_CENTER); + table->attach(*widget, col, row, 1, 1); +} + +Gtk::Grid * CloneTiler::table_x_y_rand(int values) +{ + auto table = Gtk::manage(new Gtk::Grid()); + table->set_row_spacing(6); + table->set_column_spacing(8); + + table->set_border_width(VB_MARGIN); + + { + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + hb->set_homogeneous(false); + + auto i = Glib::wrap(sp_get_icon_image("object-rows", GTK_ICON_SIZE_MENU)); + hb->pack_start(*i, false, false, 2); + + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<small>Per row:</small>")); + hb->pack_start(*l, false, false, 2); + + table_attach(table, hb, 0, 1, 2); + } + + { + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + hb->set_homogeneous(false); + + auto i = Glib::wrap(sp_get_icon_image("object-columns", GTK_ICON_SIZE_MENU)); + hb->pack_start(*i, false, false, 2); + + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<small>Per column:</small>")); + hb->pack_start(*l, false, false, 2); + + table_attach(table, hb, 0, 1, 3); + } + + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(_("<small>Randomize:</small>")); + table_attach(table, l, 0, 1, 4); + } + + return table; +} + +void CloneTiler::pick_switched(PickType v) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt(prefs_path + "pick", v); +} + +void CloneTiler::switch_to_create() +{ + for (auto w : _rowscols) { + w->set_sensitive(true); + } + for (auto w : _widthheight) { + w->set_sensitive(false); + } + + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + "fillrect", false); +} + + +void CloneTiler::switch_to_fill() +{ + for (auto w : _rowscols) { + w->set_sensitive(false); + } + for (auto w : _widthheight) { + w->set_sensitive(true); + } + + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + "fillrect", true); +} + +void CloneTiler::fill_width_changed() +{ + auto const raw_dist = fill_width->get_value(); + auto const unit = unit_menu->getUnit(); + auto const pixels = Inkscape::Util::Quantity::convert(raw_dist, unit, "px"); + + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble(prefs_path + "fillwidth", pixels); +} + +void CloneTiler::fill_height_changed() +{ + auto const raw_dist = fill_height->get_value(); + auto const unit = unit_menu->getUnit(); + auto const pixels = Inkscape::Util::Quantity::convert(raw_dist, unit, "px"); + + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble(prefs_path + "fillheight", pixels); +} + +void CloneTiler::unit_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gdouble width_pixels = prefs->getDouble(prefs_path + "fillwidth"); + gdouble height_pixels = prefs->getDouble(prefs_path + "fillheight"); + + Inkscape::Util::Unit const *unit = unit_menu->getUnit(); + + gdouble width_value = Inkscape::Util::Quantity::convert(width_pixels, "px", unit); + gdouble height_value = Inkscape::Util::Quantity::convert(height_pixels, "px", unit); + fill_width->set_value(width_value); + fill_height->set_value(height_value); +} + +void CloneTiler::do_pick_toggled() +{ + auto prefs = Inkscape::Preferences::get(); + auto active = _b->get_active(); + prefs->setBool(prefs_path + "dotrace", active); + + if (_dotrace) { + _dotrace->set_sensitive(active); + } +} + +void CloneTiler::show_page_trace() +{ + nb->set_current_page(6); + _b->set_active(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/ui/dialog/clonetiler.h b/src/ui/dialog/clonetiler.h new file mode 100644 index 0000000..8713d10 --- /dev/null +++ b/src/ui/dialog/clonetiler.h @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Clone tiling dialog + */ +/* Authors: + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2004 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef __SP_CLONE_TILER_H__ +#define __SP_CLONE_TILER_H__ + +#include "ui/dialog/dialog-base.h" +#include "ui/widget/color-picker.h" + +namespace Gtk { + class Box; + class ComboBox; + class Grid; + class Notebook; + class SizeGroup; + class ToggleButton; +} + +class SPItem; +class SPObject; + +namespace Geom { + class Rect; + class Affine; +} + +namespace Inkscape { +namespace UI { + +namespace Widget { + class CheckButtonInternal; + class UnitMenu; +} + +namespace Dialog { + +class CloneTiler : public DialogBase +{ +public: + CloneTiler(); + ~CloneTiler() override; + + static CloneTiler &getInstance() { return *new CloneTiler(); } + void show_page_trace(); +protected: + enum PickType { + PICK_COLOR, + PICK_OPACITY, + PICK_R, + PICK_G, + PICK_B, + PICK_H, + PICK_S, + PICK_L + }; + + Gtk::Box * new_tab(Gtk::Notebook *nb, const gchar *label); + Gtk::Grid * table_x_y_rand(int values); + Gtk::Widget * spinbox(const char *tip, + const Glib::ustring &attr, + double lower, + double upper, + const gchar *suffix, + bool exponent = false); + Gtk::Widget * checkbox(const char *tip, + const Glib::ustring &attr); + void table_attach(Gtk::Grid *table, Gtk::Widget *widget, float align, int row, int col); + + void symgroup_changed(Gtk::ComboBox *cb); + void on_picker_color_changed(guint rgba); + void trace_hide_tiled_clones_recursively(SPObject *from); + guint number_of_clones(SPObject *obj); + void trace_setup(SPDocument *doc, gdouble zoom, SPItem *original); + guint32 trace_pick(Geom::Rect box); + void trace_finish(); + bool is_a_clone_of(SPObject *tile, SPObject *obj); + Geom::Rect transform_rect(Geom::Rect const &r, Geom::Affine const &m); + double randomize01(double val, double rand); + + void apply(); + void change_selection(Inkscape::Selection *selection); + void checkbox_toggled(Gtk::ToggleButton *tb, + Glib::ustring const &attr); + void do_pick_toggled(); + void external_change(); + void fill_width_changed(); + void fill_height_changed(); + void keep_bbox_toggled(); + void on_remove_button_clicked() {remove();} + void pick_switched(PickType); + void pick_to(Gtk::ToggleButton *tb, + Glib::ustring const &pref); + void remove(bool do_undo = true); + void reset(); + void reset_recursive(Gtk::Widget *w); + void switch_to_create(); + void switch_to_fill(); + void unclump(); + void unit_changed(); + void value_changed(Glib::RefPtr<Gtk::Adjustment> &adj, Glib::ustring const &pref); + void xy_changed(Glib::RefPtr<Gtk::Adjustment> &adj, Glib::ustring const &pref); + + Geom::Affine get_transform( + // symmetry group + int type, + + // row, column + int i, int j, + + // center, width, height of the tile + double cx, double cy, + double w, double h, + + // values from the dialog: + // Shift + double shiftx_per_i, double shifty_per_i, + double shiftx_per_j, double shifty_per_j, + double shiftx_rand, double shifty_rand, + double shiftx_exp, double shifty_exp, + int shiftx_alternate, int shifty_alternate, + int shiftx_cumulate, int shifty_cumulate, + int shiftx_excludew, int shifty_excludeh, + + // Scale + double scalex_per_i, double scaley_per_i, + double scalex_per_j, double scaley_per_j, + double scalex_rand, double scaley_rand, + double scalex_exp, double scaley_exp, + double scalex_log, double scaley_log, + int scalex_alternate, int scaley_alternate, + int scalex_cumulate, int scaley_cumulate, + + // Rotation + double rotate_per_i, double rotate_per_j, + double rotate_rand, + int rotate_alternatei, int rotate_alternatej, + int rotate_cumulatei, int rotate_cumulatej + ); + + +private: + CloneTiler(CloneTiler const &d) = delete; + CloneTiler& operator=(CloneTiler const &d) = delete; + + UI::Widget::CheckButtonInternal *_b; + UI::Widget::CheckButtonInternal *_cb_keep_bbox; + Gtk::Notebook *nb = nullptr; + Inkscape::UI::Widget::ColorPicker *color_picker; + Glib::RefPtr<Gtk::SizeGroup> table_row_labels; + Inkscape::UI::Widget::UnitMenu *unit_menu; + + Glib::RefPtr<Gtk::Adjustment> fill_width; + Glib::RefPtr<Gtk::Adjustment> fill_height; + + sigc::connection selectChangedConn; + sigc::connection externChangedConn; + sigc::connection color_changed_connection; + sigc::connection unitChangedConn; + + // Variables that used to be set using GObject + Gtk::Box *_buttons_on_tiles; + Gtk::Box *_dotrace; + Gtk::Label *_status; + std::vector<Gtk::Widget*> _rowscols; + std::vector<Gtk::Widget*> _widthheight; +}; + +enum { + TILE_P1, + TILE_P2, + TILE_PM, + TILE_PG, + TILE_CM, + TILE_PMM, + TILE_PMG, + TILE_PGG, + TILE_CMM, + TILE_P4, + TILE_P4M, + TILE_P4G, + TILE_P3, + TILE_P31M, + TILE_P3M1, + TILE_P6, + TILE_P6M +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + +#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/ui/dialog/color-item.cpp b/src/ui/dialog/color-item.cpp new file mode 100644 index 0000000..1daadd9 --- /dev/null +++ b/src/ui/dialog/color-item.cpp @@ -0,0 +1,667 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkscape color swatch UI item. + */ +/* Authors: + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cerrno> + +#include <gtkmm/label.h> +#include <glibmm/i18n.h> + +#include "color-item.h" + +#include "desktop.h" + +#include "desktop-style.h" +#include "display/cairo-utils.h" +#include "document.h" +#include "document-undo.h" +#include "inkscape.h" // for SP_ACTIVE_DESKTOP +#include "message-context.h" + +#include "io/resource.h" +#include "io/sys.h" +#include "svg/svg-color.h" +#include "ui/icon-names.h" +#include "ui/widget/gradient-vector-selector.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static std::vector<std::string> mimeStrings; +static std::map<std::string, guint> mimeToInt; + + +void +ColorItem::handleClick() { + buttonClicked(false); +} + +void +ColorItem::handleSecondaryClick(gint /*arg1*/) { + buttonClicked(true); +} + +bool +ColorItem::handleEnterNotify(GdkEventCrossing* /*event*/) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if ( desktop ) { + gchar* msg = g_strdup_printf(_("Color: <b>%s</b>; <b>Click</b> to set fill, <b>Shift+click</b> to set stroke"), + def.descr.c_str()); + desktop->tipsMessageContext()->set(Inkscape::INFORMATION_MESSAGE, msg); + g_free(msg); + } + + return false; +} + +bool +ColorItem::handleLeaveNotify(GdkEventCrossing* /*event*/) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + if ( desktop ) { + desktop->tipsMessageContext()->clear(); + } + + return false; +} + +static bool getBlock( std::string& dst, guchar ch, std::string const & str ) +{ + bool good = false; + std::string::size_type pos = str.find(ch); + if ( pos != std::string::npos ) + { + std::string::size_type pos2 = str.find( '(', pos ); + if ( pos2 != std::string::npos ) { + std::string::size_type endPos = str.find( ')', pos2 ); + if ( endPos != std::string::npos ) { + dst = str.substr( pos2 + 1, (endPos - pos2 - 1) ); + good = true; + } + } + } + return good; +} + +static bool popVal( guint64& numVal, std::string& str ) +{ + bool good = false; + std::string::size_type endPos = str.find(','); + if ( endPos == std::string::npos ) { + endPos = str.length(); + } + + if ( endPos != std::string::npos && endPos > 0 ) { + std::string xxx = str.substr( 0, endPos ); + const gchar* ptr = xxx.c_str(); + gchar* endPtr = nullptr; + numVal = g_ascii_strtoull( ptr, &endPtr, 10 ); + if ( (numVal == G_MAXUINT64) && (ERANGE == errno) ) { + // overflow + } else if ( (numVal == 0) && (endPtr == ptr) ) { + // failed conversion + } else { + good = true; + str.erase( 0, endPos + 1 ); + } + } + + return good; +} + +// TODO resolve this more cleanly: +extern bool colorItemHandleButtonPress(GdkEventButton* event, UI::Widget::Preview *preview, gpointer user_data); + +void +ColorItem::drag_begin(const Glib::RefPtr<Gdk::DragContext> &dc) +{ + using Inkscape::IO::Resource::get_path; + using Inkscape::IO::Resource::PIXMAPS; + using Inkscape::IO::Resource::SYSTEM; + int width = 32; + int height = 24; + + if (def.getType() != ege::PaintDef::RGB){ + GError *error; + gsize bytesRead = 0; + gsize bytesWritten = 0; + gchar *localFilename = g_filename_from_utf8(get_path(SYSTEM, PIXMAPS, "remove-color.png"), -1, &bytesRead, + &bytesWritten, &error); + auto pixbuf = Gdk::Pixbuf::create_from_file(localFilename, width, height, false); + g_free(localFilename); + dc->set_icon(pixbuf, 0, 0); + } else { + Glib::RefPtr<Gdk::Pixbuf> pixbuf; + if (getGradient() ){ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_pattern_t *gradient = getGradient()->create_preview_pattern(width); + cairo_t *ct = cairo_create(s); + cairo_set_source(ct, gradient); + cairo_paint(ct); + cairo_destroy(ct); + cairo_pattern_destroy(gradient); + cairo_surface_flush(s); + + pixbuf = Glib::wrap(ink_pixbuf_create_from_cairo_surface(s)); + } else { + pixbuf = Gdk::Pixbuf::create( Gdk::COLORSPACE_RGB, false, 8, width, height ); + guint32 fillWith = (0xff000000 & (def.getR() << 24)) + | (0x00ff0000 & (def.getG() << 16)) + | (0x0000ff00 & (def.getB() << 8)); + pixbuf->fill( fillWith ); + } + dc->set_icon(pixbuf, 0, 0); + } +} + +//"drag-drop" +// gboolean dragDropColorData( GtkWidget *widget, +// GdkDragContext *drag_context, +// gint x, +// gint y, +// guint time, +// gpointer user_data) +// { +// // TODO finish + +// return TRUE; +// } + + +SwatchPage::SwatchPage() + : _prefWidth(0) +{ +} + +SwatchPage::~SwatchPage() += default; + + +ColorItem::ColorItem(ege::PaintDef::ColorType type) : + def(type), + _isFill(false), + _isStroke(false), + _isLive(false), + _linkIsTone(false), + _linkPercent(0), + _linkGray(0), + _linkSrc(nullptr), + _grad(nullptr), + _pattern(nullptr) +{ +} + +ColorItem::ColorItem( unsigned int r, unsigned int g, unsigned int b, Glib::ustring& name ) : + def( r, g, b, name.raw() ), + _isFill(false), + _isStroke(false), + _isLive(false), + _linkIsTone(false), + _linkPercent(0), + _linkGray(0), + _linkSrc(nullptr), + _grad(nullptr), + _pattern(nullptr) +{ +} + +ColorItem::~ColorItem() +{ + if (_pattern != nullptr) { + cairo_pattern_destroy(_pattern); + } +} + +ColorItem::ColorItem(ColorItem const &other) : + Inkscape::UI::Previewable() +{ + if ( this != &other ) { + *this = other; + } +} + +ColorItem &ColorItem::operator=(ColorItem const &other) +{ + if ( this != &other ) { + def = other.def; + + // TODO - correct linkage + _linkSrc = other._linkSrc; + g_message("Erk!"); + } + return *this; +} + +void ColorItem::setState( bool fill, bool stroke ) +{ + if ( (_isFill != fill) || (_isStroke != stroke) ) { + _isFill = fill; + _isStroke = stroke; + + for ( auto widget : _previews ) { + auto preview = dynamic_cast<UI::Widget::Preview *>(widget); + + if (preview) { + int val = preview->get_linked(); + val &= ~(UI::Widget::PREVIEW_FILL | UI::Widget::PREVIEW_STROKE); + if ( _isFill ) { + val |= UI::Widget::PREVIEW_FILL; + } + if ( _isStroke ) { + val |= UI::Widget::PREVIEW_STROKE; + } + preview->set_linked(static_cast<UI::Widget::LinkType>(val)); + } + } + } +} + +void ColorItem::setGradient(SPGradient *grad) +{ + if (_grad != grad) { + _grad = grad; + // TODO regen and push to listeners + } + + setName( gr_prepare_label(_grad) ); +} + +void ColorItem::setName(const Glib::ustring name) +{ + //def.descr = name; + + for (auto widget : _previews) { + auto preview = dynamic_cast<UI::Widget::Preview *>(widget); + auto label = dynamic_cast<Gtk::Label *>(widget); + if (preview) { + preview->set_tooltip_text(name); + } + else if (label) { + label->set_text(name); + } + } +} + +void ColorItem::setPattern(cairo_pattern_t *pattern) +{ + if (pattern) { + cairo_pattern_reference(pattern); + } + if (_pattern) { + cairo_pattern_destroy(_pattern); + } + _pattern = pattern; + + _updatePreviews(); +} + +void +ColorItem::_dragGetColorData(const Glib::RefPtr<Gdk::DragContext>& /*drag_context*/, + Gtk::SelectionData &data, + guint info, + guint /*time*/) +{ + std::string key; + if ( info < mimeStrings.size() ) { + key = mimeStrings[info]; + } else { + g_warning("ERROR: unknown value (%d)", info); + } + + if ( !key.empty() ) { + char* tmp = nullptr; + int len = 0; + int format = 0; + def.getMIMEData(key, tmp, len, format); + if ( tmp ) { + data.set(key, format, (guchar*)tmp, len ); + delete[] tmp; + } + } +} + +void ColorItem::_dropDataIn( GtkWidget */*widget*/, + GdkDragContext */*drag_context*/, + gint /*x*/, gint /*y*/, + GtkSelectionData */*data*/, + guint /*info*/, + guint /*event_time*/, + gpointer /*user_data*/) +{ +} + +void ColorItem::_colorDefChanged(void* data) +{ + ColorItem* item = reinterpret_cast<ColorItem*>(data); + if ( item ) { + item->_updatePreviews(); + } +} + +void ColorItem::_updatePreviews() +{ + for (auto widget : _previews) { + auto preview = dynamic_cast<UI::Widget::Preview *>(widget); + if (preview) { + _regenPreview(preview); + preview->queue_draw(); + } + } + + for (auto & _listener : _listeners) { + guint r = def.getR(); + guint g = def.getG(); + guint b = def.getB(); + + if ( _listener->_linkIsTone ) { + r = ( (_listener->_linkPercent * _listener->_linkGray) + ((100 - _listener->_linkPercent) * r) ) / 100; + g = ( (_listener->_linkPercent * _listener->_linkGray) + ((100 - _listener->_linkPercent) * g) ) / 100; + b = ( (_listener->_linkPercent * _listener->_linkGray) + ((100 - _listener->_linkPercent) * b) ) / 100; + } else { + r = ( (_listener->_linkPercent * 255) + ((100 - _listener->_linkPercent) * r) ) / 100; + g = ( (_listener->_linkPercent * 255) + ((100 - _listener->_linkPercent) * g) ) / 100; + b = ( (_listener->_linkPercent * 255) + ((100 - _listener->_linkPercent) * b) ) / 100; + } + + _listener->def.setRGB( r, g, b ); + } +} + +void ColorItem::_regenPreview(UI::Widget::Preview * preview) +{ + if ( def.getType() != ege::PaintDef::RGB ) { + using Inkscape::IO::Resource::get_path; + using Inkscape::IO::Resource::PIXMAPS; + using Inkscape::IO::Resource::SYSTEM; + GError *error = nullptr; + gsize bytesRead = 0; + gsize bytesWritten = 0; + gchar *localFilename = + g_filename_from_utf8(get_path(SYSTEM, PIXMAPS, "remove-color.png"), -1, &bytesRead, &bytesWritten, &error); + auto pixbuf = Gdk::Pixbuf::create_from_file(localFilename); + if (!pixbuf) { + g_warning("Null pixbuf for %p [%s]", localFilename, localFilename ); + } + g_free(localFilename); + + preview->set_pixbuf(pixbuf); + } + else if ( !_pattern ){ + preview->set_color((def.getR() << 8) | def.getR(), + (def.getG() << 8) | def.getG(), + (def.getB() << 8) | def.getB() ); + } else { + // These correspond to PREVIEW_PIXBUF_WIDTH and VBLOCK from swatches.cpp + // TODO: the pattern to draw should be in the widget that draws the preview, + // so the preview can be scalable + int w = 128; + int h = 16; + + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); + cairo_t *ct = cairo_create(s); + cairo_set_source(ct, _pattern); + cairo_paint(ct); + cairo_destroy(ct); + cairo_surface_flush(s); + + auto pixbuf = Glib::wrap(ink_pixbuf_create_from_cairo_surface(s)); + preview->set_pixbuf(pixbuf); + } + + preview->set_linked(static_cast<UI::Widget::LinkType>( (_linkSrc ? UI::Widget::PREVIEW_LINK_IN : 0) + | (_listeners.empty() ? 0 : UI::Widget::PREVIEW_LINK_OUT) + | (_isLive ? UI::Widget::PREVIEW_LINK_OTHER:0)) ); +} + +Gtk::Widget* ColorItem::createWidget() { + auto widget = dynamic_cast<UI::Widget::Preview*>(_getPreview(Inkscape::UI::Widget::PREVIEW_STYLE_ICON, + Inkscape::UI::Widget::VIEW_TYPE_GRID, Inkscape::UI::Widget::PREVIEW_SIZE_TINY, 100, 0)); + + if (widget) widget->set_freesize(true); + + return widget; +} + +Gtk::Widget* +ColorItem::getPreview(UI::Widget::PreviewStyle style, + UI::Widget::ViewType view, + UI::Widget::PreviewSize size, + guint ratio, + guint border) +{ + auto widget = _getPreview(style, view, size, ratio, border); + _previews.push_back( widget ); + return widget; +} + + +Gtk::Widget* ColorItem::_getPreview(UI::Widget::PreviewStyle style, + UI::Widget::ViewType view, UI::Widget::PreviewSize size, + guint ratio, guint border) { + + Gtk::Widget* widget = nullptr; + if ( style == UI::Widget::PREVIEW_STYLE_BLURB) { + Gtk::Label *lbl = new Gtk::Label(def.descr); + lbl->set_halign(Gtk::ALIGN_START); + lbl->set_valign(Gtk::ALIGN_CENTER); + widget = lbl; + } else { + auto preview = Gtk::manage(new UI::Widget::Preview()); + preview->set_name("ColorItemPreview"); + + _regenPreview(preview); + + preview->set_details((UI::Widget::ViewType)view, + (UI::Widget::PreviewSize)size, + ratio, + border ); + + def.addCallback( _colorDefChanged, this ); + preview->set_focus_on_click(false); + preview->set_tooltip_text(def.descr); + + preview->signal_clicked().connect(sigc::mem_fun(*this, &ColorItem::handleClick)); + preview->signal_alt_clicked().connect(sigc::mem_fun(*this, &ColorItem::handleSecondaryClick)); + preview->signal_button_press_event().connect(sigc::bind(sigc::ptr_fun(&colorItemHandleButtonPress), preview, this)); + + { + auto listing = def.getMIMETypes(); + std::vector<Gtk::TargetEntry> entries; + + for ( auto str : listing ) { + auto target = str.c_str(); + guint flags = 0; + if ( mimeToInt.find(str) == mimeToInt.end() ){ + // these next lines are order-dependent: + mimeToInt[str] = mimeStrings.size(); + mimeStrings.push_back(str); + } + auto info = mimeToInt[target]; + Gtk::TargetEntry entry(target, (Gtk::TargetFlags)flags, info); + entries.push_back(entry); + } + + preview->drag_source_set(entries, Gdk::BUTTON1_MASK, + Gdk::DragAction(Gdk::ACTION_MOVE | Gdk::ACTION_COPY) ); + } + + preview->signal_drag_data_get().connect(sigc::mem_fun(*this, &ColorItem::_dragGetColorData)); + preview->signal_drag_begin().connect(sigc::mem_fun(*this, &ColorItem::drag_begin)); + preview->signal_enter_notify_event().connect(sigc::mem_fun(*this, &ColorItem::handleEnterNotify)); + preview->signal_leave_notify_event().connect(sigc::mem_fun(*this, &ColorItem::handleLeaveNotify)); + + widget = preview; + } + + return widget; +} + +void ColorItem::buttonClicked(bool secondary) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + char const * attrName = secondary ? "stroke" : "fill"; + + SPCSSAttr *css = sp_repr_css_attr_new(); + Glib::ustring descr; + switch (def.getType()) { + case ege::PaintDef::CLEAR: { + // TODO actually make this clear + sp_repr_css_set_property( css, attrName, "none" ); + descr = secondary? _("Remove stroke color") : _("Remove fill color"); + break; + } + case ege::PaintDef::NONE: { + sp_repr_css_set_property( css, attrName, "none" ); + descr = secondary? _("Set stroke color to none") : _("Set fill color to none"); + break; + } +//mark + case ege::PaintDef::RGB: { + Glib::ustring colorspec; + if ( _grad ){ + colorspec = "url(#"; + colorspec += _grad->getId(); + colorspec += ")"; + } else { + gchar c[64]; + guint32 rgba = (def.getR() << 24) | (def.getG() << 16) | (def.getB() << 8) | 0xff; + sp_svg_write_color(c, sizeof(c), rgba); + colorspec = c; + } +//end mark + sp_repr_css_set_property( css, attrName, colorspec.c_str() ); + descr = secondary? _("Set stroke color from swatch") : _("Set fill color from swatch"); + break; + } + } + sp_desktop_set_style(desktop, css); + sp_repr_css_attr_unref(css); + + DocumentUndo::done( desktop->getDocument(), descr.c_str(), INKSCAPE_ICON("swatches")); + } +} + +void ColorItem::_wireMagicColors( SwatchPage *colorSet ) +{ + if ( colorSet ) + { + for ( boost::ptr_vector<ColorItem>::iterator it = colorSet->_colors.begin(); it != colorSet->_colors.end(); ++it ) + { + std::string::size_type pos = it->def.descr.find("*{"); + if ( pos != std::string::npos ) + { + std::string subby = it->def.descr.substr( pos + 2 ); + std::string::size_type endPos = subby.find("}*"); + if ( endPos != std::string::npos ) + { + subby.erase( endPos ); + //g_message("FOUND MAGIC at '%s'", (*it)->def.descr.c_str()); + //g_message(" '%s'", subby.c_str()); + + if ( subby.find('E') != std::string::npos ) + { + it->def.setEditable( true ); + } + + if ( subby.find('L') != std::string::npos ) + { + it->_isLive = true; + } + + std::string part; + // Tint. index + 1 more val. + if ( getBlock( part, 'T', subby ) ) { + guint64 colorIndex = 0; + if ( popVal( colorIndex, part ) ) { + guint64 percent = 0; + if ( popVal( percent, part ) ) { + it->_linkTint( colorSet->_colors[colorIndex], percent ); + } + } + } + + // Shade/tone. index + 1 or 2 more val. + if ( getBlock( part, 'S', subby ) ) { + guint64 colorIndex = 0; + if ( popVal( colorIndex, part ) ) { + guint64 percent = 0; + if ( popVal( percent, part ) ) { + guint64 grayLevel = 0; + if ( !popVal( grayLevel, part ) ) { + grayLevel = 0; + } + it->_linkTone( colorSet->_colors[colorIndex], percent, grayLevel ); + } + } + } + + } + } + } + } +} + + +void ColorItem::_linkTint( ColorItem& other, int percent ) +{ + if ( !_linkSrc ) + { + other._listeners.push_back(this); + _linkIsTone = false; + _linkPercent = percent; + if ( _linkPercent > 100 ) + _linkPercent = 100; + if ( _linkPercent < 0 ) + _linkPercent = 0; + _linkGray = 0; + _linkSrc = &other; + + ColorItem::_colorDefChanged(&other); + } +} + +void ColorItem::_linkTone( ColorItem& other, int percent, int grayLevel ) +{ + if ( !_linkSrc ) + { + other._listeners.push_back(this); + _linkIsTone = true; + _linkPercent = percent; + if ( _linkPercent > 100 ) + _linkPercent = 100; + if ( _linkPercent < 0 ) + _linkPercent = 0; + _linkGray = grayLevel; + _linkSrc = &other; + + ColorItem::_colorDefChanged(&other); + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/color-item.h b/src/ui/dialog/color-item.h new file mode 100644 index 0000000..8ca7d9e --- /dev/null +++ b/src/ui/dialog/color-item.h @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Inkscape color swatch UI item. + */ +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_COLOR_ITEM_H +#define SEEN_DIALOGS_COLOR_ITEM_H + +#include <boost/ptr_container/ptr_vector.hpp> + +#include "widgets/ege-paint-def.h" +#include "ui/previewable.h" + +class SPGradient; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ColorItem; + +class SwatchPage +{ +public: + SwatchPage(); + ~SwatchPage(); + + Glib::ustring _name; + int _prefWidth; + boost::ptr_vector<ColorItem> _colors; +}; + + +/** + * The color swatch you see on screen as a clickable box. + */ +class ColorItem : public Inkscape::UI::Previewable +{ + friend void _loadPaletteFile( gchar const *filename ); +public: + ColorItem( ege::PaintDef::ColorType type ); + ColorItem( unsigned int r, unsigned int g, unsigned int b, + Glib::ustring& name ); + ~ColorItem() override; + ColorItem(ColorItem const &other); + virtual ColorItem &operator=(ColorItem const &other); + Gtk::Widget* getPreview(UI::Widget::PreviewStyle style, + UI::Widget::ViewType view, + UI::Widget::PreviewSize size, + guint ratio, + guint border) override; + void buttonClicked(bool secondary = false); + + void setGradient(SPGradient *grad); + SPGradient * getGradient() const { return _grad; } + void setPattern(cairo_pattern_t *pattern); + void setName(const Glib::ustring name); + + void setState( bool fill, bool stroke ); + bool isFill() { return _isFill; } + bool isStroke() { return _isStroke; } + + ege::PaintDef def; + + Gtk::Widget* createWidget(); + +private: + Gtk::Widget* _getPreview(UI::Widget::PreviewStyle style, + UI::Widget::ViewType view, UI::Widget::PreviewSize size, + guint ratio, guint border); + + static void _dropDataIn( GtkWidget *widget, + GdkDragContext *drag_context, + gint x, gint y, + GtkSelectionData *data, + guint info, + guint event_time, + gpointer user_data); + + void _dragGetColorData(const Glib::RefPtr<Gdk::DragContext> &drag_context, + Gtk::SelectionData &data, + guint info, + guint time); + + static void _wireMagicColors( SwatchPage *colorSet ); + static void _colorDefChanged(void* data); + + void _updatePreviews(); + void _regenPreview(UI::Widget::Preview * preview); + + void _linkTint( ColorItem& other, int percent ); + void _linkTone( ColorItem& other, int percent, int grayLevel ); + void drag_begin(const Glib::RefPtr<Gdk::DragContext> &dc); + void handleClick(); + void handleSecondaryClick(gint arg1); + bool handleEnterNotify(GdkEventCrossing* event); + bool handleLeaveNotify(GdkEventCrossing* event); + + std::vector<Gtk::Widget*> _previews; + + bool _isFill; + bool _isStroke; + bool _isLive; + bool _linkIsTone; + int _linkPercent; + int _linkGray; + ColorItem* _linkSrc; + SPGradient* _grad; + cairo_pattern_t *_pattern; + std::vector<ColorItem*> _listeners; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_DIALOGS_COLOR_ITEM_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/ui/dialog/command-palette.cpp b/src/ui/dialog/command-palette.cpp new file mode 100644 index 0000000..32d2ca9 --- /dev/null +++ b/src/ui/dialog/command-palette.cpp @@ -0,0 +1,1636 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for adding a live path effect. + * + * Author: + * Abhay Raj Singh <abhayonlyone@gmail.com> + * + * Copyright (C) 2020 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "command-palette.h" + +#include <cstddef> +#include <cstring> +#include <ctime> +#include <gdk/gdkkeysyms.h> +#include <giomm/action.h> +#include <giomm/application.h> +#include <giomm/file.h> +#include <giomm/fileinfo.h> +#include <glib/gi18n.h> +#include <glibconfig.h> +#include <glibmm/convert.h> +#include <glibmm/date.h> +#include <glibmm/error.h> +#include <glibmm/i18n.h> +#include <glibmm/markup.h> +#include <glibmm/ustring.h> +#include <gtkmm/application.h> +#include <gtkmm/box.h> +#include <gtkmm/enums.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/label.h> +#include <gtkmm/messagedialog.h> +#include <gtkmm/recentinfo.h> +#include <iostream> +#include <iterator> +#include <memory> +#include <optional> +#include <ostream> +#include <sigc++/adaptors/bind.h> +#include <sigc++/functors/mem_fun.h> +#include <string> + +#include "actions/actions-extra-data.h" +#include "file.h" +#include "gc-anchored.h" +#include "include/glibmm_version.h" +#include "inkscape-application.h" +#include "inkscape-window.h" +#include "inkscape.h" +#include "io/resource.h" +#include "message-context.h" +#include "message-stack.h" +#include "object/uri.h" +#include "preferences.h" +#include "ui/interface.h" +#include "xml/repr.h" + +namespace Inkscape { +class MessageStack; +namespace UI { +namespace Dialog { + +namespace { +template <typename T> +void debug_print(T variable) +{ + std::cerr << variable << std::endl; +} +} // namespace + +// constructor +CommandPalette::CommandPalette() +{ + // setup _builder + { + auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-main.glade"); + try { + _builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for command palette dialog"); + return; + } + } + + // Setup Base UI Components + _builder->get_widget("CPBase", _CPBase); + _builder->get_widget("CPHeader", _CPHeader); + _builder->get_widget("CPListBase", _CPListBase); + + _builder->get_widget("CPSearchBar", _CPSearchBar); + _builder->get_widget("CPFilter", _CPFilter); + + _builder->get_widget("CPSuggestions", _CPSuggestions); + _builder->get_widget("CPHistory", _CPHistory); + + _builder->get_widget("CPSuggestionsScroll", _CPSuggestionsScroll); + _builder->get_widget("CPHistoryScroll", _CPHistoryScroll); + + _CPBase->add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | + Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK | Gdk::KEY_PRESS_MASK); + + // TODO: Customise on user language RTL, LTR or better user preference + _CPBase->set_halign(Gtk::ALIGN_CENTER); + _CPBase->set_valign(Gtk::ALIGN_START); + + _CPFilter->signal_key_press_event().connect(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_escape), + false); + _CPSuggestions->signal_key_press_event().connect(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_escape), false); + _CPHistory->signal_key_press_event().connect(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_escape), false); + set_mode(CPMode::SEARCH); + + _CPSuggestions->set_activate_on_single_click(); + _CPSuggestions->set_selection_mode(Gtk::SELECTION_SINGLE); + + // Setup operations [actions, extensions] + { + // setup recent files + { + //TODO: refactor this ============================== + // this code is repeated in menubar.cpp + auto recent_manager = Gtk::RecentManager::get_default(); + auto recent_files = recent_manager->get_items(); // all recent files not necessarily inkscape only + + int max_files = Inkscape::Preferences::get()->getInt("/options/maxrecentdocuments/value"); + + for (auto const &recent_file : recent_files) { + // check if given was generated by inkscape + bool valid_file = recent_file->has_application(g_get_prgname()) or + recent_file->has_application("org.inkscape.Inkscape") or + recent_file->has_application("inkscape") or + recent_file->has_application("inkscape.exe"); + + valid_file = valid_file and recent_file->exists(); + + if (not valid_file) { + continue; + } + + if (max_files-- <= 0) { + break; + } + + append_recent_file_operation(recent_file->get_uri_display(), true, + false); // open - second param true to append in _CPSuggestions + append_recent_file_operation(recent_file->get_uri_display(), true, + true); // import - last param true for import operation + } + // ================================================== + } + } + + // History management + { + const auto history = _history_xml.get_operation_history(); + + for (const auto &page : history) { + // second params false to append in history + switch (page.history_type) { + case HistoryType::ACTION: + generate_action_operation(get_action_ptr_name(page.data), false); + break; + case HistoryType::IMPORT_FILE: + append_recent_file_operation(page.data, false, true); + break; + case HistoryType::OPEN_FILE: + append_recent_file_operation(page.data, false, false); + break; + default: + continue; + } + } + } + // for `enter to execute` feature + _CPSuggestions->signal_row_activated().connect(sigc::mem_fun(*this, &CommandPalette::on_row_activated)); +} + +void CommandPalette::open() +{ + if (not _win_doc_actions_loaded) { + // loading actions can be very slow + load_app_actions(); + // win doc don't exist at construction so loading at first time opening Command Palette + load_win_doc_actions(); + _win_doc_actions_loaded = true; + } + _CPBase->show_all(); + _CPFilter->grab_focus(); + _is_open = true; +} + +void CommandPalette::close() +{ + _CPBase->hide(); + + // Reset filtering - show all suggestions + _CPFilter->set_text(""); + _CPSuggestions->invalidate_filter(); + + set_mode(CPMode::SEARCH); + + _is_open = false; +} + +void CommandPalette::toggle() +{ + if (not _is_open) { + open(); + return; + } + close(); +} + +void CommandPalette::append_recent_file_operation(const Glib::ustring &path, bool is_suggestion, bool is_import) +{ + static const auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-operation.glade"); + Glib::RefPtr<Gtk::Builder> operation_builder; + try { + operation_builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for Command Palette operation dialog"); + } + + // declaring required widgets pointers + Gtk::EventBox *CPOperation; + Gtk::Box *CPSynapseBox; + + Gtk::Label *CPGroup; + Gtk::Label *CPName; + Gtk::Label *CPShortcut; + Gtk::Button *CPActionFullName; + Gtk::Label *CPDescription; + + // Reading widgets + operation_builder->get_widget("CPOperation", CPOperation); + operation_builder->get_widget("CPSynapseBox", CPSynapseBox); + + operation_builder->get_widget("CPGroup", CPGroup); + operation_builder->get_widget("CPName", CPName); + operation_builder->get_widget("CPShortcut", CPShortcut); + operation_builder->get_widget("CPActionFullName", CPActionFullName); + operation_builder->get_widget("CPDescription", CPDescription); + + const auto file = Gio::File::create_for_path(path); + if (file->query_exists()) { + const Glib::ustring file_name = file->get_basename(); + + if (is_import) { + // Used for Activate row signal of listbox and not + CPGroup->set_text("import"); + CPActionFullName->set_label("import"); // For filtering only + + } else { + CPGroup->set_text("open"); + CPActionFullName->set_label("open"); // For filtering only + } + + // Hide for recent_file, not required + CPActionFullName->set_no_show_all(); + CPActionFullName->hide(); + + CPName->set_text((is_import ? _("Import") : _("Open")) + (": " + file_name)); + CPName->set_tooltip_text((is_import ? ("Import") : ("Open")) + (": " + file_name)); // Tooltip_text are not translatable + CPDescription->set_text(path); + CPDescription->set_tooltip_text(path); + + { + Glib::DateTime mod_time; +#if GLIBMM_CHECK_VERSION(2, 62, 0) + mod_time = file->query_info()->get_modification_date_time(); + // Using this to reduce instead of ActionFullName widget because fullname is searched +#else + mod_time.create_now_local(file->query_info()->modification_time()); +#endif + CPShortcut->set_text(mod_time.format("%d %b %R")); + } + // Add to suggestions + if (is_suggestion) { + _CPSuggestions->append(*CPOperation); + } else { + _CPHistory->append(*CPOperation); + } + } +} + +bool CommandPalette::generate_action_operation(const ActionPtrName &action_ptr_name, bool is_suggestion) +{ + static const auto app = InkscapeApplication::instance(); + static const auto gapp = app->gtk_app(); + static InkActionExtraData &action_data = app->get_action_extra_data(); + static const bool show_full_action_name = + Inkscape::Preferences::get()->getBool("/options/commandpalette/showfullactionname/value"); + static const auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-operation.glade"); + + Glib::RefPtr<Gtk::Builder> operation_builder; + try { + operation_builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for Command Palette operation dialog"); + return false; + } + + // declaring required widgets pointers + Gtk::EventBox *CPOperation; + Gtk::Box *CPSynapseBox; + + Gtk::Label *CPGroup; + Gtk::Label *CPName; + Gtk::Label *CPShortcut; + Gtk::Label *CPDescription; + Gtk::Button *CPActionFullName; + + // Reading widgets + operation_builder->get_widget("CPOperation", CPOperation); + operation_builder->get_widget("CPSynapseBox", CPSynapseBox); + + operation_builder->get_widget("CPGroup", CPGroup); + operation_builder->get_widget("CPName", CPName); + operation_builder->get_widget("CPShortcut", CPShortcut); + operation_builder->get_widget("CPActionFullName", CPActionFullName); + operation_builder->get_widget("CPDescription", CPDescription); + + CPGroup->set_text(action_data.get_section_for_action(Glib::ustring(action_ptr_name.second))); + + // Setting CPName + { + auto name = action_data.get_label_for_action(action_ptr_name.second); + auto untranslated_name = action_data.get_label_for_action(action_ptr_name.second, false); + if (name.empty()) { + // If action doesn't have a label, set the name = full action name + name = action_ptr_name.second; + untranslated_name = action_ptr_name.second; + } + + CPName->set_text(name); + CPName->set_tooltip_text(untranslated_name); + } + + { + CPActionFullName->set_label(action_ptr_name.second); + + if (not show_full_action_name) { + CPActionFullName->set_no_show_all(); + CPActionFullName->hide(); + } else { + CPActionFullName->signal_clicked().connect( + sigc::bind<Glib::ustring>(sigc::mem_fun(*this, &CommandPalette::on_action_fullname_clicked), + action_ptr_name.second), + false); + } + } + + { + std::vector<Glib::ustring> accels = gapp->get_accels_for_action(action_ptr_name.second); + std::stringstream ss; + for (const auto &accel : accels) { + ss << accel << ','; + } + std::string accel_label = ss.str(); + + if (not accel_label.empty()) { + accel_label.pop_back(); + CPShortcut->set_text(accel_label); + } else { + CPShortcut->set_no_show_all(); + CPShortcut->hide(); + } + } + + CPDescription->set_text(action_data.get_tooltip_for_action(action_ptr_name.second)); + CPDescription->set_tooltip_text(action_data.get_tooltip_for_action(action_ptr_name.second, false)); + + // Add to suggestions + if (is_suggestion) { + _CPSuggestions->append(*CPOperation); + } else { + _CPHistory->append(*CPOperation); + } + + return true; +} + +void CommandPalette::on_search() +{ + _CPSuggestions->unset_sort_func(); + _CPSuggestions->set_sort_func(sigc::mem_fun(*this, &CommandPalette::on_sort)); + _search_text = _CPFilter->get_text(); + _CPSuggestions->invalidate_filter(); // Remove old filter constraint and apply new one + if (auto top_row = _CPSuggestions->get_row_at_y(0); top_row) { + _CPSuggestions->select_row(*top_row); // select top row + } +} + +bool CommandPalette::on_filter_full_action_name(Gtk::ListBoxRow *child) +{ + if (auto CPActionFullName = get_full_action_name(child); + CPActionFullName and _search_text == CPActionFullName->get_label()) { + return true; + } + return false; +} + +bool CommandPalette::on_filter_recent_file(Gtk::ListBoxRow *child, bool const is_import) +{ + auto CPActionFullName = get_full_action_name(child); + if (is_import) { + if (CPActionFullName and CPActionFullName->get_label() == "import") { + auto [CPName, CPDescription] = get_name_desc(child); + if (CPDescription && CPDescription->get_text() == _search_text) { + return true; + } + } + return false; + } + if (CPActionFullName and CPActionFullName->get_label() == "open") { + auto [CPName, CPDescription] = get_name_desc(child); + if (CPDescription && CPDescription->get_text() == _search_text) { + return true; + } + } + return false; +} + +bool CommandPalette::on_key_press_cpfilter_escape(GdkEventKey *evt) +{ + if (evt->keyval == GDK_KEY_Escape || evt->keyval == GDK_KEY_question) { + close(); + return true; // stop propagation of key press, not needed anymore + } + return false; // Pass the key event which are not used +} + +bool CommandPalette::on_key_press_cpfilter_search_mode(GdkEventKey *evt) +{ + auto key = evt->keyval; + if (key == GDK_KEY_Return or key == GDK_KEY_Linefeed) { + if (auto selected_row = _CPSuggestions->get_selected_row(); selected_row) { + selected_row->activate(); + } + return true; + } else if (key == GDK_KEY_Up) { + if (not _CPHistory->get_children().empty()) { + set_mode(CPMode::HISTORY); + return true; + } + } + return false; +} + +bool CommandPalette::on_key_press_cpfilter_history_mode(GdkEventKey *evt) +{ + if (evt->keyval == GDK_KEY_BackSpace) { + return true; + } + return false; +} + +/** + * Executes action when enter pressed + */ +bool CommandPalette::on_key_press_cpfilter_input_mode(GdkEventKey *evt, const ActionPtrName &action_ptr_name) +{ + switch (evt->keyval) { + case GDK_KEY_Return: + [[fallthrough]]; + case GDK_KEY_Linefeed: + execute_action(action_ptr_name, _CPFilter->get_text()); + close(); + return true; + } + return false; +} + +void CommandPalette::hide_suggestions() +{ + _CPBase->set_size_request(-1, 10); + _CPListBase->hide(); +} +void CommandPalette::show_suggestions() +{ + _CPBase->set_size_request(-1, _max_height_requestable); + _CPListBase->show_all(); +} + +void CommandPalette::on_action_fullname_clicked(const Glib::ustring &action_fullname) +{ + static auto clipboard = Gtk::Clipboard::get(); + clipboard->set_text(action_fullname); + clipboard->store(); +} + +void CommandPalette::on_row_activated(Gtk::ListBoxRow *activated_row) +{ + // this is set to import/export or full action name + const auto full_action_name = get_full_action_name(activated_row)->get_label(); + if (full_action_name == "import" or full_action_name == "open") { + const auto [name, description] = get_name_desc(activated_row); + operate_recent_file(description->get_text(), full_action_name == "import"); + } else { + ask_action_parameter(get_action_ptr_name(full_action_name)); + // this is an action + } +} + +void CommandPalette::on_history_selection_changed(Gtk::ListBoxRow *lb) +{ + // set the search box text to current selection + if (const auto name_label = get_name_desc(lb).first; name_label) { + _CPFilter->set_text(name_label->get_text()); + } +} + +bool CommandPalette::operate_recent_file(Glib::ustring const &uri, bool const import) +{ + static auto prefs = Inkscape::Preferences::get(); + + bool write_to_history = true; + + // if the last element in CPHistory is already this, don't update history file + if (not _CPHistory->get_children().empty()) { + if (const auto last_operation = _history_xml.get_last_operation(); last_operation.has_value()) { + if (uri == last_operation->data) { + bool last_operation_was_import = last_operation->history_type == HistoryType::IMPORT_FILE; + // As previous uri is verfied to be the same as current uri we can write to history if current and + // previous operation are not the same. + // For example: if we want to import and previous operation was import (with same uri) we should not + // write ot history, similarly if current is open and previous was open to then dont WTH. + // But in case previous operation was open and current is import and vice-versa we should write to + // history. + if (not(import xor last_operation_was_import)) { + write_to_history = false; + } + } + } + } + + if (import) { + prefs->setBool("/options/onimport", true); + file_import(SP_ACTIVE_DOCUMENT, uri, nullptr); + prefs->setBool("/options/onimport", true); + + if (write_to_history) { + _history_xml.add_import(uri); + } + + close(); + return true; + } + + // open + { + get_action_ptr_name("app.file-open").first->activate(uri); + if (write_to_history) { + _history_xml.add_open(uri); + } + } + + close(); + return true; +} // namespace Dialog + +/** + * Maybe replaced by: Temporary arrangement may be replaced by snippets + * This can help us provide parameters for multiple argument function + * whose actions take a string as param + */ +bool CommandPalette::ask_action_parameter(const ActionPtrName &action_ptr_name) +{ + // Avoid writing same last action again + // TODO: Merge the if else parts + if (const auto last_of_history = _history_xml.get_last_operation(); last_of_history.has_value()) { + // operation history is not empty + const auto last_full_action_name = last_of_history->data; + if (last_full_action_name != action_ptr_name.second) { + // last action is not the same so write this one + _history_xml.add_action(action_ptr_name.second); // to history file + generate_action_operation(action_ptr_name, false); // to _CPHistory + } + } else { + // History is empty so no need to check + _history_xml.add_action(action_ptr_name.second); // to history file + generate_action_operation(action_ptr_name, false); // to _CPHistory + } + + // Checking if action has handleable parameter type + TypeOfVariant action_param_type = get_action_variant_type(action_ptr_name.first); + if (action_param_type == TypeOfVariant::UNKNOWN) { + std::cerr << "CommandPalette::ask_action_parameter: unhandled action value type (Unknown Type) " + << action_ptr_name.second << std::endl; + return false; + } + + if (action_param_type != TypeOfVariant::NONE) { + set_mode(CPMode::INPUT); + + _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect( + sigc::bind<ActionPtrName>(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_input_mode), + action_ptr_name), + false); + + // get type string NOTE: Temporary should be replaced by adding some data to InkActionExtraDataj + Glib::ustring type_string; + switch (action_param_type) { + case TypeOfVariant::BOOL: + type_string = "bool"; + break; + case TypeOfVariant::INT: + type_string = "integer"; + break; + case TypeOfVariant::DOUBLE: + type_string = "double"; + break; + case TypeOfVariant::STRING: + type_string = "string"; + break; + case TypeOfVariant::TUPLE_DD: + type_string = "pair of doubles"; + break; + default: + break; + } + + const auto app = InkscapeApplication::instance(); + InkActionHintData &action_hint_data = app->get_action_hint_data(); + auto action_hint = action_hint_data.get_tooltip_hint_for_action(action_ptr_name.second, false); + + + // Indicate user about what to enter FIXME Dialog generation + if (action_hint.length()) { + _CPFilter->set_placeholder_text(action_hint); + _CPFilter->set_tooltip_text(action_hint); + } else { + _CPFilter->set_placeholder_text("Enter a " + type_string + "..."); + _CPFilter->set_tooltip_text("Enter a " + type_string + "..."); + } + + + return true; + } + + execute_action(action_ptr_name, ""); + close(); + + return true; +} + +/** + * Color removal + */ +void CommandPalette::remove_color(Gtk::Label *label, const Glib::ustring &subject, bool tooltip) +{ + if (tooltip) { + label->set_tooltip_text(subject); + } else if (label->get_use_markup()) { + label->set_text(subject); + } +} + +/** + * Color addition + */ +Glib::ustring make_bold(const Glib::ustring &search) +{ + // TODO: Add a CSS class that changes the color of the search + return "<span weight=\"bold\">" + search + "</span>"; +} + +void CommandPalette::add_color(Gtk::Label *label, const Glib::ustring &search, const Glib::ustring &subject, bool tooltip) +{ + Glib::ustring text = ""; + Glib::ustring subject_string = subject.lowercase(); + Glib::ustring search_string = search.lowercase(); + int j = 0; + + if (search_string.length() > 7) { + for (gunichar i : search_string) { + if (i == ' ') { + continue; + } + while (j < subject_string.length()) { + if (i == subject_string[j]) { + text += make_bold(Glib::Markup::escape_text(subject.substr(j, 1))); + j++; + break; + } else { + text += subject[j]; + } + j++; + } + } + if (j < subject.length()) { + text += Glib::Markup::escape_text(subject.substr(j)); + } + } else { + std::map<gunichar, int> search_string_character; + + for (const auto &character : search_string) { + search_string_character[character]++; + } + + int subject_length = subject_string.length(); + + for (int i = 0; i < subject_length; i++) { + if (search_string_character[subject_string[i]]--) { + text += make_bold(Glib::Markup::escape_text(subject.substr(i, 1))); + } else { + text += subject[i]; + } + } + } + + if (tooltip) { + label->set_tooltip_markup(text); + } else { + label->set_markup(text); + } +} + +/** + * Color addition for description text + * Coloring complete consecutive search text in the description text + */ +void CommandPalette::add_color_description(Gtk::Label *label, const Glib::ustring &search) +{ + Glib::ustring subject = label->get_text(); + + Glib::ustring const subject_normalize = subject.lowercase().normalize(); + Glib::ustring const search_normalize = search.lowercase().normalize(); + + auto const position = subject_normalize.find(search_normalize); + auto const search_length = search_normalize.size(); + + subject = Glib::Markup::escape_text(subject.substr(0, position)) + + make_bold(Glib::Markup::escape_text(subject.substr(position, search_length))) + + Glib::Markup::escape_text(subject.substr(position + search_length)); + + label->set_markup(subject); +} + +/** + * The Searching algorithm consists of fuzzy search and fuzzy points. + * + * Ever search of the label can contain up to three subjects to search + * CPName text,CPName tooltip text,CPDescription text + * + * Fuzzy search searches the search text in these subjects and returns a boolean + * Searching of a search text as a subsequence of the subject + * + * Fuzzy points give an integer of a particular search text concerning a particular subject. + * Less the fuzzy point more is the precedence. + * + * Special case for CPDescription text search by searching text as a substring of the subject + * + * TODO: Adding more conditions in fuzzy points and fuzzy search for creating better user experience + */ + +bool CommandPalette::fuzzy_tolerance_search(const Glib::ustring &subject, const Glib::ustring &search) +{ + Glib::ustring subject_string = subject.lowercase(); + Glib::ustring search_string = search.lowercase(); + std::map<gunichar, int> subject_string_character, search_string_character; + for (const auto &character : subject_string) { + subject_string_character[character]++; + } + for (const auto &character : search_string) { + search_string_character[character]++; + } + for (const auto &character : search_string_character) { + auto [alphabet, occurrence] = character; + if (subject_string_character[alphabet] < occurrence) { + return false; + } + } + return true; +} + +bool CommandPalette::fuzzy_search(const Glib::ustring &subject, const Glib::ustring &search) +{ + Glib::ustring subject_string = subject.lowercase(); + Glib::ustring search_string = search.lowercase(); + + for (int j = 0, i = 0; i < search_string.length(); i++) { + bool alphabet_present = false; + + while (j < subject_string.length()) { + if (search_string[i] == subject_string[j]) { + alphabet_present = true; + j++; + break; + } + j++; + } + + if (!alphabet_present) { + return false; // If not present + } + } + + return true; +} + +/** + * Searching the full search_text in the subject string + * used for CPDescription text + */ +bool CommandPalette::normal_search(const Glib::ustring &subject, const Glib::ustring &search) +{ + if (subject.lowercase().find(search.lowercase()) != -1) { + return true; + } + return false; +} + +/** + * Calculates the fuzzy_point + */ +int CommandPalette::fuzzy_points(const Glib::ustring &subject, const Glib::ustring &search) +{ + int fuzzy_cost = 100; // Taking initial fuzzy_cost as 100 + + constexpr int SEQUENTIAL_BONUS = -15; // bonus for adjacent matches + constexpr int SEPARATOR_BONUS = -30; // bonus if search occurs after a separator + constexpr int CAMEL_BONUS = -30; // bonus if search is uppercase and subject is lower + constexpr int FIRST_LETTET_BONUS = -15; // bonus if the first letter is matched + constexpr int LEADING_LETTER_PENALTY = +5; // penalty applied for every letter in subject before the first match + constexpr int MAX_LEADING_LETTER_PENALTY = +15; // maximum penalty for leading letters + constexpr int UNMATCHED_LETTER_PENALTY = +1; // penalty for every letter that doesn't matter + + Glib::ustring subject_string = subject.lowercase(); + Glib::ustring search_string = search.lowercase(); + + bool sequential_compare = false; + bool leading_letter = true; + int total_leading_letter_penalty = 0; + int j = 0, i = 0; + + while (i < search_string.length() && j < subject_string.length()) { + if (search_string[i] != subject_string[j]) { + j++; + sequential_compare = false; + fuzzy_cost += UNMATCHED_LETTER_PENALTY; + + if (leading_letter) { + if (total_leading_letter_penalty < MAX_LEADING_LETTER_PENALTY) { + fuzzy_cost += LEADING_LETTER_PENALTY; + total_leading_letter_penalty += LEADING_LETTER_PENALTY; + } + } + + continue; + } + + if (search_string[i] == subject_string[j]) { + leading_letter = false; + + if (j > 0 && subject_string[j - 1] == ' ') { + fuzzy_cost += SEPARATOR_BONUS; + } + + if (i == 0 && j == 0) { + fuzzy_cost += FIRST_LETTET_BONUS; + } + + if (search[i] == subject_string[j]) { + fuzzy_cost += CAMEL_BONUS; + } + + if (sequential_compare) { + fuzzy_cost += SEQUENTIAL_BONUS; + } + + sequential_compare = true; + i++; + } + } + + return fuzzy_cost; +} + +int CommandPalette::fuzzy_tolerance_points(const Glib::ustring &subject, const Glib::ustring &search) +{ + int fuzzy_cost = 200; // Taking initial fuzzy_cost as 200 + constexpr int FIRST_LETTET_BONUS = -15; // bonus if the first letter is matched + + Glib::ustring subject_string = subject.lowercase(); + Glib::ustring search_string = search.lowercase(); + + std::map<gunichar, int> search_string_character; + + for (const auto &character : search_string) { + search_string_character[character]++; + } + + for (const auto &character : search_string_character) { + auto [alphabet, occurrence] = character; + for (int i = 0; i < subject_string.length() && occurrence; i++) { + if (subject_string[i] == alphabet) { + if (i == 0) + fuzzy_cost += FIRST_LETTET_BONUS; + fuzzy_cost += i; + occurrence--; + } + } + } + + return fuzzy_cost; +} + +int CommandPalette::on_filter_general(Gtk::ListBoxRow *child) +{ + auto [CPName, CPDescription] = get_name_desc(child); + if (CPName) { + remove_color(CPName, CPName->get_text()); + remove_color(CPName, CPName->get_tooltip_text(), true); + } + if (CPDescription) { + remove_color(CPDescription, CPDescription->get_text()); + } + + if (_search_text.empty()) { + return 1; + } // Every operation is visible if search text is empty + + if (CPName) { + if (fuzzy_search(CPName->get_text(), _search_text)) { + add_color(CPName, _search_text, CPName->get_text()); + return fuzzy_points(CPName->get_text(), _search_text); + } + + if (fuzzy_search(CPName->get_tooltip_text(), _search_text)) { + add_color(CPName, _search_text, CPName->get_tooltip_text(), true); + return fuzzy_points(CPName->get_tooltip_text(), _search_text); + } + + if (fuzzy_tolerance_search(CPName->get_text(), _search_text)) { + add_color(CPName, _search_text, CPName->get_text()); + return fuzzy_tolerance_points(CPName->get_text(), _search_text); + } + + if (fuzzy_tolerance_search(CPName->get_tooltip_text(), _search_text)) { + add_color(CPName, _search_text, CPName->get_tooltip_text(), true); + return fuzzy_tolerance_points(CPName->get_tooltip_text(), _search_text); + } + } + if (CPDescription && normal_search(CPDescription->get_text(), _search_text)) { + add_color_description(CPDescription, _search_text); + return fuzzy_points(CPDescription->get_text(), _search_text); + } + + return 0; +} + +int CommandPalette::fuzzy_points_compare(int fuzzy_points_count_1, int fuzzy_points_count_2, int text_len_1, + int text_len_2) +{ + if (fuzzy_points_count_1 && fuzzy_points_count_2) { + if (fuzzy_points_count_1 < fuzzy_points_count_2) { + return -1; + } else if (fuzzy_points_count_1 == fuzzy_points_count_2) { + if (text_len_1 > text_len_2) { + return 1; + } else { + return -1; + } + } else { + return 1; + } + } + + if (fuzzy_points_count_1 == 0 && fuzzy_points_count_2) { + return 1; + } + if (fuzzy_points_count_2 == 0 && fuzzy_points_count_1) { + return -1; + } + + return 0; +} + +/** + * compare different rows for order of display + * priority of comparison + * 1) CPName->get_text() + * 2) CPName->get_tooltip_text() + * 3) CPDescription->get_text() + */ +int CommandPalette::on_sort(Gtk::ListBoxRow *row1, Gtk::ListBoxRow *row2) +{ + // tests for fuzzy_search + assert(fuzzy_search("Export background", "ebo") == true); + assert(fuzzy_search("Query y", "qyy") == true); + assert(fuzzy_search("window close", "qt") == false); + + // tests for fuzzy_points + assert(fuzzy_points("Export background", "ebo") == -22); + assert(fuzzy_points("Query y", "qyy") == -16); + assert(fuzzy_points("window close", "wc") == 2); + + // tests for fuzzy_tolerance_search + assert(fuzzy_tolerance_search("object to path", "ebo") == true); + assert(fuzzy_tolerance_search("execute verb", "qyy") == false); + assert(fuzzy_tolerance_search("color mode", "moco") == true); + + // tests for fuzzy_tolerance_points + assert(fuzzy_tolerance_points("object to path", "ebo") == 189); + assert(fuzzy_tolerance_points("execute verb", "vec") == 196); + assert(fuzzy_tolerance_points("color mode", "moco") == 195); + + if (_search_text.empty()) { + return -1; + } // No change in the order + + auto [cp_name_1, cp_description_1] = get_name_desc(row1); + auto [cp_name_2, cp_description_2] = get_name_desc(row2); + + int fuzzy_points_count_1 = 0, fuzzy_points_count_2 = 0; + int text_len_1 = 0, text_len_2 = 0; + int points_compare = 0; + + constexpr int TOOLTIP_PENALTY = 100; + constexpr int DESCRIPTION_PENALTY = 500; + + if (cp_name_1 && cp_name_2) { + if (fuzzy_search(cp_name_1->get_text(), _search_text)) { + text_len_1 = cp_name_1->get_text().length(); + fuzzy_points_count_1 = fuzzy_points(cp_name_1->get_text(), _search_text); + } + if (fuzzy_search(cp_name_2->get_text(), _search_text)) { + text_len_2 = cp_name_2->get_text().length(); + fuzzy_points_count_2 = fuzzy_points(cp_name_2->get_text(), _search_text); + } + + points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2); + if (points_compare != 0) { + return points_compare; + } + + if (fuzzy_tolerance_search(cp_name_1->get_text(), _search_text)) { + text_len_1 = cp_name_1->get_text().length(); + fuzzy_points_count_1 = fuzzy_tolerance_points(cp_name_1->get_text(), _search_text); + } + if (fuzzy_tolerance_search(cp_name_2->get_text(), _search_text)) { + text_len_2 = cp_name_2->get_text().length(); + fuzzy_points_count_2 = fuzzy_tolerance_points(cp_name_2->get_text(), _search_text); + } + + points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2); + if (points_compare != 0) { + return points_compare; + } + + if (fuzzy_search(cp_name_1->get_tooltip_text(), _search_text)) { + text_len_1 = cp_name_1->get_tooltip_text().length(); + fuzzy_points_count_1 = fuzzy_points(cp_name_1->get_tooltip_text(), _search_text) + TOOLTIP_PENALTY; + } + if (fuzzy_search(cp_name_2->get_tooltip_text(), _search_text)) { + text_len_2 = cp_name_2->get_tooltip_text().length(); + fuzzy_points_count_2 = fuzzy_points(cp_name_2->get_tooltip_text(), _search_text) + TOOLTIP_PENALTY; + } + + points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2); + if (points_compare != 0) { + return points_compare; + } + + if (fuzzy_tolerance_search(cp_name_1->get_tooltip_text(), _search_text)) { + text_len_1 = cp_name_1->get_tooltip_text().length(); + fuzzy_points_count_1 = fuzzy_tolerance_points(cp_name_1->get_tooltip_text(), _search_text) + + TOOLTIP_PENALTY; // Adding a constant integer to decrease the prefrence + } + if (fuzzy_tolerance_search(cp_name_2->get_tooltip_text(), _search_text)) { + text_len_2 = cp_name_2->get_tooltip_text().length(); + fuzzy_points_count_2 = fuzzy_tolerance_points(cp_name_2->get_tooltip_text(), _search_text) + + TOOLTIP_PENALTY; // Adding a constant integer to decrease the prefrence + } + points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2); + if (points_compare != 0) { + return points_compare; + } + } + + if (cp_description_1 && normal_search(cp_description_1->get_text(), _search_text)) { + text_len_1 = cp_description_1->get_text().length(); + fuzzy_points_count_1 = fuzzy_points(cp_description_1->get_text(), _search_text) + + DESCRIPTION_PENALTY; // Adding a constant integer to decrease the prefrence + } + if (cp_description_2 && normal_search(cp_description_2->get_text(), _search_text)) { + text_len_2 = cp_description_2->get_text().length(); + fuzzy_points_count_2 = fuzzy_points(cp_description_2->get_text(), _search_text) + + DESCRIPTION_PENALTY; // Adding a constant integer to decrease the prefrence + } + + points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2); + if (points_compare != 0) { + return points_compare; + } + return 0; +} + +void CommandPalette::set_mode(CPMode mode) +{ + switch (mode) { + case CPMode::SEARCH: + if (_mode == CPMode::SEARCH) { + return; + } + + _CPFilter->set_text(""); + _CPFilter->set_icon_from_icon_name("edit-find-symbolic"); + _CPFilter->set_placeholder_text("Search operation..."); + _CPFilter->set_tooltip_text("Search operation..."); + show_suggestions(); + + // Show Suggestions instead of history + _CPHistoryScroll->set_no_show_all(); + _CPHistoryScroll->hide(); + + _CPSuggestionsScroll->set_no_show_all(false); + _CPSuggestionsScroll->show_all(); + + _CPSuggestions->unset_filter_func(); + _CPSuggestions->set_filter_func(sigc::mem_fun(*this, &CommandPalette::on_filter_general)); + + _cpfilter_search_connection.disconnect(); // to be sure + _cpfilter_key_press_connection.disconnect(); + + _cpfilter_search_connection = + _CPFilter->signal_search_changed().connect(sigc::mem_fun(*this, &CommandPalette::on_search)); + _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect( + sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_search_mode), false); + + _search_text = ""; + _CPSuggestions->invalidate_filter(); + break; + + case CPMode::INPUT: + if (_mode == CPMode::INPUT) { + return; + } + _cpfilter_search_connection.disconnect(); + _cpfilter_key_press_connection.disconnect(); + + hide_suggestions(); + _CPFilter->set_text(""); + _CPFilter->grab_focus(); + + _CPFilter->set_icon_from_icon_name("input-keyboard"); + _CPFilter->set_placeholder_text("Enter action argument"); + _CPFilter->set_tooltip_text("Enter action argument"); + + break; + + case CPMode::SHELL: + if (_mode == CPMode::SHELL) { + return; + } + + hide_suggestions(); + _CPFilter->set_icon_from_icon_name("gtk-search"); + _cpfilter_search_connection.disconnect(); + _cpfilter_key_press_connection.disconnect(); + + break; + + case CPMode::HISTORY: + if (_mode == CPMode::HISTORY) { + return; + } + + if (_CPHistory->get_children().empty()) { + return; + } + + // Show history instead of suggestions + _CPSuggestionsScroll->set_no_show_all(); + _CPHistoryScroll->set_no_show_all(false); + + _CPSuggestionsScroll->hide(); + _CPHistoryScroll->show_all(); + + _CPFilter->set_icon_from_icon_name("format-justify-fill"); + _CPFilter->set_icon_tooltip_text(N_("History mode")); + _cpfilter_search_connection.disconnect(); + _cpfilter_key_press_connection.disconnect(); + + _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect( + sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_history_mode), false); + + _CPHistory->signal_row_selected().connect( + sigc::mem_fun(*this, &CommandPalette::on_history_selection_changed)); + _CPHistory->signal_row_activated().connect(sigc::mem_fun(*this, &CommandPalette::on_row_activated)); + + { + // select last row + const auto last_row = _CPHistory->get_row_at_index(_CPHistory->get_children().size() - 1); + _CPHistory->select_row(*last_row); + last_row->grab_focus(); + } + + { + // FIXME: scroll to bottom + const auto adjustment = _CPHistoryScroll->get_vadjustment(); + adjustment->set_value(adjustment->get_upper()); + } + + break; + } + _mode = mode; +} + +/** + * Calls actions with parameters + */ +CommandPalette::ActionPtrName CommandPalette::get_action_ptr_name(const Glib::ustring &full_action_name) +{ + static auto gapp = InkscapeApplication::instance()->gtk_app(); + // TODO: Optimisation: only try to assign if null, make static + const auto win = InkscapeApplication::instance()->get_active_window(); + const auto doc = InkscapeApplication::instance()->get_active_document(); + auto action_domain_string = full_action_name.substr(0, full_action_name.find('.')); // app, win, doc + auto action_name = full_action_name.substr(full_action_name.find('.') + 1); + + ActionPtr action_ptr; + if (action_domain_string == "app") { + action_ptr = gapp->lookup_action(action_name); + } else if (action_domain_string == "win" and win) { + action_ptr = win->lookup_action(action_name); + } else if (action_domain_string == "doc" and doc) { + if (const auto map = doc->getActionGroup(); map) { + action_ptr = map->lookup_action(action_name); + } + } + + return {action_ptr, full_action_name}; +} + +bool CommandPalette::execute_action(const ActionPtrName &action_ptr_name, const Glib::ustring &value) +{ + if (not value.empty()) { + _history_xml.add_action_parameter(action_ptr_name.second, value); + } + auto [action_ptr, action_name] = action_ptr_name; + + switch (get_action_variant_type(action_ptr)) { + case TypeOfVariant::BOOL: + if (value == "1" || value == "t" || value == "true" || value.empty()) { + action_ptr->activate(Glib::Variant<bool>::create(true)); + } else if (value == "0" || value == "f" || value == "false") { + action_ptr->activate(Glib::Variant<bool>::create(false)); + } else { + std::cerr << "CommandPalette::execute_action: Invalid boolean value: " << action_name << ":" << value + << std::endl; + } + break; + case TypeOfVariant::INT: + try { + action_ptr->activate(Glib::Variant<int>::create(std::stoi(value))); + } catch (...) { + if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) { + dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter an integer number.")); + } + } + break; + case TypeOfVariant::DOUBLE: + try { + action_ptr->activate(Glib::Variant<double>::create(std::stod(value))); + } catch (...) { + if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) { + dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter a decimal number.")); + } + } + break; + case TypeOfVariant::STRING: + action_ptr->activate(Glib::Variant<Glib::ustring>::create(value)); + break; + case TypeOfVariant::TUPLE_DD: + try { + double d0 = 0; + double d1 = 0; + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s*,\\s*", value); + + try { + if (tokens.size() != 2) { + throw std::invalid_argument("requires two numbers"); + } + } catch (...) { + throw; + } + + try { + d0 = std::stod(tokens[0]); + d1 = std::stod(tokens[1]); + } catch (...) { + throw; + } + + auto variant = Glib::Variant<std::tuple<double, double>>::create(std::tuple<double, double>(d0, d1)); + action_ptr->activate(variant); + } catch (...) { + if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) { + dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter two comma separated numbers.")); + } + } + break; + case TypeOfVariant::UNKNOWN: + std::cerr << "CommandPalette::execute_action: unhandled action value type (Unknown Type) " << action_name + << std::endl; + break; + case TypeOfVariant::NONE: + default: + action_ptr->activate(); + break; + } + return false; +} + +TypeOfVariant CommandPalette::get_action_variant_type(const ActionPtr &action_ptr) +{ + const GVariantType *gtype = g_action_get_parameter_type(action_ptr->gobj()); + if (gtype) { + Glib::VariantType type = action_ptr->get_parameter_type(); + if (type.get_string() == "b") { + return TypeOfVariant::BOOL; + } else if (type.get_string() == "i") { + return TypeOfVariant::INT; + } else if (type.get_string() == "d") { + return TypeOfVariant::DOUBLE; + } else if (type.get_string() == "s") { + return TypeOfVariant::STRING; + } else if (type.get_string() == "(dd)") { + return TypeOfVariant::TUPLE_DD; + } else { + std::cerr << "CommandPalette::get_action_variant_type: unknown variant type: " << type.get_string() << std::endl; + return TypeOfVariant::UNKNOWN; + } + } + // With value. + return TypeOfVariant::NONE; +} + +std::pair<Gtk::Label *, Gtk::Label *> CommandPalette::get_name_desc(Gtk::ListBoxRow *child) +{ + auto event_box = dynamic_cast<Gtk::EventBox *>(child->get_child()); + if (event_box) { + // NOTE: These variables have same name as in the glade file command-palette-operation.glade + // FIXME: When structure of Gladefile of CPOperation changes, refactor this + auto CPSynapseBox = dynamic_cast<Gtk::Box *>(event_box->get_child()); + if (CPSynapseBox) { + auto synapse_children = CPSynapseBox->get_children(); + auto CPName = dynamic_cast<Gtk::Label *>(synapse_children[0]); + auto CPDescription = dynamic_cast<Gtk::Label *>(synapse_children[1]); + + return std::pair(CPName, CPDescription); + } + } + + return std::pair(nullptr, nullptr); +} + +Gtk::Button *CommandPalette::get_full_action_name(Gtk::ListBoxRow *child) +{ + auto event_box = dynamic_cast<Gtk::EventBox *>(child->get_child()); + if (event_box) { + auto CPSynapseBox = dynamic_cast<Gtk::Box *>(event_box->get_child()); + if (CPSynapseBox) { + auto synapse_children = CPSynapseBox->get_children(); + auto CPActionFullName = dynamic_cast<Gtk::Button *>(synapse_children[2]); + + return CPActionFullName; + } + } + + return nullptr; +} + +void CommandPalette::load_app_actions() +{ + auto gapp = InkscapeApplication::instance()->gtk_app(); + std::vector<ActionPtrName> all_actions_info; + + std::vector<Glib::ustring> actions = gapp->list_actions(); + for (const auto &action : actions) { + generate_action_operation(get_action_ptr_name("app." + action), true); + } +} +void CommandPalette::load_win_doc_actions() +{ + if (auto window = InkscapeApplication::instance()->get_active_window(); window) { + std::vector<Glib::ustring> actions = window->list_actions(); + for (auto action : actions) { + generate_action_operation(get_action_ptr_name("win." + action), true); + } + + if (auto document = window->get_document(); document) { + auto map = document->getActionGroup(); + if (map) { + std::vector<Glib::ustring> actions = map->list_actions(); + for (auto action : actions) { + generate_action_operation(get_action_ptr_name("doc." + action), true); + } + } else { + std::cerr << "CommandPalette::load_win_doc_actions: No document map!" << std::endl; + } + } + } +} + +Gtk::Box *CommandPalette::get_base_widget() +{ + return _CPBase; +} + +// CPHistoryXML --------------------------------------------------------------- +CPHistoryXML::CPHistoryXML() + : _file_path(IO::Resource::profile_path("cphistory.xml")) +{ + _xml_doc = sp_repr_read_file(_file_path.c_str(), nullptr); + if (not _xml_doc) { + _xml_doc = sp_repr_document_new("cphistory"); + + /* STRUCTURE EXAMPLE ------------------ Illustration 1 + <cphistory> + <operations> + <action> full.action_name </action> + <import> uri </import> + <export> uri </export> + </operations> + <params> + <action name="app.transfor-rotate"> + <param> 30 </param> + <param> 23.5 </param> + </action> + </params> + </cphistory> + */ + + // Just a pointer, we don't own it, don't free/release/delete + auto root = _xml_doc->root(); + + // add operation history in this element + auto operations = _xml_doc->createElement("operations"); + root->appendChild(operations); + + // add param history in this element + auto params = _xml_doc->createElement("params"); + root->appendChild(params); + + // This was created by allocated + Inkscape::GC::release(operations); + Inkscape::GC::release(params); + + // only save if created new + save(); + } + + // Only two children :) check and ensure Illustration 1 + _operations = _xml_doc->root()->firstChild(); + _params = _xml_doc->root()->lastChild(); +} + +CPHistoryXML::~CPHistoryXML() +{ + Inkscape::GC::release(_xml_doc); +} +void CPHistoryXML::add_action(const std::string &full_action_name) +{ + add_operation(HistoryType::ACTION, full_action_name); +} + +void CPHistoryXML::add_import(const std::string &uri) +{ + add_operation(HistoryType::IMPORT_FILE, uri); +} +void CPHistoryXML::add_open(const std::string &uri) +{ + add_operation(HistoryType::OPEN_FILE, uri); +} + +void CPHistoryXML::add_action_parameter(const std::string &full_action_name, const std::string ¶m) +{ + /* Creates + * <params> + * +1 <action name="full.action-name"> + * + <param>30</param> + * + <param>60</param> + * + <param>90</param> + * +1 <action name="full.action-name"> + * <params> + * + * + : generally creates + * +1: creates once + */ + const auto parameter_node = _xml_doc->createElement("param"); + const auto parameter_text = _xml_doc->createTextNode(param.c_str()); + + parameter_node->appendChild(parameter_text); + Inkscape::GC::release(parameter_text); + + for (auto action_iter = _params->firstChild(); action_iter; action_iter = action_iter->next()) { + // If this action's node already exists + if (full_action_name == action_iter->attribute("name")) { + // If the last parameter was the same don't do anything, inner text is also a node hence 2 times last + // child + if (action_iter->lastChild()->lastChild() && action_iter->lastChild()->lastChild()->content() == param) { + Inkscape::GC::release(parameter_node); + return; + } + + // If last current than parameter is different, add current + action_iter->appendChild(parameter_node); + Inkscape::GC::release(parameter_node); + + save(); + return; + } + } + + // only encountered when the actions element doesn't already exists,so we create that action's element + const auto action_node = _xml_doc->createElement("action"); + action_node->setAttribute("name", full_action_name.c_str()); + action_node->appendChild(parameter_node); + + _params->appendChild(action_node); + save(); + + Inkscape::GC::release(action_node); + Inkscape::GC::release(parameter_node); +} + +std::optional<History> CPHistoryXML::get_last_operation() +{ + auto last_child = _operations->lastChild(); + if (last_child) { + if (const auto operation_type = _get_operation_type(last_child); operation_type.has_value()) { + // inner text is a text Node thus last child + return History{*operation_type, last_child->lastChild()->content()}; + } + } + return std::nullopt; +} +std::vector<History> CPHistoryXML::get_operation_history() const +{ + // TODO: add max items in history + std::vector<History> history; + for (auto operation_iter = _operations->firstChild(); operation_iter; operation_iter = operation_iter->next()) { + if (const auto operation_type = _get_operation_type(operation_iter); operation_type.has_value()) { + history.emplace_back(*operation_type, operation_iter->firstChild()->content()); + } + } + return history; +} + +std::vector<std::string> CPHistoryXML::get_action_parameter_history(const std::string &full_action_name) const +{ + std::vector<std::string> params; + for (auto action_iter = _params->firstChild(); action_iter; action_iter = action_iter->prev()) { + // If this action's node already exists + if (full_action_name == action_iter->attribute("name")) { + // lastChild and prev for LIFO order + for (auto param_iter = _params->lastChild(); param_iter; param_iter = param_iter->prev()) { + params.emplace_back(param_iter->content()); + } + return params; + } + } + // action not used previously so no params; + return {}; +} + +void CPHistoryXML::save() const +{ + sp_repr_save_file(_xml_doc, _file_path.c_str()); +} + +void CPHistoryXML::add_operation(const HistoryType history_type, const std::string &data) +{ + std::string operation_type_name; + switch (history_type) { + // see Illustration 1 + case HistoryType::ACTION: + operation_type_name = "action"; + break; + case HistoryType::IMPORT_FILE: + operation_type_name = "import"; + break; + case HistoryType::OPEN_FILE: + operation_type_name = "open"; + break; + default: + return; + } + auto operation_to_add = _xml_doc->createElement(operation_type_name.c_str()); // action, import, open + auto operation_data = _xml_doc->createTextNode(data.c_str()); + operation_data->setContent(data.c_str()); + + operation_to_add->appendChild(operation_data); + _operations->appendChild(operation_to_add); + + Inkscape::GC::release(operation_data); + Inkscape::GC::release(operation_to_add); + + save(); +} +std::optional<HistoryType> CPHistoryXML::_get_operation_type(Inkscape::XML::Node *operation) +{ + const std::string operation_type_name = operation->name(); + + if (operation_type_name == "action") { + return HistoryType::ACTION; + } else if (operation_type_name == "import") { + return HistoryType::IMPORT_FILE; + } else if (operation_type_name == "open") { + return HistoryType::OPEN_FILE; + } else { + return std::nullopt; + // unknown HistoryType + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/command-palette.h b/src/ui/dialog/command-palette.h new file mode 100644 index 0000000..e0316ac --- /dev/null +++ b/src/ui/dialog/command-palette.h @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * CommandPalette: Class providing Command Palette feature + * + * Authors: + * Abhay Raj Singh <abhayonlyone@gmail.com> + * + * Copyright (C) 2020 Autors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_COMMAND_PALETTE_H +#define INKSCAPE_DIALOG_COMMAND_PALETTE_H + +#include <utility> +#include <vector> + +#include <giomm/action.h> +#include <giomm/application.h> +#include <glibmm/refptr.h> +#include <glibmm/ustring.h> +#include <gtkmm/box.h> +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/label.h> +#include <gtkmm/listbox.h> +#include <gtkmm/listboxrow.h> +#include <gtkmm/recentinfo.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/searchbar.h> +#include <gtkmm/searchentry.h> +#include <gtkmm/viewport.h> + +#include "inkscape.h" +#include "ui/dialog/align-and-distribute.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +// Enables using switch case +enum class TypeOfVariant +{ + NONE, + UNKNOWN, + BOOL, + INT, + DOUBLE, + STRING, + TUPLE_DD +}; + +enum class CPMode +{ + SEARCH, + INPUT, // Input arguments + SHELL, + HISTORY +}; + +enum class HistoryType +{ + LPE, + ACTION, + OPEN_FILE, + IMPORT_FILE, +}; + +struct History +{ + HistoryType history_type; + std::string data; + + History(HistoryType ht, std::string &&data) + : history_type(ht) + , data(data) + {} +}; + +class CPHistoryXML +{ +public: + // constructors, asssignment, destructor + CPHistoryXML(); + ~CPHistoryXML(); + + // Handy wrappers for code clearity + void add_action(const std::string &full_action_name); + + void add_import(const std::string &uri); + void add_open(const std::string &uri); + + // Remember parameter for action + void add_action_parameter(const std::string &full_action_name, const std::string ¶m); + + std::optional<History> get_last_operation(); + + // To construct _CPHistory + std::vector<History> get_operation_history() const; + // To get parameter history when an action is selected, LIFO stack like so more recent first + std::vector<std::string> get_action_parameter_history(const std::string &full_action_name) const; + +private: + void save() const; + + void add_operation(const HistoryType history_type, const std::string &data); + + static std::optional<HistoryType> _get_operation_type(Inkscape::XML::Node *operation); + + const std::string _file_path; + + Inkscape::XML::Document *_xml_doc; + // handy for xml doc child + Inkscape::XML::Node *_operations; + Inkscape::XML::Node *_params; +}; + +class CommandPalette +{ +public: // API + CommandPalette(); + ~CommandPalette() = default; + + CommandPalette(CommandPalette const &) = delete; // no copy + CommandPalette &operator=(CommandPalette const &) = delete; // no assignment + + void open(); + void close(); + void toggle(); + + Gtk::Box *get_base_widget(); + +private: // Helpers + using ActionPtr = Glib::RefPtr<Gio::Action>; + using ActionPtrName = std::pair<ActionPtr, Glib::ustring>; + + /** + * Insert actions in _CPSuggestions + */ + void load_app_actions(); + void load_win_doc_actions(); + + void append_recent_file_operation(const Glib::ustring &path, bool is_suggestion, bool is_import = true); + bool generate_action_operation(const ActionPtrName &action_ptr_name, const bool is_suggestion); + +private: // Signal handlers + void on_search(); + + int on_filter_general(Gtk::ListBoxRow *child); + bool on_filter_full_action_name(Gtk::ListBoxRow *child); + bool on_filter_recent_file(Gtk::ListBoxRow *child, bool const is_import); + + bool on_key_press_cpfilter_escape(GdkEventKey *evt); + bool on_key_press_cpfilter_search_mode(GdkEventKey *evt); + bool on_key_press_cpfilter_input_mode(GdkEventKey *evt, const ActionPtrName &action_ptr_name); + bool on_key_press_cpfilter_history_mode(GdkEventKey *evt); + + /** + * when search bar is empty + */ + void hide_suggestions(); + + /** + * when search bar isn't empty + */ + void show_suggestions(); + + void on_row_activated(Gtk::ListBoxRow *activated_row); + void on_history_selection_changed(Gtk::ListBoxRow *lb); + + bool operate_recent_file(Glib::ustring const &uri, bool const import); + + void on_action_fullname_clicked(const Glib::ustring &action_fullname); + + /** + * Implements text matching logic + */ + static bool fuzzy_search(const Glib::ustring &subject, const Glib::ustring &search); + static bool normal_search(const Glib::ustring &subject, const Glib::ustring &search); + static bool fuzzy_tolerance_search(const Glib::ustring &subject, const Glib::ustring &search); + static int fuzzy_points(const Glib::ustring &subject, const Glib::ustring &search); + static int fuzzy_tolerance_points(const Glib::ustring &subject, const Glib::ustring &search); + static int fuzzy_points_compare(int fuzzy_points_count_1, int fuzzy_points_count_2, int text_len_1, int text_len_2); + int on_sort(Gtk::ListBoxRow *row1, Gtk::ListBoxRow *row2); + void set_mode(CPMode mode); + + /** + * Color addition in searched character + */ + void add_color(Gtk::Label *label, const Glib::ustring &search, const Glib::ustring &subject, bool tooltip=false); + void remove_color(Gtk::Label *label, const Glib::ustring &subject, bool tooltip=false); + static void add_color_description(Gtk::Label *label, const Glib::ustring &search); + + /** + * Executes Action + */ + bool ask_action_parameter(const ActionPtrName &action); + static ActionPtrName get_action_ptr_name(const Glib::ustring &full_action_name); + bool execute_action(const ActionPtrName &action, const Glib::ustring &value); + + static TypeOfVariant get_action_variant_type(const ActionPtr &action_ptr); + + static std::pair<Gtk::Label *, Gtk::Label *> get_name_desc(Gtk::ListBoxRow *child); + Gtk::Button *get_full_action_name(Gtk::ListBoxRow *child); + +private: // variables + // Widgets + Glib::RefPtr<Gtk::Builder> _builder; + + Gtk::Box *_CPBase; + Gtk::Box *_CPHeader; + Gtk::Box *_CPListBase; + + Gtk::SearchBar *_CPSearchBar; + Gtk::SearchEntry *_CPFilter; + + Gtk::ListBox *_CPSuggestions; + Gtk::ListBox *_CPHistory; + + Gtk::ScrolledWindow *_CPSuggestionsScroll; + Gtk::ScrolledWindow *_CPHistoryScroll; + + // Data + const int _max_height_requestable = 360; + Glib::ustring _search_text; + + // States + bool _is_open = false; + bool _win_doc_actions_loaded = false; + + // History + CPHistoryXML _history_xml; + /** + * Remember the mode we are in helps in unnecessary signal disconnection and reconnection + * Used by set_mode() + */ + CPMode _mode = CPMode::SHELL; + // Default value other than SEARCH required + // set_mode() switches between mode hence checks if it already in the target mode. + // Constructed value is sometimes SEARCH being the first Item for now + // set_mode() never attaches the on search listener then + // This initialising value can be any thing other than the initial required mode + // Example currently it's open in search mode + + /** + * Stores the search connection to deactivate when not needed + */ + sigc::connection _cpfilter_search_connection; + /** + * Stores the key_press connection to deactivate when not needed + */ + sigc::connection _cpfilter_key_press_connection; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_DIALOG_COMMAND_PALETTE_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/ui/dialog/debug.cpp b/src/ui/dialog/debug.cpp new file mode 100644 index 0000000..b9e7f28 --- /dev/null +++ b/src/ui/dialog/debug.cpp @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A dialog that displays log messages. + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004 The Inkscape Organization + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include <gtkmm/dialog.h> +#include <gtkmm/textview.h> +#include <gtkmm/menubar.h> +#include <gtkmm/scrolledwindow.h> +#include <glibmm/i18n.h> + +#include "debug.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A very simple dialog for displaying Inkscape messages - implementation. + */ +class DebugDialogImpl : public DebugDialog, public Gtk::Dialog +{ +public: + DebugDialogImpl(); + ~DebugDialogImpl() override; + + void show() override; + void hide() override; + void clear() override; + void message(char const *msg) override; + void captureLogMessages() override; + void releaseLogMessages() override; + +private: + Gtk::MenuBar menuBar; + Gtk::Menu fileMenu; + Gtk::ScrolledWindow textScroll; + Gtk::TextView messageText; + + //Handler ID's + guint handlerDefault; + guint handlerGlibmm; + guint handlerAtkmm; + guint handlerPangomm; + guint handlerGdkmm; + guint handlerGtkmm; +}; + +void DebugDialogImpl::clear() +{ + Glib::RefPtr<Gtk::TextBuffer> buffer = messageText.get_buffer(); + buffer->erase(buffer->begin(), buffer->end()); +} + +DebugDialogImpl::DebugDialogImpl() +{ + set_title(_("Messages")); + set_size_request(300, -1); + auto mainVBox = get_content_area(); + + //## Add a menu for clear() + Gtk::MenuItem* item = Gtk::manage(new Gtk::MenuItem(_("_File"), true)); + item->set_submenu(fileMenu); + menuBar.append(*item); + + item = Gtk::manage(new Gtk::MenuItem(_("_Clear"), true)); + item->signal_activate().connect(sigc::mem_fun(*this, &DebugDialogImpl::clear)); + fileMenu.append(*item); + + item = Gtk::manage(new Gtk::MenuItem(_("Capture log messages"))); + item->signal_activate().connect(sigc::mem_fun(*this, &DebugDialogImpl::captureLogMessages)); + fileMenu.append(*item); + + item = Gtk::manage(new Gtk::MenuItem(_("Release log messages"))); + item->signal_activate().connect(sigc::mem_fun(*this, &DebugDialogImpl::releaseLogMessages)); + fileMenu.append(*item); + + mainVBox->pack_start(menuBar, Gtk::PACK_SHRINK); + + + //### Set up the text widget + messageText.set_editable(false); + textScroll.add(messageText); + textScroll.set_policy(Gtk::POLICY_ALWAYS, Gtk::POLICY_ALWAYS); + mainVBox->pack_start(textScroll); + + show_all_children(); + + message("ready."); + message("enable log display by setting "); + message("dialogs.debug 'redirect' attribute to 1 in preferences.xml"); + + handlerDefault = 0; + handlerGlibmm = 0; + handlerAtkmm = 0; + handlerPangomm = 0; + handlerGdkmm = 0; + handlerGtkmm = 0; +} + + +DebugDialog *DebugDialog::create() +{ + DebugDialog *dialog = new DebugDialogImpl(); + return dialog; +} + +DebugDialogImpl::~DebugDialogImpl() += default; + +void DebugDialogImpl::show() +{ + //call super() + Gtk::Dialog::show(); + //sp_transientize(GTK_WIDGET(gobj())); //Make transient + raise(); + Gtk::Dialog::present(); +} + +void DebugDialogImpl::hide() +{ + // call super + Gtk::Dialog::hide(); +} + +void DebugDialogImpl::message(char const *msg) +{ + Glib::RefPtr<Gtk::TextBuffer> buffer = messageText.get_buffer(); + Glib::ustring uMsg = msg; + if (uMsg[uMsg.length()-1] != '\n') + uMsg += '\n'; + buffer->insert (buffer->end(), uMsg); +} + +/* static instance, to reduce dependencies */ +static DebugDialog *debugDialogInstance = nullptr; + +DebugDialog *DebugDialog::getInstance() +{ + if (!debugDialogInstance) { + debugDialogInstance = new DebugDialogImpl(); + } + return debugDialogInstance; +} + + + +void DebugDialog::showInstance() +{ + DebugDialog *debugDialog = getInstance(); + debugDialog->show(); + // this is not a real memleak because getInstance() only creates a debug dialog once, and returns that instance for all subsequent calls + // cppcheck-suppress memleak +} + + + + +/*##### THIS IS THE IMPORTANT PART ##### */ +static void dialogLoggingFunction(const gchar */*log_domain*/, + GLogLevelFlags /*log_level*/, + const gchar *messageText, + gpointer user_data) +{ + DebugDialogImpl *dlg = static_cast<DebugDialogImpl *>(user_data); + dlg->message(messageText); +} + + +void DebugDialogImpl::captureLogMessages() +{ + /* + This might likely need more code, to capture Gtkmm + and Glibmm warnings, or maybe just simply grab stdout/stderr + */ + GLogLevelFlags flags = (GLogLevelFlags) (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | + G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE | + G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG); + if ( !handlerDefault ) { + handlerDefault = g_log_set_handler(nullptr, flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerGlibmm ) { + handlerGlibmm = g_log_set_handler("glibmm", flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerAtkmm ) { + handlerAtkmm = g_log_set_handler("atkmm", flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerPangomm ) { + handlerPangomm = g_log_set_handler("pangomm", flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerGdkmm ) { + handlerGdkmm = g_log_set_handler("gdkmm", flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerGtkmm ) { + handlerGtkmm = g_log_set_handler("gtkmm", flags, + dialogLoggingFunction, (gpointer)this); + } + message("log capture started"); +} + +void DebugDialogImpl::releaseLogMessages() +{ + if ( handlerDefault ) { + g_log_remove_handler(nullptr, handlerDefault); + handlerDefault = 0; + } + if ( handlerGlibmm ) { + g_log_remove_handler("glibmm", handlerGlibmm); + handlerGlibmm = 0; + } + if ( handlerAtkmm ) { + g_log_remove_handler("atkmm", handlerAtkmm); + handlerAtkmm = 0; + } + if ( handlerPangomm ) { + g_log_remove_handler("pangomm", handlerPangomm); + handlerPangomm = 0; + } + if ( handlerGdkmm ) { + g_log_remove_handler("gdkmm", handlerGdkmm); + handlerGdkmm = 0; + } + if ( handlerGtkmm ) { + g_log_remove_handler("gtkmm", handlerGtkmm); + handlerGtkmm = 0; + } + message("log capture discontinued"); +} + + + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/debug.h b/src/ui/dialog/debug.h new file mode 100644 index 0000000..4520c73 --- /dev/null +++ b/src/ui/dialog/debug.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for displaying Inkscape messages + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_DIALOGS_DEBUGDIALOG_H +#define SEEN_UI_DIALOGS_DEBUGDIALOG_H + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/** + * @brief A very simple dialog for displaying Inkscape messages. + * + * Messages sent to g_log(), g_warning(), g_message(), ets, are routed here, + * in order to avoid messing with the startup console. + */ +class DebugDialog +{ +public: + DebugDialog() = default;; + /** + * Factory method + */ + static DebugDialog *create(); + + /** + * Destructor + */ + virtual ~DebugDialog() = default;; + + + /** + * Show the dialog + */ + virtual void show() = 0; + + /** + * Do not show the dialog + */ + virtual void hide() = 0; + + /** + * @brief Clear all information from the dialog + * + * Also a public method. Remove all text from the dialog + */ + virtual void clear() = 0; + + /** + * Display a message + */ + virtual void message(char const *msg) = 0; + + /** + * Redirect g_log() messages to this widget + */ + virtual void captureLogMessages() = 0; + + /** + * Return g_log() messages to normal handling + */ + virtual void releaseLogMessages() = 0; + + /** + * Factory method. Use this to create a new DebugDialog + */ + static DebugDialog *getInstance(); + + /** + * Show the instance above + */ + static void showInstance(); +}; + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +#endif /* __DEBUGDIALOG_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/ui/dialog/dialog-base.cpp b/src/ui/dialog/dialog-base.cpp new file mode 100644 index 0000000..8ddc5c4 --- /dev/null +++ b/src/ui/dialog/dialog-base.cpp @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** @file + * @brief A base class for all dialogs. + * + * Authors: see git history + * Tavmjong Bah + * + * Copyright (c) 2018 Tavmjong Bah, Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "dialog-base.h" + +#include <glibmm/i18n.h> +#include <glibmm/main.h> +#include <glibmm/refptr.h> +#include <gtkmm/cssprovider.h> +#include <gtkmm/notebook.h> +#include <gtkmm/scrolledwindow.h> +#include <iostream> + +#include "inkscape.h" +#include "desktop.h" +#include "ui/dialog/dialog-data.h" +#include "ui/dialog/dialog-notebook.h" +#include "ui/dialog-events.h" +// get_latin_keyval +#include "ui/tools/tool-base.h" +#include "widgets/spw-utilities.h" +#include "ui/widget/canvas.h" +#include "ui/util.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * DialogBase constructor. + * + * @param prefs_path characteristic path to load/save dialog position. + * @param dialog_type is the "type" string for the dialog. + */ +DialogBase::DialogBase(gchar const *prefs_path, Glib::ustring dialog_type) + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) + , desktop(nullptr) + , document(nullptr) + , selection(nullptr) + , _name("DialogBase") + , _prefs_path(prefs_path) + , _dialog_type(dialog_type) + , _app(InkscapeApplication::instance()) +{ + auto const &dialog_data = get_dialog_data(); + + // Derive a pretty display name for the dialog. + auto it = dialog_data.find(dialog_type); + if (it != dialog_data.end()) { + + _name = it->second.label; // Already translated + + // remove ellipsis and mnemonics + int pos = _name.find("...", 0); + if (pos >= 0 && pos < _name.length() - 2) { + _name.erase(pos, 3); + } + pos = _name.find("…", 0); + if (pos >= 0 && pos < _name.length()) { + _name.erase(pos, 1); + } + pos = _name.find("_", 0); + if (pos >= 0 && pos < _name.length()) { + _name.erase(pos, 1); + } + } + + set_name(_dialog_type); // Essential for dialog functionality + property_margin().set_value(1); // Essential for dialog UI + ensure_size(); +} + +DialogBase::~DialogBase() { +#ifdef _WIN32 + // this is bad, but it supposedly fixes some resizng problem on Windows + ensure_size(); +#endif + + unsetDesktop(); +}; + +void DialogBase::ensure_size() { + if (desktop) { + resize_widget_children(desktop->getToplevel()); + } +} + +void DialogBase::on_map() { + // Update asks the dialogs if they need their Gtk widgets updated. + update(); + // Set the desktop on_map, although we might want to be smarter about this. + // Note: Inkscape::Application::instance().active_desktop() is used here, as it contains current desktop at + // the time of dialog creation. Formerly used _app.get_active_view() did not at application start-up. + setDesktop(Inkscape::Application::instance().active_desktop()); + parent_type::on_map(); +} + +bool DialogBase::on_key_press_event(GdkEventKey* key_event) { + switch (Inkscape::UI::Tools::get_latin_keyval(key_event)) { + case GDK_KEY_Escape: + defocus_dialog(); + return true; + } + + return parent_type::on_key_press_event(key_event); +} + + +/** + * Highlight notebook where dialog already exists. + */ +void DialogBase::blink() +{ + Gtk::Notebook *notebook = dynamic_cast<Gtk::Notebook *>(get_parent()); + if (notebook && notebook->get_is_drawable()) { + // Switch notebook to this dialog. + notebook->set_current_page(notebook->page_num(*this)); + notebook->get_style_context()->add_class("blink"); + + // Add timer to turn off blink. + sigc::slot<bool> slot = sigc::mem_fun(*this, &DialogBase::blink_off); + sigc::connection connection = Glib::signal_timeout().connect(slot, 1000); // msec + } +} + +void DialogBase::focus_dialog() { + if (auto window = dynamic_cast<Gtk::Window*>(get_toplevel())) { + window->present(); + } + + // widget that had focus, if any + if (auto child = get_focus_child()) { + child->grab_focus(); + } + else { + // find first focusable widget + if (auto child = sp_find_focusable_widget(this)) { + child->grab_focus(); + } + } +} + +void DialogBase::defocus_dialog() { + if (auto wnd = dynamic_cast<Gtk::Window*>(get_toplevel())) { + // defocus floating dialog: + sp_dialog_defocus_cpp(wnd); + + // for docked dialogs, move focus to canvas + if (auto desktop = getDesktop()) { + desktop->getCanvas()->grab_focus(); + } + } +} + +/** + * Callback to reset the dialog highlight. + */ +bool DialogBase::blink_off() +{ + Gtk::Notebook *notebook = dynamic_cast<Gtk::Notebook *>(get_parent()); + if (notebook && notebook->get_is_drawable()) { + notebook->get_style_context()->remove_class("blink"); + } + return false; +} + +/** + * Called when the desktop might have changed for this dialog. + */ +void DialogBase::setDesktop(SPDesktop *new_desktop) +{ + if (desktop == new_desktop) { + return; + } + + unsetDesktop(); + + if (new_desktop) { + desktop = new_desktop; + + if (desktop->selection) { + selection = desktop->selection; + _select_changed = selection->connectChanged(sigc::mem_fun(*this, &DialogBase::selectionChanged_impl)); + _select_modified = selection->connectModified(sigc::mem_fun(*this, &DialogBase::selectionModified_impl)); + } + + _doc_replaced = desktop->connectDocumentReplaced(sigc::hide<0>(sigc::mem_fun(*this, &DialogBase::setDocument))); + _desktop_destroyed = desktop->connectDestroy(sigc::mem_fun(*this, &DialogBase::desktopDestroyed)); + this->setDocument(desktop->getDocument()); + + if (desktop->selection) { + this->selectionChanged(selection); + } + set_sensitive(true); + } + + desktopReplaced(); +} + +// +void DialogBase::fix_inner_scroll(Gtk::Widget *scrollwindow) +{ + auto scrollwin = dynamic_cast<Gtk::ScrolledWindow *>(scrollwindow); + auto viewport = dynamic_cast<Gtk::ScrolledWindow *>(scrollwin->get_child()); + Gtk::Widget *child = nullptr; + if (viewport) { //some widgets has viewportother not + child = viewport->get_child(); + } else { + child = scrollwin->get_child(); + } + if (child && scrollwin) { + Glib::RefPtr<Gtk::Adjustment> adjustment = scrollwin->get_vadjustment(); + child->signal_scroll_event().connect([=](GdkEventScroll* event) { + auto container = dynamic_cast<Gtk::Container *>(this); + if (container) { + std::vector<Gtk::Widget*> widgets = container->get_children(); + if (widgets.size()) { + auto parentscroll = dynamic_cast<Gtk::ScrolledWindow *>(widgets[0]); + if (parentscroll) { + if (event->delta_y > 0 && (adjustment->get_value() + adjustment->get_page_size()) == adjustment->get_upper()) { + parentscroll->event((GdkEvent*)event); + return true; + } else if (event->delta_y < 0 && adjustment->get_value() == adjustment->get_lower()) { + parentscroll->event((GdkEvent*)event); + return true; + } + } + } + } + return false; + }); + } +} + +/** + * implementation method that call to main function only when tab is showing + */ +void +DialogBase::selectionChanged_impl(Inkscape::Selection *selection) { + if (_showing) { + selectionChanged(selection); + } +} + +/** + * implementation method that call to main function only when tab is showing + */ +void +DialogBase::selectionModified_impl(Inkscape::Selection *selection, guint flags) { + if (_showing) { + selectionModified(selection, flags); + } +} + +/** + * function called from notebook dialog that performs an update of the dialog and sets the dialog showing state true + */ +void +DialogBase::setShowing(bool showing) { + _showing = showing; + selectionChanged(getSelection()); +} + +/** + * Called to destruct desktops, must not call virtuals + */ +void DialogBase::unsetDesktop() +{ + desktop = nullptr; + document = nullptr; + selection = nullptr; + _desktop_destroyed.disconnect(); + _doc_replaced.disconnect(); + _select_changed.disconnect(); + _select_modified.disconnect(); +} + +void DialogBase::desktopDestroyed(SPDesktop* old_desktop) +{ + if (old_desktop == desktop && desktop) { + unsetDesktop(); + set_sensitive(false); + } +} + +/** + * Called when the document might have changed, called from setDesktop too. + */ +void DialogBase::setDocument(SPDocument *new_document) +{ + if (document != new_document) { + document = new_document; + documentReplaced(); + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/dialog-base.h b/src/ui/dialog/dialog-base.h new file mode 100644 index 0000000..4e3f7e4 --- /dev/null +++ b/src/ui/dialog/dialog-base.h @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef INK_DIALOG_BASE_H +#define INK_DIALOG_BASE_H + +/** @file + * @brief A base class for all dialogs. + * + * Authors: see git history + * Tavmjong Bah + * + * Copyright (c) 2018 Tavmjong Bah, Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/ustring.h> +#include <gtkmm/box.h> + +#include "inkscape-application.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * DialogBase is the base class for the dialog system. + * + * Each dialog has a reference to the application, in order to update it's inner focus + * (be it of the active desktop, document, selection, etc.) in the update() method. + * + * DialogsBase derived classes' instances live in DialogNotebook classes and are managed by + * DialogContainer classes. DialogContainer instances can have at most one type of dialog, + * differentiated by the associated type. + */ +class DialogBase : public Gtk::Box +{ + using parent_type = Gtk::Box; + +public: + DialogBase(gchar const *prefs_path = nullptr, Glib::ustring dialog_type = ""); + ~DialogBase() override; + + /** + * The update() method is essential to Gtk state management. DialogBase implementations get updated whenever + * a new focus event happens if they are in a DialogWindow or if they are in the currently focused window. + * + * DO NOT use update to keep SPDesktop, SPDocument or Selection states, use the virtual functions below. + */ + virtual void update() {} + + // Public for future use, say if the desktop is smartly set when docking dialogs. + void setDesktop(SPDesktop *new_desktop); + + void on_map() override; + + /* + * Often the dialog won't request the right size until the window has + * been pushed to resize all it's children. We do this on dialog creation + * and destruction. + */ + void ensure_size(); + + // Getters and setters + Glib::ustring get_name() { return _name; }; + const Glib::ustring& getPrefsPath() const { return _prefs_path; } + Glib::ustring const &get_type() const { return _dialog_type; } + + void blink(); + // find focusable widget to grab focus + void focus_dialog(); + // return focus back to canvas + void defocus_dialog(); + bool getShowing() { return _showing; } + // fix children scrolled windows to send outer scroll when his own reach limits + void fix_inner_scroll(Gtk::Widget *child); + // Too many dialogs have unprotected calls to ask for this data + SPDesktop *getDesktop() const { return desktop; } +protected: + InkscapeApplication *getApp() const { return _app; } + SPDocument *getDocument() const { return document; } + Selection *getSelection() const { return selection; } + friend class DialogNotebook; + void setShowing(bool showing); + Glib::ustring _name; // Gtk widget name (must be set!) + Glib::ustring const _prefs_path; // Stores characteristic path for loading/saving the dialog position. + Glib::ustring const _dialog_type; // Type of dialog (we could just use _pref_path?). +private: + bool blink_off(); // timer callback + bool on_key_press_event(GdkEventKey* key_event) override; + // return if dialog is on visible tab + bool _showing = true; + void unsetDesktop(); + void desktopDestroyed(SPDesktop* old_desktop); + void setDocument(SPDocument *new_document); + /** + * Called when the desktop has certainly changed. It may have changed to nullptr + * when destructing the dialog, so the override should expect nullptr too. + */ + virtual void desktopReplaced() {} + virtual void documentReplaced() {} + void selectionChanged_impl(Inkscape::Selection *selection); + void selectionModified_impl(Inkscape::Selection *selection, guint flags); + virtual void selectionChanged(Inkscape::Selection *selection) {}; + virtual void selectionModified(Inkscape::Selection *selection, guint flags) {}; + + sigc::connection _desktop_destroyed; + sigc::connection _doc_replaced; + sigc::connection _select_changed; + sigc::connection _select_modified; + + InkscapeApplication *_app; // Used for state management + SPDesktop *desktop; + SPDocument *document; + Selection *selection; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INK_DIALOG_BASE_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/ui/dialog/dialog-container.cpp b/src/ui/dialog/dialog-container.cpp new file mode 100644 index 0000000..a060e63 --- /dev/null +++ b/src/ui/dialog/dialog-container.cpp @@ -0,0 +1,1111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** @file + * @brief A widget that manages DialogNotebook's and other widgets inside a horizontal DialogMultipaned. + * + * Authors: see git history + * Tavmjong Bah + * + * Copyright (c) 2018 Tavmjong Bah, Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "dialog-container.h" + +#include <glibmm/i18n.h> +#include <giomm/file.h> +#include <glibmm/keyfile.h> +#include <gtkmm/box.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/image.h> + +#include "enums.h" +#include "inkscape-application.h" +#include "inkscape-window.h" +// #include "ui/dialog/align-and-distribute.h" +#include "ui/dialog/clonetiler.h" +#include "ui/dialog/dialog-data.h" +#include "ui/dialog/dialog-multipaned.h" +#include "ui/dialog/dialog-notebook.h" +#include "ui/dialog/dialog-window.h" +#include "ui/dialog/document-properties.h" +#include "ui/dialog/export.h" +#include "ui/dialog/fill-and-stroke.h" +#include "ui/dialog/filter-effects-dialog.h" +#include "ui/dialog/find.h" +#include "ui/dialog/glyphs.h" +#include "ui/dialog/icon-preview.h" +#include "ui/dialog/inkscape-preferences.h" +#include "ui/dialog/input.h" +#include "ui/dialog/livepatheffect-editor.h" +#include "ui/dialog/memory.h" +#include "ui/dialog/messages.h" +#include "ui/dialog/object-attributes.h" +#include "ui/dialog/object-properties.h" +#include "ui/dialog/objects.h" +#include "ui/dialog/paint-servers.h" +#include "ui/dialog/prototype.h" +#include "ui/dialog/selectorsdialog.h" +#include "ui/shortcuts.h" +#if WITH_GSPELL +#include "ui/dialog/spellcheck.h" +#endif +#include "ui/dialog/styledialog.h" +#include "ui/dialog/svg-fonts-dialog.h" +#include "ui/dialog/swatches.h" +#include "ui/dialog/symbols.h" +#include "ui/dialog/text-edit.h" +#include "ui/dialog/tile.h" +#include "ui/dialog/tracedialog.h" +#include "ui/dialog/transformation.h" +#include "ui/dialog/undo-history.h" +#include "ui/dialog/xml-tree.h" +#include "ui/icon-names.h" +#include "ui/widget/canvas-grid.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +DialogContainer::~DialogContainer() { + // delete columns; desktop widget deletes dialog container before it get "unrealized", + // so it doesn't get a chance to remove them + delete columns; +} + +DialogContainer::DialogContainer(InkscapeWindow* inkscape_window) + : _inkscape_window(inkscape_window) +{ + g_assert(_inkscape_window != nullptr); + + get_style_context()->add_class("DialogContainer"); + + // Setup main column + columns = Gtk::manage(new DialogMultipaned(Gtk::ORIENTATION_HORIZONTAL)); + + connections.emplace_back(columns->signal_prepend_drag_data().connect( + sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::prepend_drop), columns))); + + connections.emplace_back(columns->signal_append_drag_data().connect( + sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::append_drop), columns))); + + // Setup drop targets. + target_entries.emplace_back(Gtk::TargetEntry("GTK_NOTEBOOK_TAB")); + columns->set_target_entries(target_entries); + + add(*columns); + + // Should probably be moved to window. + // connections.emplace_back(signal_unmap().connect(sigc::mem_fun(*this, &DialogContainer::cb_on_unmap))); + + show_all_children(); +} + +DialogMultipaned *DialogContainer::create_column() +{ + DialogMultipaned *column = Gtk::manage(new DialogMultipaned(Gtk::ORIENTATION_VERTICAL)); + + connections.emplace_back(column->signal_prepend_drag_data().connect( + sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::prepend_drop), column))); + + connections.emplace_back(column->signal_append_drag_data().connect( + sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::append_drop), column))); + + connections.emplace_back(column->signal_now_empty().connect( + sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::column_empty), column))); + + column->set_target_entries(target_entries); + + return column; +} + +/** + * Get an instance of a DialogBase dialog using the associated dialog name. + */ +DialogBase *DialogContainer::dialog_factory(const Glib::ustring& dialog_type) +{ + + // clang-format off + if( dialog_type == "AlignDistribute") return &Inkscape::UI::Dialog::ArrangeDialog::getInstance(); + else if(dialog_type == "CloneTiler") return &Inkscape::UI::Dialog::CloneTiler::getInstance(); + else if(dialog_type == "DocumentProperties") return &Inkscape::UI::Dialog::DocumentProperties::getInstance(); + else if(dialog_type == "Export") return &Inkscape::UI::Dialog::Export::getInstance(); + else if(dialog_type == "FillStroke") return &Inkscape::UI::Dialog::FillAndStroke::getInstance(); + else if(dialog_type == "FilterEffects") return &Inkscape::UI::Dialog::FilterEffectsDialog::getInstance(); + else if(dialog_type == "Find") return &Inkscape::UI::Dialog::Find::getInstance(); + else if(dialog_type == "Glyphs") return &Inkscape::UI::Dialog::GlyphsPanel::getInstance(); + else if(dialog_type == "IconPreview") return &Inkscape::UI::Dialog::IconPreviewPanel::getInstance(); + else if(dialog_type == "Input") return &Inkscape::UI::Dialog::InputDialog::getInstance(); + else if(dialog_type == "LivePathEffect") return &Inkscape::UI::Dialog::LivePathEffectEditor::getInstance(); + else if(dialog_type == "Memory") return &Inkscape::UI::Dialog::Memory::getInstance(); + else if(dialog_type == "Messages") return &Inkscape::UI::Dialog::Messages::getInstance(); + else if(dialog_type == "ObjectAttributes") return &Inkscape::UI::Dialog::ObjectAttributes::getInstance(); + else if(dialog_type == "ObjectProperties") return &Inkscape::UI::Dialog::ObjectProperties::getInstance(); + else if(dialog_type == "Objects") return &Inkscape::UI::Dialog::ObjectsPanel::getInstance(); + else if(dialog_type == "PaintServers") return &Inkscape::UI::Dialog::PaintServersDialog::getInstance(); + else if(dialog_type == "Preferences") return &Inkscape::UI::Dialog::InkscapePreferences::getInstance(); + else if(dialog_type == "Selectors") return &Inkscape::UI::Dialog::SelectorsDialog::getInstance(); + else if(dialog_type == "SVGFonts") return &Inkscape::UI::Dialog::SvgFontsDialog::getInstance(); + else if(dialog_type == "Swatches") return &Inkscape::UI::Dialog::SwatchesPanel::getInstance(); + else if(dialog_type == "Symbols") return &Inkscape::UI::Dialog::SymbolsDialog::getInstance(); + else if(dialog_type == "Text") return &Inkscape::UI::Dialog::TextEdit::getInstance(); + else if(dialog_type == "Trace") return &Inkscape::UI::Dialog::TraceDialog::getInstance(); + else if(dialog_type == "Transform") return &Inkscape::UI::Dialog::Transformation::getInstance(); + else if(dialog_type == "UndoHistory") return &Inkscape::UI::Dialog::UndoHistory::getInstance(); + else if(dialog_type == "XMLEditor") return &Inkscape::UI::Dialog::XmlTree::getInstance(); +#if WITH_GSPELL + else if(dialog_type == "Spellcheck") return &Inkscape::UI::Dialog::SpellCheck::getInstance(); +#endif +#ifdef DEBUG + else if(dialog_type == "Prototype") return &Inkscape::UI::Dialog::Prototype::getInstance(); +#endif + else { + std::cerr << "DialogContainer::dialog_factory: Unhandled dialog: " << dialog_type << std::endl; + return nullptr; + } + // clang-format on +} + +// Create the notebook tab +Gtk::Widget *DialogContainer::create_notebook_tab(Glib::ustring label_str, Glib::ustring image_str, const Glib::ustring shortcut) +{ + Gtk::Label *label = Gtk::manage(new Gtk::Label(label_str)); + Gtk::Image *image = Gtk::manage(new Gtk::Image()); + Gtk::Button *close = Gtk::manage(new Gtk::Button()); + image->set_from_icon_name(image_str, Gtk::ICON_SIZE_MENU); + Gtk::Box *tab = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 2)); + close->set_image_from_icon_name("window-close"); + close->set_halign(Gtk::ALIGN_END); + close->set_tooltip_text(_("Close Tab")); + close->get_style_context()->add_class("close-button"); + Glib::ustring label_str_fix = label_str; + label_str_fix = Glib::Regex::create("\\W")->replace_literal(label_str_fix, 0, "-", (Glib::RegexMatchFlags)0); + tab->get_style_context()->add_class(label_str_fix); + tab->pack_start(*image); + tab->pack_end(*close); + tab->pack_end(*label); + tab->show_all(); + + // Workaround to the fact that Gtk::Box doesn't receive on_button_press event + Gtk::EventBox *cover = Gtk::manage(new Gtk::EventBox()); + cover->add(*tab); + + // Add shortcut tooltip + if (shortcut.size() > 0) { + auto tlabel = shortcut; + int pos = tlabel.find("&", 0); + if (pos >= 0 && pos < tlabel.length()) { + tlabel.replace(pos, 1, "&"); + } + tab->set_tooltip_markup(label_str + " (<b>" + tlabel + "</b>)"); + } else { + tab->set_tooltip_text(label_str); + } + + return cover; +} + +// find dialog's multipaned parent; is there a better way? +DialogMultipaned* get_dialog_parent(DialogBase* dialog) { + if (!dialog) return nullptr; + + // dialogs are nested inside Gtk::Notebook + if (auto notebook = dynamic_cast<Gtk::Notebook*>(dialog->get_parent())) { + // notebooks are inside viewport, inside scrolled window + if (auto viewport = dynamic_cast<Gtk::Viewport*>(notebook->get_parent())) { + if (auto scroll = dynamic_cast<Gtk::ScrolledWindow*>(viewport->get_parent())) { + // finally get the panel + if (auto panel = dynamic_cast<DialogMultipaned*>(scroll->get_parent())) { + return panel; + } + } + } + } + + return nullptr; +} + +/** + * Add new dialog to the current container or in a floating window, based on preferences. + */ +void DialogContainer::new_dialog(const Glib::ustring& dialog_type ) +{ + // Open all dialogs as floating, if set in preferences + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs == nullptr) { + return; + } + + int dockable = prefs->getInt("/options/dialogtype/value", PREFS_DIALOGS_BEHAVIOR_DOCKABLE); + bool floating = DialogManager::singleton().should_open_floating(dialog_type); + if (dockable == PREFS_DIALOGS_BEHAVIOR_FLOATING || floating) { + new_floating_dialog(dialog_type); + } else { + new_dialog(dialog_type, nullptr); + } + + if (DialogBase* dialog = find_existing_dialog(dialog_type)) { + dialog->focus_dialog(); + } +} + + +DialogBase* DialogContainer::find_existing_dialog(const Glib::ustring& dialog_type) { + DialogBase *existing_dialog = get_dialog(dialog_type); + if (!existing_dialog) { + existing_dialog = DialogManager::singleton().find_floating_dialog(dialog_type); + } + return existing_dialog; +} + +/** + * Overloaded new_dialog + */ +void DialogContainer::new_dialog(const Glib::ustring& dialog_type, DialogNotebook *notebook) +{ + columns->ensure_multipaned_children(); + + // Limit each container to containing one of any type of dialog. + if (DialogBase* existing_dialog = find_existing_dialog(dialog_type)) { + // make sure parent window is not hidden/collapsed + if (auto panel = get_dialog_parent(existing_dialog)) { + panel->show(); + } + // found existing dialog; blink & exit + existing_dialog->blink(); + return; + } + + // Create the dialog widget + DialogBase *dialog = dialog_factory(dialog_type); + + if (!dialog) { + std::cerr << "DialogContainer::new_dialog(): couldn't find dialog for: " << dialog_type << std::endl; + return; + } + + // manage the dialog instance + dialog = Gtk::manage(dialog); + + // Create the notebook tab + auto const &dialog_data = get_dialog_data(); + Glib::ustring image("inkscape-logo"); + auto it = dialog_data.find(dialog_type); + if (it != dialog_data.end()) { + image = it->second.icon_name; + } + + Glib::ustring label; + Glib::ustring action_name = "win.dialog-open('" + dialog_type + "')"; + auto app = InkscapeApplication::instance(); + std::vector<Glib::ustring> accels = app->gtk_app()->get_accels_for_action(action_name); + if (accels.size() > 0) { + guint key = 0; + Gdk::ModifierType mods; + Gtk::AccelGroup::parse(accels[0], key, mods); + label = Gtk::AccelGroup::get_label(key, mods); + } + + Gtk::Widget *tab = create_notebook_tab(dialog->get_name(), image, label); + + // If not from notebook menu add at top of last column. + if (!notebook) { + // Look to see if last column contains a multipane. If not, add one. + DialogMultipaned *last_column = dynamic_cast<DialogMultipaned *>(columns->get_last_widget()); + if (!last_column) { + last_column = create_column(); + columns->append(last_column); + } + + // Look to see if first widget in column is notebook, if not add one. + notebook = dynamic_cast<DialogNotebook *>(last_column->get_first_widget()); + if (!notebook) { + notebook = Gtk::manage(new DialogNotebook(this)); + last_column->prepend(notebook); + } + } + + // Add dialog + notebook->add_page(*dialog, *tab, dialog->get_name()); + + if (auto panel = dynamic_cast<DialogMultipaned*>(notebook->get_parent())) { + // if panel is collapsed, show it now, or else new dialog will be mysteriously missing + panel->show(); + } +} + +// recreate dialogs hosted (docked) in a floating DialogWindow; window will be created +bool DialogContainer::recreate_dialogs_from_state(InkscapeWindow* inkscape_window, const Glib::KeyFile* keyfile) +{ + g_assert(inkscape_window != nullptr); + + bool restored = false; + // Step 1: check if we want to load the state + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int save_state = prefs->getInt("/options/savedialogposition/value", PREFS_DIALOGS_STATE_SAVE); + if (save_state == PREFS_DIALOGS_STATE_NONE) { + return restored; // User has turned off this feature in Preferences + } + + // if it isn't dockable, all saved docked dialogs are made floating + bool is_dockable = + prefs->getInt("/options/dialogtype/value", PREFS_DIALOGS_BEHAVIOR_DOCKABLE) != PREFS_DIALOGS_BEHAVIOR_FLOATING; + + if (!is_dockable) + return false; // not applicable if docking is off + + // Step 2: get the number of windows; should be 1 + int windows_count = 0; + try { + windows_count = keyfile->get_integer("Windows", "Count"); + } catch (Glib::Error &error) { + std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl; + } + + // Step 3: for each window, load its state. + for (int window_idx = 0; window_idx < windows_count; ++window_idx) { + Glib::ustring group_name = "Window" + std::to_string(window_idx); + + bool has_position = keyfile->has_key(group_name, "Position") && keyfile->get_boolean(group_name, "Position"); + window_position_t pos; + if (has_position) { // floating window position recorded? + pos.x = keyfile->get_integer(group_name, "x"); + pos.y = keyfile->get_integer(group_name, "y"); + pos.width = keyfile->get_integer(group_name, "width"); + pos.height = keyfile->get_integer(group_name, "height"); + } + // Step 3.0: read the window parameters + int column_count = 0; + try { + column_count = keyfile->get_integer(group_name, "ColumnCount"); + } catch (Glib::Error &error) { + std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl; + } + + // Step 3.1: get the window's container columns where we want to create the dialogs + DialogWindow *dialog_window = new DialogWindow(inkscape_window, nullptr); + DialogContainer *active_container = dialog_window->get_container(); + DialogMultipaned *active_columns = active_container ? active_container->get_columns() : nullptr; + + if (!active_container || !active_columns) { + continue; + } + + // Step 3.2: for each column, load its state + for (int column_idx = 0; column_idx < column_count; ++column_idx) { + Glib::ustring column_group_name = group_name + "Column" + std::to_string(column_idx); + + // Step 3.2.0: read the column parameters + int notebook_count = 0; + bool before_canvas = false; + try { + notebook_count = keyfile->get_integer(column_group_name, "NotebookCount"); + if (keyfile->has_key(column_group_name, "BeforeCanvas")) { + before_canvas = keyfile->get_boolean(column_group_name, "BeforeCanvas"); + } + } catch (Glib::Error &error) { + std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl; + } + + // Step 3.2.1: create the column + DialogMultipaned *column = active_container->create_column(); + + before_canvas ? active_columns->prepend(column) : active_columns->append(column); + + // Step 3.2.2: for each noteboook, load its dialogs + for (int notebook_idx = 0; notebook_idx < notebook_count; ++notebook_idx) { + Glib::ustring key = "Notebook" + std::to_string(notebook_idx) + "Dialogs"; + + // Step 3.2.2.0 read the list of dialogs in the current notebook + std::vector<Glib::ustring> dialogs; + try { + dialogs = keyfile->get_string_list(column_group_name, key); + } catch (Glib::Error &error) { + std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl; + } + + if (!dialogs.size()) { + continue; + } + + DialogNotebook *notebook = nullptr; + auto const &dialog_data = get_dialog_data(); + + // Step 3.2.2.1 create each dialog in the current notebook + for (auto type : dialogs) { + if (DialogManager::singleton().find_floating_dialog(type)) { + // avoid duplicates + continue; + } + + if (dialog_data.find(type) != dialog_data.end()) { + if (!notebook) { + notebook = Gtk::manage(new DialogNotebook(active_container)); + column->append(notebook); + } + active_container->new_dialog(type, notebook); + } else { + std::cerr << "recreate_dialogs_from_state: invalid dialog type: " << type << std::endl; + } + } + } + } + + if (has_position) { + dm_restore_window_position(*dialog_window, pos); + } + else { + dialog_window->update_window_size_to_fit_children(); + } + dialog_window->show_all(); + restored = true; + } + + return restored; +} + +/** + * Add a new floating dialog (or reuse existing one if it's already up) + */ +DialogWindow *DialogContainer::new_floating_dialog(const Glib::ustring& dialog_type) +{ + return create_new_floating_dialog(dialog_type, true); +} + +DialogWindow *DialogContainer::create_new_floating_dialog(const Glib::ustring& dialog_type, bool blink) +{ + // check if this dialog is already open + if (DialogBase* existing_dialog = find_existing_dialog(dialog_type)) { + // found existing dialog; blink & exit + if (blink) { + existing_dialog->blink(); + // show its window if it is hidden + if (auto window = DialogManager::singleton().find_floating_dialog_window(dialog_type)) { + DialogManager::singleton().set_floating_dialog_visibility(window, true); + } + } + return nullptr; + } + + // check if this dialog *was* open and floating; if so recreate its window + if (auto state = DialogManager::singleton().find_dialog_state(dialog_type)) { + if (recreate_dialogs_from_state(_inkscape_window, state.get())) { + return nullptr; + } + } + + // Create the dialog widget + DialogBase *dialog = dialog_factory(dialog_type); + + if (!dialog) { + std::cerr << "DialogContainer::new_dialog(): couldn't find dialog for: " << dialog_type << std::endl; + return nullptr; + } + + // manage the dialog instance + dialog = Gtk::manage(dialog); + + // Create the notebook tab + gchar* image = nullptr; + + Glib::ustring label; + Glib::ustring action_name = "win.dialog-open('" + dialog_type + "')"; + auto app = InkscapeApplication::instance(); + std::vector<Glib::ustring> accels = app->gtk_app()->get_accels_for_action(action_name); + if (accels.size() > 0) { + guint key = 0; + Gdk::ModifierType mods; + Gtk::AccelGroup::parse(accels[0], key, mods); + label = Gtk::AccelGroup::get_label(key, mods); + } + + Gtk::Widget *tab = + create_notebook_tab(dialog->get_name(), image ? Glib::ustring(image) : INKSCAPE_ICON("inkscape-logo"), label); + + // New temporary noteboook + DialogNotebook *notebook = Gtk::manage(new DialogNotebook(this)); + notebook->add_page(*dialog, *tab, dialog->get_name()); + + return notebook->pop_tab_callback(); +} + +// toggle dialogs (visibility) is invoked on a top container embedded in Inkscape window +void DialogContainer::toggle_dialogs() +{ + // check how many dialog panels are visible and how many are hidden + // we use this info to decide what it means to toggle visibility + int visible = 0; + int hidden = 0; + for (auto child : columns->get_children()) { + // only examine panels, skip drop zones and handles + if (auto panel = dynamic_cast<DialogMultipaned*>(child)) { + if (panel->is_visible()) { + ++visible; + } + else { + ++hidden; + } + } + } + + // next examine floating dialogs + auto windows = DialogManager::singleton().get_all_floating_dialog_windows(); + for (auto wnd : windows) { + if (wnd->is_visible()) { + ++visible; + } + else { + ++hidden; + } + } + + bool show_dialogs = true; + // if some dialogs are hidden, toggle will first show them; + // another option could be to hide all if some dialogs are visible + if (hidden > 0) { + show_dialogs = true; + } + else { + // if everything's visible, hide them + show_dialogs = false; + } + + // set visibility of floating dialogs + for (auto wnd : windows) { + DialogManager::singleton().set_floating_dialog_visibility(wnd, show_dialogs); + } + + // set visibility of docked dialogs + columns->toggle_multipaned_children(show_dialogs); +} + +// Update dialogs +void DialogContainer::update_dialogs() +{ + for_each(dialogs.begin(), dialogs.end(), [&](auto dialog) { dialog.second->update(); }); +} + +void DialogContainer::set_inkscape_window(InkscapeWindow* inkscape_window) +{ + g_assert(inkscape_window != nullptr); + _inkscape_window = inkscape_window; + auto desktop = _inkscape_window->get_desktop(); + for_each(dialogs.begin(), dialogs.end(), [&](auto dialog) { dialog.second->setDesktop(desktop); }); +} + +bool DialogContainer::has_dialog_of_type(DialogBase *dialog) +{ + return (dialogs.find(dialog->get_type()) != dialogs.end()); +} + +DialogBase *DialogContainer::get_dialog(const Glib::ustring& dialog_type) +{ + auto found = dialogs.find(dialog_type); + if (found != dialogs.end()) { + return found->second; + } + return nullptr; +} + +// Add dialog to list. +void DialogContainer::link_dialog(DialogBase *dialog) +{ + dialogs.insert(std::pair<Glib::ustring, DialogBase *>(dialog->get_type(), dialog)); + + DialogWindow *window = dynamic_cast<DialogWindow *>(get_toplevel()); + if (window) { + window->update_dialogs(); + } + else { + // dialog without DialogWindow has been docked; remove it's floating state + // so if user closes and reopens it, it shows up docked again, not floating + DialogManager::singleton().remove_dialog_floating_state(dialog->get_type()); + } +} + +// Remove dialog from list. +void DialogContainer::unlink_dialog(DialogBase *dialog) +{ + if (!dialog) { + return; + } + + auto found = dialogs.find(dialog->get_type()); + if (found != dialogs.end()) { + dialogs.erase(found); + } + + DialogWindow *window = dynamic_cast<DialogWindow *>(get_toplevel()); + if (window) { + window->update_dialogs(); + } +} + +/** + * Load last open window's dialog configuration state. + * + * For the keyfile format, check `save_container_state()`. + */ +void DialogContainer::load_container_state(Glib::KeyFile *keyfile, bool include_floating) +{ + // Step 1: check if we want to load the state + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // if it isn't dockable, all saved docked dialogs are made floating + bool is_dockable = + prefs->getInt("/options/dialogtype/value", PREFS_DIALOGS_BEHAVIOR_DOCKABLE) != PREFS_DIALOGS_BEHAVIOR_FLOATING; + + // Step 2: get the number of windows + int windows_count = keyfile->get_integer("Windows", "Count"); + + // Step 3: for each window, load its state. Only the first window is not floating (the others are DialogWindow) + for (int window_idx = 0; window_idx < windows_count; ++window_idx) { + if (window_idx > 0 && !include_floating) + break; + + Glib::ustring group_name = "Window" + std::to_string(window_idx); + + // Step 3.0: read the window parameters + int column_count = 0; + bool floating = window_idx != 0; + window_position_t pos; + bool has_position = false; + try { + column_count = keyfile->get_integer(group_name, "ColumnCount"); + floating = keyfile->get_boolean(group_name, "Floating"); + if (keyfile->has_key(group_name, "Position") && keyfile->get_boolean(group_name, "Position")) { + pos.x = keyfile->get_integer(group_name, "x"); + pos.y = keyfile->get_integer(group_name, "y"); + pos.width = keyfile->get_integer(group_name, "width"); + pos.height = keyfile->get_integer(group_name, "height"); + has_position = true; + } + } catch (Glib::Error &error) { + std::cerr << "DialogContainer::load_container_state: " << error.what().raw() << std::endl; + } + + // Step 3.1: get the window's container columns where we want to create the dialogs + DialogContainer *active_container = nullptr; + DialogMultipaned *active_columns = nullptr; + DialogWindow *dialog_window = nullptr; + + if (is_dockable) { + if (floating) { + dialog_window = new DialogWindow(_inkscape_window, nullptr); + if (dialog_window) { + active_container = dialog_window->get_container(); + active_columns = dialog_window->get_container()->get_columns(); + } + } else { + active_container = this; + active_columns = columns; + } + + if (!active_container || !active_columns) { + continue; + } + } + + // Step 3.2: for each column, load its state + for (int column_idx = 0; column_idx < column_count; ++column_idx) { + Glib::ustring column_group_name = group_name + "Column" + std::to_string(column_idx); + + // Step 3.2.0: read the column parameters + int notebook_count = 0; + bool before_canvas = false; + try { + notebook_count = keyfile->get_integer(column_group_name, "NotebookCount"); + before_canvas = keyfile->get_boolean(column_group_name, "BeforeCanvas"); + } catch (Glib::Error &error) { + std::cerr << "DialogContainer::load_container_state: " << error.what().raw() << std::endl; + } + + // Step 3.2.1: create the column + DialogMultipaned *column = nullptr; + if (is_dockable) { + column = active_container->create_column(); + if (!column) { + continue; + } + + before_canvas ? active_columns->prepend(column) : active_columns->append(column); + } + + // Step 3.2.2: for each noteboook, load its dialogs + for (int notebook_idx = 0; notebook_idx < notebook_count; ++notebook_idx) { + Glib::ustring key = "Notebook" + std::to_string(notebook_idx) + "Dialogs"; + + // Step 3.2.2.0 read the list of dialogs in the current notebook + std::vector<Glib::ustring> dialogs; + try { + dialogs = keyfile->get_string_list(column_group_name, key); + } catch (Glib::Error &error) { + std::cerr << "DialogContainer::load_container_state: " << error.what().raw() << std::endl; + } + + if (!dialogs.size()) { + continue; + } + + DialogNotebook *notebook = nullptr; + if (is_dockable) { + notebook = Gtk::manage(new DialogNotebook(active_container)); + column->append(notebook); + } + + auto const &dialog_data = get_dialog_data(); + // Step 3.2.2.1 create each dialog in the current notebook + for (auto type : dialogs) { + + if (dialog_data.find(type) != dialog_data.end()) { + if (is_dockable) { + active_container->new_dialog(type, notebook); + } else { + dialog_window = create_new_floating_dialog(type, false); + } + } else { + std::cerr << "load_container_state: invalid dialog type: " << type << std::endl; + } + } + } + } + + if (dialog_window) { + if (has_position) { + dm_restore_window_position(*dialog_window, pos); + } + else { + dialog_window->update_window_size_to_fit_children(); + } + dialog_window->show_all(); + } + } +} + +void save_wnd_position(Glib::KeyFile *keyfile, const Glib::ustring &group_name, const window_position_t *position) +{ + keyfile->set_boolean(group_name, "Position", position != nullptr); + if (position) { // floating window position? + keyfile->set_integer(group_name, "x", position->x); + keyfile->set_integer(group_name, "y", position->y); + keyfile->set_integer(group_name, "width", position->width); + keyfile->set_integer(group_name, "height", position->height); + } +} + +// get *this* container's state only; store window 'position' in the state if given +std::shared_ptr<Glib::KeyFile> DialogContainer::get_container_state(const window_position_t *position) const +{ + std::shared_ptr<Glib::KeyFile> keyfile = std::make_shared<Glib::KeyFile>(); + + DialogMultipaned *window = columns; + const int window_idx = 0; + + // Step 2: save the number of windows + keyfile->set_integer("Windows", "Count", 1); + + // Step 3.0: get all the multipanes of the window + std::vector<DialogMultipaned *> multipanes; + + for (auto const &column : window->get_children()) { + if (auto paned = dynamic_cast<DialogMultipaned *>(column)) { + multipanes.push_back(paned); + } + } + + // Step 3.1: for each non-empty column, save its data. + int column_count = 0; // non-empty columns count + for (size_t column_idx = 0; column_idx < multipanes.size(); ++column_idx) { + Glib::ustring group_name = "Window" + std::to_string(window_idx) + "Column" + std::to_string(column_idx); + int notebook_count = 0; // non-empty notebooks count + + // Step 3.1.0: for each notebook, get its dialogs + for (auto const &columns_widget : multipanes[column_idx]->get_children()) { + if (auto dialog_notebook = dynamic_cast<DialogNotebook *>(columns_widget)) { + std::vector<Glib::ustring> dialogs; + + for (auto const &widget : dialog_notebook->get_notebook()->get_children()) { + if (DialogBase *dialog = dynamic_cast<DialogBase *>(widget)) { + dialogs.push_back(dialog->get_type()); + } + } + + // save the dialogs type + Glib::ustring key = "Notebook" + std::to_string(notebook_count) + "Dialogs"; + keyfile->set_string_list(group_name, key, dialogs); + + // increase the notebook count + notebook_count++; + } + } + + // Step 3.1.1: increase the column count + if (notebook_count != 0) { + column_count++; + } + + // Step 3.1.2: Save the column's data + keyfile->set_integer(group_name, "NotebookCount", notebook_count); + } + + // Step 3.2: save the window group + Glib::ustring group_name = "Window" + std::to_string(window_idx); + keyfile->set_integer(group_name, "ColumnCount", column_count); + save_wnd_position(keyfile.get(), group_name, position); + + return keyfile; +} + +/** + * Save container state. The configuration of open dialogs and the relative positions of the notebooks are saved. + * + * The structure of such a KeyFile is: + * + * There is a "Windows" group that records the number of the windows: + * [Windows] + * Count=1 + * + * A "WindowX" group saves the number of columns the window's container has and whether the window is floating: + * + * [Window0] + * ColumnCount=1 + * Floating=false + * + * For each column, we have a "WindowWColumnX" group, where X is the index of the column. "BeforeCanvas" checks + * if the column is before the canvas or not. "NotebookCount" records how many notebooks are in each column and + * "NotebookXDialogs" records a list of the types for the dialogs in notebook X. + * + * [Window0Column0] + * Notebook0Dialogs=Text; + * NotebookCount=2 + * BeforeCanvas=false + * + */ +std::unique_ptr<Glib::KeyFile> DialogContainer::save_container_state() +{ + std::unique_ptr<Glib::KeyFile> keyfile = std::make_unique<Glib::KeyFile>(); + auto app = InkscapeApplication::instance(); + + // Step 1: get all the container columns (in order, from the current container and all DialogWindow containers) + std::vector<DialogMultipaned *> windows(1, columns); + std::vector<DialogWindow *> dialog_windows(1, nullptr); + + for (auto const &window : app->gtk_app()->get_windows()) { + DialogWindow *dialog_window = dynamic_cast<DialogWindow *>(window); + if (dialog_window) { + windows.push_back(dialog_window->get_container()->get_columns()); + dialog_windows.push_back(dialog_window); + } + } + + // Step 2: save the number of windows + keyfile->set_integer("Windows", "Count", windows.size()); + + // Step 3: for each window, save its data. Only the first window is not floating (the others are DialogWindow) + for (int window_idx = 0; window_idx < (int)windows.size(); ++window_idx) { + // Step 3.0: get all the multipanes of the window + std::vector<DialogMultipaned *> multipanes; + + // used to check if the column is before or after canvas + std::vector<DialogMultipaned *>::iterator multipanes_it = multipanes.begin(); + bool canvas_seen = window_idx != 0; // no floating windows (window_idx > 0) have a canvas + int before_canvas_columns_count = 0; + + for (auto const &column : windows[window_idx]->get_children()) { + if (!canvas_seen) { + UI::Widget::CanvasGrid *canvas = dynamic_cast<UI::Widget::CanvasGrid *>(column); + if (canvas) { + canvas_seen = true; + } else { + DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(column); + if (paned) { + multipanes_it = multipanes.insert(multipanes_it, paned); + before_canvas_columns_count++; + } + } + } else { + DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(column); + if (paned) { + multipanes.push_back(paned); + } + } + } + + // Step 3.1: for each non-empty column, save its data. + int column_count = 0; // non-empty columns count + for (int column_idx = 0; column_idx < (int)multipanes.size(); ++column_idx) { + Glib::ustring group_name = "Window" + std::to_string(window_idx) + "Column" + std::to_string(column_idx); + int notebook_count = 0; // non-empty notebooks count + + // Step 3.1.0: for each notebook, get its dialogs' types + for (auto const &columns_widget : multipanes[column_idx]->get_children()) { + DialogNotebook *dialog_notebook = dynamic_cast<DialogNotebook *>(columns_widget); + + if (dialog_notebook) { + std::vector<Glib::ustring> dialogs; + + for (auto const &widget : dialog_notebook->get_notebook()->get_children()) { + DialogBase *dialog = dynamic_cast<DialogBase *>(widget); + if (dialog) { + dialogs.push_back(dialog->get_type()); + } + } + + // save the dialogs type + Glib::ustring key = "Notebook" + std::to_string(notebook_count) + "Dialogs"; + keyfile->set_string_list(group_name, key, dialogs); + + // increase the notebook count + notebook_count++; + } + } + + // Step 3.1.1: increase the column count + if (notebook_count != 0) { + column_count++; + } + + // Step 3.1.2: Save the column's data + keyfile->set_integer(group_name, "NotebookCount", notebook_count); + keyfile->set_boolean(group_name, "BeforeCanvas", (column_idx < before_canvas_columns_count)); + } + + // Step 3.2: save the window group + Glib::ustring group_name = "Window" + std::to_string(window_idx); + keyfile->set_integer(group_name, "ColumnCount", column_count); + keyfile->set_boolean(group_name, "Floating", window_idx != 0); + if (window_idx != 0) { // floating? + if (auto wnd = dynamic_cast<DialogWindow *>(dialog_windows.at(window_idx))) { + // store window position + auto pos = dm_get_window_position(*wnd); + save_wnd_position(keyfile.get(), group_name, pos ? &*pos : nullptr); + } + } + } + + return keyfile; +} + +// Signals ----------------------------------------------------- + +/** + * No zombie windows. TODO: Need to work on this as it still leaves Gtk::Window! (?) + */ +void DialogContainer::on_unrealize() { + // Disconnect all signals + for_each(connections.begin(), connections.end(), [&](auto c) { c.disconnect(); }); + + delete columns; + columns = nullptr; + + parent_type::on_unrealize(); +} + +// Create a new notebook and move page. +DialogNotebook *DialogContainer::prepare_drop(const Glib::RefPtr<Gdk::DragContext> context) +{ + Gtk::Widget *source = Gtk::Widget::drag_get_source_widget(context); + + // Find source notebook and page + Gtk::Notebook *old_notebook = dynamic_cast<Gtk::Notebook *>(source); + if (!old_notebook) { + std::cerr << "DialogContainer::prepare_drop: notebook not found!" << std::endl; + return nullptr; + } + + // Find page + Gtk::Widget *page = old_notebook->get_nth_page(old_notebook->get_current_page()); + if (!page) { + std::cerr << "DialogContainer::prepare_drop: page not found!" << std::endl; + return nullptr; + } + + // Create new notebook and move page. + DialogNotebook *new_notebook = Gtk::manage(new DialogNotebook(this)); + new_notebook->move_page(*page); + + // move_page() takes care of updating dialog lists. + return new_notebook; +} + +// Notebook page dropped on prepend target. Call function to create new notebook and then insert. +void DialogContainer::prepend_drop(const Glib::RefPtr<Gdk::DragContext> context, DialogMultipaned *multipane) +{ + DialogNotebook *new_notebook = prepare_drop(context); // Creates notebook, moves page. + if (!new_notebook) { + std::cerr << "DialogContainer::prepend_drop: no new notebook!" << std::endl; + return; + } + + if (multipane->get_orientation() == Gtk::ORIENTATION_HORIZONTAL) { + // Columns + // Create column + DialogMultipaned *column = create_column(); + column->prepend(new_notebook); + columns->prepend(column); + } else { + // Column + multipane->prepend(new_notebook); + } + + update_dialogs(); // Always update dialogs on Notebook change +} + +// Notebook page dropped on append target. Call function to create new notebook and then insert. +void DialogContainer::append_drop(const Glib::RefPtr<Gdk::DragContext> context, DialogMultipaned *multipane) +{ + DialogNotebook *new_notebook = prepare_drop(context); // Creates notebook, moves page. + if (!new_notebook) { + std::cerr << "DialogContainer::append_drop: no new notebook!" << std::endl; + return; + } + + if (multipane->get_orientation() == Gtk::ORIENTATION_HORIZONTAL) { + // Columns + // Create column + DialogMultipaned *column = create_column(); + column->append(new_notebook); + columns->append(column); + } else { + // Column + multipane->append(new_notebook); + } + + update_dialogs(); // Always update dialogs on Notebook change +} + +/** + * If a DialogMultipaned column is empty and it can be removed, remove it + */ +void DialogContainer::column_empty(DialogMultipaned *column) +{ + DialogMultipaned *parent = dynamic_cast<DialogMultipaned *>(column->get_parent()); + if (parent) { + parent->remove(*column); + } + + DialogWindow *window = dynamic_cast<DialogWindow *>(get_toplevel()); + if (window && parent) { + auto children = parent->get_children(); + // Close the DialogWindow if you're in an empty one + if (children.size() == 3 && parent->has_empty_widget()) { + window->close(); + } + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/dialog-container.h b/src/ui/dialog/dialog-container.h new file mode 100644 index 0000000..c6fa30a --- /dev/null +++ b/src/ui/dialog/dialog-container.h @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef INKSCAPE_UI_DIALOG_CONTAINER_H +#define INKSCAPE_UI_DIALOG_CONTAINER_H + +/** @file + * @brief A widget that manages DialogNotebook's and other widgets inside a horizontal DialogMultipaned. + * + * Authors: see git history + * Tavmjong Bah + * + * Copyright (c) 2018 Tavmjong Bah, Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdkmm/dragcontext.h> +#include <glibmm/refptr.h> +#include <glibmm/ustring.h> +#include <glibmm/keyfile.h> +#include <gtkmm/accelkey.h> +#include <gtkmm/box.h> +#include <gtkmm/targetentry.h> +#include <iostream> +#include <map> +#include <set> +#include <memory> +#include "dialog-manager.h" +#include "desktop.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class DialogBase; +class DialogNotebook; +class DialogMultipaned; +class DialogWindow; + +/** + * A widget that manages DialogNotebook's and other widgets inside a + * horizontal DialogMultipaned containing vertical DialogMultipaned's or other widgets. + */ +class DialogContainer : public Gtk::Box +{ + using parent_type = Gtk::Box; + +public: + DialogContainer(InkscapeWindow* inkscape_window); + ~DialogContainer() override; + + // Columns-related functions + DialogMultipaned *get_columns() { return columns; } + DialogMultipaned *create_column(); + + // Dialog-related functions + void new_dialog(const Glib::ustring& dialog_type); + + DialogWindow* new_floating_dialog(const Glib::ustring& dialog_type); + bool has_dialog_of_type(DialogBase *dialog); + DialogBase *get_dialog(const Glib::ustring& dialog_type); + void link_dialog(DialogBase *dialog); + void unlink_dialog(DialogBase *dialog); + const std::multimap<Glib::ustring, DialogBase *> *get_dialogs() { return &dialogs; }; + void toggle_dialogs(); + void update_dialogs(); // Update all linked dialogs + void set_inkscape_window(InkscapeWindow *inkscape_window); + InkscapeWindow* get_inkscape_window() { return _inkscape_window; } + + // State saving functionality + std::unique_ptr<Glib::KeyFile> save_container_state(); + void load_container_state(Glib::KeyFile* keyfile, bool include_floating); + + void restore_window_position(DialogWindow* window); + void store_window_position(DialogWindow* window); + + // get this container's state; provide window position for container embedded in DialogWindow + std::shared_ptr<Glib::KeyFile> get_container_state(const window_position_t* position) const; + void load_container_state(Glib::KeyFile& state, const std::string& window_id); + +private: + InkscapeWindow *_inkscape_window = nullptr; // Every container is attached to an InkscapeWindow. + DialogMultipaned *columns = nullptr; // The main widget inside which other children are kept. + std::vector<Gtk::TargetEntry> target_entries; // What kind of object can be dropped. + + /** + * Due to the way Gtk handles dragging between notebooks, one can + * either allow multiple instances of the same dialog in a notebook + * or restrict dialogs to docks tied to a particular document + * window. (More explicitly, use one group name for all notebooks or + * use a unique group name for each document window with related + * floating docks.) For the moment we choose the former which + * requires a multimap here as we use the dialog type as a key. + */ + std::multimap<Glib::ustring, DialogBase *>dialogs; + + void new_dialog(const Glib::ustring& dialog_type, DialogNotebook* notebook); + DialogBase *dialog_factory(const Glib::ustring& dialog_type); + Gtk::Widget *create_notebook_tab(Glib::ustring label, Glib::ustring image, const Glib::ustring shortcut); + DialogWindow* create_new_floating_dialog(const Glib::ustring& dialog_type, bool blink); + + // Signal connections + std::vector<sigc::connection> connections; + + // Handlers + void on_unrealize() override; + DialogNotebook *prepare_drop(const Glib::RefPtr<Gdk::DragContext> context); + void prepend_drop(const Glib::RefPtr<Gdk::DragContext> context, DialogMultipaned *column); + void append_drop(const Glib::RefPtr<Gdk::DragContext> context, DialogMultipaned *column); + void column_empty(DialogMultipaned *column); + DialogBase* find_existing_dialog(const Glib::ustring& dialog_type); + static bool recreate_dialogs_from_state(InkscapeWindow* inkscape_window, const Glib::KeyFile* keyfile); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_CONTAINER_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/ui/dialog/dialog-data.cpp b/src/ui/dialog/dialog-data.cpp new file mode 100644 index 0000000..4dd9aed --- /dev/null +++ b/src/ui/dialog/dialog-data.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <map> + +#include <glibmm/i18n.h> +#include <glibmm/ustring.h> + +#include "config.h" // Needed for WITH_GSPELL + +#include "ui/dialog/dialog-data.h" +#include "ui/icon-names.h" // INKSCAPE_ICON macro + +/* + * In an ideal world, this information would be in .ui files for each + * dialog (the .ui file would describe a dialog wrapped by a notebook + * tab). At the moment we create each dialog notebook tab on the fly + * so we need a place to keep this information. + */ + +std::map<Glib::ustring, DialogData> const &get_dialog_data() +{ + static std::map<Glib::ustring, DialogData> dialog_data; + + // Note the "AttrDialog" is now part of the "XMLDialog" and the "Style" dialog is part of the + // "Selectors" dialog. Also note that the "AttrDialog" does not correspond to SP_VERB_DIALOG_ATTR!!! + // (That would be the "ObjectAttributes" dialog.) + + if (dialog_data.empty()) { + dialog_data = { + // clang-format off + {"AlignDistribute", {_("_Align and Distribute"), INKSCAPE_ICON("dialog-align-and-distribute"), DialogData::Basic, ScrollProvider::NOPROVIDE }}, + {"CloneTiler", {_("Create Tiled Clones"), INKSCAPE_ICON("dialog-tile-clones"), DialogData::Basic, ScrollProvider::NOPROVIDE }}, + {"DocumentProperties", {_("_Document Properties"), INKSCAPE_ICON("document-properties"), DialogData::Settings, ScrollProvider::NOPROVIDE }}, + {"Export", {_("_Export"), INKSCAPE_ICON("document-export"), DialogData::Basic, ScrollProvider::PROVIDE }}, + {"FillStroke", {_("_Fill and Stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"), DialogData::Basic, ScrollProvider::NOPROVIDE }}, + {"FilterEffects", {_("Filter _Editor"), INKSCAPE_ICON("dialog-filters"), DialogData::Advanced, ScrollProvider::NOPROVIDE }}, + {"Find", {_("_Find/Replace"), INKSCAPE_ICON("edit-find"), DialogData::Basic, ScrollProvider::NOPROVIDE }}, + {"Glyphs", {_("_Unicode Characters"), INKSCAPE_ICON("accessories-character-map"), DialogData::Basic, ScrollProvider::NOPROVIDE }}, + {"IconPreview", {_("Icon Preview"), INKSCAPE_ICON("dialog-icon-preview"), DialogData::Basic, ScrollProvider::NOPROVIDE }}, + {"Input", {_("_Input Devices"), INKSCAPE_ICON("dialog-input-devices"), DialogData::Settings, ScrollProvider::NOPROVIDE }}, + {"LivePathEffect", {_("Path E_ffects"), INKSCAPE_ICON("dialog-path-effects"), DialogData::Advanced, ScrollProvider::NOPROVIDE }}, + {"Memory", {_("About _Memory"), INKSCAPE_ICON("dialog-memory"), DialogData::Diagnostics, ScrollProvider::NOPROVIDE }}, + {"Messages", {_("_Messages"), INKSCAPE_ICON("dialog-messages"), DialogData::Diagnostics, ScrollProvider::NOPROVIDE }}, + {"ObjectAttributes", {_("_Object attributes"), INKSCAPE_ICON("dialog-object-properties"), DialogData::Settings, ScrollProvider::NOPROVIDE }}, + {"ObjectProperties", {_("_Object Properties"), INKSCAPE_ICON("dialog-object-properties"), DialogData::Settings, ScrollProvider::NOPROVIDE }}, + {"Objects", {_("Layers and Object_s"), INKSCAPE_ICON("dialog-objects"), DialogData::Basic, ScrollProvider::PROVIDE }}, + {"PaintServers", {_("_Paint Servers"), INKSCAPE_ICON("symbols"), DialogData::Advanced, ScrollProvider::PROVIDE }}, + {"Preferences", {_("P_references"), INKSCAPE_ICON("preferences-system"), DialogData::Settings, ScrollProvider::PROVIDE }}, + {"Selectors", {_("_Selectors and CSS"), INKSCAPE_ICON("dialog-selectors"), DialogData::Advanced, ScrollProvider::PROVIDE }}, + {"SVGFonts", {_("SVG Font Editor"), INKSCAPE_ICON("dialog-svg-font"), DialogData::Advanced, ScrollProvider::NOPROVIDE }}, + {"Swatches", {_("S_watches"), INKSCAPE_ICON("swatches"), DialogData::Basic, ScrollProvider::PROVIDE }}, + {"Symbols", {_("S_ymbols"), INKSCAPE_ICON("symbols"), DialogData::Basic, ScrollProvider::PROVIDE }}, + {"Text", {_("_Text and Font"), INKSCAPE_ICON("dialog-text-and-font"), DialogData::Basic, ScrollProvider::NOPROVIDE }}, + {"Trace", {_("_Trace Bitmap"), INKSCAPE_ICON("bitmap-trace"), DialogData::Basic, ScrollProvider::NOPROVIDE }}, + {"Transform", {_("Transfor_m"), INKSCAPE_ICON("dialog-transform"), DialogData::Basic, ScrollProvider::NOPROVIDE }}, + {"UndoHistory", {_("Undo _History"), INKSCAPE_ICON("edit-undo-history"), DialogData::Basic, ScrollProvider::NOPROVIDE }}, + {"XMLEditor", {_("_XML Editor"), INKSCAPE_ICON("dialog-xml-editor"), DialogData::Advanced, ScrollProvider::NOPROVIDE }}, +#if WITH_GSPELL + {"Spellcheck", {_("Check Spellin_g"), INKSCAPE_ICON("tools-check-spelling"), DialogData::Basic, ScrollProvider::NOPROVIDE }}, +#endif +#if DEBUG + {"Prototype", {_("Prototype"), INKSCAPE_ICON("document-properties"), DialogData::Other, ScrollProvider::NOPROVIDE }}, +#endif + // clang-format on + }; + } + return dialog_data; +} + +/* + 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/ui/dialog/dialog-data.h b/src/ui/dialog/dialog-data.h new file mode 100644 index 0000000..5f17723 --- /dev/null +++ b/src/ui/dialog/dialog-data.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** @file + * @brief Basic dialog info. + * + * Authors: see git history + * Tavmjong Bah + * + * Copyright (c) 2021 Tavmjong Bah, Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <map> + +#include <glibmm/i18n.h> +#include <glibmm/ustring.h> + +enum class ScrollProvider { + PROVIDE = 0, + NOPROVIDE +}; + +class DialogData { +public: + Glib::ustring label; + Glib::ustring icon_name; + enum Category { Basic = 0, Advanced, Settings, Diagnostics, Other, _num_categories }; + Category category; + ScrollProvider provide_scroll; +}; + +// dialog categories (used to group them in a dialog submenu) +char const *const dialog_categories[DialogData::_num_categories] = { + N_("Basic"), + N_("Advanced"), + N_("Settings"), + N_("Diagnostic"), + N_("Other") +}; + +/** Get the data about all existing dialogs. */ +std::map<Glib::ustring, DialogData> const &get_dialog_data(); + +/* + 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/ui/dialog/dialog-manager.cpp b/src/ui/dialog/dialog-manager.cpp new file mode 100644 index 0000000..e082467 --- /dev/null +++ b/src/ui/dialog/dialog-manager.cpp @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "dialog-manager.h" + +#include <gdkmm/monitor.h> +#include <limits> +#ifdef G_OS_WIN32 +#include <filesystem> +namespace filesystem = std::filesystem; +#else +// Waiting for compiler on MacOS to catch up to C++x17 +#include <boost/filesystem.hpp> +namespace filesystem = boost::filesystem; +#endif + +#include "io/resource.h" +#include "inkscape-application.h" +#include "dialog-base.h" +#include "dialog-container.h" +#include "dialog-window.h" +#include "enums.h" +#include "preferences.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +std::optional<window_position_t> dm_get_window_position(Gtk::Window &window) +{ + std::optional<window_position_t> position = std::nullopt; + + const int max = std::numeric_limits<int>::max(); + int x = max; + int y = max; + int width = 0; + int height = 0; + // gravity NW to include window decorations + window.property_gravity() = Gdk::GRAVITY_NORTH_WEST; + window.get_position(x, y); + window.get_size(width, height); + + if (x != max && y != max && width > 0 && height > 0) { + position = window_position_t{x, y, width, height}; + } + + return position; +} + +void dm_restore_window_position(Gtk::Window &window, const window_position_t &position) +{ + // note: Gtk window methods are recommended over low-level Gdk ones to resize and position window + window.property_gravity() = Gdk::GRAVITY_NORTH_WEST; + window.set_default_size(position.width, position.height); + // move & resize positions window on the screen making sure it is not clipped + // (meaning it is visible; this works with two monitors too) + window.move(position.x, position.y); + window.resize(position.width, position.height); +} + +DialogManager &DialogManager::singleton() +{ + static DialogManager dm; + return dm; +} + +// store complete dialog window state (including its container state) +void DialogManager::store_state(DialogWindow &wnd) +{ + // get window's size and position + if (auto pos = dm_get_window_position(wnd)) { + if (auto container = wnd.get_container()) { + // get container's state + auto state = container->get_container_state(&*pos); + + // find dialogs hosted in this window + for (auto dlg : *container->get_dialogs()) { + _floating_dialogs[dlg.first] = state; + } + } + } +} + +// +bool DialogManager::should_open_floating(const Glib::ustring& dialog_type) +{ + return _floating_dialogs.count(dialog_type) > 0; +} + +void DialogManager::set_floating_dialog_visibility(DialogWindow* wnd, bool show) { + if (!wnd) return; + + if (show) { + if (wnd->is_visible()) return; + + // wnd->present(); - not sure which one is better, show or present... + wnd->show(); + _hidden_dlg_windows.erase(wnd); + // re-add it to application; hiding removed it + if (auto app = InkscapeApplication::instance()) { + app->gtk_app()->add_window(*wnd); + } + } + else { + if (!wnd->is_visible()) return; + + _hidden_dlg_windows.insert(wnd); + wnd->hide(); + } +} + +std::vector<DialogWindow*> DialogManager::get_all_floating_dialog_windows() { + std::vector<Gtk::Window*> windows = InkscapeApplication::instance()->gtk_app()->get_windows(); + + std::vector<DialogWindow*> result(_hidden_dlg_windows.begin(), _hidden_dlg_windows.end()); + for (auto wnd : windows) { + if (auto dlg_wnd = dynamic_cast<DialogWindow*>(wnd)) { + result.push_back(dlg_wnd); + } + } + + return result; +} + +DialogWindow* DialogManager::find_floating_dialog_window(const Glib::ustring& dialog_type) { + auto windows = get_all_floating_dialog_windows(); + + for (auto dlg_wnd : windows) { + if (auto container = dlg_wnd->get_container()) { + if (container->get_dialog(dialog_type)) { + return dlg_wnd; + } + } + } + + return nullptr; +} + +DialogBase *DialogManager::find_floating_dialog(const Glib::ustring& dialog_type) +{ + auto windows = get_all_floating_dialog_windows(); + + for (auto dlg_wnd : windows) { + if (auto container = dlg_wnd->get_container()) { + if (auto dlg = container->get_dialog(dialog_type)) { + return dlg; + } + } + } + + return nullptr; +} + +std::shared_ptr<Glib::KeyFile> DialogManager::find_dialog_state(const Glib::ustring& dialog_type) +{ + auto it = _floating_dialogs.find(dialog_type); + if (it != _floating_dialogs.end()) { + return it->second; + } + return nullptr; +} + +const char dialogs_state[] = "dialogs-state-ex.ini"; +const char save_dialog_position[] = "/options/savedialogposition/value"; +const char transient_group[] = "transient"; + +// list of dialogs sharing the same state +std::vector<Glib::ustring> DialogManager::count_dialogs(const Glib::KeyFile *state) const +{ + std::vector<Glib::ustring> dialogs; + if (!state) return dialogs; + + for (auto dlg : _floating_dialogs) { + if (dlg.second.get() == state) { + dialogs.emplace_back(dlg.first); + } + } + return dialogs; +} + +void DialogManager::save_dialogs_state(DialogContainer *docking_container) +{ + if (!docking_container) return; + + // check if we want to save the state + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int save_state = prefs->getInt(save_dialog_position, PREFS_DIALOGS_STATE_SAVE); + if (save_state == PREFS_DIALOGS_STATE_NONE) return; + + // save state of docked dialogs and currently open floating ones + auto keyfile = docking_container->save_container_state(); + + // save transient state of floating dialogs that user might have opened interacting with the app + int idx = 1; + for (auto dlg : _floating_dialogs) { + auto state = dlg.second.get(); + auto&& type = dlg.first; + auto index = std::to_string(idx++); + // state may be empty; all that means it that dialog hasn't been opened yet, + // but when it is, then it should be open in a floating state + keyfile->set_string(transient_group, "state" + index, state ? state->to_data() : ""); + auto dialogs = count_dialogs(state); + if (!state) { + dialogs.emplace_back(type); + } + keyfile->set_string_list(transient_group, "dialogs" + index, dialogs); + } + keyfile->set_integer(transient_group, "count", _floating_dialogs.size()); + + std::string filename = Glib::build_filename(Inkscape::IO::Resource::profile_path(), dialogs_state); + try { + keyfile->save_to_file(filename); + } catch (Glib::FileError &error) { + std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl; + } +} + +// load transient dialog state - it includes state of floating dialogs that may or may not be open +void DialogManager::load_transient_state(Glib::KeyFile *file) +{ + int count = file->get_integer(transient_group, "count"); + for (int i = 0; i < count; ++i) { + auto index = std::to_string(i + 1); + auto dialogs = file->get_string_list(transient_group, "dialogs" + index); + auto state = file->get_string(transient_group, "state" + index); + + auto keyfile = std::make_shared<Glib::KeyFile>(); + if (!state.empty()) { + keyfile->load_from_data(state); + } + for (auto type : dialogs) { + _floating_dialogs[type] = keyfile; + } + } +} + +// restore state of dialogs; populate docking container and open visible floating dialogs +void DialogManager::restore_dialogs_state(DialogContainer *docking_container, bool include_floating) +{ + if (!docking_container) return; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int save_state = prefs->getInt(save_dialog_position, PREFS_DIALOGS_STATE_SAVE); + if (save_state == PREFS_DIALOGS_STATE_NONE) return; + + try { + auto keyfile = std::make_unique<Glib::KeyFile>(); + std::string filename = Glib::build_filename(Inkscape::IO::Resource::profile_path(), dialogs_state); + +#ifdef G_OS_WIN32 + bool exists = filesystem::exists(filesystem::u8path(filename)); +#else + bool exists = filesystem::exists(filesystem::path(filename)); +#endif + + if (exists && keyfile->load_from_file(filename)) { + // restore visible dialogs first; that state is up-to-date + docking_container->load_container_state(keyfile.get(), include_floating); + + // then load transient data too; it may be older than above + if (include_floating) { + try { + load_transient_state(keyfile.get()); + } catch (Glib::Error &error) { + std::cerr << G_STRFUNC << ": transient state not loaded - " << error.what().raw() << std::endl; + } + } + } + else { + // state not available or not valid; prepare defaults + dialog_defaults(); + } + } catch (Glib::Error &error) { + std::cerr << G_STRFUNC << ": dialogs state not loaded - " << error.what().raw() << std::endl; + } +} + +void DialogManager::remove_dialog_floating_state(const Glib::ustring& dialog_type) { + auto it = _floating_dialogs.find(dialog_type); + if (it != _floating_dialogs.end()) { + _floating_dialogs.erase(it); + } +} + +// apply defaults when dialog state cannot be loaded / doesn't exist: +// here we can define which dialogs should by default be open floating rather then docked +void DialogManager::dialog_defaults() { + std::shared_ptr<Glib::KeyFile> floating; + // strings are dialog types + _floating_dialogs["CloneTiler"] = floating; + _floating_dialogs["DocumentProperties"] = floating; + _floating_dialogs["FilterEffects"] = floating; + _floating_dialogs["Input"] = floating; + _floating_dialogs["Preferences"] = floating; + _floating_dialogs["XMLEditor"] = floating; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/dialog-manager.h b/src/ui/dialog/dialog-manager.h new file mode 100644 index 0000000..fc4cd9b --- /dev/null +++ b/src/ui/dialog/dialog-manager.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef INKSCAPE_UI_DIALOG_MANAGER_H +#define INKSCAPE_UI_DIALOG_MANAGER_H + +#include <glibmm/keyfile.h> +#include <gtkmm/window.h> +#include <map> +#include <set> +#include <memory> +#include <optional> +#include <vector> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class DialogWindow; +class DialogBase; +class DialogContainer; + +struct window_position_t +{ + int x, y, width, height; +}; + +// try to read window's geometry +std::optional<window_position_t> dm_get_window_position(Gtk::Window &window); + +// restore window's geometry +void dm_restore_window_position(Gtk::Window &window, const window_position_t &position); + +class DialogManager +{ +public: + static DialogManager &singleton(); + + // store complete dialog window state (including its container state) + void store_state(DialogWindow &wnd); + + // return true if dialog 'type' should be opened as floating + bool should_open_floating(const Glib::ustring& dialog_type); + + // find instance of dialog 'type' in one of currently open floating dialog windows + DialogBase *find_floating_dialog(const Glib::ustring& dialog_type); + + // find window hosting floating dialog + DialogWindow* find_floating_dialog_window(const Glib::ustring& dialog_type); + + // find floating window state hosting dialog 'code', if there was one + std::shared_ptr<Glib::KeyFile> find_dialog_state(const Glib::ustring& dialog_type); + + // remove dialog floating state + void remove_dialog_floating_state(const Glib::ustring& dialog_type); + + // save configuration of docked and floating dialogs + void save_dialogs_state(DialogContainer *docking_container); + + // restore state of dialogs + void restore_dialogs_state(DialogContainer *docking_container, bool include_floating); + + // find all floating dialog windows + std::vector<DialogWindow*> get_all_floating_dialog_windows(); + + // show/hide dialog window and keep track of it + void set_floating_dialog_visibility(DialogWindow* wnd, bool show); + +private: + DialogManager() = default; + ~DialogManager() = default; + + std::vector<Glib::ustring> count_dialogs(const Glib::KeyFile *state) const; + void load_transient_state(Glib::KeyFile *keyfile); + void dialog_defaults(); + + // transient dialog state for floating windows user closes + std::map<std::string, std::shared_ptr<Glib::KeyFile>> _floating_dialogs; + // temp set used when dialogs are hidden (F12 toggle) + std::set<DialogWindow*> _hidden_dlg_windows; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#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/ui/dialog/dialog-multipaned.cpp b/src/ui/dialog/dialog-multipaned.cpp new file mode 100644 index 0000000..531c636 --- /dev/null +++ b/src/ui/dialog/dialog-multipaned.cpp @@ -0,0 +1,1280 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** @file + * @brief A widget with multiple panes. Agnostic to type what kind of widgets panes contain. + * + * Authors: see git history + * Tavmjong Bah + * + * Copyright (c) 2020 Tavmjong Bah, Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "dialog-multipaned.h" + +#include <glibmm/i18n.h> +#include <glibmm/objectbase.h> +#include <gtkmm/container.h> +#include <gtkmm/image.h> +#include <gtkmm/label.h> +#include <iostream> +#include <numeric> + +#include "ui/dialog/dialog-notebook.h" +#include "ui/util.h" +#include "ui/widget/canvas-grid.h" +#include "dialog-window.h" + +#define DROPZONE_SIZE 5 +#define DROPZONE_EXPANSION 15 +#define HANDLE_SIZE 12 +#define HANDLE_CROSS_SIZE 25 + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/* + * References: + * https://blog.gtk.org/2017/06/ + * https://developer.gnome.org/gtkmm-tutorial/stable/sec-custom-containers.html.en + * https://wiki.gnome.org/HowDoI/Gestures + * + * The children widget sizes are "sticky". They change a minimal + * amount when the parent widget is resized or a child is added or + * removed. + * + * A gesture is used to track handle movement. This must be attached + * to the parent widget (the offset_x/offset_y values are relative to + * the widget allocation which changes for the handles as they are + * moved). + */ + +int get_handle_size() { + return HANDLE_SIZE; +} + +/* ============ MyDropZone ============ */ + +std::list<MyDropZone *> MyDropZone::_instances_list; + +MyDropZone::MyDropZone(Gtk::Orientation orientation) + : Glib::ObjectBase("MultipanedDropZone") + , Gtk::Orientable() + , Gtk::EventBox() +{ + set_name("MultipanedDropZone"); + set_orientation(orientation); + set_size(DROPZONE_SIZE); + + get_style_context()->add_class("backgnd-passive"); + + signal_drag_motion().connect([=](const Glib::RefPtr<Gdk::DragContext>& ctx, int x, int y, guint time) { + if (!_active) { + _active = true; + add_highlight(); + set_size(DROPZONE_SIZE + DROPZONE_EXPANSION); + } + return true; + }); + + signal_drag_leave().connect([=](const Glib::RefPtr<Gdk::DragContext>&, guint time) { + if (_active) { + _active = false; + set_size(DROPZONE_SIZE); + } + }); + + _instances_list.push_back(this); +} + +MyDropZone::~MyDropZone() +{ + _instances_list.remove(this); +} + +void MyDropZone::add_highlight_instances() +{ + for (auto *instance : _instances_list) { + instance->add_highlight(); + } +} + +void MyDropZone::remove_highlight_instances() +{ + for (auto *instance : _instances_list) { + instance->remove_highlight(); + // instance->set_size(DROPZONE_SIZE); + } +} + +void MyDropZone::add_highlight() +{ + const auto &style = get_style_context(); + style->remove_class("backgnd-passive"); + style->add_class("backgnd-active"); +} + +void MyDropZone::remove_highlight() +{ + const auto &style = get_style_context(); + style->remove_class("backgnd-active"); + style->add_class("backgnd-passive"); +} + +void MyDropZone::set_size(int size) +{ + if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) { + set_size_request(size, -1); + } else { + set_size_request(-1, size); + } +} + +/* ============ MyHandle ============ */ + +MyHandle::MyHandle(Gtk::Orientation orientation, int size = get_handle_size()) + : Glib::ObjectBase("MultipanedHandle") + , Gtk::Orientable() + , Gtk::EventBox() + , _cross_size(0) + , _child(nullptr) +{ + set_name("MultipanedHandle"); + set_orientation(orientation); + + add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::POINTER_MOTION_MASK); + + Gtk::Image *image = Gtk::manage(new Gtk::Image()); + if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) { + image->set_from_icon_name("view-more-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR); + set_size_request(size, -1); + } else { + image->set_from_icon_name("view-more-horizontal-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR); + set_size_request(-1, size); + } + image->set_pixel_size(size); + add(*image); + + // Signal + signal_size_allocate().connect(sigc::mem_fun(*this, &MyHandle::resize_handler)); + + show_all(); +} + +// draw rectangle with rounded corners +void rounded_rectangle(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double w, double h, double r) { + cr->begin_new_sub_path(); + cr->arc(x + r, y + r, r, M_PI, 3 * M_PI / 2); + cr->arc(x + w - r, y + r, r, 3 * M_PI / 2, 2 * M_PI); + cr->arc(x + w - r, y + h - r, r, 0, M_PI / 2); + cr->arc(x + r, y + h - r, r, M_PI / 2, M_PI); + cr->close_path(); +} + +// part of the handle where clicking makes it automatically collapse/expand docked dialogs +Cairo::Rectangle MyHandle::get_active_click_zone() { + const Gtk::Allocation& allocation = get_allocation(); + double width = allocation.get_width(); + double height = allocation.get_height(); + double h = height / 5; + + Cairo::Rectangle rect = { .x = 0, .y = (height - h) / 2, .width = width, .height = h }; + return rect; +} + +bool MyHandle::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) { + bool ret = EventBox::on_draw(cr); + + // show click indicator/highlight? + if (_click_indicator && is_click_resize_active() && !_dragging) { + auto rect = get_active_click_zone(); + + if (rect.width > 4 && rect.height > 0) { + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + Gdk::RGBA fg = style_context->get_color(get_state_flags()); + rounded_rectangle(cr, rect.x + 2, rect.y, rect.width - 4, rect.height, 3); + cr->set_source_rgba(fg.get_red(), fg.get_green(), fg.get_blue(), 0.26); + cr->fill(); + } + } + + return ret; +} + +void MyHandle::set_dragging(bool dragging) { + if (_dragging != dragging) { + _dragging = dragging; + if (_click_indicator) { + queue_draw(); + } + } +} + +/** + * Change the mouse pointer into a resize icon to show you can drag. + */ +bool MyHandle::on_enter_notify_event(GdkEventCrossing *crossing_event) +{ + auto window = get_window(); + auto display = get_display(); + + if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) { + auto cursor = Gdk::Cursor::create(display, "col-resize"); + window->set_cursor(cursor); + } else { + auto cursor = Gdk::Cursor::create(display, "row-resize"); + window->set_cursor(cursor); + } + + update_click_indicator(crossing_event->x, crossing_event->y); + + return false; +} + +bool MyHandle::on_leave_notify_event(GdkEventCrossing* crossing_event) { + show_click_indicator(false); + return false; +} + +void MyHandle::show_click_indicator(bool show) { + if (!is_click_resize_active()) return; + + if (show != _click_indicator) { + _click_indicator = show; + this->queue_draw(); + } +} + +void MyHandle::update_click_indicator(double x, double y) { + if (!is_click_resize_active()) return; + + auto rect = get_active_click_zone(); + bool inside = + x >= rect.x && x < rect.x + rect.width && + y >= rect.y && y < rect.y + rect.height; + + show_click_indicator(inside); +} + +bool MyHandle::is_click_resize_active() const { + return get_orientation() == Gtk::ORIENTATION_HORIZONTAL; +} + +bool MyHandle::on_button_press_event(GdkEventButton* button_event) { + // detect single-clicks + _click = button_event->button == 1 && button_event->type == GDK_BUTTON_PRESS; + return false; +} + +bool MyHandle::on_button_release_event(GdkEventButton* event) { + // single-click on active zone? + if (_click && event->type == GDK_BUTTON_RELEASE && event->button == 1 && _click_indicator) { + _click = false; + _dragging = false; + // handle clicked + if (is_click_resize_active()) { + toggle_multipaned(); + return true; + } + } + + _click = false; + return false; +} + +void MyHandle::toggle_multipaned() { + // visibility toggle of multipaned in a floating dialog window doesn't make sense; skip + if (dynamic_cast<DialogWindow*>(get_toplevel())) return; + + auto panel = dynamic_cast<DialogMultipaned*>(get_parent()); + if (!panel) return; + + auto children = panel->get_children(); + Gtk::Widget* multi = nullptr; // multipaned widget to toggle + bool left_side = true; // panels to the left of canvas + size_t i = 0; + + // find multipaned widget to resize; it is adjacent (sibling) to 'this' handle in widget hierarchy + for (auto widget : children) { + if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(widget)) { + // widget past canvas are on the right side (of canvas) + left_side = false; + } + + if (widget == this) { + if (left_side && i > 0) { + // handle to the left of canvas toggles preceeding panel + multi = dynamic_cast<DialogMultipaned*>(children[i - 1]); + } + else if (!left_side && i + 1 < children.size()) { + // handle to the right of canvas toggles next panel + multi = dynamic_cast<DialogMultipaned*>(children[i + 1]); + } + + if (multi) { + if (multi->is_visible()) { + multi->hide(); + } + else { + multi->show(); + } + // resize parent + panel->children_toggled(); + } + break; + } + + ++i; + } +} + +bool MyHandle::on_motion_notify_event(GdkEventMotion* motion_event) { + // motion invalidates click; it activates resizing + _click = false; + // show_click_indicator(false); + update_click_indicator(motion_event->x, motion_event->y); + return false; +} + +/** + * This allocation handler function is used to add/remove handle icons in order to be able + * to hide completely a transversal handle into the sides of a DialogMultipaned. + * + * The image has a specific size set up in the constructor and will not naturally shrink/hide. + * In conclusion, we remove it from the handle and save it into an internal reference. + */ +void MyHandle::resize_handler(Gtk::Allocation &allocation) +{ + int size = (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) ? allocation.get_height() : allocation.get_width(); + + if (_cross_size > size && HANDLE_CROSS_SIZE > size && !_child) { + _child = get_child(); + remove(); + } else if (_cross_size < size && HANDLE_CROSS_SIZE < size && _child) { + add(*_child); + _child = nullptr; + } + + _cross_size = size; +} + +/* ============ DialogMultipaned ============= */ + +DialogMultipaned::DialogMultipaned(Gtk::Orientation orientation) + : Glib::ObjectBase("DialogMultipaned") + , Gtk::Orientable() + , Gtk::Container() + , _empty_widget(nullptr) +{ + set_name("DialogMultipaned"); + set_orientation(orientation); + set_has_window(false); + set_redraw_on_allocate(false); + + // ============= Add dropzones ============== + MyDropZone *dropzone_s = Gtk::manage(new MyDropZone(orientation)); + MyDropZone *dropzone_e = Gtk::manage(new MyDropZone(orientation)); + + dropzone_s->set_parent(*this); + dropzone_e->set_parent(*this); + + children.push_back(dropzone_s); + children.push_back(dropzone_e); + + // ============ Connect signals ============= + gesture = Gtk::GestureDrag::create(*this); + + _connections.emplace_back( + gesture->signal_drag_begin().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_begin))); + _connections.emplace_back(gesture->signal_drag_end().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_end))); + _connections.emplace_back( + gesture->signal_drag_update().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_update))); + + _connections.emplace_back( + signal_drag_data_received().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_data))); + _connections.emplace_back( + dropzone_s->signal_drag_data_received().connect(sigc::mem_fun(*this, &DialogMultipaned::on_prepend_drag_data))); + _connections.emplace_back( + dropzone_e->signal_drag_data_received().connect(sigc::mem_fun(*this, &DialogMultipaned::on_append_drag_data))); + + // add empty widget to initiate the container + add_empty_widget(); + + show_all(); +} + +DialogMultipaned::~DialogMultipaned() +{ + // Disconnect all signals + for_each(_connections.begin(), _connections.end(), [&](auto c) { c.disconnect(); }); + /* + for (std::vector<Gtk::Widget *>::iterator it = children.begin(); it != children.end();) { + if (dynamic_cast<DialogMultipaned *>(*it) || dynamic_cast<DialogNotebook *>(*it)) { + delete *it; + } else { + it++; + } + } + */ + + for (;;) { + auto it = std::find_if(children.begin(), children.end(), [](auto w) { + return dynamic_cast<DialogMultipaned *>(w) || dynamic_cast<DialogNotebook *>(w); + }); + if (it != children.end()) { + // delete dialog multipanel or notebook; this action results in its removal from 'children'! + delete *it; + } else { + // no more dialog panels + break; + } + } + + // need to remove CanvasGrid from this container to avoid on idle repainting and crash: + // Gtk:ERROR:../gtk/gtkwidget.c:5871:gtk_widget_get_frame_clock: assertion failed: (window != NULL) + // Bail out! Gtk:ERROR:../gtk/gtkwidget.c:5871:gtk_widget_get_frame_clock: assertion failed: (window != NULL) + for (auto child : children) { + if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(child)) { + remove(*child); + } + } + + children.clear(); +} + +void DialogMultipaned::prepend(Gtk::Widget *child) +{ + remove_empty_widget(); // Will remove extra widget if existing + + // If there are MyMultipane children that are empty, they will be removed + for (auto const &child1 : children) { + DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(child1); + if (paned && paned->has_empty_widget()) { + remove(*child1); + remove_empty_widget(); + } + } + + if (child) { + // Add handle + if (children.size() > 2) { + MyHandle *my_handle = Gtk::manage(new MyHandle(get_orientation())); + my_handle->set_parent(*this); + children.insert(children.begin() + 1, my_handle); // After start dropzone + } + + // Add child + children.insert(children.begin() + 1, child); + if (!child->get_parent()) + child->set_parent(*this); + + // Ideally, we would only call child->show() here and assume that the + // child has already configured visibility of all its own children. + child->show_all(); + } +} + +void DialogMultipaned::append(Gtk::Widget *child) +{ + remove_empty_widget(); // Will remove extra widget if existing + + // If there are MyMultipane children that are empty, they will be removed + for (auto const &child1 : children) { + DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(child1); + if (paned && paned->has_empty_widget()) { + remove(*child1); + remove_empty_widget(); + } + } + + if (child) { + // Add handle + if (children.size() > 2) { + MyHandle *my_handle = Gtk::manage(new MyHandle(get_orientation())); + my_handle->set_parent(*this); + children.insert(children.end() - 1, my_handle); // Before end dropzone + } + + // Add child + children.insert(children.end() - 1, child); + if (!child->get_parent()) + child->set_parent(*this); + + // See comment in DialogMultipaned::prepend + child->show_all(); + } +} + +void DialogMultipaned::add_empty_widget() +{ + const int EMPTY_WIDGET_SIZE = 60; // magic number + + // The empty widget is a label + auto label = Gtk::manage(new Gtk::Label(_("You can drop dockable dialogs here."))); + label->set_line_wrap(); + label->set_justify(Gtk::JUSTIFY_CENTER); + label->set_valign(Gtk::ALIGN_CENTER); + label->set_vexpand(); + + append(label); + _empty_widget = label; + + if (get_orientation() == Gtk::ORIENTATION_VERTICAL) { + int dropzone_size = (get_height() - EMPTY_WIDGET_SIZE) / 2; + if (dropzone_size > DROPZONE_SIZE) { + set_dropzone_sizes(dropzone_size, dropzone_size); + } + } +} + +void DialogMultipaned::remove_empty_widget() +{ + if (_empty_widget) { + auto it = std::find(children.begin(), children.end(), _empty_widget); + if (it != children.end()) { + children.erase(it); + } + _empty_widget->unparent(); + _empty_widget = nullptr; + } + + if (get_orientation() == Gtk::ORIENTATION_VERTICAL) { + set_dropzone_sizes(DROPZONE_SIZE, DROPZONE_SIZE); + } +} + +Gtk::Widget *DialogMultipaned::get_first_widget() +{ + if (children.size() > 2) { + return children[1]; + } else { + return nullptr; + } +} + +Gtk::Widget *DialogMultipaned::get_last_widget() +{ + if (children.size() > 2) { + return children[children.size() - 2]; + } else { + return nullptr; + } +} + +/** + * Set the sizes of the DialogMultipaned dropzones. + * @param start, the size you want or -1 for the default `DROPZONE_SIZE` + * @param end, the size you want or -1 for the default `DROPZONE_SIZE` + */ +void DialogMultipaned::set_dropzone_sizes(int start, int end) +{ + bool orientation = get_orientation() == Gtk::ORIENTATION_HORIZONTAL; + + if (start == -1) { + start = DROPZONE_SIZE; + } + + MyDropZone *dropzone_s = dynamic_cast<MyDropZone *>(children[0]); + + if (dropzone_s) { + if (orientation) { + dropzone_s->set_size_request(start, -1); + } else { + dropzone_s->set_size_request(-1, start); + } + } + + if (end == -1) { + end = DROPZONE_SIZE; + } + + MyDropZone *dropzone_e = dynamic_cast<MyDropZone *>(children[children.size() - 1]); + + if (dropzone_e) { + if (orientation) { + dropzone_e->set_size_request(end, -1); + } else { + dropzone_e->set_size_request(-1, end); + } + } +} + +/** + * Show/hide as requested all children of this container that are of type multipaned + */ +void DialogMultipaned::toggle_multipaned_children(bool show) +{ + _handle = -1; + _drag_handle = -1; + + for (auto child : children) { + if (auto panel = dynamic_cast<DialogMultipaned*>(child)) { + if (show) { + panel->show(); + } + else { + panel->hide(); + } + } + } +} + +/** + * Ensure that this dialog container is visible. + */ +void DialogMultipaned::ensure_multipaned_children() +{ + toggle_multipaned_children(true); + // hide_multipaned = false; + // queue_allocate(); +} + +// ****************** OVERRIDES ****************** + +// The following functions are here to define the behavior of our custom container + +Gtk::SizeRequestMode DialogMultipaned::get_request_mode_vfunc() const +{ + if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) { + return Gtk::SIZE_REQUEST_WIDTH_FOR_HEIGHT; + } else { + return Gtk::SIZE_REQUEST_HEIGHT_FOR_WIDTH; + } +} + +void DialogMultipaned::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const +{ + minimum_width = 0; + natural_width = 0; + for (auto const &child : children) { + if (child && child->is_visible()) { + int child_minimum_width = 0; + int child_natural_width = 0; + child->get_preferred_width(child_minimum_width, child_natural_width); + if (get_orientation() == Gtk::ORIENTATION_VERTICAL) { + minimum_width = std::max(minimum_width, child_minimum_width); + natural_width = std::max(natural_width, child_natural_width); + } else { + minimum_width += child_minimum_width; + natural_width += child_natural_width; + } + } + } +} + +void DialogMultipaned::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const +{ + minimum_height = 0; + natural_height = 0; + for (auto const &child : children) { + if (child && child->is_visible()) { + int child_minimum_height = 0; + int child_natural_height = 0; + child->get_preferred_height(child_minimum_height, child_natural_height); + if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) { + minimum_height = std::max(minimum_height, child_minimum_height); + natural_height = std::max(natural_height, child_natural_height); + } else { + minimum_height += child_minimum_height; + natural_height += child_natural_height; + } + } + } +} + +void DialogMultipaned::get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const +{ + minimum_width = 0; + natural_width = 0; + for (auto const &child : children) { + if (child && child->is_visible()) { + int child_minimum_width = 0; + int child_natural_width = 0; + child->get_preferred_width_for_height(height, child_minimum_width, child_natural_width); + if (get_orientation() == Gtk::ORIENTATION_VERTICAL) { + minimum_width = std::max(minimum_width, child_minimum_width); + natural_width = std::max(natural_width, child_natural_width); + } else { + minimum_width += child_minimum_width; + natural_width += child_natural_width; + } + } + } +} + +void DialogMultipaned::get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const +{ + minimum_height = 0; + natural_height = 0; + for (auto const &child : children) { + if (child && child->is_visible()) { + int child_minimum_height = 0; + int child_natural_height = 0; + child->get_preferred_height_for_width(width, child_minimum_height, child_natural_height); + if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) { + minimum_height = std::max(minimum_height, child_minimum_height); + natural_height = std::max(natural_height, child_natural_height); + } else { + minimum_height += child_minimum_height; + natural_height += child_natural_height; + } + } + } +} + + +void DialogMultipaned::children_toggled() { + _handle = -1; + _drag_handle = -1; + queue_allocate(); +} + +/** + * This function allocates the sizes of the children widgets (be them internal or not) from + * the container's allocated size. + * + * Natural width: The width the widget really wants. + * Minimum width: The minimum width for a widget to be useful. + * Minimum <= Natural. + */ +void DialogMultipaned::on_size_allocate(Gtk::Allocation &allocation) +{ + set_allocation(allocation); + bool horizontal = get_orientation() == Gtk::ORIENTATION_HORIZONTAL; + + if (_drag_handle != -1) { // Exchange allocation between the widgets on either side of moved handle + // Allocation values calculated in on_drag_update(); + children[_drag_handle - 1]->size_allocate(allocation1); + children[_drag_handle]->size_allocate(allocationh); + children[_drag_handle + 1]->size_allocate(allocation2); + _drag_handle = -1; + } + + { + std::vector<bool> expandables; // Is child expandable? + std::vector<int> sizes_minimums; // Difference between allocated space and minimum space. + std::vector<int> sizes_naturals; // Difference between allocated space and natural space. + std::vector<int> sizes(children.size(), 0); // The new allocation sizes + std::vector<int> sizes_current; // The current sizes along main axis + int left = horizontal ? allocation.get_width() : allocation.get_height(); + + int index = 0; + + int canvas_index = -1; + for (auto &child : children) { + bool visible; + + Inkscape::UI::Widget::CanvasGrid *canvas = dynamic_cast<Inkscape::UI::Widget::CanvasGrid *>(child); + if (canvas) { + canvas_index = index; + } + + { + visible = child->get_visible(); + expandables.push_back(child->compute_expand(get_orientation())); + + Gtk::Requisition req_minimum; + Gtk::Requisition req_natural; + child->get_preferred_size(req_minimum, req_natural); + if (child == _resizing_widget1 || child == _resizing_widget2) { + // ignore limits for widget being resized interactively and use their current size + req_minimum.width = req_minimum.height = 0; + auto alloc = child->get_allocation(); + req_natural.width = alloc.get_width(); + req_natural.height = alloc.get_height(); + } + + sizes_minimums.push_back(visible ? horizontal ? req_minimum.width : req_minimum.height : 0); + sizes_naturals.push_back(visible ? horizontal ? req_natural.width : req_natural.height : 0); + } + + Gtk::Allocation child_allocation = child->get_allocation(); + sizes_current.push_back(visible ? horizontal ? child_allocation.get_width() : child_allocation.get_height() + : 0); + index++; + } + + // Precalculate the minimum, natural and current totals + int sum_minimums = std::accumulate(sizes_minimums.begin(), sizes_minimums.end(), 0); + int sum_naturals = std::accumulate(sizes_naturals.begin(), sizes_naturals.end(), 0); + int sum_current = std::accumulate(sizes_current.begin(), sizes_current.end(), 0); + + if (sum_naturals <= left) { + sizes = sizes_naturals; + left -= sum_naturals; + } else if (sum_minimums <= left && left < sum_naturals) { + sizes = sizes_minimums; + left -= sum_minimums; + } + + if (canvas_index >= 0) { // give remaining space to canvas element + sizes[canvas_index] += left; + } else { // or, if in a sub-dialogmultipaned, give it evenly to widgets + + int d = 0; + for (int i = 0; i < (int)children.size(); ++i) { + if (expandables[i]) { + d++; + } + } + + if (d > 0) { + int idx = 0; + for (int i = 0; i < (int)children.size(); ++i) { + if (expandables[i]) { + sizes[i] += (left / d); + if (idx < (left % d)) + sizes[i]++; + idx++; + } + } + } + } + left = 0; + + // Check if we actually need to change the sizes on the main axis + left = horizontal ? allocation.get_width() : allocation.get_height(); + if (left == sum_current) { + bool valid = true; + for (int i = 0; i < (int)children.size(); ++i) { + valid = valid && (sizes_minimums[i] <= sizes_current[i]) && // is it over the minimums? + (expandables[i] || sizes_current[i] <= sizes_naturals[i]); // but does it want to be expanded? + if (!valid) + break; + } + if (valid) + sizes = sizes_current; // The current sizes are good, don't change anything; + } + + // Set x and y values of allocations (widths should be correct). + int current_x = allocation.get_x(); + int current_y = allocation.get_y(); + + // Allocate + for (int i = 0; i < (int)children.size(); ++i) { + Gtk::Allocation child_allocation = children[i]->get_allocation(); + child_allocation.set_x(current_x); + child_allocation.set_y(current_y); + + int size = sizes[i]; + + if (horizontal) { + child_allocation.set_width(size); + current_x += size; + child_allocation.set_height(allocation.get_height()); + } else { + child_allocation.set_height(size); + current_y += size; + child_allocation.set_width(allocation.get_width()); + } + + children[i]->size_allocate(child_allocation); + } + } +} + +void DialogMultipaned::forall_vfunc(gboolean, GtkCallback callback, gpointer callback_data) +{ + for (auto const &child : children) { + if (child) { + callback(child->gobj(), callback_data); + } + } +} + +void DialogMultipaned::on_add(Gtk::Widget *child) +{ + if (child) { + append(child); + } +} + +/** + * Callback when a widget is removed from DialogMultipaned and executes the removal. + * It does not remove handles or dropzones. + */ +void DialogMultipaned::on_remove(Gtk::Widget *child) +{ + if (child) { + MyDropZone *dropzone = dynamic_cast<MyDropZone *>(child); + if (dropzone) { + return; + } + MyHandle *my_handle = dynamic_cast<MyHandle *>(child); + if (my_handle) { + return; + } + + const bool visible = child->get_visible(); + if (children.size() > 2) { + auto it = std::find(children.begin(), children.end(), child); + if (it != children.end()) { // child found + if (it + 2 != children.end()) { // not last widget + my_handle = dynamic_cast<MyHandle *>(*(it + 1)); + my_handle->unparent(); + child->unparent(); + children.erase(it, it + 2); + } else { // last widget + if (children.size() == 3) { // only widget + child->unparent(); + children.erase(it); + } else { // not only widget, delete preceding handle + my_handle = dynamic_cast<MyHandle *>(*(it - 1)); + my_handle->unparent(); + child->unparent(); + children.erase(it - 1, it + 1); + } + } + } + } + if (visible) { + queue_resize(); + } + + if (children.size() == 2) { + add_empty_widget(); + _empty_widget->set_size_request(300, -1); + _signal_now_empty.emit(); + } + } +} + +void DialogMultipaned::on_drag_begin(double start_x, double start_y) +{ + _hide_widget1 = _hide_widget2 = nullptr; + _resizing_widget1 = _resizing_widget2 = nullptr; + // We clicked on handle. + bool found = false; + int child_number = 0; + Gtk::Allocation allocation = get_allocation(); + for (auto const &child : children) { + MyHandle *my_handle = dynamic_cast<MyHandle *>(child); + if (my_handle) { + Gtk::Allocation child_allocation = my_handle->get_allocation(); + + // Did drag start in handle? + int x = child_allocation.get_x() - allocation.get_x(); + int y = child_allocation.get_y() - allocation.get_y(); + if (x < start_x && start_x < x + child_allocation.get_width() && y < start_y && + start_y < y + child_allocation.get_height()) { + found = true; + my_handle->set_dragging(true); + break; + } + } + ++child_number; + } + + if (!found) { + gesture->set_state(Gtk::EVENT_SEQUENCE_DENIED); + return; + } + + if (child_number < 1 || child_number > (int)(children.size() - 2)) { + std::cerr << "DialogMultipaned::on_drag_begin: Invalid child (" << child_number << "!!" << std::endl; + gesture->set_state(Gtk::EVENT_SEQUENCE_DENIED); + return; + } + + gesture->set_state(Gtk::EVENT_SEQUENCE_CLAIMED); + + // Save for use in on_drag_update(). + _handle = child_number; + start_allocation1 = children[_handle - 1]->get_allocation(); + if (!children[_handle - 1]->is_visible()) { + start_allocation1.set_width(0); + start_allocation1.set_height(0); + } + start_allocationh = children[_handle]->get_allocation(); + start_allocation2 = children[_handle + 1]->get_allocation(); + if (!children[_handle + 1]->is_visible()) { + start_allocation2.set_width(0); + start_allocation2.set_height(0); + } +} + +void DialogMultipaned::on_drag_end(double offset_x, double offset_y) +{ + if (_handle >= 0 && _handle < children.size()) { + if (auto my_handle = dynamic_cast<MyHandle*>(children[_handle])) { + my_handle->set_dragging(false); + } + } + + gesture->set_state(Gtk::EVENT_SEQUENCE_DENIED); + _handle = -1; + _drag_handle = -1; + if (_hide_widget1) { + _hide_widget1->hide(); + } + if (_hide_widget2) { + _hide_widget2->hide(); + } + _hide_widget1 = nullptr; + _hide_widget2 = nullptr; + _resizing_widget1 = nullptr; + _resizing_widget2 = nullptr; + + queue_allocate(); // reimpose limits if any were bent during interactive resizing +} + +// docking panels in application window can be collapsed (to left or right side) to make more +// room for canvas; this functionality is only meaningful in app window, not in floating dialogs +bool can_collapse(Gtk::Widget* widget, Gtk::Widget* handle) { + // can only collapse DialogMultipaned widgets + if (!widget || dynamic_cast<DialogMultipaned*>(widget) == nullptr) return false; + + // collapsing is not supported in floating dialogs + if (dynamic_cast<DialogWindow*>(widget->get_toplevel())) return false; + + auto parent = handle->get_parent(); + if (!parent) return false; + + // find where the resizing handle is in relation to canvas area: left or right side; + // next, find where the panel is in relation to the handle: on its left or right + bool left_side = true; + bool left_handle = false; + size_t panel_index = 0; + size_t handle_index = 0; + size_t i = 0; + for (auto child : parent->get_children()) { + if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(child)) { + left_side = false; + } + else if (child == handle) { + left_handle = left_side; + handle_index = i; + } + else if (child == widget) { + panel_index = i; + } + ++i; + } + + if (left_handle && panel_index < handle_index) { + return true; + } + if (!left_handle && panel_index > handle_index) { + return true; + } + + return false; +} + +// return minimum widget size; this fn works for hidden widgets too +int get_min_width(Gtk::Widget* widget) { + bool hidden = !widget->is_visible(); + if (hidden) widget->show(); + int minimum_size = 0; + int natural_size = 0; + widget->get_preferred_width(minimum_size, natural_size); + if (hidden) widget->hide(); + return minimum_size; +} + +// Different docking resizing activities use easing functions to speed up or slow down certain phases of resizing +// Below are two picewise linear functions used for that purpose + +// easing function for revealing collapsed panels +double reveal_curve(double val, double size) { + if (size > 0 && val <= size && val >= 0) { + // slow start (resistance to opening) and then quick reveal + auto x = val / size; + auto pos = x; + if (x <= 0.2) { + pos = x * 0.25; + } + else { + pos = x * 9.5 - 1.85; + if (pos > 1) pos = 1; + } + return size * pos; + } + + return val; +} + +// easing function for collapsing panels +// note: factors for x dictate how fast resizing happens when moving mouse (with 1 being at the same speed); +// other constants are to make this fn produce values in 0..1 range and seamlessly connect three segments +double collapse_curve(double val, double size) { + if (size > 0 && val <= size && val >= 0) { + // slow start (resistance), short pause and then quick collapse + auto x = val / size; + auto pos = x; + if (x < 0.5) { + // fast collapsing + pos = x * 10 - 5 + 0.92; + if (pos < 0) { + // panel collapsed + pos = 0; + } + } + else if (x < 0.6) { + // pause + pos = 0.2 * 0.6 + 0.8; // = 0.92; + } + else { + // resistance to collapsing (move slow, x 0.2 decrease) + pos = x * 0.2 + 0.8; + } + return size * pos; + } + + return val; +} + +void DialogMultipaned::on_drag_update(double offset_x, double offset_y) +{ + if (_handle < 0) return; + + auto child1 = children[_handle - 1]; + auto child2 = children[_handle + 1]; + allocation1 = children[_handle - 1]->get_allocation(); + allocationh = children[_handle]->get_allocation(); + allocation2 = children[_handle + 1]->get_allocation(); + + // HACK: The bias prevents erratic resizing when dragging the handle fast, outside the bounds of the app. + const int BIAS = 1; + + if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) { + + auto handle = children[_handle]; + + // function to resize panel + auto resize_fn = [](Gtk::Widget* handle, Gtk::Widget* child, int start_width, double& offset_x) { + int minimum_size = get_min_width(child); + auto width = start_width + offset_x; + bool resizing = false; + Gtk::Widget* hide = nullptr; + + if (!child->is_visible() && can_collapse(child, handle)) { + child->show(); + resizing = true; + } + + if (width < minimum_size) { + if (can_collapse(child, handle)) { + resizing = true; + auto w = start_width == 0 ? reveal_curve(width, minimum_size) : collapse_curve(width, minimum_size); + offset_x = w - start_width; + // facilitate closing/opening panels: users don't have to drag handle all the + // way to collapse/expand a panel, they just need to move it fraction of the way; + // note: those thresholds correspond to the easing functions used + auto threshold = start_width == 0 ? minimum_size * 0.20 : minimum_size * 0.42; + hide = width <= threshold ? child : nullptr; + } + else { + offset_x = -(start_width - minimum_size) + BIAS; + } + } + + return std::make_pair(resizing, hide); + }; + + /* + TODO NOTE: + Resizing should ideally take into account all columns, not just adjacent ones (left and right here). + Without it, expanding second collapsed column does not work, since first one may already have min width, + and cannot be shrunk anymore. Instead it should be pushed out of the way (canvas should be shrunk). + */ + + // panel on the left + auto action1 = resize_fn(handle, child1, start_allocation1.get_width(), offset_x); + _resizing_widget1 = action1.first ? child1 : nullptr; + _hide_widget1 = action1.second ? child1 : nullptr; + + // panel on the right (needs reversing offset_x, so it can use the same logic) + offset_x = -offset_x; + auto action2 = resize_fn(handle, child2, start_allocation2.get_width(), offset_x); + _resizing_widget2 = action2.first ? child2 : nullptr; + _hide_widget2 = action2.second ? child2 : nullptr; + offset_x = -offset_x; + + // set new sizes; they may temporarily violate min size panel requirements + // GTK is not happy about 0-size allocations + allocation1.set_width(start_allocation1.get_width() + offset_x); + allocationh.set_x(start_allocationh.get_x() + offset_x); + allocation2.set_x(start_allocation2.get_x() + offset_x); + allocation2.set_width(start_allocation2.get_width() - offset_x); + } else { + // nothing fancy about resizing in vertical direction; no panel collapsing happens here + int minimum_size; + int natural_size; + children[_handle - 1]->get_preferred_height(minimum_size, natural_size); + if (start_allocation1.get_height() + offset_y < minimum_size) + offset_y = -(start_allocation1.get_height() - minimum_size) + BIAS; + children[_handle + 1]->get_preferred_height(minimum_size, natural_size); + if (start_allocation2.get_height() - offset_y < minimum_size) + offset_y = start_allocation2.get_height() - minimum_size - BIAS; + + allocation1.set_height(start_allocation1.get_height() + offset_y); + allocationh.set_y(start_allocationh.get_y() + offset_y); + allocation2.set_y(start_allocation2.get_y() + offset_y); + allocation2.set_height(start_allocation2.get_height() - offset_y); + } + + _drag_handle = _handle; + queue_allocate(); // Relayout DialogMultipaned content. +} + +void DialogMultipaned::set_target_entries(const std::vector<Gtk::TargetEntry> &target_entries) +{ + drag_dest_set(target_entries); + ((MyDropZone *)children[0])->drag_dest_set(target_entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE); + ((MyDropZone *)children[children.size() - 1]) + ->drag_dest_set(target_entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE); +} + +void DialogMultipaned::on_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y, + const Gtk::SelectionData &selection_data, guint info, guint time) +{ + _signal_prepend_drag_data.emit(context); +} + +void DialogMultipaned::on_prepend_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y, + const Gtk::SelectionData &selection_data, guint info, guint time) +{ + _signal_prepend_drag_data.emit(context); +} + +void DialogMultipaned::on_append_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y, + const Gtk::SelectionData &selection_data, guint info, guint time) +{ + _signal_append_drag_data.emit(context); +} + +// Signals +sigc::signal<void, const Glib::RefPtr<Gdk::DragContext>> DialogMultipaned::signal_prepend_drag_data() +{ + resize_widget_children(this); + return _signal_prepend_drag_data; +} + +sigc::signal<void, const Glib::RefPtr<Gdk::DragContext>> DialogMultipaned::signal_append_drag_data() +{ + resize_widget_children(this); + return _signal_append_drag_data; +} + +sigc::signal<void> DialogMultipaned::signal_now_empty() +{ + return _signal_now_empty; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/dialog-multipaned.h b/src/ui/dialog/dialog-multipaned.h new file mode 100644 index 0000000..1d1bb50 --- /dev/null +++ b/src/ui/dialog/dialog-multipaned.h @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef INKSCAPE_UI_DIALOG_MULTIPANED_H +#define INKSCAPE_UI_DIALOG_MULTIPANED_H + +/** @file + * @brief A widget with multiple panes. Agnostic to type what kind of widgets panes contain. + * + * Authors: see git history + * Tavmjong Bah + * + * Copyright (c) 2020 Tavmjong Bah, Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/refptr.h> +#include <gtkmm/enums.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/gesturedrag.h> +#include <gtkmm/orientable.h> +#include <gtkmm/widget.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** A widget with multiple panes */ + +class MyDropZone; +class MyHandle; +class DialogMultipaned; + +/* ============ DROPZONE ============ */ + +/** + * Dropzones are eventboxes at the ends of a DialogMultipaned where you can drop dialogs. + */ +class MyDropZone + : public Gtk::Orientable + , public Gtk::EventBox +{ +public: + MyDropZone(Gtk::Orientation orientation); + ~MyDropZone() override; + + static void add_highlight_instances(); + static void remove_highlight_instances(); + +private: + void set_size(int size); + bool _active = false; + void add_highlight(); + void remove_highlight(); + + static std::list<MyDropZone *> _instances_list; +}; + +/* ============ HANDLE ============ */ + +/** + * Handles are event boxes that help with resizing DialogMultipaned' children. + */ +class MyHandle + : public Gtk::Orientable + , public Gtk::EventBox +{ +public: + MyHandle(Gtk::Orientation orientation, int size); + ~MyHandle() override = default; + + bool on_enter_notify_event(GdkEventCrossing *crossing_event) override; + void set_dragging(bool dragging); +private: + bool on_leave_notify_event(GdkEventCrossing* crossing_event) override; + bool on_button_press_event(GdkEventButton* button_event) override; + bool on_button_release_event(GdkEventButton *event) override; + bool on_motion_notify_event(GdkEventMotion* motion_event) override; + void toggle_multipaned(); + void update_click_indicator(double x, double y); + void show_click_indicator(bool show); + bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override; + Cairo::Rectangle get_active_click_zone(); + int _cross_size; + Gtk::Widget *_child; + void resize_handler(Gtk::Allocation &allocation); + bool is_click_resize_active() const; + bool _click = false; + bool _click_indicator = false; + bool _dragging = false; +}; + +/* ============ MULTIPANE ============ */ + +/* + * A widget with multiple panes. Agnostic to type what kind of widgets panes contain. + * Handles allow a user to resize children widgets. Drop zones allow adding widgets + * at either end. + */ +class DialogMultipaned + : public Gtk::Orientable + , public Gtk::Container +{ +public: + DialogMultipaned(Gtk::Orientation orientation = Gtk::ORIENTATION_HORIZONTAL); + ~DialogMultipaned() override; + + void prepend(Gtk::Widget *new_widget); + void append(Gtk::Widget *new_widget); + + // Getters and setters + Gtk::Widget *get_first_widget(); + Gtk::Widget *get_last_widget(); + std::vector<Gtk::Widget *> get_children() { return children; } + void set_target_entries(const std::vector<Gtk::TargetEntry> &target_entries); + bool has_empty_widget() { return (bool)_empty_widget; } + + // Signals + sigc::signal<void, const Glib::RefPtr<Gdk::DragContext>> signal_prepend_drag_data(); + sigc::signal<void, const Glib::RefPtr<Gdk::DragContext>> signal_append_drag_data(); + sigc::signal<void> signal_now_empty(); + + // UI functions + void set_dropzone_sizes(int start, int end); + void toggle_multipaned_children(bool show); + void children_toggled(); + void ensure_multipaned_children(); + +protected: + // Overrides + Gtk::SizeRequestMode get_request_mode_vfunc() const override; + void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; + void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override; + void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override; + void on_size_allocate(Gtk::Allocation &allocation) override; + + // Allow us to keep track of our widgets ourselves. + void forall_vfunc(gboolean include_internals, GtkCallback callback, gpointer callback_data) override; + + void on_add(Gtk::Widget *child) override; + void on_remove(Gtk::Widget *child) override; + + // Signals + sigc::signal<void, const Glib::RefPtr<Gdk::DragContext>> _signal_prepend_drag_data; + sigc::signal<void, const Glib::RefPtr<Gdk::DragContext>> _signal_append_drag_data; + sigc::signal<void> _signal_now_empty; + +private: + // We must manage children ourselves. + std::vector<Gtk::Widget *> children; + + // Values used when dragging handle. + int _handle = -1; // Child number of active handle + int _drag_handle = -1; + Gtk::Widget* _resizing_widget1 = nullptr; + Gtk::Widget* _resizing_widget2 = nullptr; + Gtk::Widget* _hide_widget1 = nullptr; + Gtk::Widget* _hide_widget2 = nullptr; + Gtk::Allocation start_allocation1; + Gtk::Allocation start_allocationh; + Gtk::Allocation start_allocation2; + Gtk::Allocation allocation1; + Gtk::Allocation allocationh; + Gtk::Allocation allocation2; + + Glib::RefPtr<Gtk::GestureDrag> gesture; + // Signal callbacks + void on_drag_begin(double start_x, double start_y); + void on_drag_end(double offset_x, double offset_y); + void on_drag_update(double offset_x, double offset_y); + void on_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y, + const Gtk::SelectionData &selection_data, guint info, guint time); + void on_prepend_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y, + const Gtk::SelectionData &selection_data, guint info, guint time); + void on_append_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y, + const Gtk::SelectionData &selection_data, guint info, guint time); + + // Others + Gtk::Widget *_empty_widget; // placeholder in an empty container + void add_empty_widget(); + void remove_empty_widget(); + std::vector<sigc::connection> _connections; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_MULTIPANED_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/ui/dialog/dialog-notebook.cpp b/src/ui/dialog/dialog-notebook.cpp new file mode 100644 index 0000000..babcd2d --- /dev/null +++ b/src/ui/dialog/dialog-notebook.cpp @@ -0,0 +1,994 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** @file + * @brief A wrapper for Gtk::Notebook. + * + * Authors: see git history + * Tavmjong Bah + * + * Copyright (c) 2018 Tavmjong Bah, Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "dialog-notebook.h" + +#include <vector> +#include <glibmm/i18n.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/scrollbar.h> +#include <gtkmm/separatormenuitem.h> +#include <gtkmm/menu.h> + +#include "enums.h" +#include "inkscape.h" +#include "inkscape-window.h" +#include "ui/dialog/dialog-base.h" +#include "ui/dialog/dialog-data.h" +#include "ui/dialog/dialog-container.h" +#include "ui/dialog/dialog-multipaned.h" +#include "ui/dialog/dialog-window.h" +#include "ui/icon-loader.h" +#include "ui/util.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +std::list<DialogNotebook *> DialogNotebook::_instances; + +/** + * DialogNotebook constructor. + * + * @param container the parent DialogContainer of the notebook. + */ +DialogNotebook::DialogNotebook(DialogContainer *container) + : Gtk::ScrolledWindow() + , _container(container) + , _labels_auto(true) + , _detaching_duplicate(false) + , _selected_page(nullptr) + , _label_visible(true) +{ + set_name("DialogNotebook"); + set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_NEVER); + set_shadow_type(Gtk::SHADOW_NONE); + set_vexpand(true); + set_hexpand(true); + + // =========== Getting preferences ========== + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs == nullptr) { + return; + } + gint labelstautus = prefs->getInt("/options/notebooklabels/value", PREFS_NOTEBOOK_LABELS_AUTO); + _labels_auto = labelstautus == PREFS_NOTEBOOK_LABELS_AUTO; + _labels_off = labelstautus == PREFS_NOTEBOOK_LABELS_OFF; + + // ============= Notebook menu ============== + _notebook.set_name("DockedDialogNotebook"); + _notebook.set_show_border(false); + _notebook.set_group_name("InkscapeDialogGroup"); + _notebook.set_scrollable(true); + + Gtk::MenuItem *new_menu_item = nullptr; + + int row = 0; + // Close tab + new_menu_item = Gtk::manage(new Gtk::MenuItem(_("Close Current Tab"))); + _conn.emplace_back( + new_menu_item->signal_activate().connect(sigc::mem_fun(*this, &DialogNotebook::close_tab_callback))); + _menu.attach(*new_menu_item, 0, 2, row, row + 1); + row++; + + // Close notebook + new_menu_item = Gtk::manage(new Gtk::MenuItem(_("Close Panel"))); + _conn.emplace_back( + new_menu_item->signal_activate().connect(sigc::mem_fun(*this, &DialogNotebook::close_notebook_callback))); + _menu.attach(*new_menu_item, 0, 2, row, row + 1); + row++; + + // Move to new window + new_menu_item = Gtk::manage(new Gtk::MenuItem(_("Move Tab to New Window"))); + _conn.emplace_back( + new_menu_item->signal_activate().connect([=]() { pop_tab_callback(); })); + _menu.attach(*new_menu_item, 0, 2, row, row + 1); + row++; + + // Separator menu item + // new_menu_item = Gtk::manage(new Gtk::SeparatorMenuItem()); + // _menu.attach(*new_menu_item, 0, 2, row, row + 1); + // row++; + + struct Dialog { + Glib::ustring key; + Glib::ustring label; + Glib::ustring order; + Glib::ustring icon_name; + DialogData::Category category; + ScrollProvider provide_scroll; + }; + std::vector<Dialog> all_dialogs; + auto const &dialog_data = get_dialog_data(); + all_dialogs.reserve(dialog_data.size()); + for (auto&& kv : dialog_data) { + const auto& key = kv.first; + const auto& data = kv.second; + if (data.category == DialogData::Other) { + continue; + } + // for sorting dialogs alphabetically, remove '_' (used for accelerators) + Glib::ustring order = data.label; // Already translated + auto underscore = order.find('_'); + if (underscore != Glib::ustring::npos) { + order = order.erase(underscore, 1); + } + all_dialogs.emplace_back(Dialog { + .key = key, + .label = data.label, + .order = order, + .icon_name = data.icon_name, + .category = data.category, + .provide_scroll = data.provide_scroll + }); + } + // sort by categories and then by names + std::sort(all_dialogs.begin(), all_dialogs.end(), [](const Dialog& a, const Dialog& b){ + if (a.category != b.category) return a.category < b.category; + return a.order < b.order; + }); + + int col = 0; + DialogData::Category category = DialogData::Other; + for (auto&& data : all_dialogs) { + if (data.category != category) { + if (col > 0) row++; + + auto separator = Gtk::make_managed<Gtk::SeparatorMenuItem>(); + _menu.attach(*separator, 0, 2, row, row + 1); + row++; + + category = data.category; + auto sep = Gtk::make_managed<Gtk::MenuItem>(); + sep->set_label(Glib::ustring(gettext(dialog_categories[category])).uppercase()); + sep->get_style_context()->add_class("menu-category"); + sep->set_sensitive(false); + _menu.attach(*sep, 0, 2, row, row + 1); + col = 0; + row++; + } + auto key = data.key; + auto dlg = Gtk::make_managed<Gtk::MenuItem>(); + auto box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 8); + box->pack_start(*Gtk::make_managed<Gtk::Image>(data.icon_name, Gtk::ICON_SIZE_MENU), false, true); + box->pack_start(*Gtk::make_managed<Gtk::Label>(data.label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true), false, true); + dlg->add(*box); + dlg->signal_activate().connect([=](){ + // get desktop's container, it may be different than current '_container'! + if (auto desktop = SP_ACTIVE_DESKTOP) { + if (auto container = desktop->getContainer()) { + container->new_dialog(key); + } + } + }); + _menu.attach(*dlg, col, col + 1, row, row + 1); + col++; + if (col > 1) { + col = 0; + row++; + } + } + if (prefs->getBool("/theme/symbolicIcons", true)) { + _menu.get_style_context()->add_class("symbolic"); + } + + _menu.show_all_children(); + + Gtk::Button* menubtn = Gtk::manage(new Gtk::Button()); + menubtn->set_image_from_icon_name("go-down-symbolic"); + menubtn->signal_clicked().connect([=](){ _menu.popup_at_widget(menubtn, Gdk::GRAVITY_SOUTH, Gdk::GRAVITY_NORTH, nullptr); }); + _notebook.set_action_widget(menubtn, Gtk::PACK_END); + menubtn->show(); + menubtn->set_relief(Gtk::RELIEF_NORMAL); + menubtn->set_valign(Gtk::ALIGN_CENTER); + menubtn->set_halign(Gtk::ALIGN_CENTER); + menubtn->set_can_focus(false); + menubtn->set_name("DialogMenuButton"); + + // =============== Signals ================== + _conn.emplace_back(signal_size_allocate().connect(sigc::mem_fun(*this, &DialogNotebook::on_size_allocate_scroll))); + _conn.emplace_back(_notebook.signal_drag_begin().connect(sigc::mem_fun(*this, &DialogNotebook::on_drag_begin))); + _conn.emplace_back(_notebook.signal_drag_end().connect(sigc::mem_fun(*this, &DialogNotebook::on_drag_end))); + _conn.emplace_back(_notebook.signal_page_added().connect(sigc::mem_fun(*this, &DialogNotebook::on_page_added))); + _conn.emplace_back(_notebook.signal_page_removed().connect(sigc::mem_fun(*this, &DialogNotebook::on_page_removed))); + _conn.emplace_back(_notebook.signal_switch_page().connect(sigc::mem_fun(*this, &DialogNotebook::on_page_switch))); + + // ============= Finish setup =============== + _reload_context = true; + add(_notebook); + show_all(); + + _instances.push_back(this); +} + +DialogNotebook::~DialogNotebook() +{ + // disconnect signals first, so no handlers are invoked when removing pages + for_each(_conn.begin(), _conn.end(), [&](auto c) { c.disconnect(); }); + for_each(_connmenu.begin(), _connmenu.end(), [&](auto c) { c.disconnect(); }); + for_each(_tab_connections.begin(), _tab_connections.end(), [&](auto it) { it.second.disconnect(); }); + + // Unlink and remove pages + for (int i = _notebook.get_n_pages(); i >= 0; --i) { + DialogBase *dialog = dynamic_cast<DialogBase *>(_notebook.get_nth_page(i)); + _container->unlink_dialog(dialog); + _notebook.remove_page(i); + } + + _conn.clear(); + _connmenu.clear(); + _tab_connections.clear(); + + _instances.remove(this); +} + +void DialogNotebook::add_highlight_header() +{ + const auto &style = _notebook.get_style_context(); + style->add_class("nb-highlight"); +} + +void DialogNotebook::remove_highlight_header() +{ + const auto &style = _notebook.get_style_context(); + style->remove_class("nb-highlight"); +} + +/** + * get provide scroll + */ +bool +DialogNotebook::provide_scroll(Gtk::Widget &page) { + auto const &dialog_data = get_dialog_data(); + auto dialogbase = dynamic_cast<DialogBase*>(&page); + if (dialogbase) { + auto data = dialog_data.find(dialogbase->get_type()); + if ((*data).second.provide_scroll == ScrollProvider::PROVIDE) { + return true; + } + } + return false; +} + +/** + * Set provide scroll + */ +Gtk::ScrolledWindow * +DialogNotebook::get_current_scrolledwindow(bool skip_scroll_provider) { + + gint pagenum = _notebook.get_current_page(); + Gtk::Widget *page = _notebook.get_nth_page(pagenum); + if (page) { + if (skip_scroll_provider && provide_scroll(*page)) { + return nullptr; + } + auto container = dynamic_cast<Gtk::Container *>(page); + if (container) { + std::vector<Gtk::Widget *> widgs = container->get_children(); + if (widgs.size()) { + auto scrolledwindow = dynamic_cast<Gtk::ScrolledWindow *>(widgs[0]); + if (scrolledwindow) { + return scrolledwindow; + } + } + } + } + return nullptr; +} + +/** + * Adds a widget as a new page with a tab. + */ +void DialogNotebook::add_page(Gtk::Widget &page, Gtk::Widget &tab, Glib::ustring) +{ + _reload_context = true; + page.set_vexpand(); + + auto container = dynamic_cast<Gtk::Box *>(&page); + if (container) { + auto *wrapper = Gtk::manage(new Gtk::ScrolledWindow()); + wrapper->set_vexpand(true); + wrapper->set_propagate_natural_height(true); + wrapper->set_valign(Gtk::ALIGN_FILL); + wrapper->set_overlay_scrolling(false); + wrapper->set_can_focus(false); + wrapper->get_style_context()->add_class("noborder"); + auto *wrapperbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL,0)); + wrapperbox->set_valign(Gtk::ALIGN_FILL); + wrapperbox->set_vexpand(true); + std::vector<Gtk::Widget *> widgs = container->get_children(); + for (auto widg : widgs) { + bool expand = container->child_property_expand(*widg); + bool fill = container->child_property_fill(*widg); + guint padding = container->child_property_expand(*widg); + Gtk::PackType pack_type = container->child_property_pack_type(*widg); + container->remove(*widg); + if (pack_type == Gtk::PACK_START) { + wrapperbox->pack_start(*widg, expand, fill, padding); + } else { + wrapperbox->pack_end (*widg, expand, fill, padding); + } + } + wrapper->add(*wrapperbox); + container->add(*wrapper); + if (provide_scroll(page)) { + wrapper->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_EXTERNAL); + } else { + wrapper->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + } + } + + int page_number = _notebook.append_page(page, tab); + _notebook.set_tab_reorderable(page); + _notebook.set_tab_detachable(page); + _notebook.show_all(); + _notebook.set_current_page(page_number); +} + +/** + * Moves a page from a different notebook to this one. + */ +void DialogNotebook::move_page(Gtk::Widget &page) +{ + // Find old notebook + Gtk::Notebook *old_notebook = dynamic_cast<Gtk::Notebook *>(page.get_parent()); + if (!old_notebook) { + std::cerr << "DialogNotebook::move_page: page not in notebook!" << std::endl; + return; + } + + Gtk::Widget *tab = old_notebook->get_tab_label(page); + Glib::ustring text = old_notebook->get_menu_label_text(page); + + // Keep references until re-attachment + tab->reference(); + page.reference(); + + old_notebook->detach_tab(page); + _notebook.append_page(page, *tab); + // Remove unnecessary references + tab->unreference(); + page.unreference(); + + // Set default settings for a new page + _notebook.set_tab_reorderable(page); + _notebook.set_tab_detachable(page); + _notebook.show_all(); + _reload_context = true; +} + +// ============ Notebook callbacks ============== + +/** + * Callback to close the current active tab. + */ +void DialogNotebook::close_tab_callback() +{ + int page_number = _notebook.get_current_page(); + + if (_selected_page) { + page_number = _notebook.page_num(*_selected_page); + _selected_page = nullptr; + } + + if (dynamic_cast<DialogBase*>(_notebook.get_nth_page(page_number))) { + // is this a dialog in a floating window? + if (auto window = dynamic_cast<DialogWindow*>(_container->get_toplevel())) { + // store state of floating dialog before it gets deleted + DialogManager::singleton().store_state(*window); + } + } + + // Remove page from notebook + _notebook.remove_page(page_number); + + // Delete the signal connection + remove_close_tab_callback(_selected_page); + + if (_notebook.get_n_pages() == 0) { + close_notebook_callback(); + return; + } + + // Update tab labels by comparing the sum of their widths to the allocation + Gtk::Allocation allocation = get_allocation(); + on_size_allocate_scroll(allocation); + _reload_context = true; +} + +/** + * Shutdown callback - delete the parent DialogMultipaned before destructing. + */ +void DialogNotebook::close_notebook_callback() +{ + // Search for DialogMultipaned + DialogMultipaned *multipaned = dynamic_cast<DialogMultipaned *>(get_parent()); + if (multipaned) { + multipaned->remove(*this); + } else if (get_parent()) { + std::cerr << "DialogNotebook::close_notebook_callback: Unexpected parent!" << std::endl; + get_parent()->remove(*this); + } + delete this; +} + +/** + * Callback to move the current active tab. + */ +DialogWindow* DialogNotebook::pop_tab_callback() +{ + // Find page. + Gtk::Widget *page = _notebook.get_nth_page(_notebook.get_current_page()); + + if (_selected_page) { + page = _selected_page; + _selected_page = nullptr; + } + + if (!page) { + std::cerr << "DialogNotebook::pop_tab_callback: page not found!" << std::endl; + return nullptr; + } + + // Move page to notebook in new dialog window (attached to active InkscapeWindow). + auto inkscape_window = _container->get_inkscape_window(); + auto window = new DialogWindow(inkscape_window, page); + window->show_all(); + + if (_notebook.get_n_pages() == 0) { + close_notebook_callback(); + return window; + } + + // Update tab labels by comparing the sum of their widths to the allocation + Gtk::Allocation allocation = get_allocation(); + on_size_allocate_scroll(allocation); + + return window; +} + +// ========= Signal handlers - notebook ========= + +/** + * Signal handler to pop a dragged tab into its own DialogWindow. + * + * A failed drag means that the page was not dropped on an existing notebook. + * Thus create a new window with notebook to move page to. + * + * BUG: this has inconsistent behavior on Wayland. + */ +void DialogNotebook::on_drag_end(const Glib::RefPtr<Gdk::DragContext> &context) +{ + // Remove dropzone highlights + MyDropZone::remove_highlight_instances(); + for (auto instance : _instances) { + instance->remove_highlight_header(); + } + + bool set_floating = !context->get_dest_window(); + if (!set_floating && context->get_dest_window()->get_window_type() == Gdk::WINDOW_FOREIGN) { + set_floating = true; + } + + if (set_floating) { + Gtk::Widget *source = Gtk::Widget::drag_get_source_widget(context); + + // Find source notebook and page + Gtk::Notebook *old_notebook = dynamic_cast<Gtk::Notebook *>(source); + if (!old_notebook) { + std::cerr << "DialogNotebook::on_drag_end: notebook not found!" << std::endl; + } else { + // Find page + Gtk::Widget *page = old_notebook->get_nth_page(old_notebook->get_current_page()); + if (page) { + // Move page to notebook in new dialog window + + auto inkscape_window = _container->get_inkscape_window(); + auto window = new DialogWindow(inkscape_window, page); + + // Move window to mouse pointer + if (auto device = context->get_device()) { + int x = 0, y = 0; + device->get_position(x, y); + window->move(std::max(0, x - 50), std::max(0, y - 50)); + } + + window->show_all(); + } + } + } + + // Closes the notebook if empty. + if (_notebook.get_n_pages() == 0) { + close_notebook_callback(); + return; + } + + // Update tab labels by comparing the sum of their widths to the allocation + Gtk::Allocation allocation = get_allocation(); + on_size_allocate_scroll(allocation); +} + +void DialogNotebook::on_drag_begin(const Glib::RefPtr<Gdk::DragContext> &context) +{ + MyDropZone::add_highlight_instances(); + for (auto instance : _instances) { + instance->add_highlight_header(); + } +} + +/** + * Signal handler to update dialog list when adding a page. + */ +void DialogNotebook::on_page_added(Gtk::Widget *page, int page_num) +{ + DialogBase *dialog = dynamic_cast<DialogBase *>(page); + + // Does current container/window already have such a dialog? + if (dialog && _container->has_dialog_of_type(dialog)) { + // We already have a dialog of the same type + + // Highlight first dialog + DialogBase *other_dialog = _container->get_dialog(dialog->get_type()); + other_dialog->blink(); + + // Remove page from notebook + _detaching_duplicate = true; // HACK: prevent removing the initial dialog of the same type + _notebook.detach_tab(*page); + return; + } else if (dialog) { + // We don't have a dialog of this type + + // Add to dialog list + _container->link_dialog(dialog); + } else { + // This is not a dialog + return; + } + + // add close tab signal + add_close_tab_callback(page); + + // Switch tab labels if needed + if (!_labels_auto) { + toggle_tab_labels_callback(false); + } + + // Update tab labels by comparing the sum of their widths to the allocation + Gtk::Allocation allocation = get_allocation(); + on_size_allocate_scroll(allocation); +} + +/** + * Signal handler to update dialog list when removing a page. + */ +void DialogNotebook::on_page_removed(Gtk::Widget *page, int page_num) +{ + /** + * When adding a dialog in a notebooks header zone of the same type as an existing one, + * we remove it immediately, which triggers a call to this method. We use `_detaching_duplicate` + * to prevent reemoving the initial dialog. + */ + if (_detaching_duplicate) { + _detaching_duplicate = false; + return; + } + + // Remove from dialog list + DialogBase *dialog = dynamic_cast<DialogBase *>(page); + if (dialog) { + _container->unlink_dialog(dialog); + } + + // remove old close tab signal + remove_close_tab_callback(page); +} + +/** + * We need to remove the scrollbar to snap a whole DialogNotebook to width 0. + * + */ +void DialogNotebook::on_size_allocate_scroll(Gtk::Allocation &a) +{ + // magic number + const int MIN_HEIGHT = 60; + // set or unset scrollbars to completely hide a notebook + // because we have a "blocking" scroll per tab we need to loop to aboid + // other page stop out scroll + for (auto const &page : _notebook.get_children()) { + auto *container = dynamic_cast<Gtk::Container *>(page); + if (container && !provide_scroll(*page)) { + std::vector<Gtk::Widget *> widgs = container->get_children(); + if (widgs.size()) { + auto scrolledwindow = dynamic_cast<Gtk::ScrolledWindow *>(widgs[0]); + if (scrolledwindow) { + double height = scrolledwindow->get_allocation().get_height(); + if (height > 1) { + Gtk::PolicyType policy = scrolledwindow->property_vscrollbar_policy().get_value(); + if (height >= MIN_HEIGHT && policy != Gtk::POLICY_AUTOMATIC) { + scrolledwindow->property_vscrollbar_policy().set_value(Gtk::POLICY_AUTOMATIC); + } else if(height < MIN_HEIGHT && policy != Gtk::POLICY_EXTERNAL) { + scrolledwindow->property_vscrollbar_policy().set_value(Gtk::POLICY_EXTERNAL); + } else { + // we don't need to update; break + break; + } + } + } + } + } + } + + + set_allocation(a); + // only update notebook tabs on horizontal changes + if (a.get_width() != _prev_alloc_width) { + on_size_allocate_notebook(a); + } +} + +/** + * This function hides the tab labels if necessary (and _labels_auto == true) + */ +void DialogNotebook::on_size_allocate_notebook(Gtk::Allocation &a) +{ + + // we unset scrollable when FULL mode on to prevent overflow with + // container at full size that makes an unmaximized desktop freeze + _notebook.set_scrollable(false); + if (!_labels_set_off && !_labels_auto) { + toggle_tab_labels_callback(false); + } + if (!_labels_auto) { + return; + } + + int alloc_width = get_allocation().get_width(); + // Don't update on closed dialog container, prevent console errors + if (alloc_width < 2) { + _notebook.set_scrollable(true); + return; + } + int nat_width = 0; + int initial_width = 0; + int total_width = 0; + _notebook.get_preferred_width(initial_width, nat_width); // get current notebook allocation + for (auto const &page : _notebook.get_children()) { + Gtk::EventBox *cover = dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*page)); + if (!cover) { + continue; + } + cover->show_all(); + } + _notebook.get_preferred_width(total_width, nat_width); // get full notebook allocation (all open) + prev_tabstatus = tabstatus; + if (_single_tab_width != _none_tab_width && + (_none_tab_width && _none_tab_width > alloc_width || + (_single_tab_width > alloc_width && _single_tab_width < total_width))) + { + tabstatus = TabsStatus::NONE; + if (_single_tab_width != initial_width || prev_tabstatus == TabsStatus::NONE) { + _none_tab_width = initial_width; + } + } else { + tabstatus = (alloc_width <= total_width) ? TabsStatus::SINGLE : TabsStatus::ALL; + if (total_width != initial_width && + prev_tabstatus == TabsStatus::SINGLE && + tabstatus == TabsStatus::SINGLE) + { + _single_tab_width = initial_width; + } + } + if ((_single_tab_width && !_none_tab_width) || + (_single_tab_width && _single_tab_width == _none_tab_width)) + { + _none_tab_width = _single_tab_width - 1; + } + + /* + std::cout << "::::::::::tabstatus::" << (int)tabstatus << std::endl; + std::cout << ":::::prev_tabstatus::" << (int)prev_tabstatus << std::endl; + std::cout << "::::::::alloc_width::" << alloc_width << std::endl; + std::cout << "::_prev_alloc_width::" << _prev_alloc_width << std::endl; + std::cout << "::::::initial_width::" << initial_width << std::endl; + std::cout << "::::::::::nat_width::" << nat_width << std::endl; + std::cout << "::::::::total_width::" << total_width << std::endl; + std::cout << "::::_none_tab_width::" << _none_tab_width << std::endl; + std::cout << "::_single_tab_width::" << _single_tab_width << std::endl; + std::cout << ":::::::::::::::::::::" << std::endl; + */ + + _prev_alloc_width = alloc_width; + bool show = tabstatus == TabsStatus::ALL; + toggle_tab_labels_callback(show); +} + +/** + * Signal handler to close a tab when middle-clicking. + */ +bool DialogNotebook::on_tab_click_event(GdkEventButton *event, Gtk::Widget *page) +{ + if (event->type == GDK_BUTTON_PRESS) { + if (event->button == 2) { // Close tab + _selected_page = page; + close_tab_callback(); + } else if (event->button == 3) { // Show menu + _selected_page = page; + reload_tab_menu(); + _menutabs.popup_at_pointer((GdkEvent *)event); + } + } + + return false; +} + +void DialogNotebook::on_close_button_click_event(Gtk::Widget *page) +{ + _selected_page = page; + close_tab_callback(); +} + +// ================== Helpers =================== + +/** + * Reload tab menu + */ +void DialogNotebook::reload_tab_menu() +{ + if (_reload_context) { + _reload_context = false; + Gtk::MenuItem* menuitem = nullptr; + for_each(_connmenu.begin(), _connmenu.end(), [&](auto c) { c.disconnect(); }); + _connmenu.clear(); + for (auto widget : _menutabs.get_children()) { + delete widget; + } + auto prefs = Inkscape::Preferences::get(); + bool symbolic = false; + if (prefs->getBool("/theme/symbolicIcons", false)) { + symbolic = true; + } + + for (auto const &page : _notebook.get_children()) { + Gtk::EventBox *cover = dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*page)); + if (!cover) { + continue; + } + + Gtk::Box *box = dynamic_cast<Gtk::Box *>(cover->get_child()); + + if (!box) { + continue; + } + auto childs = box->get_children(); + if (childs.size() < 2) { + continue; + } + // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can + // only hold one child + Gtk::Box *boxmenu = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + boxmenu->set_halign(Gtk::ALIGN_START); + menuitem = Gtk::manage(new Gtk::MenuItem()); + menuitem->add(*boxmenu); + + Gtk::Label *label = dynamic_cast<Gtk::Label *>(childs[1]); + Gtk::Label *labelto = Gtk::manage(new Gtk::Label(label->get_text())); + + Gtk::Image *icon = dynamic_cast<Gtk::Image *>(childs[0]); + if (icon) { + int min_width, nat_width; + icon->get_preferred_width(min_width, nat_width); + _icon_width = min_width; + auto name = icon->get_icon_name(); + if (!name.empty()) { + if (symbolic && name.find("-symbolic") == Glib::ustring::npos) { + name += Glib::ustring("-symbolic"); + } + Gtk::Image *iconend = sp_get_icon_image(name, Gtk::ICON_SIZE_MENU); + boxmenu->pack_start(*iconend, false, false, 0); + } + } + boxmenu->pack_start(*labelto, true, true, 0); + size_t pagenum = _notebook.page_num(*page); + _connmenu.emplace_back( + menuitem->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &DialogNotebook::change_page),pagenum))); + + _menutabs.append(*Gtk::manage(menuitem)); + } + } + _menutabs.show_all(); +} +/** + * Callback to toggle all tab labels to the selected state. + * @param show: whether you want the labels to show or not + */ +void DialogNotebook::toggle_tab_labels_callback(bool show) +{ + _label_visible = show; + for (auto const &page : _notebook.get_children()) { + Gtk::EventBox *cover = dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*page)); + if (!cover) { + continue; + } + + Gtk::Box *box = dynamic_cast<Gtk::Box *>(cover->get_child()); + if (!box) { + continue; + } + + Gtk::Label *label = dynamic_cast<Gtk::Label *>(box->get_children()[1]); + Gtk::Button *close = dynamic_cast<Gtk::Button *>(*box->get_children().rbegin()); + int n = _notebook.get_current_page(); + if (close && label) { + if (page != _notebook.get_nth_page(n)) { + show ? close->show() : close->hide(); + show ? label->show() : label->hide(); + } else if (tabstatus == TabsStatus::NONE || _labels_off) { + if (page != _notebook.get_nth_page(n)) { + close->hide(); + } else { + close->show(); + } + label->hide(); + } else { + close->show(); + label->show(); + } + } + } + _labels_set_off = _labels_off; + if (_prev_alloc_width && prev_tabstatus != tabstatus ) { + resize_widget_children(&_notebook); + } + if (show && _single_tab_width) { + _notebook.set_scrollable(true); + } +} + +void DialogNotebook::on_page_switch(Gtk::Widget *curr_page, guint) +{ + if (auto container = dynamic_cast<Gtk::Container *>(curr_page)) { + container->show_all_children(); + } + for (auto const &page : _notebook.get_children()) { + auto dialogbase = dynamic_cast<DialogBase*>(page); + if (dialogbase) { + std::vector<Gtk::Widget *> widgs = dialogbase->get_children(); + if (widgs.size()) { + if (curr_page == page) { + widgs[0]->show_now(); + } else { + widgs[0]->hide(); + } + } + if (_prev_alloc_width) { + dialogbase->setShowing(curr_page == page); + } + } + if (_label_visible) { + continue; + } + Gtk::EventBox *cover = dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*page)); + if (!cover) { + continue; + } + + if (cover == dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*curr_page))) { + Gtk::Box *box = dynamic_cast<Gtk::Box *>(cover->get_child()); + Gtk::Label *label = dynamic_cast<Gtk::Label *>(box->get_children()[1]); + Gtk::Button *close = dynamic_cast<Gtk::Button *>(*box->get_children().rbegin()); + + if (label) { + if (tabstatus == TabsStatus::NONE) { + label->hide(); + } else { + label->show(); + } + } + + if (close) { + if (tabstatus == TabsStatus::NONE && curr_page != page) { + close->hide(); + } else { + close->show(); + } + } + continue; + } + + Gtk::Box *box = dynamic_cast<Gtk::Box *>(cover->get_child()); + if (!box) { + continue; + } + + Gtk::Label *label = dynamic_cast<Gtk::Label *>(box->get_children()[1]); + Gtk::Button *close = dynamic_cast<Gtk::Button *>(*box->get_children().rbegin()); + + + close->hide(); + label->hide(); + } + if (_prev_alloc_width) { + if (!_label_visible) { + queue_allocate(); + } + auto window = dynamic_cast<DialogWindow*>(_container->get_toplevel()); + if (window) { + resize_widget_children(window->get_container()); + } else { + if (auto desktop = SP_ACTIVE_DESKTOP) { + if (auto container = desktop->getContainer()) { + resize_widget_children(container); + } + } + } + } +} + +/** + * Helper method that change the page + */ +void DialogNotebook::change_page(size_t pagenum) +{ + _notebook.set_current_page(pagenum); +} + +/** + * Helper method that adds the close tab signal connection for the page given. + */ +void DialogNotebook::add_close_tab_callback(Gtk::Widget *page) +{ + Gtk::Widget *tab = _notebook.get_tab_label(*page); + auto *eventbox = static_cast<Gtk::EventBox *>(tab); + auto *box = static_cast<Gtk::Box *>(*eventbox->get_children().begin()); + auto children = box->get_children(); + auto *close = static_cast<Gtk::Button *>(*children.crbegin()); + + sigc::connection close_connection = close->signal_clicked().connect( + sigc::bind<Gtk::Widget *>(sigc::mem_fun(*this, &DialogNotebook::on_close_button_click_event), page), true); + + sigc::connection tab_connection = tab->signal_button_press_event().connect( + sigc::bind<Gtk::Widget *>(sigc::mem_fun(*this, &DialogNotebook::on_tab_click_event), page), true); + + _tab_connections.insert(std::pair<Gtk::Widget *, sigc::connection>(page, tab_connection)); + _tab_connections.insert(std::pair<Gtk::Widget *, sigc::connection>(page, close_connection)); +} + +/** + * Helper method that removes the close tab signal connection for the page given. + */ +void DialogNotebook::remove_close_tab_callback(Gtk::Widget *page) +{ + auto tab_connection_it = _tab_connections.find(page); + + while (tab_connection_it != _tab_connections.end()) { + (*tab_connection_it).second.disconnect(); + _tab_connections.erase(tab_connection_it); + tab_connection_it = _tab_connections.find(page); + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/dialog-notebook.h b/src/ui/dialog/dialog-notebook.h new file mode 100644 index 0000000..6b0c7e6 --- /dev/null +++ b/src/ui/dialog/dialog-notebook.h @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef INKSCAPE_UI_DIALOG_NOTEBOOK_H +#define INKSCAPE_UI_DIALOG_NOTEBOOK_H + +/** @file + * @brief A wrapper for Gtk::Notebook. + * + * Authors: see git history + * Tavmjong Bah + * + * Copyright (c) 2018 Tavmjong Bah, Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdkmm/dragcontext.h> +#include <gtkmm/menu.h> +#include <gtkmm/notebook.h> +#include <gtkmm/radiomenuitem.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/widget.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +enum class TabsStatus { + NONE, + SINGLE, + ALL +}; + +class DialogContainer; +class DialogWindow; + +/** + * A widget that wraps a Gtk::Notebook with dialogs as pages. + * + * A notebook is fixed to a specific DialogContainer which manages the dialogs inside the notebook. + */ +class DialogNotebook : public Gtk::ScrolledWindow +{ +public: + DialogNotebook(DialogContainer *container); + ~DialogNotebook() override; + + void add_page(Gtk::Widget &page, Gtk::Widget &tab, Glib::ustring label); + void move_page(Gtk::Widget &page); + + // Getters + Gtk::Notebook *get_notebook() { return &_notebook; } + DialogContainer *get_container() { return _container; } + + // Notebook callbacks + void close_tab_callback(); + void close_notebook_callback(); + DialogWindow* pop_tab_callback(); + Gtk::ScrolledWindow * get_current_scrolledwindow(bool skip_scroll_provider); +private: + // Widgets + DialogContainer *_container; + Gtk::Menu _menu; + Gtk::Menu _menutabs; + Gtk::Notebook _notebook; + + // State variables + bool _label_visible; + bool _labels_auto; + bool _labels_off; + bool _labels_set_off = false; + bool _detaching_duplicate; + bool _reload_context = true; + gint _prev_alloc_width = 0; + gint _none_tab_width = 0; + gint _single_tab_width = 0; + gint _icon_width = 0; + TabsStatus tabstatus = TabsStatus::NONE; + TabsStatus prev_tabstatus = TabsStatus::NONE; + Gtk::Widget *_selected_page; + std::vector<sigc::connection> _conn; + std::vector<sigc::connection> _connmenu; + std::multimap<Gtk::Widget *, sigc::connection> _tab_connections; + + static std::list<DialogNotebook *> _instances; + void add_highlight_header(); + void remove_highlight_header(); + + // Signal handlers - notebook + void on_drag_begin(const Glib::RefPtr<Gdk::DragContext> &context) override; + void on_drag_end(const Glib::RefPtr<Gdk::DragContext> &context) override; + void on_page_added(Gtk::Widget *page, int page_num); + void on_page_removed(Gtk::Widget *page, int page_num); + void on_size_allocate_scroll(Gtk::Allocation &allocation); + void on_size_allocate_notebook(Gtk::Allocation &allocation); + bool on_tab_click_event(GdkEventButton *event, Gtk::Widget *page); + void on_close_button_click_event(Gtk::Widget *page); + void on_page_switch(Gtk::Widget *page, guint page_number); + // Helpers + bool provide_scroll(Gtk::Widget &page); + void preventOverflow(); + void change_page(size_t pagenum); + void reload_tab_menu(); + void toggle_tab_labels_callback(bool show); + void add_close_tab_callback(Gtk::Widget *page); + void remove_close_tab_callback(Gtk::Widget *page); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_NOTEBOOK_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/ui/dialog/dialog-window.cpp b/src/ui/dialog/dialog-window.cpp new file mode 100644 index 0000000..f91f399 --- /dev/null +++ b/src/ui/dialog/dialog-window.cpp @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** @file + * @brief A window for floating dialogs. + * + * Authors: see git history + * Tavmjong Bah + * + * Copyright (c) 2018 Tavmjong Bah, Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/dialog/dialog-window.h" + +#include <glibmm/i18n.h> +#include <gtkmm/application.h> +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <iostream> + +#include "enums.h" +#include "inkscape-application.h" +#include "inkscape-window.h" +#include "inkscape.h" +#include "preferences.h" +#include "ui/dialog/dialog-base.h" +#include "ui/dialog/dialog-container.h" +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/dialog-multipaned.h" +#include "ui/dialog/dialog-notebook.h" +#include "ui/shortcuts.h" +#include "ui/util.h" + +// Sizing constants +const int MINIMUM_WINDOW_WIDTH = 210; +const int MINIMUM_WINDOW_HEIGHT = 320; +const int INITIAL_WINDOW_WIDTH = 360; +const int INITIAL_WINDOW_HEIGHT = 520; +const int WINDOW_DROPZONE_SIZE = 10; +const int WINDOW_DROPZONE_SIZE_LARGE = 16; +const int NOTEBOOK_TAB_HEIGHT = 36; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class DialogNotebook; +class DialogContainer; + +DialogWindow::~DialogWindow() {} + +// Create a dialog window and move page from old notebook. +DialogWindow::DialogWindow(InkscapeWindow *inkscape_window, Gtk::Widget *page) + : Gtk::Window() + , _app(InkscapeApplication::instance()) + , _inkscape_window(inkscape_window) + , _title(_("Dialog Window")) +{ + g_assert(_app != nullptr); + g_assert(_inkscape_window != nullptr); + + // ============ Initialization =============== + // Setting the window type + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool window_above = true; + if (prefs) { + window_above = + prefs->getInt("/options/transientpolicy/value", PREFS_DIALOGS_WINDOWS_NORMAL) != PREFS_DIALOGS_WINDOWS_NONE; + } + + set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG); + set_transient_for(*inkscape_window); + + // Add the dialog window to our app + _app->gtk_app()->add_window(*this); + + this->signal_delete_event().connect([=](GdkEventAny *) { + DialogManager::singleton().store_state(*this); + delete this; + return true; + }); + + auto win_action_group = dynamic_cast<Gio::ActionGroup *>(inkscape_window); + if (win_action_group) { + // Must use C API as C++ API takes a RefPtr which we can't get (easily). + gtk_widget_insert_action_group(GTK_WIDGET(this->gobj()), "win", win_action_group->gobj()); + } else { + std::cerr << "DialogWindow::DialogWindow: Can't find InkscapeWindow Gio:ActionGroup!" << std::endl; + } + + insert_action_group("doc", inkscape_window->get_document()->getActionGroup()); + + // ============ Theming: icons ============== + + + // Set the style and icon theme of the new menu based on the desktop + if (auto desktop = SP_ACTIVE_DESKTOP) { + if (Gtk::Window *window = desktop->getToplevel()) { + if (window->get_style_context()->has_class("dark")) { + get_style_context()->add_class("dark"); + get_style_context()->remove_class("bright"); + } else { + get_style_context()->add_class("bright"); + get_style_context()->remove_class("dark"); + } + if (prefs->getBool("/theme/symbolicIcons", false)) { + get_style_context()->add_class("symbolic"); + get_style_context()->remove_class("regular"); + } else { + get_style_context()->add_class("regular"); + get_style_context()->remove_class("symbolic"); + } + } + } + + // ================ Window ================== + set_title(_title); + set_name(_title); + int window_width = INITIAL_WINDOW_WIDTH; + int window_height = INITIAL_WINDOW_HEIGHT; + + // =============== Outer Box ================ + Gtk::Box *box_outer = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + add(*box_outer); + + // =============== Container ================ + _container = Gtk::manage(new DialogContainer(inkscape_window)); + DialogMultipaned *columns = _container->get_columns(); + auto drop_size = Inkscape::Preferences::get()->getBool("/options/dockingzone/value", true) ? WINDOW_DROPZONE_SIZE / 2 : WINDOW_DROPZONE_SIZE; + columns->set_dropzone_sizes(drop_size, drop_size); + box_outer->pack_end(*_container); + + // If there is no page, create an empty Dialogwindow to be populated later + if (page) { + // ============= Initial Column ============= + DialogMultipaned *column = _container->create_column(); + columns->append(column); + + // ============== New Notebook ============== + DialogNotebook *dialog_notebook = Gtk::manage(new DialogNotebook(_container)); + column->append(dialog_notebook); + column->set_dropzone_sizes(drop_size, drop_size); + dialog_notebook->move_page(*page); + + // Set window title + DialogBase *dialog = dynamic_cast<DialogBase *>(page); + if (dialog) { + _title = dialog->get_name(); + set_title(_title); + } + + // Set window size considering what the dialog needs + Gtk::Requisition minimum_size, natural_size; + dialog->get_preferred_size(minimum_size, natural_size); + int overhead = 2 * (drop_size + dialog->property_margin().get_value()); + int width = natural_size.width + overhead; + int height = natural_size.height + overhead + NOTEBOOK_TAB_HEIGHT; + window_width = std::max(width, window_width); + window_height = std::max(height, window_height); + } + + // Set window sizing + set_size_request(MINIMUM_WINDOW_WIDTH, MINIMUM_WINDOW_HEIGHT); + set_default_size(window_width, window_height); + + if (page) { + update_dialogs(); + } + + // window is created hidden; don't show it now, its size needs to be restored +} + +/** + * Change InkscapeWindow that DialogWindow is linked to. + */ +void DialogWindow::set_inkscape_window(InkscapeWindow* inkscape_window) +{ + if (!inkscape_window) { + std::cerr << "DialogWindow::set_inkscape_window: no inkscape_window!" << std::endl; + return; + } + + _inkscape_window = inkscape_window; + update_dialogs(); +} + +/** + * Update all dialogs that are owned by the DialogWindow's _container. + */ +void DialogWindow::update_dialogs() +{ + g_assert(_app != nullptr); + g_assert(_container != nullptr); + g_assert(_inkscape_window != nullptr); + + _container->set_inkscape_window(_inkscape_window); + _container->update_dialogs(); // Updating dialogs is not using the _app reference here. + + // Set window title. + const std::multimap<Glib::ustring, DialogBase *> *dialogs = _container->get_dialogs(); + if (dialogs->size() > 1) { + _title = "Multiple dialogs"; + } else if (dialogs->size() == 1) { + _title = dialogs->begin()->second->get_name(); + } else { + // Should not happen... but does on closing a window! + // std::cerr << "DialogWindow::update_dialogs(): No dialogs!" << std::endl; + _title = ""; + } + + auto document_name = _inkscape_window->get_document()->getDocumentName(); + if (document_name) { + set_title(_title + " - " + Glib::ustring(document_name)); + } +} + +/** + * Update window width and height in order to fit all dialogs inisde its container. + * + * The intended use of this function is at initialization. + */ +void DialogWindow::update_window_size_to_fit_children() +{ + // Declare variables + int pos_x = 0, pos_y = 0; + int width = 0, height = 0; + int overhead = 0, baseline; + Gtk::Allocation allocation; + Gtk::Requisition minimum_size, natural_size; + + // Read needed data + get_position(pos_x, pos_y); + get_allocated_size(allocation, baseline); + const std::multimap<Glib::ustring, DialogBase *> *dialogs = _container->get_dialogs(); + + // Get largest sizes for dialogs + for (auto dialog : *dialogs) { + dialog.second->get_preferred_size(minimum_size, natural_size); + width = std::max(natural_size.width, width); + height = std::max(natural_size.height, height); + overhead = std::max(overhead, dialog.second->property_margin().get_value()); + } + + // Compute sizes including overhead + overhead = 2 * (WINDOW_DROPZONE_SIZE_LARGE + overhead); + width = width + overhead; + height = height + overhead + NOTEBOOK_TAB_HEIGHT; + + // If sizes are lower then current, don't change them + if (allocation.get_width() >= width && allocation.get_height() >= height) { + return; + } + + // Compute largest sizes on both axis + width = std::max(width, allocation.get_width()); + height = std::max(height, allocation.get_height()); + + // Compute new positions to keep window centered + pos_x = pos_x - (width - allocation.get_width()) / 2; + pos_y = pos_y - (height - allocation.get_height()) / 2; + + // Keep window inside the screen + pos_x = std::max(pos_x, 0); + pos_y = std::max(pos_y, 0); + + // Resize window + move(pos_x, pos_y); + resize(width, height); +} + +// mimic InkscapeWindow handling of shortcuts to make them work with active floating dialog window +bool DialogWindow::on_key_press_event(GdkEventKey *key_event) +{ + auto focus = get_focus(); + if (focus) { + if (focus->event(reinterpret_cast<GdkEvent *>(key_event))) { + return true; + } + } + + // Pass key event to this window or to app (via this window). + if (Gtk::Window::on_key_press_event(key_event)) { + return true; + } + + // Pass key event to active InkscapeWindow to handle win (and app) level shortcuts. + if (auto win = _app->get_active_window(); win && win->on_key_press_event(key_event)) { + return true; + } + + return false; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/dialog-window.h b/src/ui/dialog/dialog-window.h new file mode 100644 index 0000000..56d9247 --- /dev/null +++ b/src/ui/dialog/dialog-window.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef INKSCAPE_UI_DIALOG_WINDOW_H +#define INKSCAPE_UI_DIALOG_WINDOW_H + +/** @file + * @brief A window for floating docks. + * + * Authors: see git history + * Tavmjong Bah + * + * Copyright (c) 2018 Tavmjong Bah, Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/refptr.h> +#include <glibmm/ustring.h> +#include <gtkmm/applicationwindow.h> + +#include "inkscape-application.h" + +using Gtk::Label; +using Gtk::Widget; + +class InkscapeWindow; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class DialogContainer; +class DialogMultipaned; + +/** + * DialogWindow holds DialogContainer instances for undocked dialogs. + * + * It watches the last active InkscapeWindow and updates its inner dialogs, if any. + */ +class DialogWindow : public Gtk::Window +{ +public: + DialogWindow(InkscapeWindow* window, Gtk::Widget *page = nullptr); + ~DialogWindow() override; + + void set_inkscape_window(InkscapeWindow *window); + InkscapeWindow* get_inkscape_window() { return _inkscape_window; } + void update_dialogs(); + void update_window_size_to_fit_children(); + + // Getters + DialogContainer *get_container() { return _container; } + +private: + bool on_key_press_event(GdkEventKey* key_event) override; + + InkscapeApplication *_app = nullptr; + InkscapeWindow *_inkscape_window = nullptr; // The Inkscape window that dialog window is attached to, changes when mouse moves into new Inkscape window. + DialogContainer *_container = nullptr; + Glib::ustring _title; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_WINDOW_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/ui/dialog/document-properties.cpp b/src/ui/dialog/document-properties.cpp new file mode 100644 index 0000000..b9cbe86 --- /dev/null +++ b/src/ui/dialog/document-properties.cpp @@ -0,0 +1,1784 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Document properties dialog, Gtkmm-style. + */ +/* Authors: + * bulia byak <buliabyak@users.sf.net> + * Bryce W. Harrington <bryce@bryceharrington.org> + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon Phillips <jon@rejon.org> + * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm) + * Diederik van Lierop <mail@diedenrezi.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006-2008 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2000 - 2008 Authors + * + * 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 <vector> +#include "style.h" +#include "rdf.h" + +#include "actions/actions-tools.h" +#include "display/control/canvas-grid.h" +#include "document-properties.h" +#include "include/gtkmm_version.h" +#include "io/sys.h" +#include "object/sp-root.h" +#include "object/sp-script.h" +#include "object/color-profile.h" +#include "ui/dialog/filedialog.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/shape-editor.h" +#include "ui/widget/entity-entry.h" +#include "ui/widget/notebook-page.h" +#include "xml/node-event-vector.h" + +#include "page-manager.h" +#include "svg/svg-color.h" + +#include "ui/widget/page-properties.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +#define SPACE_SIZE_X 15 +#define SPACE_SIZE_Y 10 + +//=================================================== + +static void on_child_added(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node *ref, void * data); +static void on_child_removed(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node *ref, void * data); +static void on_repr_attr_changed (Inkscape::XML::Node *, gchar const *, gchar const *, gchar const *, bool, gpointer); + +static Inkscape::XML::NodeEventVector const _repr_events = { + on_child_added, // child_added + on_child_removed, // child_removed + on_repr_attr_changed, + nullptr, // content_changed + nullptr // order_changed +}; + +static void docprops_style_button(Gtk::Button& btn, char const* iconName) +{ + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show( child ); + btn.add(*Gtk::manage(Glib::wrap(child))); + btn.set_relief(Gtk::RELIEF_NONE); +} + +DocumentProperties& DocumentProperties::getInstance() +{ + DocumentProperties &instance = *new DocumentProperties(); + instance.init(); + + return instance; +} + +DocumentProperties::DocumentProperties() + : DialogBase("/dialogs/documentoptions", "DocumentProperties") + , _page_page(Gtk::manage(new UI::Widget::NotebookPage(1, 1, false, true))) + , _page_guides(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_cms(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_scripting(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_external_scripts(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_embedded_scripts(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_metadata1(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_metadata2(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + //--------------------------------------------------------------- + //General guide options + , _rcb_sgui(_("Show _guides"), _("Show or hide guides"), "showguides", _wr) + , _rcb_lgui(_("Lock all guides"), _("Toggle lock of all guides in the document"), "inkscape:lockguides", _wr) + , _rcp_gui(_("Guide co_lor:"), _("Guideline color"), _("Color of guidelines"), "guidecolor", "guideopacity", _wr) + , _rcp_hgui(_("_Highlight color:"), _("Highlighted guideline color"), _("Color of a guideline when it is under mouse"), "guidehicolor", "guidehiopacity", _wr) + , _create_guides_btn(_("Create guides around the page")) + , _delete_guides_btn(_("Delete all guides")) + //--------------------------------------------------------------- + , _grids_label_crea("", Gtk::ALIGN_START) + , _grids_button_new(C_("Grid", "_New"), _("Create new grid.")) + , _grids_button_remove(C_("Grid", "_Remove"), _("Remove selected grid.")) + , _grids_label_def("", Gtk::ALIGN_START) + , _grids_vbox(Gtk::ORIENTATION_VERTICAL) + , _grids_hbox_crea(Gtk::ORIENTATION_HORIZONTAL) + , _grids_space(Gtk::ORIENTATION_HORIZONTAL) +{ + set_spacing (0); + pack_start(_notebook, true, true); + + _notebook.append_page(*_page_page, _("Display")); + _notebook.append_page(*_page_guides, _("Guides")); + _notebook.append_page(_grids_vbox, _("Grids")); + _notebook.append_page(*_page_cms, _("Color")); + _notebook.append_page(*_page_scripting, _("Scripting")); + _notebook.append_page(*_page_metadata1, _("Metadata")); + _notebook.append_page(*_page_metadata2, _("License")); + + _wr.setUpdating (true); + build_page(); + build_guides(); + build_gridspage(); + build_cms(); + build_scripting(); + build_metadata(); + _wr.setUpdating (false); + + _grids_button_new.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::onNewGrid)); + _grids_button_remove.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::onRemoveGrid)); +} + +void DocumentProperties::init() +{ + show_all_children(); + _grids_button_remove.hide(); +} + +DocumentProperties::~DocumentProperties() +{ + for (auto & it : _rdflist) + delete it; +} + +//======================================================================== + +/** + * Helper function that sets widgets in a 2 by n table. + * arr has two entries per table row. Each row is in the following form: + * widget, widget -> function adds a widget in each column. + * nullptr, widget -> function adds a widget that occupies the row. + * label, nullptr -> function adds label that occupies the row. + * nullptr, nullptr -> function adds an empty box that occupies the row. + * This used to be a helper function for a 3 by n table + */ +void attach_all(Gtk::Grid &table, Gtk::Widget *const arr[], unsigned const n) +{ + for (unsigned i = 0, r = 0; i < n; i += 2) { + if (arr[i] && arr[i+1]) { + arr[i]->set_hexpand(); + arr[i+1]->set_hexpand(); + arr[i]->set_valign(Gtk::ALIGN_CENTER); + arr[i+1]->set_valign(Gtk::ALIGN_CENTER); + table.attach(*arr[i], 0, r, 1, 1); + table.attach(*arr[i+1], 1, r, 1, 1); + } else { + if (arr[i+1]) { + Gtk::AttachOptions yoptions = (Gtk::AttachOptions)0; + arr[i+1]->set_hexpand(); + + if (yoptions & Gtk::EXPAND) + arr[i+1]->set_vexpand(); + else + arr[i+1]->set_valign(Gtk::ALIGN_CENTER); + + table.attach(*arr[i+1], 0, r, 2, 1); + } else if (arr[i]) { + Gtk::Label& label = reinterpret_cast<Gtk::Label&>(*arr[i]); + + label.set_hexpand(); + label.set_halign(Gtk::ALIGN_START); + label.set_valign(Gtk::ALIGN_CENTER); + table.attach(label, 0, r, 2, 1); + } else { + auto space = Gtk::manage (new Gtk::Box); + space->set_size_request (SPACE_SIZE_X, SPACE_SIZE_Y); + + space->set_halign(Gtk::ALIGN_CENTER); + space->set_valign(Gtk::ALIGN_CENTER); + table.attach(*space, 0, r, 1, 1); + } + } + ++r; + } +} + +void set_namedview_bool(SPDesktop* desktop, const Glib::ustring& operation, SPAttr key, bool on) { + if (!desktop || !desktop->getDocument()) return; + + desktop->getNamedView()->change_bool_setting(key, on); + + desktop->getDocument()->setModifiedSinceSave(); + DocumentUndo::done(desktop->getDocument(), operation, ""); +} + +void set_color(SPDesktop* desktop, Glib::ustring operation, unsigned int rgba, SPAttr color_key, SPAttr opacity_key = SPAttr::INVALID) { + if (!desktop || !desktop->getDocument()) return; + + desktop->getNamedView()->change_color(rgba, color_key, opacity_key); + + desktop->getDocument()->setModifiedSinceSave(); + DocumentUndo::maybeDone(desktop->getDocument(), ("document-color-" + operation).c_str(), operation, ""); +} + +void set_document_dimensions(SPDesktop* desktop, double width, double height, const Inkscape::Util::Unit* unit) { + if (!desktop) return; + + Inkscape::Util::Quantity width_quantity = Inkscape::Util::Quantity(width, unit); + Inkscape::Util::Quantity height_quantity = Inkscape::Util::Quantity(height, unit); + SPDocument* doc = desktop->getDocument(); + Inkscape::Util::Quantity const old_height = doc->getHeight(); + auto rect = Geom::Rect(Geom::Point(0, 0), Geom::Point(width_quantity.value("px"), height_quantity.value("px"))); + doc->fitToRect(rect, false); + + // The origin for the user is in the lower left corner; this point should remain stationary when + // changing the page size. The SVG's origin however is in the upper left corner, so we must compensate for this + if (!doc->is_yaxisdown()) { + Geom::Translate const vert_offset(Geom::Point(0, (old_height.value("px") - height_quantity.value("px")))); + doc->getRoot()->translateChildItems(vert_offset); + } + // units: this is most likely not needed, units are part of document size attributes + // if (unit) { + // set_namedview_value(desktop, "", SPAttr::UNITS) + // write_str_to_xml(desktop, _("Set document unit"), "unit", unit->abbr.c_str()); + // } + doc->setWidthAndHeight(width_quantity, height_quantity, true); + + DocumentUndo::done(doc, _("Set page size"), ""); +} + +void DocumentProperties::set_viewbox_pos(SPDesktop* desktop, double x, double y) { + if (!desktop) return; + + auto document = desktop->getDocument(); + if (!document) return; + + auto box = document->getViewBox(); + document->setViewBox(Geom::Rect::from_xywh(x, y, box.width(), box.height())); + DocumentUndo::done(document, _("Set viewbox position"), ""); + update_scale_ui(desktop); +} + +void DocumentProperties::set_viewbox_size(SPDesktop* desktop, double width, double height) { + if (!desktop) return; + + auto document = desktop->getDocument(); + if (!document) return; + + auto box = document->getViewBox(); + document->setViewBox(Geom::Rect::from_xywh(box.min()[Geom::X], box.min()[Geom::Y], width, height)); + DocumentUndo::done(document, _("Set viewbox size"), ""); + update_scale_ui(desktop); +} + +// helper function to set document scale; uses magnitude of document width/height only, not computed (pixel) values +void set_document_scale_helper(SPDocument& document, double scale) { + if (scale <= 0) return; + + auto root = document.getRoot(); + auto box = document.getViewBox(); + document.setViewBox(Geom::Rect::from_xywh( + box.min()[Geom::X], box.min()[Geom::Y], + root->width.value / scale, root->height.value / scale) + ); +} + +void DocumentProperties::set_document_scale(SPDesktop* desktop, double scale) { + if (!desktop) return; + + auto document = desktop->getDocument(); + if (!document) return; + + if (scale > 0) { + set_document_scale_helper(*document, scale); + update_viewbox_ui(desktop); + update_scale_ui(desktop); + DocumentUndo::done(document, _("Set page scale"), ""); + } +} + +// document scale as a ratio of document size and viewbox size +// as described in Wiki: https://wiki.inkscape.org/wiki/index.php/Units_In_Inkscape +// for example: <svg width="100mm" height="100mm" viewBox="0 0 100 100"> will report 1:1 scale +std::optional<Geom::Scale> get_document_scale_helper(SPDocument& doc) { + auto root = doc.getRoot(); + if (root && + root->width._set && root->width.unit != SVGLength::PERCENT && + root->height._set && root->height.unit != SVGLength::PERCENT) { + if (root->viewBox_set) { + // viewbox and document size present + auto vw = root->viewBox.width(); + auto vh = root->viewBox.height(); + if (vw > 0 && vh > 0) { + return Geom::Scale(root->width.value / vw, root->height.value / vh); + } + } + else { + // no viewbox, use SVG size in pixels + auto w = root->width.computed; + auto h = root->height.computed; + if (w > 0 && h > 0) { + return Geom::Scale(root->width.value / w, root->height.value / h); + } + } + } + + // there is no scale concept applicable in the current state + return std::optional<Geom::Scale>(); +} + +void DocumentProperties::update_scale_ui(SPDesktop* desktop) { + if (!desktop) return; + + auto document = desktop->getDocument(); + if (!document) return; + + using UI::Widget::PageProperties; + if (auto scale = get_document_scale_helper(*document)) { + auto sx = (*scale)[Geom::X]; + auto sy = (*scale)[Geom::Y]; + double eps = 0.0001; // TODO: tweak this value + bool uniform = fabs(sx - sy) < eps; + _page->set_dimension(PageProperties::Dimension::Scale, sx, sx); // only report one, only one "scale" is used + _page->set_check(PageProperties::Check::NonuniformScale, !uniform); + _page->set_check(PageProperties::Check::DisabledScale, false); + } + else { + // no scale + _page->set_dimension(PageProperties::Dimension::Scale, 1, 1); + _page->set_check(PageProperties::Check::NonuniformScale, false); + _page->set_check(PageProperties::Check::DisabledScale, true); + } +} + +void DocumentProperties::update_viewbox_ui(SPDesktop* desktop) { + if (!desktop) return; + + auto document = desktop->getDocument(); + if (!document) return; + + using UI::Widget::PageProperties; + Geom::Rect viewBox = document->getViewBox(); + _page->set_dimension(PageProperties::Dimension::ViewboxPosition, viewBox.min()[Geom::X], viewBox.min()[Geom::Y]); + _page->set_dimension(PageProperties::Dimension::ViewboxSize, viewBox.width(), viewBox.height()); +} + +void DocumentProperties::build_page() +{ + using UI::Widget::PageProperties; + _page = Gtk::manage(PageProperties::create()); + _page_page->table().attach(*_page, 0, 0); + _page_page->show(); + + _page->signal_color_changed().connect([=](unsigned int color, PageProperties::Color element){ + if (_wr.isUpdating() || !_wr.desktop()) return; + + _wr.setUpdating(true); + switch (element) { + case PageProperties::Color::Desk: + set_color(_wr.desktop(), _("Desk color"), color, SPAttr::INKSCAPE_DESK_COLOR); + break; + case PageProperties::Color::Background: + set_color(_wr.desktop(), _("Background color"), color, SPAttr::PAGECOLOR, SPAttr::INKSCAPE_PAGEOPACITY); + break; + case PageProperties::Color::Border: + set_color(_wr.desktop(), _("Border color"), color, SPAttr::BORDERCOLOR, SPAttr::BORDEROPACITY); + break; + } + _wr.setUpdating(false); + }); + + _page->signal_dimmension_changed().connect([=](double x, double y, const Inkscape::Util::Unit* unit, PageProperties::Dimension element){ + if (_wr.isUpdating() || !_wr.desktop()) return; + + _wr.setUpdating(true); + switch (element) { + case PageProperties::Dimension::PageTemplate: + case PageProperties::Dimension::PageSize: + set_document_dimensions(_wr.desktop(), x, y, unit); + update_viewbox(_wr.desktop()); + break; + + case PageProperties::Dimension::ViewboxSize: + set_viewbox_size(_wr.desktop(), x, y); + break; + + case PageProperties::Dimension::ViewboxPosition: + set_viewbox_pos(_wr.desktop(), x, y); + break; + + case PageProperties::Dimension::Scale: + set_document_scale(_wr.desktop(), x); // only uniform scale; there's no 'y' in the dialog + } + _wr.setUpdating(false); + }); + + _page->signal_check_toggled().connect([=](bool checked, PageProperties::Check element){ + if (_wr.isUpdating() || !_wr.desktop()) return; + + _wr.setUpdating(true); + switch (element) { + case PageProperties::Check::Checkerboard: + set_namedview_bool(_wr.desktop(), _("Toggle checkerboard"), SPAttr::INKSCAPE_DESK_CHECKERBOARD, checked); + break; + case PageProperties::Check::Border: + set_namedview_bool(_wr.desktop(), _("Toggle page border"), SPAttr::SHOWBORDER, checked); + break; + case PageProperties::Check::BorderOnTop: + set_namedview_bool(_wr.desktop(), _("Toggle border on top"), SPAttr::BORDERLAYER, checked); + break; + case PageProperties::Check::Shadow: + set_namedview_bool(_wr.desktop(), _("Toggle page shadow"), SPAttr::SHOWPAGESHADOW, checked); + break; + case PageProperties::Check::AntiAlias: + set_namedview_bool(_wr.desktop(), _("Toggle anti-aliasing"), SPAttr::SHAPE_RENDERING, checked); + break; + } + _wr.setUpdating(false); + }); + + _page->signal_unit_changed().connect([=](const Inkscape::Util::Unit* unit, PageProperties::Units element){ + if (_wr.isUpdating() || !_wr.desktop()) return; + + if (element == PageProperties::Units::Display) { + // display only units + display_unit_change(unit); + } + else if (element == PageProperties::Units::Document) { + // not used, fired with page size + } + }); + + _page->signal_resize_to_fit().connect([=](){ + if (_wr.isUpdating() || !_wr.desktop()) return; + + if (auto document = getDocument()) { + auto &page_manager = document->getPageManager(); + page_manager.selectPage(0); + // fit page to selection or content, if there's no selection + page_manager.fitToSelection(_wr.desktop()->getSelection()); + DocumentUndo::done(document, _("Resize page to fit"), INKSCAPE_ICON("tool-pages")); + update_widgets(); + } + }); +} + +void DocumentProperties::build_guides() +{ + _page_guides->show(); + + Gtk::Label *label_gui = Gtk::manage (new Gtk::Label); + label_gui->set_markup (_("<b>Guides</b>")); + + _rcp_gui.set_margin_start(0); + _rcp_hgui.set_margin_start(0); + _rcp_gui.set_hexpand(); + _rcp_hgui.set_hexpand(); + _rcb_sgui.set_hexpand(); + auto inner = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); + inner->add(_rcb_sgui); + inner->add(_rcb_lgui); + inner->add(_rcp_gui); + inner->add(_rcp_hgui); + auto spacer = Gtk::manage(new Gtk::Label()); + Gtk::Widget *const widget_array[] = + { + label_gui, nullptr, + inner, spacer, + nullptr, nullptr, + nullptr, &_create_guides_btn, + nullptr, &_delete_guides_btn + }; + attach_all(_page_guides->table(), widget_array, G_N_ELEMENTS(widget_array)); + inner->set_hexpand(false); + + // Must use C API until GTK4. + gtk_actionable_set_action_name(GTK_ACTIONABLE(_create_guides_btn.gobj()), "doc.create-guides-around-page"); + gtk_actionable_set_action_name(GTK_ACTIONABLE(_delete_guides_btn.gobj()), "doc.delete-all-guides"); +} + +/// Populates the available color profiles combo box +void DocumentProperties::populate_available_profiles(){ + _AvailableProfilesListStore->clear(); // Clear any existing items in the combo box + + // Iterate through the list of profiles and add the name to the combo box. + bool home = true; // initial value doesn't matter, it's just to avoid a compiler warning + bool first = true; + for (auto &profile: ColorProfile::getProfileFilesWithNames()) { + Gtk::TreeModel::Row row; + + // add a separator between profiles from the user's home directory and system profiles + if (!first && profile.isInHome != home) + { + row = *(_AvailableProfilesListStore->append()); + row[_AvailableProfilesListColumns.fileColumn] = "<separator>"; + row[_AvailableProfilesListColumns.nameColumn] = "<separator>"; + row[_AvailableProfilesListColumns.separatorColumn] = true; + } + home = profile.isInHome; + first = false; + + row = *(_AvailableProfilesListStore->append()); + row[_AvailableProfilesListColumns.fileColumn] = profile.filename; + row[_AvailableProfilesListColumns.nameColumn] = profile.name; + row[_AvailableProfilesListColumns.separatorColumn] = false; + } +} + +/** + * Cleans up name to remove disallowed characters. + * Some discussion at http://markmail.org/message/bhfvdfptt25kgtmj + * Allowed ASCII first characters: ':', 'A'-'Z', '_', 'a'-'z' + * Allowed ASCII remaining chars add: '-', '.', '0'-'9', + * + * @param str the string to clean up. + */ +static void sanitizeName( Glib::ustring& str ) +{ + if (str.size() > 0) { + char val = str.at(0); + if (((val < 'A') || (val > 'Z')) + && ((val < 'a') || (val > 'z')) + && (val != '_') + && (val != ':')) { + str.insert(0, "_"); + } + for (Glib::ustring::size_type i = 1; i < str.size(); i++) { + char val = str.at(i); + if (((val < 'A') || (val > 'Z')) + && ((val < 'a') || (val > 'z')) + && ((val < '0') || (val > '9')) + && (val != '_') + && (val != ':') + && (val != '-') + && (val != '.')) { + str.replace(i, 1, "-"); + } + } + } +} + +/// Links the selected color profile in the combo box to the document +void DocumentProperties::linkSelectedProfile() +{ + //store this profile in the SVG document (create <color-profile> element in the XML) + if (auto document = getDocument()){ + // Find the index of the currently-selected row in the color profiles combobox + Gtk::TreeModel::iterator iter = _AvailableProfilesList.get_active(); + if (!iter) + return; + + // Read the filename and description from the list of available profiles + Glib::ustring file = (*iter)[_AvailableProfilesListColumns.fileColumn]; + Glib::ustring name = (*iter)[_AvailableProfilesListColumns.nameColumn]; + + std::vector<SPObject *> current = document->getResourceList( "iccprofile" ); + for (auto obj : current) { + Inkscape::ColorProfile* prof = reinterpret_cast<Inkscape::ColorProfile*>(obj); + if (!strcmp(prof->href, file.c_str())) + return; + } + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *cprofRepr = xml_doc->createElement("svg:color-profile"); + gchar* tmp = g_strdup(name.c_str()); + Glib::ustring nameStr = tmp ? tmp : "profile"; // TODO add some auto-numbering to avoid collisions + sanitizeName(nameStr); + cprofRepr->setAttribute("name", nameStr); + cprofRepr->setAttribute("xlink:href", Glib::filename_to_uri(Glib::filename_from_utf8(file))); + cprofRepr->setAttribute("id", file); + + + // Checks whether there is a defs element. Creates it when needed + Inkscape::XML::Node *defsRepr = sp_repr_lookup_name(xml_doc, "svg:defs"); + if (!defsRepr) { + defsRepr = xml_doc->createElement("svg:defs"); + xml_doc->root()->addChild(defsRepr, nullptr); + } + + g_assert(document->getDefs()); + defsRepr->addChild(cprofRepr, nullptr); + + // TODO check if this next line was sometimes needed. It being there caused an assertion. + //Inkscape::GC::release(defsRepr); + + // inform the document, so we can undo + DocumentUndo::done(document, _("Link Color Profile"), ""); + + populate_linked_profiles_box(); + } +} + +struct _cmp { + bool operator()(const SPObject * const & a, const SPObject * const & b) + { + const Inkscape::ColorProfile &a_prof = reinterpret_cast<const Inkscape::ColorProfile &>(*a); + const Inkscape::ColorProfile &b_prof = reinterpret_cast<const Inkscape::ColorProfile &>(*b); + gchar *a_name_casefold = g_utf8_casefold(a_prof.name, -1 ); + gchar *b_name_casefold = g_utf8_casefold(b_prof.name, -1 ); + int result = g_strcmp0(a_name_casefold, b_name_casefold); + g_free(a_name_casefold); + g_free(b_name_casefold); + return result < 0; + } +}; + +template <typename From, typename To> +struct static_caster { To * operator () (From * value) const { return static_cast<To *>(value); } }; + +void DocumentProperties::populate_linked_profiles_box() +{ + _LinkedProfilesListStore->clear(); + if (auto document = getDocument()) { + std::vector<SPObject *> current = document->getResourceList( "iccprofile" ); + if (! current.empty()) { + _emb_profiles_observer.set((*(current.begin()))->parent); + } + + std::set<Inkscape::ColorProfile *> _current; + std::transform(current.begin(), + current.end(), + std::inserter(_current, _current.begin()), + static_caster<SPObject, Inkscape::ColorProfile>()); + + for (auto &profile: _current) { + Gtk::TreeModel::Row row = *(_LinkedProfilesListStore->append()); + row[_LinkedProfilesListColumns.nameColumn] = profile->name; + // row[_LinkedProfilesListColumns.previewColumn] = "Color Preview"; + } + } +} + +void DocumentProperties::external_scripts_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _ExternalScriptsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void DocumentProperties::embedded_scripts_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _EmbeddedScriptsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void DocumentProperties::linked_profiles_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _EmbProfContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void DocumentProperties::cms_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem) +{ + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _EmbProfContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _EmbProfContextMenu.accelerate(parent); +} + + +void DocumentProperties::external_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem) +{ + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _ExternalScriptsContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _ExternalScriptsContextMenu.accelerate(parent); +} + +void DocumentProperties::embedded_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem) +{ + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _EmbeddedScriptsContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _EmbeddedScriptsContextMenu.accelerate(parent); +} + +void DocumentProperties::onColorProfileSelectRow() +{ + Glib::RefPtr<Gtk::TreeSelection> sel = _LinkedProfilesList.get_selection(); + if (sel) { + _unlink_btn.set_sensitive(sel->count_selected_rows () > 0); + } +} + + +void DocumentProperties::removeSelectedProfile(){ + Glib::ustring name; + if(_LinkedProfilesList.get_selection()) { + Gtk::TreeModel::iterator i = _LinkedProfilesList.get_selection()->get_selected(); + + if(i){ + name = (*i)[_LinkedProfilesListColumns.nameColumn]; + } else { + return; + } + } + if (auto document = getDocument()) { + std::vector<SPObject *> current = document->getResourceList( "iccprofile" ); + for (auto obj : current) { + Inkscape::ColorProfile* prof = reinterpret_cast<Inkscape::ColorProfile*>(obj); + if (!name.compare(prof->name)){ + prof->deleteObject(true, false); + DocumentUndo::done(document, _("Remove linked color profile"), ""); + break; // removing the color profile likely invalidates part of the traversed list, stop traversing here. + } + } + } + + populate_linked_profiles_box(); + onColorProfileSelectRow(); +} + +bool DocumentProperties::_AvailableProfilesList_separator(const Glib::RefPtr<Gtk::TreeModel>& model, const Gtk::TreeModel::iterator& iter) +{ + bool separator = (*iter)[_AvailableProfilesListColumns.separatorColumn]; + return separator; +} + +void DocumentProperties::build_cms() +{ + _page_cms->show(); + Gtk::Label *label_link= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_link->set_markup (_("<b>Linked Color Profiles:</b>")); + Gtk::Label *label_avail = Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_avail->set_markup (_("<b>Available Color Profiles:</b>")); + + _unlink_btn.set_tooltip_text(_("Unlink Profile")); + docprops_style_button(_unlink_btn, INKSCAPE_ICON("list-remove")); + + gint row = 0; + + label_link->set_hexpand(); + label_link->set_halign(Gtk::ALIGN_START); + label_link->set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(*label_link, 0, row, 3, 1); + + row++; + + _LinkedProfilesListScroller.set_hexpand(); + _LinkedProfilesListScroller.set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(_LinkedProfilesListScroller, 0, row, 3, 1); + + row++; + + Gtk::Box* spacer = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + spacer->set_size_request(SPACE_SIZE_X, SPACE_SIZE_Y); + + spacer->set_hexpand(); + spacer->set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(*spacer, 0, row, 3, 1); + + row++; + + label_avail->set_hexpand(); + label_avail->set_halign(Gtk::ALIGN_START); + label_avail->set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(*label_avail, 0, row, 3, 1); + + row++; + + _AvailableProfilesList.set_hexpand(); + _AvailableProfilesList.set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(_AvailableProfilesList, 0, row, 1, 1); + + _unlink_btn.set_halign(Gtk::ALIGN_CENTER); + _unlink_btn.set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(_unlink_btn, 2, row, 1, 1); + + // Set up the Available Profiles combo box + _AvailableProfilesListStore = Gtk::ListStore::create(_AvailableProfilesListColumns); + _AvailableProfilesList.set_model(_AvailableProfilesListStore); + _AvailableProfilesList.pack_start(_AvailableProfilesListColumns.nameColumn); + _AvailableProfilesList.set_row_separator_func(sigc::mem_fun(*this, &DocumentProperties::_AvailableProfilesList_separator)); + _AvailableProfilesList.signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::linkSelectedProfile) ); + + populate_available_profiles(); + + //# Set up the Linked Profiles combo box + _LinkedProfilesListStore = Gtk::ListStore::create(_LinkedProfilesListColumns); + _LinkedProfilesList.set_model(_LinkedProfilesListStore); + _LinkedProfilesList.append_column(_("Profile Name"), _LinkedProfilesListColumns.nameColumn); +// _LinkedProfilesList.append_column(_("Color Preview"), _LinkedProfilesListColumns.previewColumn); + _LinkedProfilesList.set_headers_visible(false); +// TODO restore? _LinkedProfilesList.set_fixed_height_mode(true); + + populate_linked_profiles_box(); + + _LinkedProfilesListScroller.add(_LinkedProfilesList); + _LinkedProfilesListScroller.set_shadow_type(Gtk::SHADOW_IN); + _LinkedProfilesListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _LinkedProfilesListScroller.set_size_request(-1, 90); + + _unlink_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeSelectedProfile)); + + _LinkedProfilesList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onColorProfileSelectRow) ); + + _LinkedProfilesList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &DocumentProperties::linked_profiles_list_button_release)); + cms_create_popup_menu(_LinkedProfilesList, sigc::mem_fun(*this, &DocumentProperties::removeSelectedProfile)); + + if (auto document = getDocument()) { + std::vector<SPObject *> current = document->getResourceList( "defs" ); + if (!current.empty()) { + _emb_profiles_observer.set((*(current.begin()))->parent); + } + _emb_profiles_observer.signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::populate_linked_profiles_box)); + onColorProfileSelectRow(); + } +} + +void DocumentProperties::build_scripting() +{ + _page_scripting->show(); + + _page_scripting->table().attach(_scripting_notebook, 0, 0, 1, 1); + + _scripting_notebook.append_page(*_page_external_scripts, _("External scripts")); + _scripting_notebook.append_page(*_page_embedded_scripts, _("Embedded scripts")); + + //# External scripts tab + _page_external_scripts->show(); + Gtk::Label *label_external= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_external->set_markup (_("<b>External script files:</b>")); + + _external_add_btn.set_tooltip_text(_("Add the current file name or browse for a file")); + docprops_style_button(_external_add_btn, INKSCAPE_ICON("list-add")); + + _external_remove_btn.set_tooltip_text(_("Remove")); + docprops_style_button(_external_remove_btn, INKSCAPE_ICON("list-remove")); + + gint row = 0; + + label_external->set_hexpand(); + label_external->set_halign(Gtk::ALIGN_START); + label_external->set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(*label_external, 0, row, 3, 1); + + row++; + + _ExternalScriptsListScroller.set_hexpand(); + _ExternalScriptsListScroller.set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(_ExternalScriptsListScroller, 0, row, 3, 1); + + row++; + + Gtk::Box* spacer_external = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + spacer_external->set_size_request(SPACE_SIZE_X, SPACE_SIZE_Y); + + spacer_external->set_hexpand(); + spacer_external->set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(*spacer_external, 0, row, 3, 1); + + row++; + + _script_entry.set_hexpand(); + _script_entry.set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(_script_entry, 0, row, 1, 1); + + _external_add_btn.set_halign(Gtk::ALIGN_CENTER); + _external_add_btn.set_valign(Gtk::ALIGN_CENTER); + _external_add_btn.set_margin_start(2); + _external_add_btn.set_margin_end(2); + + _page_external_scripts->table().attach(_external_add_btn, 1, row, 1, 1); + + _external_remove_btn.set_halign(Gtk::ALIGN_CENTER); + _external_remove_btn.set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(_external_remove_btn, 2, row, 1, 1); + + //# Set up the External Scripts box + _ExternalScriptsListStore = Gtk::ListStore::create(_ExternalScriptsListColumns); + _ExternalScriptsList.set_model(_ExternalScriptsListStore); + _ExternalScriptsList.append_column(_("Filename"), _ExternalScriptsListColumns.filenameColumn); + _ExternalScriptsList.set_headers_visible(true); +// TODO restore? _ExternalScriptsList.set_fixed_height_mode(true); + + + //# Embedded scripts tab + _page_embedded_scripts->show(); + Gtk::Label *label_embedded= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_embedded->set_markup (_("<b>Embedded script files:</b>")); + + _embed_new_btn.set_tooltip_text(_("New")); + docprops_style_button(_embed_new_btn, INKSCAPE_ICON("list-add")); + + _embed_remove_btn.set_tooltip_text(_("Remove")); + docprops_style_button(_embed_remove_btn, INKSCAPE_ICON("list-remove")); + + _embed_button_box.set_layout (Gtk::BUTTONBOX_START); + _embed_button_box.add(_embed_new_btn); + _embed_button_box.add(_embed_remove_btn); + + row = 0; + + label_embedded->set_hexpand(); + label_embedded->set_halign(Gtk::ALIGN_START); + label_embedded->set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(*label_embedded, 0, row, 3, 1); + + row++; + + _EmbeddedScriptsListScroller.set_hexpand(); + _EmbeddedScriptsListScroller.set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(_EmbeddedScriptsListScroller, 0, row, 3, 1); + + row++; + + _embed_button_box.set_hexpand(); + _embed_button_box.set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(_embed_button_box, 0, row, 1, 1); + + row++; + + Gtk::Box* spacer_embedded = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + spacer_embedded->set_size_request(SPACE_SIZE_X, SPACE_SIZE_Y); + spacer_embedded->set_hexpand(); + spacer_embedded->set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(*spacer_embedded, 0, row, 3, 1); + + row++; + + //# Set up the Embedded Scripts box + _EmbeddedScriptsListStore = Gtk::ListStore::create(_EmbeddedScriptsListColumns); + _EmbeddedScriptsList.set_model(_EmbeddedScriptsListStore); + _EmbeddedScriptsList.append_column(_("Script ID"), _EmbeddedScriptsListColumns.idColumn); + _EmbeddedScriptsList.set_headers_visible(true); +// TODO restore? _EmbeddedScriptsList.set_fixed_height_mode(true); + + //# Set up the Embedded Scripts content box + Gtk::Label *label_embedded_content= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_embedded_content->set_markup (_("<b>Content:</b>")); + + label_embedded_content->set_hexpand(); + label_embedded_content->set_halign(Gtk::ALIGN_START); + label_embedded_content->set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(*label_embedded_content, 0, row, 3, 1); + + row++; + + _EmbeddedContentScroller.set_hexpand(); + _EmbeddedContentScroller.set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(_EmbeddedContentScroller, 0, row, 3, 1); + + _EmbeddedContentScroller.add(_EmbeddedContent); + _EmbeddedContentScroller.set_shadow_type(Gtk::SHADOW_IN); + _EmbeddedContentScroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _EmbeddedContentScroller.set_size_request(-1, 140); + + _EmbeddedScriptsList.signal_cursor_changed().connect(sigc::mem_fun(*this, &DocumentProperties::changeEmbeddedScript)); + _EmbeddedScriptsList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onEmbeddedScriptSelectRow) ); + + _ExternalScriptsList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onExternalScriptSelectRow) ); + + _EmbeddedContent.get_buffer()->signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::editEmbeddedScript)); + + populate_script_lists(); + + _ExternalScriptsListScroller.add(_ExternalScriptsList); + _ExternalScriptsListScroller.set_shadow_type(Gtk::SHADOW_IN); + _ExternalScriptsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _ExternalScriptsListScroller.set_size_request(-1, 90); + + _external_add_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::addExternalScript)); + + _EmbeddedScriptsListScroller.add(_EmbeddedScriptsList); + _EmbeddedScriptsListScroller.set_shadow_type(Gtk::SHADOW_IN); + _EmbeddedScriptsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _EmbeddedScriptsListScroller.set_size_request(-1, 90); + + _embed_new_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::addEmbeddedScript)); + + _external_remove_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeExternalScript)); + _embed_remove_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeEmbeddedScript)); + + _ExternalScriptsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &DocumentProperties::external_scripts_list_button_release)); + external_create_popup_menu(_ExternalScriptsList, sigc::mem_fun(*this, &DocumentProperties::removeExternalScript)); + + _EmbeddedScriptsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &DocumentProperties::embedded_scripts_list_button_release)); + embedded_create_popup_menu(_EmbeddedScriptsList, sigc::mem_fun(*this, &DocumentProperties::removeEmbeddedScript)); + +//TODO: review this observers code: + if (auto document = getDocument()) { + std::vector<SPObject *> current = document->getResourceList( "script" ); + if (! current.empty()) { + _scripts_observer.set((*(current.begin()))->parent); + } + _scripts_observer.signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::populate_script_lists)); + onEmbeddedScriptSelectRow(); + onExternalScriptSelectRow(); + } +} + +void DocumentProperties::build_metadata() +{ + using Inkscape::UI::Widget::EntityEntry; + + _page_metadata1->show(); + + Gtk::Label *label = Gtk::manage (new Gtk::Label); + label->set_markup (_("<b>Dublin Core Entities</b>")); + label->set_halign(Gtk::ALIGN_START); + label->set_valign(Gtk::ALIGN_CENTER); + _page_metadata1->table().attach (*label, 0,0,2,1); + + /* add generic metadata entry areas */ + struct rdf_work_entity_t * entity; + int row = 1; + for (entity = rdf_work_entities; entity && entity->name; entity++, row++) { + if ( entity->editable == RDF_EDIT_GENERIC ) { + EntityEntry *w = EntityEntry::create (entity, _wr); + _rdflist.push_back (w); + + w->_label.set_halign(Gtk::ALIGN_START); + w->_label.set_valign(Gtk::ALIGN_CENTER); + _page_metadata1->table().attach(w->_label, 0, row, 1, 1); + + w->_packable->set_hexpand(); + w->_packable->set_valign(Gtk::ALIGN_CENTER); + _page_metadata1->table().attach(*w->_packable, 1, row, 1, 1); + } + } + + Gtk::Button *button_save = Gtk::manage (new Gtk::Button(_("_Save as default"),true)); + button_save->set_tooltip_text(_("Save this metadata as the default metadata")); + Gtk::Button *button_load = Gtk::manage (new Gtk::Button(_("Use _default"),true)); + button_load->set_tooltip_text(_("Use the previously saved default metadata here")); + + auto box_buttons = Gtk::manage (new Gtk::ButtonBox); + + box_buttons->set_layout(Gtk::BUTTONBOX_END); + box_buttons->set_spacing(4); + box_buttons->pack_start(*button_save, true, true, 6); + box_buttons->pack_start(*button_load, true, true, 6); + _page_metadata1->pack_end(*box_buttons, false, false, 0); + + button_save->signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::save_default_metadata)); + button_load->signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::load_default_metadata)); + + _page_metadata2->show(); + + row = 0; + Gtk::Label *llabel = Gtk::manage (new Gtk::Label); + llabel->set_markup (_("<b>License</b>")); + llabel->set_halign(Gtk::ALIGN_START); + llabel->set_valign(Gtk::ALIGN_CENTER); + _page_metadata2->table().attach(*llabel, 0, row, 2, 1); + + /* add license selector pull-down and URI */ + ++row; + _licensor.init (_wr); + + _licensor.set_hexpand(); + _licensor.set_valign(Gtk::ALIGN_CENTER); + _page_metadata2->table().attach(_licensor, 0, row, 2, 1); +} + +void DocumentProperties::addExternalScript(){ + + auto document = getDocument(); + if (!document) + return; + + if (_script_entry.get_text().empty() ) { + // Click Add button with no filename, show a Browse dialog + browseExternalScript(); + } + + if (!_script_entry.get_text().empty()) { + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *scriptRepr = xml_doc->createElement("svg:script"); + scriptRepr->setAttributeOrRemoveIfEmpty("xlink:href", _script_entry.get_text()); + _script_entry.set_text(""); + + xml_doc->root()->addChild(scriptRepr, nullptr); + + // inform the document, so we can undo + DocumentUndo::done(document, _("Add external script..."), ""); + + populate_script_lists(); + } +} + +static Inkscape::UI::Dialog::FileOpenDialog * selectPrefsFileInstance = nullptr; + +void DocumentProperties::browseExternalScript() { + + //# Get the current directory for finding files + static Glib::ustring open_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + + Glib::ustring attr = prefs->getString(_prefs_path); + if (!attr.empty()) open_path = attr; + + //# Test if the open_path directory exists + if (!Inkscape::IO::file_test(open_path.c_str(), + (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) + open_path = ""; + + //# If no open path, default to our home directory + if (open_path.empty()) { + open_path = g_get_home_dir(); + open_path.append(G_DIR_SEPARATOR_S); + } + + //# Create a dialog + SPDesktop *desktop = getDesktop(); + if (desktop && !selectPrefsFileInstance) { + selectPrefsFileInstance = + Inkscape::UI::Dialog::FileOpenDialog::create( + *desktop->getToplevel(), + open_path, + Inkscape::UI::Dialog::CUSTOM_TYPE, + _("Select a script to load")); + selectPrefsFileInstance->addFilterMenu("Javascript Files", "*.js"); + } + + //# Show the dialog + bool const success = selectPrefsFileInstance->show(); + + if (!success) { + return; + } + + //# User selected something. Get name and type + Glib::ustring fileName = selectPrefsFileInstance->getFilename(); + + _script_entry.set_text(fileName); +} + +void DocumentProperties::addEmbeddedScript(){ + if(auto document = getDocument()) { + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *scriptRepr = xml_doc->createElement("svg:script"); + + xml_doc->root()->addChild(scriptRepr, nullptr); + + // inform the document, so we can undo + DocumentUndo::done(document, _("Add embedded script..."), ""); + populate_script_lists(); + } +} + +void DocumentProperties::removeExternalScript(){ + Glib::ustring name; + if(_ExternalScriptsList.get_selection()) { + Gtk::TreeModel::iterator i = _ExternalScriptsList.get_selection()->get_selected(); + + if(i){ + name = (*i)[_ExternalScriptsListColumns.filenameColumn]; + } else { + return; + } + } + + auto document = getDocument(); + if (!document) + return; + std::vector<SPObject *> current = document->getResourceList( "script" ); + for (auto obj : current) { + if (obj) { + SPScript* script = dynamic_cast<SPScript *>(obj); + if (script && (name == script->xlinkhref)) { + + //XML Tree being used directly here while it shouldn't be. + Inkscape::XML::Node *repr = obj->getRepr(); + if (repr){ + sp_repr_unparent(repr); + + // inform the document, so we can undo + DocumentUndo::done(document, _("Remove external script"), ""); + } + } + } + } + + populate_script_lists(); +} + +void DocumentProperties::removeEmbeddedScript(){ + Glib::ustring id; + if(_EmbeddedScriptsList.get_selection()) { + Gtk::TreeModel::iterator i = _EmbeddedScriptsList.get_selection()->get_selected(); + + if(i){ + id = (*i)[_EmbeddedScriptsListColumns.idColumn]; + } else { + return; + } + } + + if (auto document = getDocument()) { + if (auto obj = document->getObjectById(id)) { + //XML Tree being used directly here while it shouldn't be. + if (auto repr = obj->getRepr()){ + sp_repr_unparent(repr); + + // inform the document, so we can undo + DocumentUndo::done(document, _("Remove embedded script"), ""); + } + } + } + + populate_script_lists(); +} + +void DocumentProperties::onExternalScriptSelectRow() +{ + Glib::RefPtr<Gtk::TreeSelection> sel = _ExternalScriptsList.get_selection(); + if (sel) { + _external_remove_btn.set_sensitive(sel->count_selected_rows () > 0); + } +} + +void DocumentProperties::onEmbeddedScriptSelectRow() +{ + Glib::RefPtr<Gtk::TreeSelection> sel = _EmbeddedScriptsList.get_selection(); + if (sel) { + _embed_remove_btn.set_sensitive(sel->count_selected_rows () > 0); + } +} + +void DocumentProperties::changeEmbeddedScript(){ + Glib::ustring id; + if(_EmbeddedScriptsList.get_selection()) { + Gtk::TreeModel::iterator i = _EmbeddedScriptsList.get_selection()->get_selected(); + + if(i){ + id = (*i)[_EmbeddedScriptsListColumns.idColumn]; + } else { + return; + } + } + + auto document = getDocument(); + if (!document) + return; + + bool voidscript=true; + std::vector<SPObject *> current = document->getResourceList( "script" ); + for (auto obj : current) { + if (id == obj->getId()){ + int count = (int) obj->children.size(); + + if (count>1) + g_warning("TODO: Found a script element with multiple (%d) child nodes! We must implement support for that!", count); + + //XML Tree being used directly here while it shouldn't be. + SPObject* child = obj->firstChild(); + //TODO: shouldn't we get all children instead of simply the first child? + + if (child && child->getRepr()){ + const gchar* content = child->getRepr()->content(); + if (content){ + voidscript=false; + _EmbeddedContent.get_buffer()->set_text(content); + } + } + } + } + + if (voidscript) + _EmbeddedContent.get_buffer()->set_text(""); +} + +void DocumentProperties::editEmbeddedScript(){ + Glib::ustring id; + if(_EmbeddedScriptsList.get_selection()) { + Gtk::TreeModel::iterator i = _EmbeddedScriptsList.get_selection()->get_selected(); + + if(i){ + id = (*i)[_EmbeddedScriptsListColumns.idColumn]; + } else { + return; + } + } + + auto document = getDocument(); + if (!document) + return; + + for (auto obj : document->getResourceList("script")) { + if (id == obj->getId()) { + //XML Tree being used directly here while it shouldn't be. + Inkscape::XML::Node *repr = obj->getRepr(); + if (repr){ + auto tmp = obj->children | boost::adaptors::transformed([](SPObject& o) { return &o; }); + std::vector<SPObject*> vec(tmp.begin(), tmp.end()); + for (auto &child: vec) { + child->deleteObject(); + } + obj->appendChildRepr(document->getReprDoc()->createTextNode(_EmbeddedContent.get_buffer()->get_text().c_str())); + + //TODO repr->set_content(_EmbeddedContent.get_buffer()->get_text()); + + // inform the document, so we can undo + DocumentUndo::done(document, _("Edit embedded script"), ""); + } + } + } +} + +void DocumentProperties::populate_script_lists(){ + _ExternalScriptsListStore->clear(); + _EmbeddedScriptsListStore->clear(); + auto document = getDocument(); + if (!document) + return; + + std::vector<SPObject *> current = getDocument()->getResourceList( "script" ); + if (!current.empty()) { + SPObject *obj = *(current.begin()); + g_assert(obj != nullptr); + _scripts_observer.set(obj->parent); + } + for (auto obj : current) { + SPScript* script = dynamic_cast<SPScript *>(obj); + g_assert(script != nullptr); + if (script->xlinkhref) + { + Gtk::TreeModel::Row row = *(_ExternalScriptsListStore->append()); + row[_ExternalScriptsListColumns.filenameColumn] = script->xlinkhref; + } + else // Embedded scripts + { + Gtk::TreeModel::Row row = *(_EmbeddedScriptsListStore->append()); + row[_EmbeddedScriptsListColumns.idColumn] = obj->getId(); + } + } +} + +/** +* Called for _updating_ the dialog. DO NOT call this a lot. It's expensive! +*/ +void DocumentProperties::update_gridspage() +{ + SPNamedView *nv = getDesktop()->getNamedView(); + + int prev_page_count = _grids_notebook.get_n_pages(); + int prev_page_pos = _grids_notebook.get_current_page(); + + //remove all tabs + while (_grids_notebook.get_n_pages() != 0) { + _grids_notebook.remove_page(-1); // this also deletes the page. + } + + //add tabs + for(auto grid : nv->grids) { + if (!grid->repr->attribute("id")) continue; // update_gridspage is called again when "id" is added + Glib::ustring name(grid->repr->attribute("id")); + const char *icon = nullptr; + switch (grid->getGridType()) { + case GRID_RECTANGULAR: + icon = "grid-rectangular"; + break; + case GRID_AXONOMETRIC: + icon = "grid-axonometric"; + break; + default: + break; + } + _grids_notebook.append_page(*grid->newWidget(), _createPageTabLabel(name, icon)); + } + _grids_notebook.show_all(); + + int cur_page_count = _grids_notebook.get_n_pages(); + if (cur_page_count > 0) { + _grids_button_remove.set_sensitive(true); + + // The following is not correct if grid added/removed via XML + if (cur_page_count == prev_page_count + 1) { + _grids_notebook.set_current_page(cur_page_count - 1); + } else if (cur_page_count == prev_page_count) { + _grids_notebook.set_current_page(prev_page_pos); + } else if (cur_page_count == prev_page_count - 1) { + _grids_notebook.set_current_page(prev_page_pos < 1 ? 0 : prev_page_pos - 1); + } + } else { + _grids_button_remove.set_sensitive(false); + } +} + +/** + * Build grid page of dialog. + */ +void DocumentProperties::build_gridspage() +{ + /// \todo FIXME: gray out snapping when grid is off. + /// Dissenting view: you want snapping without grid. + + _grids_label_crea.set_markup(_("<b>Creation</b>")); + _grids_label_def.set_markup(_("<b>Defined grids</b>")); + _grids_hbox_crea.pack_start(_grids_combo_gridtype, true, true); + _grids_hbox_crea.pack_start(_grids_button_new, true, true); + + for (gint t = 0; t <= GRID_MAXTYPENR; t++) { + _grids_combo_gridtype.append( CanvasGrid::getName( (GridType) t ) ); + } + _grids_combo_gridtype.set_active_text( CanvasGrid::getName(GRID_RECTANGULAR) ); + + _grids_space.set_size_request (SPACE_SIZE_X, SPACE_SIZE_Y); + + _grids_vbox.set_name("NotebookPage"); + _grids_vbox.set_border_width(4); + _grids_vbox.set_spacing(4); + _grids_vbox.pack_start(_grids_label_crea, false, false); + _grids_vbox.pack_start(_grids_hbox_crea, false, false); + _grids_vbox.pack_start(_grids_space, false, false); + _grids_vbox.pack_start(_grids_label_def, false, false); + _grids_vbox.pack_start(_grids_notebook, false, false); + _grids_vbox.pack_start(_grids_button_remove, false, false); +} + + +void DocumentProperties::update_viewbox(SPDesktop* desktop) { + if (!desktop) return; + + auto* document = desktop->getDocument(); + if (!document) return; + + using UI::Widget::PageProperties; + SPRoot* root = document->getRoot(); + if (root->viewBox_set) { + auto& vb = root->viewBox; + _page->set_dimension(PageProperties::Dimension::ViewboxPosition, vb.min()[Geom::X], vb.min()[Geom::Y]); + _page->set_dimension(PageProperties::Dimension::ViewboxSize, vb.width(), vb.height()); + } + + update_scale_ui(desktop); +} + +/** + * Update dialog widgets from desktop. Also call updateWidget routines of the grids. + */ +void DocumentProperties::update_widgets() +{ + auto desktop = getDesktop(); + auto document = getDocument(); + if (_wr.isUpdating() || !document) return; + + auto nv = desktop->getNamedView(); + auto &page_manager = document->getPageManager(); + + _wr.setUpdating(true); + + SPRoot *root = document->getRoot(); + + double doc_w = root->width.value; + Glib::ustring doc_w_unit = unit_table.getUnit(root->width.unit)->abbr; + bool percent = doc_w_unit == "%"; + if (doc_w_unit == "") { + doc_w_unit = "px"; + } else if (doc_w_unit == "%" && root->viewBox_set) { + doc_w_unit = "px"; + doc_w = root->viewBox.width(); + } + double doc_h = root->height.value; + Glib::ustring doc_h_unit = unit_table.getUnit(root->height.unit)->abbr; + percent = percent || doc_h_unit == "%"; + if (doc_h_unit == "") { + doc_h_unit = "px"; + } else if (doc_h_unit == "%" && root->viewBox_set) { + doc_h_unit = "px"; + doc_h = root->viewBox.height(); + } + using UI::Widget::PageProperties; + // dialog's behavior is not entirely correct when document sizes are expressed in '%', so put up a disclaimer + _page->set_check(PageProperties::Check::UnsupportedSize, percent); + + _page->set_dimension(PageProperties::Dimension::PageSize, doc_w, doc_h); + _page->set_unit(PageProperties::Units::Document, doc_w_unit); + + update_viewbox_ui(desktop); + update_scale_ui(desktop); + + if (nv->display_units) { + _page->set_unit(PageProperties::Units::Display, nv->display_units->abbr); + } + _page->set_check(PageProperties::Check::Checkerboard, nv->desk_checkerboard); + _page->set_color(PageProperties::Color::Desk, nv->desk_color); + _page->set_color(PageProperties::Color::Background, page_manager.background_color); + _page->set_check(PageProperties::Check::Border, page_manager.border_show); + _page->set_check(PageProperties::Check::BorderOnTop, page_manager.border_on_top); + _page->set_color(PageProperties::Color::Border, page_manager.border_color); + _page->set_check(PageProperties::Check::Shadow, page_manager.shadow_show); + + _page->set_check(PageProperties::Check::AntiAlias, root->style->shape_rendering.computed != SP_CSS_SHAPE_RENDERING_CRISPEDGES); + + //-----------------------------------------------------------guide page + + _rcb_sgui.setActive (nv->getShowGuides()); + _rcb_lgui.setActive (nv->getLockGuides()); + _rcp_gui.setRgba32 (nv->guidecolor); + _rcp_hgui.setRgba32 (nv->guidehicolor); + + //-----------------------------------------------------------grids page + + update_gridspage(); + + //------------------------------------------------Color Management page + + populate_linked_profiles_box(); + populate_available_profiles(); + + //-----------------------------------------------------------meta pages + /* update the RDF entities */ + if (auto document = getDocument()) { + for (auto & it : _rdflist) + it->update(document); + + _licensor.update(document); + } + _wr.setUpdating (false); +} + +// TODO: copied from fill-and-stroke.cpp factor out into new ui/widget file? +Gtk::Box& +DocumentProperties::_createPageTabLabel(const Glib::ustring& label, const char *label_image) +{ + Gtk::Box *_tab_label_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + _tab_label_box->set_spacing(4); + + auto img = Gtk::manage(sp_get_icon_image(label_image, Gtk::ICON_SIZE_MENU)); + _tab_label_box->pack_start(*img); + + Gtk::Label *_tab_label = Gtk::manage(new Gtk::Label(label, true)); + _tab_label_box->pack_start(*_tab_label); + _tab_label_box->show_all(); + + return *_tab_label_box; +} + +//-------------------------------------------------------------------- + +void DocumentProperties::on_response (int id) +{ + if (id == Gtk::RESPONSE_DELETE_EVENT || id == Gtk::RESPONSE_CLOSE) + { + _rcp_gui.closeWindow(); + _rcp_hgui.closeWindow(); + } + + if (id == Gtk::RESPONSE_CLOSE) + hide(); +} + +void DocumentProperties::load_default_metadata() +{ + /* Get the data RDF entities data from preferences*/ + for (auto & it : _rdflist) { + it->load_from_preferences (); + } +} + +void DocumentProperties::save_default_metadata() +{ + /* Save these RDF entities to preferences*/ + if (auto document = getDocument()) { + for (auto & it : _rdflist) { + it->save_to_preferences(document); + } + } +} + +void DocumentProperties::watch_connection::connect(Inkscape::XML::Node* node, const Inkscape::XML::NodeEventVector& vector, void* data) { + disconnect(); + if (!node) return; + + _node = node; + _data = data; + node->addListener(&vector, data); +} + +void DocumentProperties::watch_connection::disconnect() { + if (_node) { + _node->removeListenerByData(_data); + _node = nullptr; + _data = nullptr; + } +} + +void DocumentProperties::documentReplaced() +{ + _root_connection.disconnect(); + _namedview_connection.disconnect(); + + if (auto desktop = getDesktop()) { + _wr.setDesktop(desktop); + _namedview_connection.connect(desktop->getNamedView()->getRepr(), _repr_events, this); + if (auto document = desktop->getDocument()) { + _root_connection.connect(document->getRoot()->getRepr(), _repr_events, this); + } + populate_linked_profiles_box(); + update_widgets(); + } +} + +void DocumentProperties::update() +{ + update_widgets(); +} + +static void on_child_added(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node */*child*/, Inkscape::XML::Node */*ref*/, void *data) +{ + if (DocumentProperties *dialog = static_cast<DocumentProperties *>(data)) + dialog->update_gridspage(); +} + +static void on_child_removed(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node */*child*/, Inkscape::XML::Node */*ref*/, void *data) +{ + if (DocumentProperties *dialog = static_cast<DocumentProperties *>(data)) + dialog->update_gridspage(); +} + + + +/** + * Called when XML node attribute changed; updates dialog widgets. + */ +static void on_repr_attr_changed(Inkscape::XML::Node *, gchar const *, gchar const *, gchar const *, bool, gpointer data) +{ + if (DocumentProperties *dialog = static_cast<DocumentProperties *>(data)) + dialog->update_widgets(); +} + + +/*######################################################################## +# BUTTON CLICK HANDLERS (callbacks) +########################################################################*/ + +void DocumentProperties::onNewGrid() +{ + if (auto desktop = getDesktop()) { + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + Glib::ustring typestring = _grids_combo_gridtype.get_active_text(); + CanvasGrid::writeNewGridToRepr(repr, getDocument(), CanvasGrid::getGridTypeFromName(typestring.c_str())); + + // toggle grid showing to ON: + desktop->showGrids(true); + } +} + + +void DocumentProperties::onRemoveGrid() +{ + gint pagenum = _grids_notebook.get_current_page(); + if (pagenum == -1) // no pages + return; + + SPNamedView *nv = getDesktop()->getNamedView(); + Inkscape::CanvasGrid * found_grid = nullptr; + if( pagenum < (gint)nv->grids.size()) + found_grid = nv->grids[pagenum]; + + if (auto document = getDocument()) { + if (found_grid) { + // delete the grid that corresponds with the selected tab + // when the grid is deleted from SVG, the SPNamedview handler automatically deletes the object, so found_grid becomes an invalid pointer! + found_grid->repr->parent()->removeChild(found_grid->repr); + DocumentUndo::done(document, _("Remove grid"), INKSCAPE_ICON("document-properties")); + } + } +} + +/* This should not effect anything in the SVG tree (other than "inkscape:document-units"). + This should only effect values displayed in the GUI. */ +void DocumentProperties::display_unit_change(const Inkscape::Util::Unit* doc_unit) +{ + SPDocument *document = getDocument(); + // Don't execute when change is being undone + if (!document || !DocumentUndo::getUndoSensitive(document)) { + return; + } + // Don't execute when initializing widgets + if (_wr.isUpdating()) { + return; + } + + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + /*Inkscape::Util::Unit const *old_doc_unit = unit_table.getUnit("px"); + if(repr->attribute("inkscape:document-units")) { + old_doc_unit = unit_table.getUnit(repr->attribute("inkscape:document-units")); + }*/ + // Inkscape::Util::Unit const *doc_unit = _rum_deflt.getUnit(); + + // Set document unit + Inkscape::SVGOStringStream os; + os << doc_unit->abbr; + repr->setAttribute("inkscape:document-units", os.str()); + + // Disable changing of SVG Units. The intent here is to change the units in the UI, not the units in SVG. + // This code should be moved (and fixed) once we have an "SVG Units" setting that sets what units are used in SVG data. +#if 0 + // Set viewBox + if (doc->getRoot()->viewBox_set) { + gdouble scale = Inkscape::Util::Quantity::convert(1, old_doc_unit, doc_unit); + doc->setViewBox(doc->getRoot()->viewBox*Geom::Scale(scale)); + } else { + Inkscape::Util::Quantity width = doc->getWidth(); + Inkscape::Util::Quantity height = doc->getHeight(); + doc->setViewBox(Geom::Rect::from_xywh(0, 0, width.value(doc_unit), height.value(doc_unit))); + } + + // TODO: Fix bug in nodes tool instead of switching away from it + if (get_active_tool(get_desktop()) == "Node") { + set_active_tool(get_desktop(), "Select"); + } + + // Scale and translate objects + // set transform options to scale all things with the transform, so all things scale properly after the viewbox change. + /// \todo this "low-level" code of changing viewbox/unit should be moved somewhere else + + // save prefs + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs->getBool("/options/transform/stroke", true); + bool transform_rectcorners = prefs->getBool("/options/transform/rectcorners", true); + bool transform_pattern = prefs->getBool("/options/transform/pattern", true); + bool transform_gradient = prefs->getBool("/options/transform/gradient", true); + + prefs->setBool("/options/transform/stroke", true); + prefs->setBool("/options/transform/rectcorners", true); + prefs->setBool("/options/transform/pattern", true); + prefs->setBool("/options/transform/gradient", true); + { + ShapeEditor::blockSetItem(true); + gdouble viewscale = 1.0; + Geom::Rect vb = doc->getRoot()->viewBox; + if ( !vb.hasZeroArea() ) { + gdouble viewscale_w = doc->getWidth().value("px") / vb.width(); + gdouble viewscale_h = doc->getHeight().value("px")/ vb.height(); + viewscale = std::min(viewscale_h, viewscale_w); + } + gdouble scale = Inkscape::Util::Quantity::convert(1, old_doc_unit, doc_unit); + doc->getRoot()->scaleChildItemsRec(Geom::Scale(scale), Geom::Point(-viewscale*doc->getRoot()->viewBox.min()[Geom::X] + + (doc->getWidth().value("px") - viewscale*doc->getRoot()->viewBox.width())/2, + viewscale*doc->getRoot()->viewBox.min()[Geom::Y] + + (doc->getHeight().value("px") + viewscale*doc->getRoot()->viewBox.height())/2), + false); + ShapeEditor::blockSetItem(false); + } + prefs->setBool("/options/transform/stroke", transform_stroke); + prefs->setBool("/options/transform/rectcorners", transform_rectcorners); + prefs->setBool("/options/transform/pattern", transform_pattern); + prefs->setBool("/options/transform/gradient", transform_gradient); +#endif + + document->setModifiedSinceSave(); + + DocumentUndo::done(document, _("Changed default display unit"), ""); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/document-properties.h b/src/ui/dialog/document-properties.h new file mode 100644 index 0000000..26bae47 --- /dev/null +++ b/src/ui/dialog/document-properties.h @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * \brief Document Properties dialog + */ +/* Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * Bryce W. Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2006-2008 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_DOCUMENT_PREFERENCES_H +#define INKSCAPE_UI_DIALOG_DOCUMENT_PREFERENCES_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <cstddef> +#include <gtkmm/buttonbox.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/liststore.h> +#include <gtkmm/notebook.h> +#include <gtkmm/textview.h> +#include <sigc++/sigc++.h> + +#include "ui/dialog/dialog-base.h" +#include "ui/widget/licensor.h" +#include "ui/widget/registered-widget.h" +#include "ui/widget/registry.h" +#include "ui/widget/tolerance-slider.h" +#include "xml/helper-observer.h" + +namespace Inkscape { + namespace XML { + class Node; + } + namespace UI { + namespace Widget { + class EntityEntry; + class NotebookPage; + class PageProperties; + } + namespace Dialog { + +typedef std::list<UI::Widget::EntityEntry*> RDElist; + +class DocumentProperties : public DialogBase +{ +public: + void update_widgets(); + static DocumentProperties &getInstance(); + static void destroy(); + + void documentReplaced() override; + + void update() override; + void update_gridspage(); + +protected: + void build_page(); + void build_grid(); + void build_guides(); + void build_snap(); + void build_gridspage(); + + void build_cms(); + void build_scripting(); + void build_metadata(); + void init(); + + virtual void on_response (int); + void populate_available_profiles(); + void populate_linked_profiles_box(); + void linkSelectedProfile(); + void removeSelectedProfile(); + void onColorProfileSelectRow(); + void linked_profiles_list_button_release(GdkEventButton* event); + void cms_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem); + + void external_scripts_list_button_release(GdkEventButton* event); + void embedded_scripts_list_button_release(GdkEventButton* event); + void populate_script_lists(); + void addExternalScript(); + void browseExternalScript(); + void addEmbeddedScript(); + void removeExternalScript(); + void removeEmbeddedScript(); + void changeEmbeddedScript(); + void onExternalScriptSelectRow(); + void onEmbeddedScriptSelectRow(); + void editEmbeddedScript(); + void external_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem); + void embedded_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem); + void load_default_metadata(); + void save_default_metadata(); + void update_viewbox(SPDesktop* desktop); + void update_scale_ui(SPDesktop* desktop); + void update_viewbox_ui(SPDesktop* desktop); + void set_document_scale(SPDesktop* desktop, double scale_x); + void set_viewbox_pos(SPDesktop* desktop, double x, double y); + void set_viewbox_size(SPDesktop* desktop, double width, double height); + + Inkscape::XML::SignalObserver _emb_profiles_observer, _scripts_observer; + Gtk::Notebook _notebook; + + UI::Widget::NotebookPage *_page_page; + UI::Widget::NotebookPage *_page_guides; + UI::Widget::NotebookPage *_page_cms; + UI::Widget::NotebookPage *_page_scripting; + + Gtk::Notebook _scripting_notebook; + UI::Widget::NotebookPage *_page_external_scripts; + UI::Widget::NotebookPage *_page_embedded_scripts; + + UI::Widget::NotebookPage *_page_metadata1; + UI::Widget::NotebookPage *_page_metadata2; + + Gtk::Box _grids_vbox; + + UI::Widget::Registry _wr; + //--------------------------------------------------------------- + UI::Widget::RegisteredCheckButton _rcb_sgui; + UI::Widget::RegisteredCheckButton _rcb_lgui; + UI::Widget::RegisteredColorPicker _rcp_gui; + UI::Widget::RegisteredColorPicker _rcp_hgui; + Gtk::Button _create_guides_btn; + Gtk::Button _delete_guides_btn; + //--------------------------------------------------------------- + UI::Widget::PageProperties* _page; + //--------------------------------------------------------------- + Gtk::Button _unlink_btn; + class AvailableProfilesColumns : public Gtk::TreeModel::ColumnRecord + { + public: + AvailableProfilesColumns() + { add(fileColumn); add(nameColumn); add(separatorColumn); } + Gtk::TreeModelColumn<Glib::ustring> fileColumn; + Gtk::TreeModelColumn<Glib::ustring> nameColumn; + Gtk::TreeModelColumn<bool> separatorColumn; + }; + AvailableProfilesColumns _AvailableProfilesListColumns; + Glib::RefPtr<Gtk::ListStore> _AvailableProfilesListStore; + Gtk::ComboBox _AvailableProfilesList; + bool _AvailableProfilesList_separator(const Glib::RefPtr<Gtk::TreeModel>& model, const Gtk::TreeModel::iterator& iter); + class LinkedProfilesColumns : public Gtk::TreeModel::ColumnRecord + { + public: + LinkedProfilesColumns() + { add(nameColumn); add(previewColumn); } + Gtk::TreeModelColumn<Glib::ustring> nameColumn; + Gtk::TreeModelColumn<Glib::ustring> previewColumn; + }; + LinkedProfilesColumns _LinkedProfilesListColumns; + Glib::RefPtr<Gtk::ListStore> _LinkedProfilesListStore; + Gtk::TreeView _LinkedProfilesList; + Gtk::ScrolledWindow _LinkedProfilesListScroller; + Gtk::Menu _EmbProfContextMenu; + + //--------------------------------------------------------------- + Gtk::Button _external_add_btn; + Gtk::Button _external_remove_btn; + Gtk::Button _embed_new_btn; + Gtk::Button _embed_remove_btn; + Gtk::ButtonBox _embed_button_box; + + class ExternalScriptsColumns : public Gtk::TreeModel::ColumnRecord + { + public: + ExternalScriptsColumns() + { add(filenameColumn); } + Gtk::TreeModelColumn<Glib::ustring> filenameColumn; + }; + ExternalScriptsColumns _ExternalScriptsListColumns; + class EmbeddedScriptsColumns : public Gtk::TreeModel::ColumnRecord + { + public: + EmbeddedScriptsColumns() + { add(idColumn); } + Gtk::TreeModelColumn<Glib::ustring> idColumn; + }; + EmbeddedScriptsColumns _EmbeddedScriptsListColumns; + Glib::RefPtr<Gtk::ListStore> _ExternalScriptsListStore; + Glib::RefPtr<Gtk::ListStore> _EmbeddedScriptsListStore; + Gtk::TreeView _ExternalScriptsList; + Gtk::TreeView _EmbeddedScriptsList; + Gtk::ScrolledWindow _ExternalScriptsListScroller; + Gtk::ScrolledWindow _EmbeddedScriptsListScroller; + Gtk::Menu _ExternalScriptsContextMenu; + Gtk::Menu _EmbeddedScriptsContextMenu; + Gtk::Entry _script_entry; + Gtk::TextView _EmbeddedContent; + Gtk::ScrolledWindow _EmbeddedContentScroller; + //--------------------------------------------------------------- + + Gtk::Notebook _grids_notebook; + Gtk::Box _grids_hbox_crea; + Gtk::Label _grids_label_crea; + Gtk::Button _grids_button_new; + Gtk::Button _grids_button_remove; + Gtk::ComboBoxText _grids_combo_gridtype; + Gtk::Label _grids_label_def; + Gtk::Box _grids_space; + //--------------------------------------------------------------- + + RDElist _rdflist; + UI::Widget::Licensor _licensor; + + Gtk::Box& _createPageTabLabel(const Glib::ustring& label, const char *label_image); + +private: + DocumentProperties(); + ~DocumentProperties() override; + + // callback methods for buttons on grids page. + void onNewGrid(); + void onRemoveGrid(); + + // callback for display unit change + void display_unit_change(const Inkscape::Util::Unit* unit); + + struct watch_connection { + ~watch_connection() { disconnect(); } + void connect(Inkscape::XML::Node* node, const Inkscape::XML::NodeEventVector& vector, void* data); + void disconnect(); + private: + Inkscape::XML::Node* _node = nullptr; + void* _data = nullptr; + }; + // nodes connected to listeners + watch_connection _namedview_connection; + watch_connection _root_connection; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_DOCUMENT_PREFERENCES_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/ui/dialog/export-batch.cpp b/src/ui/dialog/export-batch.cpp new file mode 100644 index 0000000..ae92f9e --- /dev/null +++ b/src/ui/dialog/export-batch.cpp @@ -0,0 +1,768 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Anshudhar Kumar Singh <anshudhar2001@gmail.com> + * + * Copyright (C) 1999-2007, 2021 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "export-batch.h" + +#include <glibmm/convert.h> +#include <glibmm/i18n.h> +#include <glibmm/miscutils.h> +#include <gtkmm.h> +#include <png.h> +#include <regex> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "extension/db.h" +#include "extension/output.h" +#include "file.h" +#include "helper/png-write.h" +#include "inkscape-window.h" +#include "inkscape.h" +#include "io/resource.h" +#include "io/sys.h" +#include "layer-manager.h" +#include "message-stack.h" +#include "object/object-set.h" +#include "object/sp-namedview.h" +#include "object/sp-page.h" +#include "object/sp-root.h" +#include "page-manager.h" +#include "preferences.h" +#include "selection-chemistry.h" +#include "ui/dialog-events.h" +#include "ui/dialog/export.h" +#include "ui/dialog/dialog-notebook.h" +#include "ui/dialog/filedialog.h" +#include "ui/interface.h" +#include "ui/widget/export-lists.h" +#include "ui/widget/export-preview.h" +#include "ui/widget/scrollprotected.h" +#include "ui/widget/unit-menu.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class BatchItem : public Gtk::FlowBoxChild +{ +public: + BatchItem(SPItem *item); + BatchItem(SPPage *page); + ~BatchItem() override = default; + + Glib::ustring getLabel() { return _label_str; } + SPItem *getItem() { return _item; } + SPPage *getPage() { return _page; } + bool isActive() { return _selector.get_active(); } + void refresh(bool hide = false); + void refreshHide(const std::vector<SPItem *> &list) { _preview.refreshHide(list); } + void setDocument(SPDocument *doc) { _preview.setDocument(doc); } + +private: + void init(SPDocument *doc, Glib::ustring label); + + Glib::ustring _label_str; + Gtk::Grid _grid; + Gtk::Label _label; + Gtk::CheckButton _selector; + ExportPreview _preview; + SPItem *_item = nullptr; + SPPage *_page = nullptr; + bool is_hide = false; +}; + +BatchItem::BatchItem(SPItem *item) +{ + _item = item; + + Glib::ustring id = _item->defaultLabel(); + if (id.empty()) { + if (auto _id = _item->getId()) { + id = _id; + } else { + id = "no-id"; + } + } + init(_item->document, id); +} + +BatchItem::BatchItem(SPPage *page) +{ + _page = page; + + Glib::ustring label = _page->getDefaultLabel(); + if (auto id = _page->label()) { + label = id; + } + init(_page->document, label); +} + +void BatchItem::init(SPDocument *doc, Glib::ustring label) { + _label_str = label; + + _grid.set_row_spacing(5); + _grid.set_column_spacing(5); + _grid.set_valign(Gtk::Align::ALIGN_CENTER); + + _selector.set_active(true); + _selector.set_can_focus(false); + _selector.set_margin_start(2); + _selector.set_margin_bottom(2); + + _preview.set_name("export_preview_batch"); + _preview.setItem(_item); + _preview.setDocument(doc); + _preview.setSize(64); + _preview.set_halign(Gtk::ALIGN_CENTER); + _preview.set_valign(Gtk::ALIGN_CENTER); + + _label.set_width_chars(10); + _label.set_ellipsize(Pango::ELLIPSIZE_END); + _label.set_halign(Gtk::Align::ALIGN_CENTER); + _label.set_text(label); + + set_valign(Gtk::Align::ALIGN_START); + set_halign(Gtk::Align::ALIGN_START); + add(_grid); + show(); + this->set_can_focus(false); + this->set_tooltip_text(label); + + // This initially packs the widgets with a hidden preview. + refresh(!is_hide); +} + +void BatchItem::refresh(bool hide) +{ + if (_page) { + auto b = _page->getDesktopRect(); + _preview.setDbox(b.left(), b.right(), b.top(), b.bottom()); + } + + // When hiding the preview, we show the items as a checklist + // So all items must be packed differently on refresh. + if (hide != is_hide) { + is_hide = hide; + _grid.remove(_selector); + _grid.remove(_label); + _grid.remove(_preview); + + if (hide) { + _selector.set_valign(Gtk::Align::ALIGN_BASELINE); + _label.set_xalign(0.0); + _grid.attach(_selector, 0, 1, 1, 1); + _grid.attach(_label, 1, 1, 1, 1); + } else { + _selector.set_valign(Gtk::Align::ALIGN_END); + _label.set_xalign(0.5); + _grid.attach(_selector, 0, 1, 1, 1); + _grid.attach(_label, 0, 2, 2, 1); + _grid.attach(_preview, 0, 0, 2, 2); + } + show_all_children(); + } + + if (!hide) { + _preview.queueRefresh(); + } +} + + +void BatchExport::initialise(const Glib::RefPtr<Gtk::Builder> &builder) +{ + builder->get_widget("b_s_selection", selection_buttons[SELECTION_SELECTION]); + selection_names[SELECTION_SELECTION] = "selection"; + builder->get_widget("b_s_layers", selection_buttons[SELECTION_LAYER]); + selection_names[SELECTION_LAYER] = "layer"; + builder->get_widget("b_s_pages", selection_buttons[SELECTION_PAGE]); + selection_names[SELECTION_PAGE] = "page"; + + builder->get_widget("b_preview_box", preview_container); + builder->get_widget("b_show_preview", show_preview); + builder->get_widget("b_num_elements", num_elements); + builder->get_widget("b_hide_all", hide_all); + builder->get_widget("b_filename", filename_entry); + builder->get_widget("b_export", export_btn); + builder->get_widget("b_progress_bar", _prog); + builder->get_widget_derived("b_export_list", export_list); + + Inkscape::UI::Widget::ScrollTransfer<Gtk::ScrolledWindow> *temp = nullptr; + builder->get_widget_derived("b_pbox_scroll", temp); + builder->get_widget_derived("b_scroll", temp); +} + +void BatchExport::selectionModified(Inkscape::Selection *selection, guint flags) +{ + if (!_desktop || _desktop->getSelection() != selection) { + return; + } + if (!(flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + return; + } + refreshItems(); +} + +void BatchExport::selectionChanged(Inkscape::Selection *selection) +{ + if (!_desktop || _desktop->getSelection() != selection) { + return; + } + selection_buttons[SELECTION_SELECTION]->set_sensitive(!selection->isEmpty()); + if (selection->isEmpty()) { + if (current_key == SELECTION_SELECTION) { + selection_buttons[SELECTION_LAYER]->set_active(true); // This causes refresh area + // return otherwise refreshArea will be called again + // even though we are at default key, selection is the one which was original key. + prefs->setString("/dialogs/export/batchexportarea/value", selection_names[SELECTION_SELECTION]); + return; + } + } else { + Glib::ustring pref_key_name = prefs->getString("/dialogs/export/batchexportarea/value"); + if (selection_names[SELECTION_SELECTION] == pref_key_name && current_key != SELECTION_SELECTION) { + selection_buttons[SELECTION_SELECTION]->set_active(); + return; + } + } + refreshItems(); + loadExportHints(); +} + +void BatchExport::pagesChanged() +{ + if (!_desktop || !_document) return; + + bool has_pages = _document->getPageManager().hasPages(); + selection_buttons[SELECTION_PAGE]->set_sensitive(has_pages); + + if (current_key == SELECTION_PAGE && !has_pages) { + current_key = SELECTION_LAYER; + selection_buttons[SELECTION_LAYER]->set_active(); + } + + refreshItems(); + loadExportHints(); +} + +// Setup Single Export.Called by export on realize +void BatchExport::setup() +{ + if (setupDone) { + return; + } + setupDone = true; + prefs = Inkscape::Preferences::get(); + + export_list->setup(); + + // set them before connecting to signals + setDefaultSelectionMode(); + loadExportHints(); + + refreshItems(); + + // Connect Signals + for (auto [key, button] : selection_buttons) { + button->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &BatchExport::onAreaTypeToggle), key)); + } + show_preview->signal_toggled().connect(sigc::mem_fun(*this, &BatchExport::refreshPreview)); + filenameConn = filename_entry->signal_changed().connect(sigc::mem_fun(*this, &BatchExport::onFilenameModified)); + exportConn = export_btn->signal_clicked().connect(sigc::mem_fun(*this, &BatchExport::onExport)); + browseConn = filename_entry->signal_icon_release().connect(sigc::mem_fun(*this, &BatchExport::onBrowse)); + hide_all->signal_toggled().connect(sigc::mem_fun(*this, &BatchExport::refreshPreview)); +} + +void BatchExport::refreshItems() +{ + if (!_desktop || !_document) return; + + // Create New List of Items + std::set<SPItem *> itemsList; + std::set<SPPage *> pageList; + + char *num_str = nullptr; + switch (current_key) { + case SELECTION_SELECTION: { + auto items = _desktop->getSelection()->items(); + for (auto i = items.begin(); i != items.end(); ++i) { + if (SPItem *item = *i) { + // Ignore empty items (empty groups, other bad items) + if (item->visualBounds()) { + itemsList.insert(item); + } + } + } + num_str = g_strdup_printf(ngettext("%d Item", "%d Items", itemsList.size()), (int)itemsList.size()); + break; + } + case SELECTION_LAYER: { + for (auto layer : _desktop->layerManager().getAllLayers()) { + // Ignore empty layers, they have no size. + if (layer->geometricBounds()) { + itemsList.insert(layer); + } + } + num_str = g_strdup_printf(ngettext("%d Layer", "%d Layers", itemsList.size()), (int)itemsList.size()); + break; + } + case SELECTION_PAGE: { + for (auto page : _desktop->getDocument()->getPageManager().getPages()) { + pageList.insert(page); + } + num_str = g_strdup_printf(ngettext("%d Page", "%d Pages", pageList.size()), (int)pageList.size()); + break; + } + default: + break; + } + if (num_str) { + num_elements->set_text(num_str); + g_free(num_str); + } + + // Create a list of items which are already present but will be removed as they are not present anymore + std::vector<std::string> toRemove; + for (auto &[key, val] : current_items) { + if (SPItem *item = val->getItem()) { + // if item is not present in itemList add it to remove list so that we can remove it + auto itemItr = itemsList.find(item); + if (itemItr == itemsList.end() || !(*itemItr)->getId() || (*itemItr)->getId() != key) { + toRemove.push_back(key); + } + } + if (SPPage *page = val->getPage()) { + auto pageItr = pageList.find(page); + if (pageItr == pageList.end() || !(*pageItr)->getId() || (*pageItr)->getId() != key) { + toRemove.push_back(key); + } + } + } + + // now remove all the items + for (auto key : toRemove) { + if (current_items[key]) { + // Preview Boxes are GTK managed so simply removing from container will handle delete + preview_container->remove(*current_items[key]); + current_items.erase(key); + } + } + + // now add which were are new + for (auto &item : itemsList) { + if (auto id = item->getId()) { + // If an Item with same Id is already present, Skip + if (current_items[id] && current_items[id]->getItem() == item) { + continue; + } + // Add new item to the end of list + current_items[id] = Gtk::manage(new BatchItem(item)); + preview_container->insert(*current_items[id], -1); + } + } + for (auto &page : pageList) { + if (auto id = page->getId()) { + if (current_items[id] && current_items[id]->getPage() == page) { + continue; + } + current_items[id] = Gtk::manage(new BatchItem(page)); + preview_container->insert(*current_items[id], -1); + } + } + + refreshPreview(); +} + +void BatchExport::refreshPreview() +{ + if (!_desktop) return; + + // For Batch Export we are now hiding all object except current object + bool hide = hide_all->get_active(); + bool preview = show_preview->get_active(); + preview_container->set_orientation(preview ? Gtk::ORIENTATION_HORIZONTAL : Gtk::ORIENTATION_VERTICAL); + + for (auto &[key, val] : current_items) { + if (preview) { + std::vector<SPItem *> selected; + if (hide) { + if (auto item = val->getItem()) { + selected = std::vector<SPItem *>({item}); + } else if (val->getPage()) { + auto sels = _desktop->getSelection()->items(); + selected = std::vector<SPItem *>(sels.begin(), sels.end()); + } + } + val->refreshHide(selected); + } + val->refresh(!preview); + } +} + +void BatchExport::loadExportHints() +{ + SPDocument *doc = _desktop->getDocument(); + auto old_filename = filename_entry->get_text(); + if (old_filename.empty()) { + Glib::ustring filename = doc->getRoot()->getExportFilename(); + if (filename.empty()) { + Glib::ustring filename_entry_text = filename_entry->get_text(); + Glib::ustring extension = ".png"; + filename = Export::defaultFilename(doc, original_name, extension); + } + filename_entry->set_text(filename); + filename_entry->set_position(filename.length()); + doc_export_name = filename; + } +} + +// Signals CallBack + +void BatchExport::onAreaTypeToggle(selection_mode key) +{ + // Prevent executing function twice + if (!selection_buttons[key]->get_active()) { + return; + } + // If you have reached here means the current key is active one ( not sure if multiple transitions happen but + // last call will change values) + current_key = key; + prefs->setString("/dialogs/export/batchexportarea/value", selection_names[current_key]); + + refreshItems(); + loadExportHints(); +} + +void BatchExport::onFilenameModified() +{ + ; +} + +void BatchExport::onExport() +{ + interrupted = false; + if (!_desktop) + return; + export_btn->set_sensitive(false); + + // If there are no selected button, simply flash message in status bar + int num = current_items.size(); + if (current_items.size() == 0) { + _desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("No items selected.")); + export_btn->set_sensitive(true); + return; + } + + // Find and remove any extension from filename so that we can add suffix to it. + Glib::ustring filename = filename_entry->get_text(); + export_list->removeExtension(filename); + + // create vector of exports + int num_rows = export_list->get_rows(); + std::vector<Glib::ustring> suffixs; + std::vector<Inkscape::Extension::Output *> extensions; + std::vector<double> dpis; + for (int i = 0; i < num_rows; i++) { + suffixs.push_back(export_list->get_suffix(i)); + extensions.push_back(export_list->getExtension(i)); + dpis.push_back(export_list->get_dpi(i)); + } + + // We are exporting standalone items only for now + // std::vector<SPItem *> selected(_desktop->getSelection()->items().begin(), + // _desktop->getSelection()->items().end()); + bool hide = hide_all->get_active(); + + auto sels = _desktop->getSelection()->items(); + std::vector<SPItem *> selected_items(sels.begin(), sels.end()); + + // Start Exporting Each Item + for (int i = 0; i < num_rows; i++) { + auto suffix = suffixs[i]; + auto omod = extensions[i]; + float dpi = dpis[i]; + + if (!omod || omod->deactivated() || !omod->prefs()) { + continue; + } + + int count = 0; + for (auto i = current_items.begin(); i != current_items.end() && !interrupted; ++i) { + count++; + + BatchItem *batchItem = i->second; + if (!batchItem->isActive()) { + continue; + } + + SPItem *item = batchItem->getItem(); + SPPage *page = batchItem->getPage(); + + std::vector<SPItem *> show_only; + Geom::Rect area; + if (item) { + if (auto bounds = item->documentVisualBounds()) { + area = *bounds; + } else { + continue; + } + show_only.emplace_back(item); + } else if (page) { + area = page->getDesktopRect(); + show_only = selected_items; // Maybe stuff here + } else { + continue; + } + + Glib::ustring id = batchItem->getLabel(); + if (id.empty()) { + continue; + } + + Glib::ustring item_filename = filename + "_" + id; + if (!suffix.empty()) { + if (omod->is_raster()) { + // Put the dpi in at the user's requested location. + suffix = std::regex_replace(suffix.c_str(), std::regex("\\{dpi\\}"), std::to_string((int)dpi)); + } + item_filename = item_filename + "_" + suffix; + } + + bool found = Export::unConflictFilename(_document, item_filename, omod->get_extension()); + if (!found) { + continue; + } + + delete prog_dlg; + prog_dlg = create_progress_dialog(Glib::ustring::compose(_("Exporting %1 files"), num)); + prog_dlg->set_export_panel(this); + setExporting(true, Glib::ustring::compose(_("Exporting %1 files"), num)); + prog_dlg->set_current(count); + prog_dlg->set_total(num); + + onProgressCallback(0.0, prog_dlg); + + if (omod->is_raster()) { + unsigned long int width = (int)(area.width() * dpi / DPI_BASE + 0.5); + unsigned long int height = (int)(area.height() * dpi / DPI_BASE + 0.5); + + Export::exportRaster( + area, width, height, dpi, item_filename, true, onProgressCallback, + prog_dlg, omod, hide ? &show_only : nullptr); + } else { + setExporting(true, Glib::ustring::compose(_("Exporting %1"), filename)); + auto copy_doc = _document->copy(); + Export::exportVector(omod, copy_doc.get(), item_filename, true, &show_only, page); + } + + if (prog_dlg) { + delete prog_dlg; + prog_dlg = nullptr; + } + } + } + // Do this right at the end to finish up + setExporting(false); +} + +void BatchExport::onBrowse(Gtk::EntryIconPosition pos, const GdkEventButton *ev) +{ + if (!_app || !_app->get_active_window()) { + return; + } + Gtk::Window *window = _app->get_active_window(); + browseConn.block(); + Glib::ustring filename = Glib::filename_from_utf8(filename_entry->get_text()); + + if (filename.empty()) { + filename = Export::defaultFilename(_document, filename, ".png"); + } + + Inkscape::UI::Dialog::FileSaveDialog *dialog = Inkscape::UI::Dialog::FileSaveDialog::create( + *window, filename, Inkscape::UI::Dialog::RASTER_TYPES, _("Select a filename for exporting"), "", "", + Inkscape::Extension::FILE_SAVE_METHOD_EXPORT); + + if (dialog->show()) { + filename = dialog->getFilename(); + // Remove extension and don't add a new one, for obvious reasons. + export_list->removeExtension(filename); + + filename_entry->set_text(filename); + filename_entry->set_position(filename.length()); + + // deleting dialog before exporting is important + // proper delete function should be made for dialog IMO + delete dialog; + onExport(); + } else { + delete dialog; + } + browseConn.unblock(); +} + +void BatchExport::setDefaultSelectionMode() +{ + current_key = (selection_mode)0; // default key + bool found = false; + Glib::ustring pref_key_name = prefs->getString("/dialogs/export/batchexportarea/value"); + for (auto [key, name] : selection_names) { + if (pref_key_name == name) { + current_key = key; + found = true; + break; + } + } + if (!found) { + pref_key_name = selection_names[current_key]; + } + if (_desktop) { + if (auto _sel = _desktop->getSelection()) { + selection_buttons[SELECTION_SELECTION]->set_sensitive(!_sel->isEmpty()); + } + selection_buttons[SELECTION_PAGE]->set_sensitive(_document->getPageManager().hasPages()); + } + if (!selection_buttons[current_key]->get_sensitive()) { + current_key = SELECTION_LAYER; + } + selection_buttons[current_key]->set_active(true); + + // we need to set pref key because signals above will set set pref == current key but we sometimes change + // current key like selection key + prefs->setString("/dialogs/export/batchexportarea/value", pref_key_name); +} + +void BatchExport::setExporting(bool exporting, Glib::ustring const &text) +{ + if (exporting) { + _prog->set_text(text); + _prog->set_fraction(0.0); + _prog->set_sensitive(true); + export_btn->set_sensitive(false); + } else { + _prog->set_text(""); + _prog->set_fraction(0.0); + _prog->set_sensitive(false); + export_btn->set_sensitive(true); + } +} + +ExportProgressDialog *BatchExport::create_progress_dialog(Glib::ustring progress_text) +{ + // dont forget to delete it later + auto dlg = new ExportProgressDialog(_("Export in progress"), true); + dlg->set_transient_for(*(INKSCAPE.active_desktop()->getToplevel())); + + Gtk::ProgressBar *prg = Gtk::manage(new Gtk::ProgressBar()); + prg->set_text(progress_text); + dlg->set_progress(prg); + auto CA = dlg->get_content_area(); + CA->pack_start(*prg, FALSE, FALSE, 4); + + Gtk::Button *btn = dlg->add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + + btn->signal_clicked().connect(sigc::mem_fun(*this, &BatchExport::onProgressCancel)); + dlg->signal_delete_event().connect(sigc::mem_fun(*this, &BatchExport::onProgressDelete)); + + dlg->show_all(); + return dlg; +} + +/// Called when dialog is deleted +bool BatchExport::onProgressDelete(GdkEventAny * /*event*/) +{ + interrupted = true; + prog_dlg->set_stopped(); + return TRUE; +} + +/// Called when progress is cancelled +void BatchExport::onProgressCancel() +{ + interrupted = true; + prog_dlg->set_stopped(); +} + +/// Called for every progress iteration +unsigned int BatchExport::onProgressCallback(float value, void *dlg) +{ + auto dlg2 = reinterpret_cast<ExportProgressDialog *>(dlg); + + auto self = dynamic_cast<BatchExport *>(dlg2->get_export_panel()); + + if (!self || self->interrupted) + return FALSE; + + auto current = dlg2->get_current(); + auto total = dlg2->get_total(); + if (total > 0) { + double completed = current; + completed /= static_cast<double>(total); + + value = completed + (value / static_cast<double>(total)); + } + + auto prg = dlg2->get_progress(); + prg->set_fraction(value); + + if (self) { + self->_prog->set_fraction(value); + } + + int evtcount = 0; + while ((evtcount < 16) && gdk_events_pending()) { + Gtk::Main::iteration(false); + evtcount += 1; + } + + Gtk::Main::iteration(false); + return TRUE; +} + +void BatchExport::setDesktop(SPDesktop *desktop) +{ + if (desktop != _desktop) { + _pages_changed_connection.disconnect(); + _desktop = desktop; + } +} + +void BatchExport::setDocument(SPDocument *document) +{ + if (!_desktop) { + document = nullptr; + } + + _document = document; + _pages_changed_connection.disconnect(); + if (document) { + // when the page selected is changes, update the export area + _pages_changed_connection = document->getPageManager().connectPagesChanged([=]() { pagesChanged(); }); + } + for (auto &[key, val] : current_items) { + val->setDocument(document); + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/export-batch.h b/src/ui/dialog/export-batch.h new file mode 100644 index 0000000..9c49f29 --- /dev/null +++ b/src/ui/dialog/export-batch.h @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Anshudhar Kumar Singh <anshudhar2001@gmail.com> + * + * Copyright (C) 1999-2007, 2021 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_EXPORT_BATCH_H +#define SP_EXPORT_BATCH_H + +#include <gtkmm.h> + +#include "ui/widget/scrollprotected.h" + +class InkscapeApplication; +class SPDocument; +class SPDesktop; + +namespace Inkscape { + class Preferences; + class Selection; + +namespace UI { +namespace Dialog { + +class ExportList; +class BatchItem; +class ExportProgressDialog; + +class BatchExport : public Gtk::Box +{ +public: + BatchExport() {}; + BatchExport(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade) + : Gtk::Box(cobject){}; + ~BatchExport() override = default; + +private: + InkscapeApplication *_app; + SPDesktop *_desktop = nullptr; + SPDocument *_document = nullptr; + +private: + bool setupDone = false; // To prevent setup() call add connections again. + +public: + void setApp(InkscapeApplication *app) { _app = app; } + void setDocument(SPDocument *document); + void setDesktop(SPDesktop *desktop); + void selectionChanged(Inkscape::Selection *selection); + void selectionModified(Inkscape::Selection *selection, guint flags); + void pagesChanged(); + +private: + enum selection_mode + { + SELECTION_LAYER = 0, // Default is alaways placed first + SELECTION_SELECTION, + SELECTION_PAGE, + }; + +private: + typedef Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton> SpinButton; + + std::map<selection_mode, Gtk::RadioButton *> selection_buttons; + Gtk::FlowBox *preview_container = nullptr; + Gtk::CheckButton *show_preview = nullptr; + Gtk::Label *num_elements = nullptr; + Gtk::CheckButton *hide_all = nullptr; + Gtk::Entry *filename_entry = nullptr; + Gtk::Button *export_btn = nullptr; + Gtk::ProgressBar *_prog = nullptr; + ExportList *export_list = nullptr; + + // Store all items to be displayed in flowbox + std::map<std::string, BatchItem *> current_items; + + bool filename_modified; + Glib::ustring original_name; + Glib::ustring doc_export_name; + + Inkscape::Preferences *prefs = nullptr; + std::map<selection_mode, Glib::ustring> selection_names; + selection_mode current_key; + +public: + // initialise variables from builder + void initialise(const Glib::RefPtr<Gtk::Builder> &builder); + void setup(); + +private: + void setDefaultSelectionMode(); + void onFilenameModified(); + void onAreaTypeToggle(selection_mode key); + void onExport(); + void onBrowse(Gtk::EntryIconPosition pos, const GdkEventButton *ev); + + void refreshPreview(); + void refreshItems(); + void loadExportHints(); + +public: + void refresh() + { + refreshItems(); + loadExportHints(); + }; + +private: + void setExporting(bool exporting, Glib::ustring const &text = ""); + ExportProgressDialog *create_progress_dialog(Glib::ustring progress_text); + /** + * Callback to be used in for loop to update the progress bar. + * + * @param value number between 0 and 1 indicating the fraction of progress (0.17 = 17 % progress) + * @param dlg void pointer to the Gtk::Dialog progress dialog + */ + static unsigned int onProgressCallback(float value, void *dlg); + + /** + * Callback for pressing the cancel button. + */ + void onProgressCancel(); + + /** + * Callback invoked on closing the progress dialog. + */ + bool onProgressDelete(GdkEventAny *event); + +private: + ExportProgressDialog *prog_dlg = nullptr; + bool interrupted; + + // Gtk Signals + sigc::connection filenameConn; + sigc::connection exportConn; + sigc::connection browseConn; + sigc::connection selectionModifiedConn; + sigc::connection selectionChangedConn; + // SVG Signals + sigc::connection _pages_changed_connection; +}; +} // namespace Dialog +} // namespace UI +} // namespace Inkscape +#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/ui/dialog/export-single.cpp b/src/ui/dialog/export-single.cpp new file mode 100644 index 0000000..dc17227 --- /dev/null +++ b/src/ui/dialog/export-single.cpp @@ -0,0 +1,990 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Anshudhar Kumar Singh <anshudhar2001@gmail.com> + * + * Copyright (C) 1999-2007, 2021 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "export-single.h" + +#include <glibmm/convert.h> +#include <glibmm/i18n.h> +#include <glibmm/miscutils.h> +#include <gtkmm.h> +#include <png.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "extension/db.h" +#include "extension/output.h" +#include "file.h" +#include "helper/png-write.h" +#include "inkscape-window.h" +#include "inkscape.h" +#include "io/resource.h" +#include "io/sys.h" +#include "message-stack.h" +#include "object/object-set.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" +#include "object/sp-page.h" +#include "page-manager.h" +#include "preferences.h" +#include "selection-chemistry.h" +#include "ui/dialog-events.h" +#include "ui/dialog/dialog-notebook.h" +#include "ui/dialog/export.h" +#include "ui/dialog/filedialog.h" +#include "ui/icon-names.h" +#include "ui/interface.h" +#include "ui/widget/export-lists.h" +#include "ui/widget/export-preview.h" +#include "ui/widget/scrollprotected.h" +#include "ui/widget/unit-menu.h" +#ifdef _WIN32 + +#endif + +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/** + * Initialise Builder Objects. Called in Export constructor. + */ +void SingleExport::initialise(const Glib::RefPtr<Gtk::Builder> &builder) +{ + builder->get_widget("si_s_document", selection_buttons[SELECTION_DRAWING]); + selection_names[SELECTION_DRAWING] = "drawing"; + builder->get_widget("si_s_page", selection_buttons[SELECTION_PAGE]); + selection_names[SELECTION_PAGE] = "page"; + builder->get_widget("si_s_selection", selection_buttons[SELECTION_SELECTION]); + selection_names[SELECTION_SELECTION] = "selection"; + builder->get_widget("si_s_custom", selection_buttons[SELECTION_CUSTOM]); + selection_names[SELECTION_CUSTOM] = "custom"; + + builder->get_widget_derived("si_left_sb", spin_buttons[SPIN_X0]); + builder->get_widget_derived("si_right_sb", spin_buttons[SPIN_X1]); + builder->get_widget_derived("si_top_sb", spin_buttons[SPIN_Y0]); + builder->get_widget_derived("si_bottom_sb", spin_buttons[SPIN_Y1]); + builder->get_widget_derived("si_height_sb", spin_buttons[SPIN_HEIGHT]); + builder->get_widget_derived("si_width_sb", spin_buttons[SPIN_WIDTH]); + + builder->get_widget("si_label_left", spin_labels[SPIN_X0]); + builder->get_widget("si_label_right", spin_labels[SPIN_X1]); + builder->get_widget("si_label_top", spin_labels[SPIN_Y0]); + builder->get_widget("si_label_bottom", spin_labels[SPIN_Y1]); + builder->get_widget("si_label_height", spin_labels[SPIN_HEIGHT]); + builder->get_widget("si_label_width", spin_labels[SPIN_WIDTH]); + + builder->get_widget_derived("si_img_height_sb", spin_buttons[SPIN_BMHEIGHT]); + builder->get_widget_derived("si_img_width_sb", spin_buttons[SPIN_BMWIDTH]); + builder->get_widget_derived("si_dpi_sb", spin_buttons[SPIN_DPI]); + + builder->get_widget("page_prev", page_prev); + page_prev->signal_clicked().connect([=]() { + if (_document && _document->getPageManager().selectPrevPage()) { + _document->getPageManager().zoomToSelectedPage(_desktop); + } + }); + + builder->get_widget("page_next", page_next); + page_next->signal_clicked().connect([=]() { + if (_document && _document->getPageManager().selectNextPage()) { + _document->getPageManager().zoomToSelectedPage(_desktop); + } + }); + + // builder->get_widget("si_show_export_area", show_export_area); + builder->get_widget_derived("si_units", units); + builder->get_widget("si_units_row", si_units_row); + builder->get_widget("si_area_name", si_name_label); + + builder->get_widget("si_hide_all", si_hide_all); + builder->get_widget("si_show_preview", si_show_preview); + builder->get_widget("si_default_opts", si_default_opts); + builder->get_widget_derived("si_preview", preview); + + builder->get_widget_derived("si_extention", si_extension_cb); + builder->get_widget("si_filename", si_filename_entry); + builder->get_widget("si_export", si_export); + + builder->get_widget("si_progress", _prog); +} + +// Inkscape Selection Modified CallBack +void SingleExport::selectionModified(Inkscape::Selection *selection, guint flags) +{ + if (!_desktop || _desktop->getSelection() != selection) { + return; + } + if (!(flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + return; + } + refreshArea(); + // Do not load export hits for modifications +} + +void SingleExport::selectionChanged(Inkscape::Selection *selection) +{ + if (!_desktop || _desktop->getSelection() != selection) { + return; + } + + Glib::ustring pref_key_name = prefs->getString("/dialogs/export/exportarea/value"); + for (auto [key, name] : selection_names) { + if (name == pref_key_name && current_key != key && key != SELECTION_SELECTION) { + selection_buttons[key]->set_active(true); + current_key = key; + break; + } + } + if (selection->isEmpty()) { + selection_buttons[SELECTION_SELECTION]->set_sensitive(false); + if (current_key == SELECTION_SELECTION) { + selection_buttons[(selection_mode)0]->set_active(true); // This causes refresh area + // even though we are at default key, selection is the one which was original key. + prefs->setString("/dialogs/export/exportarea/value", selection_names[SELECTION_SELECTION]); + // return otherwise refreshArea will be called again + return; + } + } else { + selection_buttons[SELECTION_SELECTION]->set_sensitive(true); + if (selection_names[SELECTION_SELECTION] == pref_key_name && current_key != SELECTION_SELECTION) { + selection_buttons[SELECTION_SELECTION]->set_active(); + return; + } + } + + refreshArea(); + loadExportHints(); +} + +// Setup Single Export.Called by export on realize +void SingleExport::setup() +{ + if (setupDone) { + // We need to setup only once + return; + } + setupDone = true; + prefs = Inkscape::Preferences::get(); + si_extension_cb->setup(); + + setupUnits(); + setupSpinButtons(); + + // set them before connecting to signals + setDefaultSelectionMode(); + + // Refresh values to sync them with defaults. + refreshArea(); + refreshPage(); + loadExportHints(); + + // Connect Signals Here + for (auto [key, button] : selection_buttons) { + button->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SingleExport::onAreaTypeToggle), key)); + } + units->signal_changed().connect(sigc::mem_fun(*this, &SingleExport::onUnitChanged)); + extensionConn = si_extension_cb->signal_changed().connect(sigc::mem_fun(*this, &SingleExport::onExtensionChanged)); + exportConn = si_export->signal_clicked().connect(sigc::mem_fun(*this, &SingleExport::onExport)); + filenameConn = si_filename_entry->signal_changed().connect(sigc::mem_fun(*this, &SingleExport::onFilenameModified)); + browseConn = si_filename_entry->signal_icon_release().connect(sigc::mem_fun(*this, &SingleExport::onBrowse)); + si_filename_entry->signal_activate().connect(sigc::mem_fun(*this, &SingleExport::onExport)); + si_show_preview->signal_toggled().connect(sigc::mem_fun(*this, &SingleExport::refreshPreview)); + si_hide_all->signal_toggled().connect(sigc::mem_fun(*this, &SingleExport::refreshPreview)); + + si_default_opts->set_active(prefs->getBool("/dialogs/export/defaultopts", true)); +} + +// Setup units combobox +void SingleExport::setupUnits() +{ + units->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR); + if (_desktop) { + units->setUnit(_desktop->getNamedView()->display_units->abbr); + } +} + +// Create all spin buttons +void SingleExport::setupSpinButtons() +{ + setupSpinButton<sb_type>(spin_buttons[SPIN_X0], 0.0, -1000000.0, 1000000.0, 0.1, 1.0, EXPORT_COORD_PRECISION, true, + &SingleExport::onAreaXChange, SPIN_X0); + setupSpinButton<sb_type>(spin_buttons[SPIN_X1], 0.0, -1000000.0, 1000000.0, 0.1, 1.0, EXPORT_COORD_PRECISION, true, + &SingleExport::onAreaXChange, SPIN_X1); + setupSpinButton<sb_type>(spin_buttons[SPIN_Y0], 0.0, -1000000.0, 1000000.0, 0.1, 1.0, EXPORT_COORD_PRECISION, true, + &SingleExport::onAreaYChange, SPIN_Y0); + setupSpinButton<sb_type>(spin_buttons[SPIN_Y1], 0.0, -1000000.0, 1000000.0, 0.1, 1.0, EXPORT_COORD_PRECISION, true, + &SingleExport::onAreaYChange, SPIN_Y1); + + setupSpinButton<sb_type>(spin_buttons[SPIN_HEIGHT], 0.0, 0.0, PNG_UINT_31_MAX, 0.1, 1.0, EXPORT_COORD_PRECISION, + true, &SingleExport::onAreaYChange, SPIN_HEIGHT); + setupSpinButton<sb_type>(spin_buttons[SPIN_WIDTH], 0.0, 0.0, PNG_UINT_31_MAX, 0.1, 1.0, EXPORT_COORD_PRECISION, + true, &SingleExport::onAreaXChange, SPIN_WIDTH); + + setupSpinButton<sb_type>(spin_buttons[SPIN_BMHEIGHT], 1.0, 1.0, 1000000.0, 1.0, 10.0, 0, true, + &SingleExport::onDpiChange, SPIN_BMHEIGHT); + setupSpinButton<sb_type>(spin_buttons[SPIN_BMWIDTH], 1.0, 1.0, 1000000.0, 1.0, 10.0, 0, true, + &SingleExport::onDpiChange, SPIN_BMWIDTH); + setupSpinButton<sb_type>(spin_buttons[SPIN_DPI], prefs->getDouble("/dialogs/export/defaultxdpi/value", DPI_BASE), + 1.0, 100000.0, 0.1, 1.0, 2, true, &SingleExport::onDpiChange, SPIN_DPI); +} + +template <typename T> +void SingleExport::setupSpinButton(Gtk::SpinButton *sb, double val, double min, double max, double step, double page, + int digits, bool sensitive, void (SingleExport::*cb)(T), T param) +{ + if (sb) { + sb->set_digits(digits); + sb->set_increments(step, page); + sb->set_range(min, max); + sb->set_value(val); + sb->set_sensitive(sensitive); + sb->set_width_chars(0); + sb->set_max_width_chars(0); + if (cb) { + auto signal = sb->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, cb), param)); + // add signals to list to block all easily + spinButtonConns.push_back(signal); + } + } +} + +void SingleExport::refreshArea() +{ + if (_document) { + Geom::OptRect bbox; + + switch (current_key) { + case SELECTION_SELECTION: + if ((_desktop->getSelection())->isEmpty() == false) { + bbox = _desktop->getSelection()->visualBounds(); + break; + } + case SELECTION_DRAWING: + bbox = _document->getRoot()->desktopVisualBounds(); + if (bbox) { + break; + } + case SELECTION_PAGE: + bbox = _document->getPageManager().getSelectedPageRect(); + break; + case SELECTION_CUSTOM: + break; + default: + break; + } + if (current_key != SELECTION_CUSTOM && bbox) { + setArea(bbox->min()[Geom::X], bbox->min()[Geom::Y], bbox->max()[Geom::X], bbox->max()[Geom::Y]); + } + } + refreshPreview(); +} + +void SingleExport::refreshPage() +{ + bool pages = current_key == SELECTION_PAGE; + si_name_label->set_visible(pages); + page_prev->set_visible(pages); + page_next->set_visible(pages); + + auto &page_manager = _document->getPageManager(); + page_prev->set_sensitive(page_manager.hasPrevPage()); + page_next->set_sensitive(page_manager.hasNextPage()); + + if (auto page = page_manager.getSelected()) { + if (auto label = page->label()) { + si_name_label->set_text(label); + } else { + si_name_label->set_text(page->getDefaultLabel()); + } + } else { + si_name_label->set_text(_("First Page")); + } +} + +void SingleExport::loadExportHints() +{ + if (filename_modified) return; + + Glib::ustring old_filename = si_filename_entry->get_text(); + Glib::ustring filename; + Geom::Point dpi; + switch (current_key) { + case SELECTION_PAGE: + if (auto page = _document->getPageManager().getSelected()) { + dpi = page->getExportDpi(); + filename = page->getExportFilename(); + if (filename.empty()) { + filename = Export::filePathFromId(_document, page->getLabel(), old_filename); + } + break; + } + // No page means page is drawing, continue. + case SELECTION_CUSTOM: + case SELECTION_DRAWING: + { + dpi = _document->getRoot()->getExportDpi(); + filename = _document->getRoot()->getExportFilename(); + break; + } + case SELECTION_SELECTION: + { + auto selection = _desktop->getSelection(); + if (selection->isEmpty()) break; + + // Get filename and dpi from selected items + for (auto item : selection->items()) { + if (!dpi.x()) { + dpi = item->getExportDpi(); + } + if (filename.empty()) { + filename = item->getExportFilename(); + } + } + + if (filename.empty()) { + filename = Export::filePathFromObject(_document, selection->firstItem(), old_filename); + } + break; + } + default: + break; + } + if (filename.empty()) { + filename = Export::defaultFilename(_document, old_filename, ".png"); + } + if (auto ext = si_extension_cb->getExtension()) { + si_extension_cb->removeExtension(filename); + ext->add_extension(filename); + } + + original_name = filename; + si_filename_entry->set_text(filename); + si_filename_entry->set_position(filename.length()); + + if (dpi.x() != 0.0) { // XXX Should this deal with dpi.y() ? + spin_buttons[SPIN_DPI]->set_value(dpi.x()); + } +} + +void SingleExport::saveExportHints(SPObject *target) +{ + if (target) { + target->setExportFilename(si_filename_entry->get_text()); + target->setExportDpi(Geom::Point( + spin_buttons[SPIN_DPI]->get_value(), + spin_buttons[SPIN_DPI]->get_value() + )); + } +} + +void SingleExport::setArea(double x0, double y0, double x1, double y1) +{ + blockSpinConns(true); + + Unit const *unit = units->getUnit(); + auto px = unit_table.getUnit("px"); + spin_buttons[SPIN_X0]->get_adjustment()->set_value(px->convert(x0, unit)); + spin_buttons[SPIN_X1]->get_adjustment()->set_value(px->convert(x1, unit)); + spin_buttons[SPIN_Y0]->get_adjustment()->set_value(px->convert(y0, unit)); + spin_buttons[SPIN_Y1]->get_adjustment()->set_value(px->convert(y1, unit)); + + areaXChange(SPIN_X1); + areaYChange(SPIN_Y1); + + blockSpinConns(false); +} + +// Signals CallBack + +void SingleExport::onUnitChanged() +{ + refreshArea(); +} + +void SingleExport::onAreaTypeToggle(selection_mode key) +{ + // Prevent executing function twice + if (!selection_buttons[key]->get_active()) { + return; + } + // If you have reached here means the current key is active one ( not sure if multiple transitions happen but + // last call will change values) + current_key = key; + prefs->setString("/dialogs/export/exportarea/value", selection_names[current_key]); + + refreshPage(); + refreshArea(); + loadExportHints(); + toggleSpinButtonVisibility(); +} + +void SingleExport::toggleSpinButtonVisibility() +{ + bool show = current_key == SELECTION_CUSTOM; + spin_buttons[SPIN_X0]->set_visible(show); + spin_buttons[SPIN_X1]->set_visible(show); + spin_buttons[SPIN_Y0]->set_visible(show); + spin_buttons[SPIN_Y1]->set_visible(show); + spin_buttons[SPIN_WIDTH]->set_visible(show); + spin_buttons[SPIN_HEIGHT]->set_visible(show); + + spin_labels[SPIN_X0]->set_visible(show); + spin_labels[SPIN_X1]->set_visible(show); + spin_labels[SPIN_Y0]->set_visible(show); + spin_labels[SPIN_Y1]->set_visible(show); + spin_labels[SPIN_WIDTH]->set_visible(show); + spin_labels[SPIN_HEIGHT]->set_visible(show); + + si_units_row->set_visible(show); +} + +void SingleExport::onAreaXChange(sb_type type) +{ + blockSpinConns(true); + areaXChange(type); + selection_buttons[SELECTION_CUSTOM]->set_active(true); + refreshPreview(); + blockSpinConns(false); +} +void SingleExport::onAreaYChange(sb_type type) +{ + blockSpinConns(true); + areaYChange(type); + selection_buttons[SELECTION_CUSTOM]->set_active(true); + refreshPreview(); + blockSpinConns(false); +} +void SingleExport::onDpiChange(sb_type type) +{ + blockSpinConns(true); + dpiChange(type); + blockSpinConns(false); +} + +void SingleExport::onFilenameModified() +{ + extensionConn.block(); + Glib::ustring filename = si_filename_entry->get_text(); + + if (original_name == filename) { + filename_modified = false; + } else { + filename_modified = true; + } + + si_extension_cb->setExtensionFromFilename(filename); + + extensionConn.unblock(); +} + +void SingleExport::onExtensionChanged() +{ + filenameConn.block(); + Glib::ustring filename = si_filename_entry->get_text(); + if (auto ext = si_extension_cb->getExtension()) { + si_extension_cb->removeExtension(filename); + ext->add_extension(filename); + } + si_filename_entry->set_text(filename); + si_filename_entry->set_position(filename.length()); + filenameConn.unblock(); +} + +void SingleExport::onExport() +{ + interrupted = false; + if (!_desktop || !_document) + return; + + auto &page_manager = _document->getPageManager(); + auto selection = _desktop->getSelection(); + si_export->set_sensitive(false); + bool exportSuccessful = false; + auto omod = si_extension_cb->getExtension(); + if (!omod) { + si_export->set_sensitive(true); + return; + } + + bool selected_only = si_hide_all->get_active(); + Unit const *unit = units->getUnit(); + Glib::ustring filename = si_filename_entry->get_text(); + + float x0 = unit->convert(spin_buttons[SPIN_X0]->get_value(), "px"); + float x1 = unit->convert(spin_buttons[SPIN_X1]->get_value(), "px"); + float y0 = unit->convert(spin_buttons[SPIN_Y0]->get_value(), "px"); + float y1 = unit->convert(spin_buttons[SPIN_Y1]->get_value(), "px"); + auto area = Geom::Rect(Geom::Point(x0, y0), Geom::Point(x1, y1)); + + bool default_opts = si_default_opts->get_active(); + prefs->setBool("/dialogs/export/defaultopts", default_opts); + if (!default_opts && !omod->prefs()) { + si_export->set_sensitive(true); + return; // cancel button + } + + if (omod->is_raster()) { + area *= _desktop->dt2doc(); + unsigned long int width = int(spin_buttons[SPIN_BMWIDTH]->get_value() + 0.5); + unsigned long int height = int(spin_buttons[SPIN_BMHEIGHT]->get_value() + 0.5); + + float dpi = spin_buttons[SPIN_DPI]->get_value(); + + /* TRANSLATORS: %1 will be the filename, %2 the width, and %3 the height of the image */ + prog_dlg = create_progress_dialog(Glib::ustring::compose(_("Exporting %1 (%2 x %3)"), filename, width, height)); + prog_dlg->set_export_panel(this); + setExporting(true, Glib::ustring::compose(_("Exporting %1 (%2 x %3)"), filename, width, height)); + prog_dlg->set_current(0); + prog_dlg->set_total(0); + + std::vector<SPItem *> selected(selection->items().begin(), selection->items().end()); + + exportSuccessful = Export::exportRaster( + area, width, height, dpi, filename, false, onProgressCallback, prog_dlg, + omod, selected_only ? &selected : nullptr); + + } else { + setExporting(true, Glib::ustring::compose(_("Exporting %1"), filename)); + + auto copy_doc = _document->copy(); + + std::vector<SPItem *> items; + if (selected_only) { + auto itemlist = selection->items(); + for (auto i = itemlist.begin(); i != itemlist.end(); ++i) { + SPItem *item = *i; + items.push_back(item); + } + } + + SPPage *page; + if (current_key == SELECTION_PAGE && page_manager.hasPages()) { + page = page_manager.getSelected(); + } else { + // To get the right kind of export, we're going to make a page + // This allows all the same raster options to work for vectors + page = copy_doc->getPageManager().newDocumentPage(area); + } + + exportSuccessful = Export::exportVector(omod, copy_doc.get(), filename, false, &items, page); + } + if (prog_dlg) { + delete prog_dlg; + prog_dlg = nullptr; + } + // Save the export hints back to the svg document + if (exportSuccessful) { + SPObject *target; + switch (current_key) { + case SELECTION_CUSTOM: + case SELECTION_DRAWING: + target = _document->getRoot(); + break; + case SELECTION_PAGE: + target = page_manager.getSelected(); + if (!target) + target = _document->getRoot(); + break; + case SELECTION_SELECTION: + target = _desktop->getSelection()->firstItem(); + break; + default: + break; + } + if (target) { + saveExportHints(target); + DocumentUndo::done(_document, _("Set Export Options"), INKSCAPE_ICON("export")); + } + } + setExporting(false); + original_name = filename; + filename_modified = false; + interrupted = false; +} + +void SingleExport::onBrowse(Gtk::EntryIconPosition pos, const GdkEventButton *ev) +{ + if (!_app || !_app->get_active_window() || !_document) { + return; + } + Gtk::Window *window = _app->get_active_window(); + browseConn.block(); + Glib::ustring filename = Glib::filename_from_utf8(si_filename_entry->get_text()); + + if (filename.empty()) { + filename = Export::defaultFilename(_document, filename, ".png"); + } + + Inkscape::UI::Dialog::FileSaveDialog *dialog = Inkscape::UI::Dialog::FileSaveDialog::create( + *window, filename, Inkscape::UI::Dialog::RASTER_TYPES, _("Select a filename for exporting"), "", "", + Inkscape::Extension::FILE_SAVE_METHOD_EXPORT); + + if (dialog->show()) { + filename = dialog->getFilename(); + if (auto ext = si_extension_cb->getExtension()) { + si_extension_cb->removeExtension(filename); + ext->add_extension(filename); + } + si_filename_entry->set_text(filename); + si_filename_entry->set_position(filename.length()); + // deleting dialog before exporting is important + delete dialog; + onExport(); + } else { + delete dialog; + } + browseConn.unblock(); +} + +// Utils Functions + +void SingleExport::blockSpinConns(bool status = true) +{ + for (auto signal : spinButtonConns) { + if (status) { + signal.block(); + } else { + signal.unblock(); + } + } +} + +void SingleExport::areaXChange(sb_type type) +{ + auto x0_adj = spin_buttons[SPIN_X0]->get_adjustment(); + auto x1_adj = spin_buttons[SPIN_X1]->get_adjustment(); + auto width_adj = spin_buttons[SPIN_WIDTH]->get_adjustment(); + + float x0, x1, dpi, width, bmwidth; + + // Get all values in px + Unit const *unit = units->getUnit(); + x0 = unit->convert(x0_adj->get_value(), "px"); + x1 = unit->convert(x1_adj->get_value(), "px"); + width = unit->convert(width_adj->get_value(), "px"); + bmwidth = spin_buttons[SPIN_BMWIDTH]->get_value(); + dpi = spin_buttons[SPIN_DPI]->get_value(); + + switch (type) { + case SPIN_X0: + bmwidth = (x1 - x0) * dpi / DPI_BASE; + if (bmwidth < SP_EXPORT_MIN_SIZE) { + x0 = x1 - (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi; + } + break; + case SPIN_X1: + bmwidth = (x1 - x0) * dpi / DPI_BASE; + if (bmwidth < SP_EXPORT_MIN_SIZE) { + x1 = x0 + (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi; + } + break; + case SPIN_WIDTH: + bmwidth = width * dpi / DPI_BASE; + if (bmwidth < SP_EXPORT_MIN_SIZE) { + width = (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi; + } + x1 = x0 + width; + break; + default: + break; + } + + width = x1 - x0; + bmwidth = floor(width * dpi / DPI_BASE + 0.5); + + auto px = unit_table.getUnit("px"); + x0_adj->set_value(px->convert(x0, unit)); + x1_adj->set_value(px->convert(x1, unit)); + width_adj->set_value(px->convert(width, unit)); + spin_buttons[SPIN_BMWIDTH]->set_value(bmwidth); +} + +void SingleExport::areaYChange(sb_type type) +{ + auto y0_adj = spin_buttons[SPIN_Y0]->get_adjustment(); + auto y1_adj = spin_buttons[SPIN_Y1]->get_adjustment(); + auto height_adj = spin_buttons[SPIN_HEIGHT]->get_adjustment(); + + float y0, y1, dpi, height, bmheight; + + // Get all values in px + Unit const *unit = units->getUnit(); + y0 = unit->convert(y0_adj->get_value(), "px"); + y1 = unit->convert(y1_adj->get_value(), "px"); + height = unit->convert(height_adj->get_value(), "px"); + bmheight = spin_buttons[SPIN_BMHEIGHT]->get_value(); + dpi = spin_buttons[SPIN_DPI]->get_value(); + + switch (type) { + case SPIN_Y0: + bmheight = (y1 - y0) * dpi / DPI_BASE; + if (bmheight < SP_EXPORT_MIN_SIZE) { + y0 = y1 - (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi; + } + break; + case SPIN_Y1: + bmheight = (y1 - y0) * dpi / DPI_BASE; + if (bmheight < SP_EXPORT_MIN_SIZE) { + y1 = y0 + (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi; + } + break; + case SPIN_HEIGHT: + bmheight = height * dpi / DPI_BASE; + if (bmheight < SP_EXPORT_MIN_SIZE) { + height = (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi; + } + y1 = y0 + height; + break; + default: + break; + } + + height = y1 - y0; + bmheight = floor(height * dpi / DPI_BASE + 0.5); + + auto px = unit_table.getUnit("px"); + y0_adj->set_value(px->convert(y0, unit)); + y1_adj->set_value(px->convert(y1, unit)); + height_adj->set_value(px->convert(height, unit)); + spin_buttons[SPIN_BMHEIGHT]->set_value(bmheight); +} + +void SingleExport::dpiChange(sb_type type) +{ + float dpi, height, width, bmheight, bmwidth; + + // Get all values in px + Unit const *unit = units->getUnit(); + height = unit->convert(spin_buttons[SPIN_HEIGHT]->get_value(), "px"); + width = unit->convert(spin_buttons[SPIN_WIDTH]->get_value(), "px"); + bmheight = spin_buttons[SPIN_BMHEIGHT]->get_value(); + bmwidth = spin_buttons[SPIN_BMWIDTH]->get_value(); + dpi = spin_buttons[SPIN_DPI]->get_value(); + + switch (type) { + case SPIN_BMHEIGHT: + if (bmheight < SP_EXPORT_MIN_SIZE) { + bmheight = SP_EXPORT_MIN_SIZE; + } + dpi = bmheight * DPI_BASE / height; + break; + case SPIN_BMWIDTH: + if (bmwidth < SP_EXPORT_MIN_SIZE) { + bmwidth = SP_EXPORT_MIN_SIZE; + } + dpi = bmwidth * DPI_BASE / width; + break; + case SPIN_DPI: + prefs->setDouble("/dialogs/export/defaultdpi/value", dpi); + break; + default: + break; + } + + bmwidth = floor(width * dpi / DPI_BASE + 0.5); + bmheight = floor(height * dpi / DPI_BASE + 0.5); + + spin_buttons[SPIN_BMHEIGHT]->set_value(bmheight); + spin_buttons[SPIN_BMWIDTH]->set_value(bmwidth); + spin_buttons[SPIN_DPI]->set_value(dpi); +} + +void SingleExport::setDefaultSelectionMode() +{ + current_key = (selection_mode)0; // default key + bool found = false; + Glib::ustring pref_key_name = prefs->getString("/dialogs/export/exportarea/value"); + for (auto [key, name] : selection_names) { + if (pref_key_name == name) { + current_key = key; + found = true; + break; + } + } + if (!found) { + pref_key_name = selection_names[current_key]; + } + + if (_desktop) { + if (current_key == SELECTION_SELECTION && (_desktop->getSelection())->isEmpty()) { + current_key = (selection_mode)0; + } + if ((_desktop->getSelection())->isEmpty()) { + selection_buttons[SELECTION_SELECTION]->set_sensitive(false); + } + if (current_key == SELECTION_CUSTOM && + (spin_buttons[SPIN_HEIGHT]->get_value() == 0 || spin_buttons[SPIN_WIDTH]->get_value() == 0)) { + Geom::OptRect bbox = _document->preferredBounds(); + setArea(bbox->min()[Geom::X], bbox->min()[Geom::Y], bbox->max()[Geom::X], bbox->max()[Geom::Y]); + } + } else { + current_key = (selection_mode)0; + } + selection_buttons[current_key]->set_active(true); + prefs->setString("/dialogs/export/exportarea/value", pref_key_name); + + toggleSpinButtonVisibility(); +} + +void SingleExport::setExporting(bool exporting, Glib::ustring const &text) +{ + if (exporting) { + _prog->set_text(text); + _prog->set_fraction(0.0); + _prog->set_sensitive(true); + si_export->set_sensitive(false); + } else { + _prog->set_text(""); + _prog->set_fraction(0.0); + _prog->set_sensitive(false); + si_export->set_sensitive(true); + } +} + +ExportProgressDialog *SingleExport::create_progress_dialog(Glib::ustring progress_text) +{ + // dont forget to delete it later + auto dlg = new ExportProgressDialog(_("Export in progress"), true); + dlg->set_transient_for(*(INKSCAPE.active_desktop()->getToplevel())); + + Gtk::ProgressBar *prg = Gtk::manage(new Gtk::ProgressBar()); + prg->set_text(progress_text); + dlg->set_progress(prg); + auto CA = dlg->get_content_area(); + CA->pack_start(*prg, FALSE, FALSE, 4); + + Gtk::Button *btn = dlg->add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + + btn->signal_clicked().connect(sigc::mem_fun(*this, &SingleExport::onProgressCancel)); + dlg->signal_delete_event().connect(sigc::mem_fun(*this, &SingleExport::onProgressDelete)); + + dlg->show_all(); + return dlg; +} + +/// Called when dialog is deleted +bool SingleExport::onProgressDelete(GdkEventAny * /*event*/) +{ + interrupted = true; + prog_dlg->set_stopped(); + return TRUE; +} +/// Called when progress is cancelled +void SingleExport::onProgressCancel() +{ + interrupted = true; + prog_dlg->set_stopped(); +} + +// Called for every progress iteration +unsigned int SingleExport::onProgressCallback(float value, void *dlg) +{ + auto dlg2 = reinterpret_cast<ExportProgressDialog *>(dlg); + + auto self = dynamic_cast<SingleExport *>(dlg2->get_export_panel()); + + if (!self || self->interrupted) + return FALSE; + + auto current = dlg2->get_current(); + auto total = dlg2->get_total(); + if (total > 0) { + double completed = current; + completed /= static_cast<double>(total); + + value = completed + (value / static_cast<double>(total)); + } + + auto prg = dlg2->get_progress(); + prg->set_fraction(value); + + if (self) { + self->_prog->set_fraction(value); + } + + int evtcount = 0; + while ((evtcount < 16) && gdk_events_pending()) { + Gtk::Main::iteration(false); + evtcount += 1; + } + + Gtk::Main::iteration(false); + return TRUE; +} + +void SingleExport::refreshPreview() +{ + if (!_desktop) { + return; + } + if (!si_show_preview->get_active()) { + preview->resetPixels(); + return; + } + + std::vector<SPItem *> selected; + if (si_hide_all->get_active()) { + // This is because selection items is not a std::vector yet. FIXME. + selected = std::vector<SPItem *>(_desktop->getSelection()->items().begin(), _desktop->getSelection()->items().end()); + } + + Unit const *unit = units->getUnit(); + float x0 = unit->convert(spin_buttons[SPIN_X0]->get_value(), "px"); + float x1 = unit->convert(spin_buttons[SPIN_X1]->get_value(), "px"); + float y0 = unit->convert(spin_buttons[SPIN_Y0]->get_value(), "px"); + float y1 = unit->convert(spin_buttons[SPIN_Y1]->get_value(), "px"); + preview->setDbox(x0, x1, y0, y1); + preview->refreshHide(selected); + preview->queueRefresh(); +} + +void SingleExport::setDesktop(SPDesktop *desktop) +{ + if (desktop != _desktop) { + _page_selected_connection.disconnect(); + _desktop = desktop; + } +} + +void SingleExport::setDocument(SPDocument *document) +{ + _document = document; + _page_selected_connection.disconnect(); + if (document) { + // when the page selected is changes, update the export area + _page_selected_connection = document->getPageManager().connectPageSelected([=](SPPage *page) { + refreshPage(); + refresh(); + }); + } + preview->setDocument(document); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/export-single.h b/src/ui/dialog/export-single.h new file mode 100644 index 0000000..c7c5470 --- /dev/null +++ b/src/ui/dialog/export-single.h @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Anshudhar Kumar Singh <anshudhar2001@gmail.com> + * + * Copyright (C) 1999-2007, 2021 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_EXPORT_SINGLE_H +#define SP_EXPORT_SINGLE_H + +#include "ui/widget/scrollprotected.h" + +class InkscapeApplication; +class SPDesktop; +class SPDocument; +class SPObject; + +namespace Inkscape { + class Selection; + class Preferences; + +namespace Util { + class Unit; +} +namespace UI { + namespace Widget { + class UnitMenu; + } +namespace Dialog { + class ExportPreview; + class ExtensionList; + class ExportProgressDialog; + +class SingleExport : public Gtk::Box +{ +public: + SingleExport(){}; + SingleExport(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade) + : Gtk::Box(cobject){}; + ~SingleExport() override{}; + +private: + InkscapeApplication *_app = nullptr; + SPDesktop *_desktop = nullptr; + SPDocument *_document = nullptr; + +private: + bool setupDone = false; // To prevent setup() call add connections again. + +public: + void setApp(InkscapeApplication *app) { _app = app; } + void setDocument(SPDocument *document); + void setDesktop(SPDesktop *desktop); + void selectionChanged(Inkscape::Selection *selection); + void selectionModified(Inkscape::Selection *selection, guint flags); + +private: + enum sb_type + { + SPIN_X0 = 0, + SPIN_X1, + SPIN_Y0, + SPIN_Y1, + SPIN_WIDTH, + SPIN_HEIGHT, + SPIN_BMWIDTH, + SPIN_BMHEIGHT, + SPIN_DPI + }; + + enum selection_mode + { + SELECTION_PAGE = 0, // Default is alaways placed first + SELECTION_SELECTION, + SELECTION_DRAWING, + SELECTION_CUSTOM, + }; + +private: + typedef Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton> SpinButton; + + std::map<sb_type, SpinButton *> spin_buttons; + std::map<sb_type, Gtk::Label *> spin_labels; + std::map<selection_mode, Gtk::RadioButton *> selection_buttons; + + Gtk::Box *si_units_row = nullptr; + Gtk::CheckButton *show_export_area = nullptr; + Inkscape::UI::Widget::UnitMenu *units = nullptr; + Gtk::Label *si_name_label = nullptr; + + Gtk::CheckButton *si_hide_all = nullptr; + Gtk::CheckButton *si_show_preview = nullptr; + Gtk::CheckButton *si_default_opts = nullptr; + + ExportPreview *preview = nullptr; + + ExtensionList *si_extension_cb = nullptr; + Gtk::Entry *si_filename_entry = nullptr; + Gtk::Button *si_export = nullptr; + Gtk::Box *adv_box = nullptr; + Gtk::ProgressBar *_prog = nullptr; + Gtk::Button *page_prev = nullptr; + Gtk::Button *page_next = nullptr; + + bool filename_modified = false; + Glib::ustring original_name; + Glib::ustring doc_export_name; + + Inkscape::Preferences *prefs = nullptr; + std::map<selection_mode, Glib::ustring> selection_names; + selection_mode current_key = (selection_mode)0; + +public: + // initialise variables from builder + void initialise(const Glib::RefPtr<Gtk::Builder> &builder); + void setup(); + +private: + void setupUnits(); + void setupExtensionList(); + void setupSpinButtons(); + void toggleSpinButtonVisibility(); + void refreshPreview(); + + // change range and callbacks to spinbuttons + template <typename T> + void setupSpinButton(Gtk::SpinButton *sb, double val, double min, double max, double step, double page, int digits, + bool sensitive, void (SingleExport::*cb)(T), T param); + + void setDefaultSelectionMode(); + void onAreaXChange(sb_type type); + void onAreaYChange(sb_type type); + void onDpiChange(sb_type type); + void onAreaTypeToggle(selection_mode key); + void onUnitChanged(); + void onFilenameModified(); + void onExtensionChanged(); + void onExport(); + void onBrowse(Gtk::EntryIconPosition pos, const GdkEventButton *ev); + void on_inkscape_selection_modified(Inkscape::Selection *selection, guint flags); + void on_inkscape_selection_changed(Inkscape::Selection *selection); + +public: + void refresh() + { + refreshArea(); + refreshPage(); + loadExportHints(); + }; + +private: + void refreshArea(); + void refreshPage(); + void loadExportHints(); + void saveExportHints(SPObject *target); + void areaXChange(sb_type type); + void areaYChange(sb_type type); + void dpiChange(sb_type type); + void setArea(double x0, double y0, double x1, double y1); + void blockSpinConns(bool status); + +private: + void setExporting(bool exporting, Glib::ustring const &text = ""); + ExportProgressDialog *create_progress_dialog(Glib::ustring progress_text); + /** + * Callback to be used in for loop to update the progress bar. + * + * @param value number between 0 and 1 indicating the fraction of progress (0.17 = 17 % progress) + * @param dlg void pointer to the Gtk::Dialog progress dialog + */ + static unsigned int onProgressCallback(float value, void *dlg); + + /** + * Callback for pressing the cancel button. + */ + void onProgressCancel(); + + /** + * Callback invoked on closing the progress dialog. + */ + bool onProgressDelete(GdkEventAny *event); + +private: + ExportProgressDialog *prog_dlg = nullptr; + bool interrupted; + +private: + // Gtk Signals + std::vector<sigc::connection> spinButtonConns; + sigc::connection filenameConn; + sigc::connection extensionConn; + sigc::connection exportConn; + sigc::connection browseConn; + // Document Signals + sigc::connection _page_selected_connection; +}; +} // namespace Dialog +} // namespace UI +} // namespace Inkscape +#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/ui/dialog/export.cpp b/src/ui/dialog/export.cpp new file mode 100644 index 0000000..4895e8e --- /dev/null +++ b/src/ui/dialog/export.cpp @@ -0,0 +1,511 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Peter Bostrom + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * Anshudhar Kumar Singh <anshudhar2001@gmail.com> + * + * Copyright (C) 1999-2007, 2012, 2021 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +// This has to be included prior to anything that includes setjmp.h, it croaks otherwise +#include "export.h" + +#include <glibmm/convert.h> +#include <glibmm/i18n.h> +#include <glibmm/miscutils.h> +#include <gtkmm.h> +#include <png.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "extension/db.h" +#include "extension/output.h" +#include "file.h" +#include "helper/png-write.h" +#include "inkscape-window.h" +#include "inkscape.h" +#include "io/resource.h" +#include "io/sys.h" +#include "message-stack.h" +#include "object/object-set.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" +#include "object/sp-page.h" +#include "preferences.h" +#include "selection-chemistry.h" +#include "ui/dialog-events.h" +#include "ui/dialog/export-single.h" +#include "ui/dialog/export-batch.h" +#include "ui/dialog/dialog-notebook.h" +#include "ui/dialog/filedialog.h" +#include "ui/interface.h" +#include "ui/widget/scrollprotected.h" +#include "ui/widget/unit-menu.h" + +#ifdef _WIN32 + +#endif + +using Inkscape::Util::unit_table; +namespace Inkscape { +namespace UI { +namespace Dialog { + +Export::Export() + : DialogBase("/dialogs/export/", "Export") +{ + std::string gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-export.glade"); + + try { + builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_error("Glade file loading failed for export screen"); + return; + } + + prefs = Inkscape::Preferences::get(); + + builder->get_widget("Export Dialog Box", container); + add(*container); + show_all_children(); + + builder->get_widget("Export Notebook", export_notebook); + + // Initialise Single Export and its objects + builder->get_widget_derived("Single Image", single_image); + single_image->initialise(builder); + + // Initialise Batch Export and its objects + builder->get_widget_derived("Batch Export", batch_export); + batch_export->initialise(builder); + + container->signal_realize().connect([=]() { + single_image->setup(); + batch_export->setup(); + setDefaultNotebookPage(); + notebook_signal = export_notebook->signal_switch_page().connect(sigc::mem_fun(*this, &Export::onNotebookPageSwitch)); + }); + container->signal_unrealize().connect([=]() { + notebook_signal.disconnect(); + }); +} + +Export::~Export() +{ + single_image->setDocument(nullptr); + single_image->setDesktop(nullptr); + batch_export->setDocument(nullptr); + batch_export->setDesktop(nullptr); +} + +// Set current page based on preference/last visited page +void Export::setDefaultNotebookPage() +{ + pages[BATCH_EXPORT] = export_notebook->page_num(*batch_export); + pages[SINGLE_IMAGE] = export_notebook->page_num(*single_image); + export_notebook->set_current_page(pages[SINGLE_IMAGE]); +} + +void Export::documentReplaced() +{ + single_image->setDocument(getDocument()); + batch_export->setDocument(getDocument()); +} + +void Export::desktopReplaced() +{ + single_image->setDesktop(getDesktop()); + single_image->setApp(getApp()); + batch_export->setDesktop(getDesktop()); + batch_export->setApp(getApp()); + // Called previously, but we need post-desktop call too + documentReplaced(); +} + +void Export::selectionChanged(Inkscape::Selection *selection) +{ + auto current_page = export_notebook->get_current_page(); + if (current_page == pages[SINGLE_IMAGE]) { + single_image->selectionChanged(selection); + } + if (current_page == pages[BATCH_EXPORT]) { + batch_export->selectionChanged(selection); + } +} +void Export::selectionModified(Inkscape::Selection *selection, guint flags) +{ + auto current_page = export_notebook->get_current_page(); + if (current_page == pages[SINGLE_IMAGE]) { + single_image->selectionModified(selection, flags); + } + if (current_page == pages[BATCH_EXPORT]) { + batch_export->selectionModified(selection, flags); + } +} + +void Export::onNotebookPageSwitch(Widget *page, guint page_number) +{ + auto desktop = getDesktop(); + if (desktop) { + auto selection = desktop->getSelection(); + + if (page_number == pages[SINGLE_IMAGE]) { + single_image->selectionChanged(selection); + } + if (page_number == pages[BATCH_EXPORT]) { + batch_export->selectionChanged(selection); + } + } +} + +std::string Export::absolutizePath(SPDocument *doc, const std::string &filename) +{ + std::string path; + // Make relative paths go from the document location, if possible: + if (!Glib::path_is_absolute(filename) && doc->getDocumentFilename()) { + auto dirname = Glib::path_get_dirname(doc->getDocumentFilename()); + if (!dirname.empty()) { + path = Glib::build_filename(dirname, filename); + } + } + if (path.empty()) { + path = filename; + } + return path; +} + +bool Export::unConflictFilename(SPDocument *doc, Glib::ustring &filename, Glib::ustring const extension) +{ + std::string path = absolutizePath(doc, Glib::filename_from_utf8(filename)); + Glib::ustring test_filename = path + extension; + if (!Inkscape::IO::file_test(test_filename.c_str(), G_FILE_TEST_EXISTS)) { + filename = test_filename; + return true; + } + for (int i = 1; i <= 100; i++) { + test_filename = path + "_copy_" + std::to_string(i) + extension; + if (!Inkscape::IO::file_test(test_filename.c_str(), G_FILE_TEST_EXISTS)) { + filename = test_filename; + return true; + } + } + return false; +} + +bool Export::exportRaster( + Geom::Rect const &area, unsigned long int const &width, unsigned long int const &height, + float const &dpi, Glib::ustring const &filename, bool overwrite, + unsigned (*callback)(float, void *), ExportProgressDialog *&prog_dialog, + Inkscape::Extension::Output *extension, std::vector<SPItem *> *items) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) + return false; + SPDocument *doc = desktop->getDocument(); + + if (area.hasZeroArea() || width == 0 || height == 0) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("The chosen area to be exported is invalid.")); + sp_ui_error_dialog(_("The chosen area to be exported is invalid")); + return false; + } + if (filename.empty()) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("You have to enter a filename.")); + sp_ui_error_dialog(_("You have to enter a filename")); + return false; + } + + if (!extension || !extension->is_raster()) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Raster Export Error")); + sp_ui_error_dialog(_("Raster export Method is used for NON RASTER EXTENSION")); + return false; + } + + float pHYs = extension->get_param_float("png_phys", dpi); + if (pHYs < 0.01) pHYs = dpi; + + bool use_interlacing = extension->get_param_bool("png_interlacing", false); + int antialiasing = extension->get_param_int("png_antialias", 2); // Cairo anti aliasing + int zlib = extension->get_param_int("png_compression", 1); // Default is 6 for png, but 1 for non-png + auto val = extension->get_param_int("png_bitdepth", 99); // corresponds to RGBA 8 + + int bit_depth = pow(2, (val & 0x0F)); + int color_type = (val & 0xF0) >> 4; + + std::string path = absolutizePath(doc, Glib::filename_from_utf8(filename)); + Glib::ustring dirname = Glib::path_get_dirname(path); + + if (dirname.empty() || + !Inkscape::IO::file_test(dirname.c_str(), (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) { + Glib::ustring safeDir = Inkscape::IO::sanitizeString(dirname.c_str()); + Glib::ustring error = + g_strdup_printf(_("Directory <b>%s</b> does not exist or is not a directory.\n"), safeDir.c_str()); + + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str()); + sp_ui_error_dialog(error.c_str()); + return false; + } + + // Do the over-write protection now, since the png is just a temp file. + if (!overwrite && !sp_ui_overwrite_file(path.c_str())) { + return false; + } + + auto fn = Glib::path_get_basename(path); + auto png_filename = path; + { + // Select the extension and set the filename to a temporary file + int tempfd_out = Glib::file_open_tmp(png_filename, "ink_ext_"); + close(tempfd_out); + } + + // Export Start Here + std::vector<SPItem *> selected; + if (items && items->size() > 0) { + selected = *items; + } + + auto bg_color = doc->getPageManager().background_color; + ExportResult result = sp_export_png_file(desktop->getDocument(), png_filename.c_str(), area, width, height, pHYs, + pHYs, // previously xdpi, ydpi. + bg_color, callback, (void *)prog_dialog, true, selected, + use_interlacing, color_type, bit_depth, zlib, antialiasing); + + bool failed = result == EXPORT_ERROR || prog_dialog->get_stopped(); + delete prog_dialog; + prog_dialog = nullptr; + if (failed) { + Glib::ustring safeFile = Inkscape::IO::sanitizeString(path.c_str()); + Glib::ustring error = g_strdup_printf(_("Could not export to filename <b>%s</b>.\n"), safeFile.c_str()); + + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str()); + sp_ui_error_dialog(error.c_str()); + return false; + } else if (result == EXPORT_OK) { + // Don't ask for preferences on every run. + try { + extension->export_raster(doc, png_filename, path.c_str(), false); + } catch (Inkscape::Extension::Output::save_failed &e) { + return false; + } + } else { + // Extensions have their own error popup, so this only tracks failures in the png step + desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Export aborted.")); + return false; + } + + auto recentmanager = Gtk::RecentManager::get_default(); + if (recentmanager && Glib::path_is_absolute(path)) { + Glib::ustring uri = Glib::filename_to_uri(path); + recentmanager->add_item(uri); + } + + Glib::ustring safeFile = Inkscape::IO::sanitizeString(path.c_str()); + desktop->messageStack()->flashF(Inkscape::INFORMATION_MESSAGE, _("Drawing exported to <b>%s</b>."), + safeFile.c_str()); + + unlink(png_filename.c_str()); + return true; +} + +bool Export::exportVector( + Inkscape::Extension::Output *extension, SPDocument *doc, + Glib::ustring const &filename, + bool overwrite, std::vector<SPItem *> *items, SPPage *page) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) + return false; + + if (filename.empty()) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("You have to enter a filename.")); + sp_ui_error_dialog(_("You have to enter a filename")); + return false; + } + + if (!extension || extension->is_raster()) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Vector Export Error")); + sp_ui_error_dialog(_("Vector export Method is used for RASTER EXTENSION")); + return false; + } + + std::string path = absolutizePath(doc, Glib::filename_from_utf8(filename)); + Glib::ustring dirname = Glib::path_get_dirname(path); + Glib::ustring safeFile = Inkscape::IO::sanitizeString(path.c_str()); + Glib::ustring safeDir = Inkscape::IO::sanitizeString(dirname.c_str()); + + if (dirname.empty() || + !Inkscape::IO::file_test(dirname.c_str(), (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) { + Glib::ustring error = + g_strdup_printf(_("Directory <b>%s</b> does not exist or is not a directory.\n"), safeDir.c_str()); + + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str()); + sp_ui_error_dialog(error.c_str()); + + return false; + } + + // Do the over-write protection now + if (!overwrite && !sp_ui_overwrite_file(path.c_str())) { + return false; + } + doc->ensureUpToDate(); + auto copy_doc = doc->copy(); + copy_doc->ensureUpToDate(); + + std::vector<SPItem *> objects = *items; + std::set<std::string> page_ids; + if (page) { + // If page then our item set is limited to the overlapping items + auto page_items = page->getOverlappingItems(); + + if (items->size() == 0) { + // Items is page_items, remove all items not in this page. + objects = page_items; + } else { + for (auto &item : page_items) { + item->getIds(page_ids); + } + } + } + + // Save the page rect, must be done before disabledPages in case page is from copy doc. + Geom::OptRect page_rect = page ? page->getDesktopRect() : Geom::OptRect(); + + // We never export multiple pages here, must be done before fitToRect and fitCanvas + copy_doc->getPageManager().disablePages(); + + // Page export ALWAYS restricts, even if nothing would be on the page. + if (objects.size() > 0 || page) { + std::vector<SPObject *> objects_to_export; + Inkscape::ObjectSet object_set(copy_doc.get()); + for (auto &object : objects) { + auto _id = object->getId(); + if (!_id || (!page_ids.empty() && page_ids.find(_id) == page_ids.end())) { + // This item is off the page so can be ignored for export + continue; + } + + SPObject *obj = copy_doc->getObjectById(_id); + if (!obj) { + Glib::ustring error = g_strdup_printf(_("Could not export to filename <b>%s</b>. (missing object)\n"), safeFile.c_str()); + + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str()); + sp_ui_error_dialog(error.c_str()); + + return false; + } + copy_doc->ensureUpToDate(); + + object_set.add(obj, true); + objects_to_export.push_back(obj); + } + + copy_doc->getRoot()->cropToObjects(objects_to_export); + + if (page) { + // Resize to page here. + copy_doc->fitToRect(*page_rect, true); + } else { + object_set.fitCanvas(true, true); + } + } + + // Remove all unused definitions + copy_doc->vacuumDocument(); + + try { + extension->save(copy_doc.get(), path.c_str()); + } catch (Inkscape::Extension::Output::save_failed &e) { + Glib::ustring safeFile = Inkscape::IO::sanitizeString(path.c_str()); + Glib::ustring error = g_strdup_printf(_("Could not export to filename <b>%s</b>.\n"), safeFile.c_str()); + + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str()); + sp_ui_error_dialog(error.c_str()); + + return false; + } + + auto recentmanager = Gtk::RecentManager::get_default(); + if (recentmanager && Glib::path_is_absolute(path)) { + Glib::ustring uri = Glib::filename_to_uri(path); + recentmanager->add_item(uri); + } + + desktop->messageStack()->flashF(Inkscape::INFORMATION_MESSAGE, _("Drawing exported to <b>%s</b>."), + safeFile.c_str()); + return true; +} + +std::string Export::filePathFromObject(SPDocument *doc, SPObject *obj, const Glib::ustring &file_entry_text) +{ + Glib::ustring id = _("bitmap"); + if (obj && obj->getId()) { + id = obj->getId(); + } + return filePathFromId(doc, id, file_entry_text); +} + +std::string Export::filePathFromId(SPDocument *doc, Glib::ustring id, const Glib::ustring &file_entry_text) +{ + g_assert(!id.empty()); + + std::string directory; + + if (!file_entry_text.empty()) { + directory = Glib::path_get_dirname(Glib::filename_from_utf8(file_entry_text)); + } + + if (directory.empty()) { + /* Grab document directory */ + const gchar *docFilename = doc->getDocumentFilename(); + if (docFilename) { + directory = Glib::path_get_dirname(docFilename); + } + } + + if (directory.empty()) { + directory = Inkscape::IO::Resource::homedir_path(nullptr); + } + + return Glib::build_filename(directory, Glib::filename_from_utf8(id)); +} + +Glib::ustring Export::defaultFilename(SPDocument *doc, Glib::ustring &filename_entry_text, Glib::ustring extension) +{ + Glib::ustring filename; + if (doc && doc->getDocumentFilename()) { + filename = doc->getDocumentFilename(); + //appendExtensionToFilename(filename, extension); + } else if (doc) { + filename = filePathFromId(doc, _("bitmap"), filename_entry_text); + filename = filename + extension; + } + return filename; +} + + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/export.h b/src/ui/dialog/export.h new file mode 100644 index 0000000..fe5f024 --- /dev/null +++ b/src/ui/dialog/export.h @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Anshudhar Kumar Singh <anshudhar2001@gmail.com> + * + * Copyright (C) 1999-2007, 2021 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_EXPORT_H +#define SP_EXPORT_H + +#include <gtkmm.h> + +#include "ui/dialog/dialog-base.h" +#include "ui/widget/scrollprotected.h" + +class SPPage; + +namespace Inkscape { + class Preferences; + namespace Util { + class Unit; + } + namespace Extension { + class Output; + } + +namespace UI { +namespace Dialog { + class SingleExport; + class BatchExport; + +enum notebook_page +{ + SINGLE_IMAGE = 0, + BATCH_EXPORT +}; + +class ExportProgressDialog : public Gtk::Dialog +{ +private: + Gtk::ProgressBar *_progress = nullptr; + Gtk::Widget *_export_panel = nullptr; + int _current = 0; + int _total = 0; + bool _stopped = false; + +public: + ExportProgressDialog(const Glib::ustring &title, bool modal = false) + : Gtk::Dialog(title, modal) + {} + + inline void set_export_panel(const decltype(_export_panel) export_panel) { _export_panel = export_panel; } + inline decltype(_export_panel) get_export_panel() const { return _export_panel; } + + inline void set_progress(const decltype(_progress) progress) { _progress = progress; } + inline decltype(_progress) get_progress() const { return _progress; } + + inline void set_current(const int current) { _current = current; } + inline int get_current() const { return _current; } + + inline void set_total(const int total) { _total = total; } + inline int get_total() const { return _total; } + + inline bool get_stopped() const { return _stopped; } + inline void set_stopped() { _stopped = true; } +}; + +class Export : public DialogBase +{ +public: + Export(); + ~Export() override; + + static Export &getInstance() { return *new Export(); } + +private: + Glib::RefPtr<Gtk::Builder> builder; + Gtk::Box *container = nullptr; // Main Container + Gtk::Notebook *export_notebook = nullptr; // Notebook Container for single and batch export + + SingleExport *single_image = nullptr; + BatchExport *batch_export = nullptr; + + Inkscape::Preferences *prefs = nullptr; + + // setup default values of widgets + void setDefaultNotebookPage(); + std::map<notebook_page, int> pages; + + sigc::connection notebook_signal; + + // signals callback + void onNotebookPageSwitch(Widget *page, guint page_number); + void documentReplaced() override; + void desktopReplaced() override; + void selectionChanged(Inkscape::Selection *selection) override; + void selectionModified(Inkscape::Selection *selection, guint flags) override; + +public: + static std::string absolutizePath(SPDocument *doc, const std::string &filename); + static bool unConflictFilename(SPDocument *doc, Glib::ustring &filename, Glib::ustring const extension); + static std::string filePathFromObject(SPDocument *doc, SPObject *obj, const Glib::ustring &file_entry_text); + static std::string filePathFromId(SPDocument *doc, Glib::ustring id, const Glib::ustring &file_entry_text); + static Glib::ustring defaultFilename(SPDocument *doc, Glib::ustring &filename_entry_text, Glib::ustring extension); + + static bool exportRaster( + Geom::Rect const &area, unsigned long int const &width, unsigned long int const &height, + float const &dpi, Glib::ustring const &filename, bool overwrite, + unsigned (*callback)(float, void *), ExportProgressDialog *&prog_dialog, + Inkscape::Extension::Output *extension, std::vector<SPItem *> *items = nullptr); + + static bool exportVector( + Inkscape::Extension::Output *extension, SPDocument *doc, Glib::ustring const &filename, + bool overwrite, std::vector<SPItem *> *items = nullptr, SPPage *page = nullptr); + +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape +#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/ui/dialog/filedialog.cpp b/src/ui/dialog/filedialog.cpp new file mode 100644 index 0000000..fdce498 --- /dev/null +++ b/src/ui/dialog/filedialog.cpp @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Implementation of the file dialog interfaces defined in filedialog.h. + */ +/* Authors: + * Bob Jamison + * Joel Holdsworth + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004-2007 Bob Jamison + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2007-2008 Joel Holdsworth + * Copyright (C) 2004-2008 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef _WIN32 +# include "filedialogimpl-win32.h" +# include "preferences.h" +#endif + +#include "filedialogimpl-gtkmm.h" + +#include "ui/dialog-events.h" +#include "extension/output.h" + +#include <glibmm/convert.h> + +namespace Inkscape +{ +namespace UI +{ +namespace Dialog +{ + +/*######################################################################### +### U T I L I T Y +#########################################################################*/ + +bool hasSuffix(const Glib::ustring &str, const Glib::ustring &ext) +{ + int strLen = str.length(); + int extLen = ext.length(); + if (extLen > strLen) + return false; + int strpos = strLen-1; + for (int extpos = extLen-1 ; extpos>=0 ; extpos--, strpos--) + { + Glib::ustring::value_type ch = str[strpos]; + if (ch != ext[extpos]) + { + if ( ((ch & 0xff80) != 0) || + static_cast<Glib::ustring::value_type>( g_ascii_tolower( static_cast<gchar>(0x07f & ch) ) ) != ext[extpos] ) + { + return false; + } + } + } + return true; +} + +bool isValidImageFile(const Glib::ustring &fileName) +{ + std::vector<Gdk::PixbufFormat>formats = Gdk::Pixbuf::get_formats(); + for (auto format : formats) + { + std::vector<Glib::ustring>extensions = format.get_extensions(); + for (auto ext : extensions) + { + if (hasSuffix(fileName, ext)) + return true; + } + } + return false; +} + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +/** + * Public factory. Called by file.cpp, among others. + */ +FileOpenDialog *FileOpenDialog::create(Gtk::Window &parentWindow, + const Glib::ustring &path, + FileDialogType fileTypes, + const char *title) +{ +#ifdef _WIN32 + FileOpenDialog *dialog = NULL; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool( "/options/desktopintegration/value")) { + dialog = new FileOpenDialogImplWin32(parentWindow, path, fileTypes, title); + } else { + dialog = new FileOpenDialogImplGtk(parentWindow, path, fileTypes, title); + } +#else + FileOpenDialog *dialog = new FileOpenDialogImplGtk(parentWindow, path, fileTypes, title); +#endif + + return dialog; +} + +Glib::ustring FileOpenDialog::getFilename() +{ + return myFilename; +} + +//######################################################################## +//# F I L E S A V E +//######################################################################## + +/** + * Public factory method. Used in file.cpp + */ +FileSaveDialog *FileSaveDialog::create(Gtk::Window& parentWindow, + const Glib::ustring &path, + FileDialogType fileTypes, + const char *title, + const Glib::ustring &default_key, + const gchar *docTitle, + const Inkscape::Extension::FileSaveMethod save_method) +{ +#ifdef _WIN32 + FileSaveDialog *dialog = NULL; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool( "/options/desktopintegration/value")) { + dialog = new FileSaveDialogImplWin32(parentWindow, path, fileTypes, title, default_key, docTitle, save_method); + } else { + dialog = new FileSaveDialogImplGtk(parentWindow, path, fileTypes, title, default_key, docTitle, save_method); + } +#else + FileSaveDialog *dialog = new FileSaveDialogImplGtk(parentWindow, path, fileTypes, title, default_key, docTitle, save_method); +#endif + return dialog; +} + +Glib::ustring FileSaveDialog::getFilename() +{ + return myFilename; +} + +Glib::ustring FileSaveDialog::getDocTitle() +{ + return myDocTitle; +} + +//void FileSaveDialog::change_path(const Glib::ustring& path) +//{ +// myFilename = path; +//} + +void FileSaveDialog::appendExtension(Glib::ustring& path, Inkscape::Extension::Output* outputExtension) +{ + if (!outputExtension) + return; + + try { + bool appendExtension = true; + Glib::ustring utf8Name = Glib::filename_to_utf8( path ); + Glib::ustring::size_type pos = utf8Name.rfind('.'); + if ( pos != Glib::ustring::npos ) { + Glib::ustring trail = utf8Name.substr( pos ); + Glib::ustring foldedTrail = trail.casefold(); + if ( (trail == ".") + | (foldedTrail != Glib::ustring( outputExtension->get_extension() ).casefold() + && ( knownExtensions.find(foldedTrail) != knownExtensions.end() ) ) ) { + utf8Name = utf8Name.erase( pos ); + } else { + appendExtension = false; + } + } + + if (appendExtension) { + utf8Name = utf8Name + outputExtension->get_extension(); + myFilename = Glib::filename_from_utf8( utf8Name ); + } + } catch ( Glib::ConvertError& e ) { + // ignore + } +} + + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/filedialog.h b/src/ui/dialog/filedialog.h new file mode 100644 index 0000000..8521f01 --- /dev/null +++ b/src/ui/dialog/filedialog.h @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Virtual base definitions for native file dialogs + */ +/* Authors: + * Bob Jamison <rwjj@earthlink.net> + * Joel Holdsworth + * Inkscape Guys + * + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2007-2008 Joel Holdsworth + * Copyright (C) 2004-2008, Inkscape Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __FILE_DIALOG_H__ +#define __FILE_DIALOG_H__ + +#include <vector> +#include <set> + +#include "extension/system.h" + +#include <glibmm/ustring.h> + +class SPDocument; + +namespace Inkscape { +namespace Extension { +class Extension; +class Output; +} +} + +namespace Inkscape +{ +namespace UI +{ +namespace Dialog +{ + +/** + * Used for setting filters and options, and + * reading them back from user selections. + */ +enum FileDialogType +{ + SVG_TYPES, + IMPORT_TYPES, + EXPORT_TYPES, + RASTER_TYPES, + EXE_TYPES, + SWATCH_TYPES, + CUSTOM_TYPE +}; + +/** + * Used for returning the type selected in a SaveAs + */ +enum FileDialogSelectionType { + SVG_NAMESPACE, + SVG_NAMESPACE_WITH_EXTENSIONS + }; + + +/** + * Return true if the string ends with the given suffix + */ +bool hasSuffix(const Glib::ustring &str, const Glib::ustring &ext); + +/** + * Return true if the image is loadable by Gdk, else false + */ +bool isValidImageFile(const Glib::ustring &fileName); + +/** + * This class provides an implementation-independent API for + * file "Open" dialogs. Using a standard interface obviates the need + * for ugly #ifdefs in file open code + */ +class FileOpenDialog +{ +public: + + + /** + * Constructor .. do not call directly + * @param path the directory where to start searching + * @param fileTypes one of FileDialogTypes + * @param title the title of the dialog + */ + FileOpenDialog() + = default;; + + /** + * Factory. + * @param path the directory where to start searching + * @param fileTypes one of FileDialogTypes + * @param title the title of the dialog + */ + static FileOpenDialog *create(Gtk::Window& parentWindow, + const Glib::ustring &path, + FileDialogType fileTypes, + const char *title); + + + /** + * Destructor. + * Perform any necessary cleanups. + */ + virtual ~FileOpenDialog() = default;; + + /** + * Show an OpenFile file selector. + * @return the selected path if user selected one, else NULL + */ + virtual bool show() = 0; + + /** + * Return the 'key' (filetype) of the selection, if any + * @return a pointer to a string if successful (which must + * be later freed with g_free(), else NULL. + */ + virtual Inkscape::Extension::Extension * getSelectionType() = 0; + + Glib::ustring getFilename(); + + virtual std::vector<Glib::ustring> getFilenames() = 0; + + virtual Glib::ustring getCurrentDirectory() = 0; + + virtual void addFilterMenu(Glib::ustring name, Glib::ustring pattern) = 0; + +protected: + /** + * Filename that was given + */ + Glib::ustring myFilename; + +}; //FileOpenDialog + + + + + + +/** + * This class provides an implementation-independent API for + * file "Save" dialogs. + */ +class FileSaveDialog +{ +public: + + /** + * Constructor. Do not call directly . Use the factory. + * @param path the directory where to start searching + * @param fileTypes one of FileDialogTypes + * @param title the title of the dialog + * @param key a list of file types from which the user can select + */ + FileSaveDialog () + = default;; + + /** + * Factory. + * @param path the directory where to start searching + * @param fileTypes one of FileDialogTypes + * @param title the title of the dialog + * @param key a list of file types from which the user can select + */ + static FileSaveDialog *create(Gtk::Window& parentWindow, + const Glib::ustring &path, + FileDialogType fileTypes, + const char *title, + const Glib::ustring &default_key, + const gchar *docTitle, + const Inkscape::Extension::FileSaveMethod save_method); + + + /** + * Destructor. + * Perform any necessary cleanups. + */ + virtual ~FileSaveDialog() = default;; + + + /** + * Show an SaveAs file selector. + * @return the selected path if user selected one, else NULL + */ + virtual bool show() =0; + + /** + * Return the 'key' (filetype) of the selection, if any + * @return a pointer to a string if successful (which must + * be later freed with g_free(), else NULL. + */ + virtual Inkscape::Extension::Extension * getSelectionType() = 0; + + virtual void setSelectionType( Inkscape::Extension::Extension * key ) = 0; + + /** + * Get the file name chosen by the user. Valid after an [OK] + */ + Glib::ustring getFilename (); + + /** + * Get the document title chosen by the user. Valid after an [OK] + */ + Glib::ustring getDocTitle (); + + virtual Glib::ustring getCurrentDirectory() = 0; + + virtual void addFileType(Glib::ustring name, Glib::ustring pattern) = 0; + +protected: + + /** + * Filename that was given + */ + Glib::ustring myFilename; + + /** + * Doc Title that was given + */ + Glib::ustring myDocTitle; + + /** + * List of known file extensions. + */ + std::map<Glib::ustring, Inkscape::Extension::Output*> knownExtensions; + + + void appendExtension(Glib::ustring& path, Inkscape::Extension::Output* outputExtension); + +}; //FileSaveDialog + + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif /* __FILE_DIALOG_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/ui/dialog/filedialogimpl-gtkmm.cpp b/src/ui/dialog/filedialogimpl-gtkmm.cpp new file mode 100644 index 0000000..c3aba06 --- /dev/null +++ b/src/ui/dialog/filedialogimpl-gtkmm.cpp @@ -0,0 +1,867 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Implementation of the file dialog interfaces defined in filedialogimpl.h. + */ +/* Authors: + * Bob Jamison + * Joel Holdsworth + * Bruno Dilly + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * + * Copyright (C) 2004-2007 Bob Jamison + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2007-2008 Joel Holdsworth + * Copyright (C) 2004-2007 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iostream> + +#include <glibmm/convert.h> +#include <glibmm/fileutils.h> +#include <glibmm/i18n.h> +#include <glibmm/miscutils.h> +#include <glibmm/regex.h> +#include <gtkmm/expander.h> + +#include "filedialogimpl-gtkmm.h" + +#include "document.h" +#include "inkscape.h" +#include "path-prefix.h" +#include "preferences.h" + +#include "extension/db.h" +#include "extension/input.h" +#include "extension/output.h" + +#include "io/resource.h" +#include "io/sys.h" + +#include "ui/dialog-events.h" +#include "ui/view/svg-view-widget.h" + +// Routines from file.cpp +#undef INK_DUMP_FILENAME_CONV + +#ifdef INK_DUMP_FILENAME_CONV +void dump_str(const gchar *str, const gchar *prefix); +void dump_ustr(const Glib::ustring &ustr); +#endif + + + +namespace Inkscape { +namespace UI { +namespace Dialog { + + + +//######################################################################## +//### U T I L I T Y +//######################################################################## + +void fileDialogExtensionToPattern(Glib::ustring &pattern, Glib::ustring &extension) +{ + for (unsigned int ch : extension) { + if (Glib::Unicode::isalpha(ch)) { + pattern += '['; + pattern += Glib::Unicode::toupper(ch); + pattern += Glib::Unicode::tolower(ch); + pattern += ']'; + } else { + pattern += ch; + } + } +} + + +void findEntryWidgets(Gtk::Container *parent, std::vector<Gtk::Entry *> &result) +{ + if (!parent) { + return; + } + std::vector<Gtk::Widget *> children = parent->get_children(); + for (auto child : children) { + GtkWidget *wid = child->gobj(); + if (GTK_IS_ENTRY(wid)) + result.push_back(dynamic_cast<Gtk::Entry *>(child)); + else if (GTK_IS_CONTAINER(wid)) + findEntryWidgets(dynamic_cast<Gtk::Container *>(child), result); + } +} + +void findExpanderWidgets(Gtk::Container *parent, std::vector<Gtk::Expander *> &result) +{ + if (!parent) + return; + std::vector<Gtk::Widget *> children = parent->get_children(); + for (auto child : children) { + GtkWidget *wid = child->gobj(); + if (GTK_IS_EXPANDER(wid)) + result.push_back(dynamic_cast<Gtk::Expander *>(child)); + else if (GTK_IS_CONTAINER(wid)) + findExpanderWidgets(dynamic_cast<Gtk::Container *>(child), result); + } +} + + +/*######################################################################### +### F I L E D I A L O G B A S E C L A S S +#########################################################################*/ + +void FileDialogBaseGtk::internalSetup() +{ + // Open executable file dialogs don't need the preview panel + if (_dialogType != EXE_TYPES) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool enablePreview = prefs->getBool(preferenceBase + "/enable_preview", true); + bool enableSVGExport = prefs->getBool(preferenceBase + "/enable_svgexport", false); + + previewCheckbox.set_label(Glib::ustring(_("Enable preview"))); + previewCheckbox.set_active(enablePreview); + + previewCheckbox.signal_toggled().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::_updatePreviewCallback)); + + svgexportCheckbox.set_label(Glib::ustring(_("Export as SVG 1.1 per settings in Preferences dialog"))); + svgexportCheckbox.set_active(enableSVGExport); + + svgexportCheckbox.signal_toggled().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::_svgexportEnabledCB)); + + // Catch selection-changed events, so we can adjust the text widget + signal_update_preview().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::_updatePreviewCallback)); + + //###### Add a preview widget + set_preview_widget(svgPreview); + set_preview_widget_active(enablePreview); + set_use_preview_label(false); + } +} + + +void FileDialogBaseGtk::cleanup(bool showConfirmed) +{ + if (_dialogType != EXE_TYPES) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (showConfirmed) { + prefs->setBool(preferenceBase + "/enable_preview", previewCheckbox.get_active()); + } + } +} + + +void FileDialogBaseGtk::_svgexportEnabledCB() +{ + bool enabled = svgexportCheckbox.get_active(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool(preferenceBase + "/enable_svgexport", enabled); +} + + + +/** + * Callback for checking if the preview needs to be redrawn + */ +void FileDialogBaseGtk::_updatePreviewCallback() +{ + bool enabled = previewCheckbox.get_active(); + + set_preview_widget_active(enabled); + + if (!enabled) + return; + + Glib::ustring fileName = get_preview_filename(); + if (fileName.empty()) { + fileName = get_preview_uri(); + } + + if (!fileName.empty()) { + svgPreview.set(fileName, _dialogType); + } else { + svgPreview.showNoPreview(); + } +} + + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +/** + * Constructor. Not called directly. Use the factory. + */ +FileOpenDialogImplGtk::FileOpenDialogImplGtk(Gtk::Window &parentWindow, const Glib::ustring &dir, + FileDialogType fileTypes, const Glib::ustring &title) + : FileDialogBaseGtk(parentWindow, title, Gtk::FILE_CHOOSER_ACTION_OPEN, fileTypes, "/dialogs/open") +{ + + + if (_dialogType == EXE_TYPES) { + /* One file at a time */ + set_select_multiple(false); + } else { + /* And also Multiple Files */ + set_select_multiple(true); + } + + set_local_only(false); + + /* Initialize to Autodetect */ + extension = nullptr; + /* No filename to start out with */ + myFilename = ""; + + /* Set our dialog type (open, import, etc...)*/ + _dialogType = fileTypes; + + + /* Set the pwd and/or the filename */ + if (dir.size() > 0) { + Glib::ustring udir(dir); + Glib::ustring::size_type len = udir.length(); + // leaving a trailing backslash on the directory name leads to the infamous + // double-directory bug on win32 + if (len != 0 && udir[len - 1] == '\\') + udir.erase(len - 1); + if (_dialogType == EXE_TYPES) { + set_filename(udir.c_str()); + } else { + set_current_folder(udir.c_str()); + } + } + + if (_dialogType != EXE_TYPES) { + set_extra_widget(previewCheckbox); + } + + //###### Add the file types menu + createFilterMenu(); + + add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + set_default(*add_button(_("_Open"), Gtk::RESPONSE_OK)); + + //###### Allow easy access to our examples folder + + using namespace Inkscape::IO::Resource; + auto examplesdir = get_path_string(SYSTEM, EXAMPLES); + if (Glib::file_test(examplesdir, Glib::FILE_TEST_IS_DIR) && // + Glib::path_is_absolute(examplesdir)) { + add_shortcut_folder(examplesdir); + } +} + +/** + * Destructor + */ +FileOpenDialogImplGtk::~FileOpenDialogImplGtk() += default; + +void FileOpenDialogImplGtk::addFilterMenu(Glib::ustring name, Glib::ustring pattern) +{ + auto allFilter = Gtk::FileFilter::create(); + allFilter->set_name(_(name.c_str())); + allFilter->add_pattern(pattern); + extensionMap[Glib::ustring(_("All Files"))] = nullptr; + add_filter(allFilter); +} + +void FileOpenDialogImplGtk::createFilterMenu() +{ + if (_dialogType == CUSTOM_TYPE) { + return; + } + + if (_dialogType == EXE_TYPES) { + auto allFilter = Gtk::FileFilter::create(); + allFilter->set_name(_("All Files")); + allFilter->add_pattern("*"); + extensionMap[Glib::ustring(_("All Files"))] = nullptr; + add_filter(allFilter); + } else { + auto allInkscapeFilter = Gtk::FileFilter::create(); + allInkscapeFilter->set_name(_("All Inkscape Files")); + + auto allFilter = Gtk::FileFilter::create(); + allFilter->set_name(_("All Files")); + allFilter->add_pattern("*"); + + auto allImageFilter = Gtk::FileFilter::create(); + allImageFilter->set_name(_("All Images")); + + auto allVectorFilter = Gtk::FileFilter::create(); + allVectorFilter->set_name(_("All Vectors")); + + auto allBitmapFilter = Gtk::FileFilter::create(); + allBitmapFilter->set_name(_("All Bitmaps")); + extensionMap[Glib::ustring(_("All Inkscape Files"))] = nullptr; + add_filter(allInkscapeFilter); + + extensionMap[Glib::ustring(_("All Files"))] = nullptr; + add_filter(allFilter); + + extensionMap[Glib::ustring(_("All Images"))] = nullptr; + add_filter(allImageFilter); + + extensionMap[Glib::ustring(_("All Vectors"))] = nullptr; + add_filter(allVectorFilter); + + extensionMap[Glib::ustring(_("All Bitmaps"))] = nullptr; + add_filter(allBitmapFilter); + + // patterns added dynamically below + Inkscape::Extension::DB::InputList extension_list; + Inkscape::Extension::db.get_input_list(extension_list); + + for (auto imod : extension_list) + { + // FIXME: would be nice to grey them out instead of not listing them + if (imod->deactivated()) + continue; + + Glib::ustring upattern("*"); + Glib::ustring extension = imod->get_extension(); + fileDialogExtensionToPattern(upattern, extension); + + Glib::ustring uname(imod->get_filetypename(true)); + + auto filter = Gtk::FileFilter::create(); + filter->set_name(uname); + filter->add_pattern(upattern); + add_filter(filter); + extensionMap[uname] = imod; + +// g_message("ext %s:%s '%s'\n", ioext->name, ioext->mimetype, upattern.c_str()); + allInkscapeFilter->add_pattern(upattern); + if (strncmp("image", imod->get_mimetype(), 5) == 0) + allImageFilter->add_pattern(upattern); + + // uncomment this to find out all mime types supported by Inkscape import/open + // g_print ("%s\n", imod->get_mimetype()); + + // I don't know of any other way to define "bitmap" formats other than by listing them + if (strncmp("image/png", imod->get_mimetype(), 9) == 0 || + strncmp("image/jpeg", imod->get_mimetype(), 10) == 0 || + strncmp("image/gif", imod->get_mimetype(), 9) == 0 || + strncmp("image/x-icon", imod->get_mimetype(), 12) == 0 || + strncmp("image/x-navi-animation", imod->get_mimetype(), 22) == 0 || + strncmp("image/x-cmu-raster", imod->get_mimetype(), 18) == 0 || + strncmp("image/x-xpixmap", imod->get_mimetype(), 15) == 0 || + strncmp("image/bmp", imod->get_mimetype(), 9) == 0 || + strncmp("image/vnd.wap.wbmp", imod->get_mimetype(), 18) == 0 || + strncmp("image/tiff", imod->get_mimetype(), 10) == 0 || + strncmp("image/x-xbitmap", imod->get_mimetype(), 15) == 0 || + strncmp("image/x-tga", imod->get_mimetype(), 11) == 0 || + strncmp("image/x-pcx", imod->get_mimetype(), 11) == 0) + { + allBitmapFilter->add_pattern(upattern); + } else { + allVectorFilter->add_pattern(upattern); + } + } + } + return; +} + +/** + * Show this dialog modally. Return true if user hits [OK] + */ +bool FileOpenDialogImplGtk::show() +{ + set_modal(TRUE); // Window + sp_transientize(GTK_WIDGET(gobj())); // Make transient + gint b = run(); // Dialog + svgPreview.showNoPreview(); + hide(); + + if (b == Gtk::RESPONSE_OK) { + // This is a hack, to avoid the warning messages that + // Gtk::FileChooser::get_filter() returns + // should be: Gtk::FileFilter *filter = get_filter(); + GtkFileChooser *gtkFileChooser = Gtk::FileChooser::gobj(); + GtkFileFilter *filter = gtk_file_chooser_get_filter(gtkFileChooser); + if (filter) { + // Get which extension was chosen, if any + extension = extensionMap[gtk_file_filter_get_name(filter)]; + } + myFilename = get_filename(); + + if (myFilename.empty()) { + myFilename = get_uri(); + } + + cleanup(true); + return true; + } else { + cleanup(false); + return false; + } +} + + + +/** + * Get the file extension type that was selected by the user. Valid after an [OK] + */ +Inkscape::Extension::Extension *FileOpenDialogImplGtk::getSelectionType() +{ + return extension; +} + + +/** + * Get the file name chosen by the user. Valid after an [OK] + */ +Glib::ustring FileOpenDialogImplGtk::getFilename() +{ + return myFilename; +} + + +/** + * To Get Multiple filenames selected at-once. + */ +std::vector<Glib::ustring> FileOpenDialogImplGtk::getFilenames() +{ + auto result_tmp = get_filenames(); + + // Copy filenames to a vector of type Glib::ustring + std::vector<Glib::ustring> result; + + for (auto it : result_tmp) + result.emplace_back(it); + + if (result.empty()) { + result = get_uris(); + } + + return result; +} + +Glib::ustring FileOpenDialogImplGtk::getCurrentDirectory() +{ + return get_current_folder(); +} + + + +//######################################################################## +//# F I L E S A V E +//######################################################################## + +/** + * Constructor + */ +FileSaveDialogImplGtk::FileSaveDialogImplGtk(Gtk::Window &parentWindow, const Glib::ustring &dir, + FileDialogType fileTypes, const Glib::ustring &title, + const Glib::ustring & /*default_key*/, const gchar *docTitle, + const Inkscape::Extension::FileSaveMethod save_method) + : FileDialogBaseGtk(parentWindow, title, Gtk::FILE_CHOOSER_ACTION_SAVE, fileTypes, + (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) ? "/dialogs/save_copy" + : "/dialogs/save_as") + , save_method(save_method) + , fromCB(false) + , checksBox(Gtk::ORIENTATION_VERTICAL) + , childBox(Gtk::ORIENTATION_HORIZONTAL) +{ + FileSaveDialog::myDocTitle = docTitle; + + /* One file at a time */ + set_select_multiple(false); + + set_local_only(false); + + /* Initialize to Autodetect */ + extension = nullptr; + /* No filename to start out with */ + myFilename = ""; + + /* Set our dialog type (save, export, etc...)*/ + _dialogType = fileTypes; + + /* Set the pwd and/or the filename */ + if (dir.size() > 0) { + Glib::ustring udir(dir); + Glib::ustring::size_type len = udir.length(); + // leaving a trailing backslash on the directory name leads to the infamous + // double-directory bug on win32 + if ((len != 0) && (udir[len - 1] == '\\')) { + udir.erase(len - 1); + } + myFilename = udir; + } + + //###### Do we want the .xxx extension automatically added? + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + fileTypeCheckbox.set_label(Glib::ustring(_("Append filename extension automatically"))); + if (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) { + fileTypeCheckbox.set_active(prefs->getBool("/dialogs/save_copy/append_extension", true)); + } else { + fileTypeCheckbox.set_active(prefs->getBool("/dialogs/save_as/append_extension", true)); + } + + fileTypeComboBox.set_size_request(200, 40); + fileTypeComboBox.signal_changed().connect(sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileTypeChangedCallback)); + + if (_dialogType != CUSTOM_TYPE) + createFilterMenu(); + + childBox.pack_start(checksBox); + childBox.pack_end(fileTypeComboBox); + checksBox.pack_start(fileTypeCheckbox); + checksBox.pack_start(previewCheckbox); + checksBox.pack_start(svgexportCheckbox); + + set_extra_widget(childBox); + + // Let's do some customization + fileNameEntry = nullptr; + Gtk::Container *cont = get_toplevel(); + std::vector<Gtk::Entry *> entries; + findEntryWidgets(cont, entries); + // g_message("Found %d entry widgets\n", entries.size()); + if (!entries.empty()) { + // Catch when user hits [return] on the text field + fileNameEntry = entries[0]; + fileNameEntry->signal_activate().connect( + sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileNameEntryChangedCallback)); + } + signal_selection_changed().connect( + sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileNameChanged)); + + // Let's do more customization + std::vector<Gtk::Expander *> expanders; + findExpanderWidgets(cont, expanders); + // g_message("Found %d expander widgets\n", expanders.size()); + if (!expanders.empty()) { + // Always show the file list + Gtk::Expander *expander = expanders[0]; + expander->set_expanded(true); + } + + // allow easy access to the user's own templates folder + using namespace Inkscape::IO::Resource; + char const *templates = Inkscape::IO::Resource::get_path(USER, TEMPLATES); + if (Inkscape::IO::file_test(templates, G_FILE_TEST_EXISTS) && + Inkscape::IO::file_test(templates, G_FILE_TEST_IS_DIR) && g_path_is_absolute(templates)) { + add_shortcut_folder(templates); + } + + // if (extension == NULL) + // checkbox.set_sensitive(FALSE); + + add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + set_default(*add_button(_("_Save"), Gtk::RESPONSE_OK)); + + show_all_children(); +} + +/** + * Destructor + */ +FileSaveDialogImplGtk::~FileSaveDialogImplGtk() += default; + +/** + * Callback for fileNameEntry widget + */ +void FileSaveDialogImplGtk::fileNameEntryChangedCallback() +{ + if (!fileNameEntry) + return; + + Glib::ustring fileName = fileNameEntry->get_text(); + if (!Glib::get_charset()) // If we are not utf8 + fileName = Glib::filename_to_utf8(fileName); + + // g_message("User hit return. Text is '%s'\n", fileName.c_str()); + + if (!Glib::path_is_absolute(fileName)) { + // try appending to the current path + // not this way: fileName = get_current_folder() + "/" + fileName; + std::vector<Glib::ustring> pathSegments; + pathSegments.emplace_back(get_current_folder()); + pathSegments.push_back(fileName); + fileName = Glib::build_filename(pathSegments); + } + + // g_message("path:'%s'\n", fileName.c_str()); + + if (Glib::file_test(fileName, Glib::FILE_TEST_IS_DIR)) { + set_current_folder(fileName); + } else if (/*Glib::file_test(fileName, Glib::FILE_TEST_IS_REGULAR)*/ true) { + // dialog with either (1) select a regular file or (2) cd to dir + // simulate an 'OK' + set_filename(fileName); + response(Gtk::RESPONSE_OK); + } +} + + + +/** + * Callback for fileNameEntry widget + */ +void FileSaveDialogImplGtk::fileTypeChangedCallback() +{ + int sel = fileTypeComboBox.get_active_row_number(); + if ((sel < 0) || (sel >= (int)fileTypes.size())) + return; + + FileType type = fileTypes[sel]; + // g_message("selected: %s\n", type.name.c_str()); + + extension = type.extension; + auto filter = Gtk::FileFilter::create(); + filter->add_pattern(type.pattern); + set_filter(filter); + + if (fromCB) { + //do not update if called from a name change + fromCB = false; + return; + } + + updateNameAndExtension(); +} + +void FileSaveDialogImplGtk::fileNameChanged() { + Glib::ustring name = get_filename(); + Glib::ustring::size_type pos = name.rfind('.'); + if ( pos == Glib::ustring::npos ) return; + Glib::ustring ext = name.substr( pos ).casefold(); + if (extension && Glib::ustring(static_cast<Inkscape::Extension::Output *>(extension)->get_extension()).casefold() == ext ) return; + if (knownExtensions.find(ext) == knownExtensions.end()) return; + fromCB = true; + fileTypeComboBox.set_active_text(knownExtensions[ext]->get_filetypename(true)); +} + +void FileSaveDialogImplGtk::addFileType(Glib::ustring name, Glib::ustring pattern) +{ + //#Let user choose + FileType guessType; + guessType.name = name; + guessType.pattern = pattern; + guessType.extension = nullptr; + fileTypeComboBox.append(guessType.name); + fileTypes.push_back(guessType); + + + fileTypeComboBox.set_active(0); + fileTypeChangedCallback(); // call at least once to set the filter +} + +void FileSaveDialogImplGtk::createFilterMenu() +{ + Inkscape::Extension::DB::OutputList extension_list; + Inkscape::Extension::db.get_output_list(extension_list); + knownExtensions.clear(); + + bool is_raster = _dialogType == RASTER_TYPES; + + for (auto omod : extension_list) { + // FIXME: would be nice to grey them out instead of not listing them + if (omod->deactivated() || (omod->is_raster() != is_raster)) + continue; + + // This extension is limited to save copy only. + if (omod->savecopy_only() && save_method != Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) + continue; + + FileType type; + type.name = omod->get_filetypename(true); + type.pattern = "*"; + Glib::ustring extension = omod->get_extension(); + knownExtensions.insert(std::pair<Glib::ustring, Inkscape::Extension::Output*>(extension.casefold(), omod)); + fileDialogExtensionToPattern(type.pattern, extension); + type.extension = omod; + fileTypeComboBox.append(type.name); + fileTypes.push_back(type); + } + + //#Let user choose + FileType guessType; + guessType.name = _("Guess from extension"); + guessType.pattern = "*"; + guessType.extension = nullptr; + fileTypeComboBox.append(guessType.name); + fileTypes.push_back(guessType); + + + fileTypeComboBox.set_active(0); + fileTypeChangedCallback(); // call at least once to set the filter +} + + + +/** + * Show this dialog modally. Return true if user hits [OK] + */ +bool FileSaveDialogImplGtk::show() +{ + change_path(myFilename); + set_modal(TRUE); // Window + sp_transientize(GTK_WIDGET(gobj())); // Make transient + gint b = run(); // Dialog + svgPreview.showNoPreview(); + set_preview_widget_active(false); + hide(); + + if (b == Gtk::RESPONSE_OK) { + updateNameAndExtension(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Store changes of the "Append filename automatically" checkbox back to preferences. + if (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) { + prefs->setBool("/dialogs/save_copy/append_extension", fileTypeCheckbox.get_active()); + } else { + prefs->setBool("/dialogs/save_as/append_extension", fileTypeCheckbox.get_active()); + } + + Inkscape::Extension::store_file_extension_in_prefs((extension != nullptr ? extension->get_id() : ""), save_method); + + cleanup(true); + + return true; + } else { + cleanup(false); + return false; + } +} + + +/** + * Get the file extension type that was selected by the user. Valid after an [OK] + */ +Inkscape::Extension::Extension *FileSaveDialogImplGtk::getSelectionType() +{ + return extension; +} + +void FileSaveDialogImplGtk::setSelectionType(Inkscape::Extension::Extension *key) +{ + // If no pointer to extension is passed in, look up based on filename extension. + if (!key) { + // Not quite UTF-8 here. + gchar *filenameLower = g_ascii_strdown(myFilename.c_str(), -1); + for (int i = 0; !key && (i < (int)fileTypes.size()); i++) { + Inkscape::Extension::Output *ext = dynamic_cast<Inkscape::Extension::Output *>(fileTypes[i].extension); + if (ext && ext->get_extension()) { + gchar *extensionLower = g_ascii_strdown(ext->get_extension(), -1); + if (g_str_has_suffix(filenameLower, extensionLower)) { + key = fileTypes[i].extension; + } + g_free(extensionLower); + } + } + g_free(filenameLower); + } + + // Ensure the proper entry in the combo box is selected. + if (key) { + extension = key; + gchar const *extensionID = extension->get_id(); + if (extensionID) { + for (int i = 0; i < (int)fileTypes.size(); i++) { + Inkscape::Extension::Extension *ext = fileTypes[i].extension; + if (ext) { + gchar const *id = ext->get_id(); + if (id && (strcmp(extensionID, id) == 0)) { + int oldSel = fileTypeComboBox.get_active_row_number(); + if (i != oldSel) { + fileTypeComboBox.set_active(i); + } + break; + } + } + } + } + } +} + +Glib::ustring FileSaveDialogImplGtk::getCurrentDirectory() +{ + return get_current_folder(); +} + + +/*void +FileSaveDialogImplGtk::change_title(const Glib::ustring& title) +{ + set_title(title); +}*/ + +/** + * Change the default save path location. + */ +void FileSaveDialogImplGtk::change_path(const Glib::ustring &path) +{ + myFilename = path; + + if (Glib::file_test(myFilename, Glib::FILE_TEST_IS_DIR)) { + // fprintf(stderr,"set_current_folder(%s)\n",myFilename.c_str()); + set_current_folder(myFilename); + } else { + // fprintf(stderr,"set_filename(%s)\n",myFilename.c_str()); + if (Glib::file_test(myFilename, Glib::FILE_TEST_EXISTS)) { + set_filename(myFilename); + } else { + std::string dirName = Glib::path_get_dirname(myFilename); + if (dirName != get_current_folder()) { + set_current_folder(dirName); + } + } + Glib::ustring basename = Glib::path_get_basename(myFilename); + // fprintf(stderr,"set_current_name(%s)\n",basename.c_str()); + try + { + set_current_name(Glib::filename_to_utf8(basename)); + } + catch (Glib::ConvertError &e) + { + g_warning("Error converting save filename to UTF-8."); + // try a fallback. + set_current_name(basename); + } + } +} + +void FileSaveDialogImplGtk::updateNameAndExtension() +{ + // Pick up any changes the user has typed in. + Glib::ustring tmp = get_filename(); + + if (tmp.empty()) { + tmp = get_uri(); + } + + if (!tmp.empty()) { + myFilename = tmp; + } + + Inkscape::Extension::Output *newOut = extension ? dynamic_cast<Inkscape::Extension::Output *>(extension) : nullptr; + if (fileTypeCheckbox.get_active() && newOut) { + // Append the file extension if it's not already present and display it in the file name entry field + appendExtension(myFilename, newOut); + change_path(myFilename); + } +} + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/filedialogimpl-gtkmm.h b/src/ui/dialog/filedialogimpl-gtkmm.h new file mode 100644 index 0000000..b16d362 --- /dev/null +++ b/src/ui/dialog/filedialogimpl-gtkmm.h @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Implementation of the file dialog interfaces defined in filedialogimpl.h + */ +/* Authors: + * Bob Jamison + * Johan Engelen <johan@shouraizou.nl> + * Joel Holdsworth + * Bruno Dilly + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004-2008 Authors + * Copyright (C) 2004-2007 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __FILE_DIALOGIMPL_H__ +#define __FILE_DIALOGIMPL_H__ + +//Gtk includes +#include <gtkmm/filechooserdialog.h> +#include <glib/gstdio.h> + +#include "filedialog.h" +#include "svg-preview.h" + +namespace Gtk { +class CheckButton; +class ComboBoxText; +class Expander; +} + +namespace Inkscape { + class URI; + +namespace UI { + +namespace View { + class SVGViewWidget; +} + +namespace Dialog { + +/*######################################################################### +### Utility +#########################################################################*/ +void +fileDialogExtensionToPattern(Glib::ustring &pattern, + Glib::ustring &extension); + +void +findEntryWidgets(Gtk::Container *parent, + std::vector<Gtk::Entry *> &result); + +void +findExpanderWidgets(Gtk::Container *parent, + std::vector<Gtk::Expander *> &result); + +class FileType +{ + public: + FileType(): name(), pattern(),extension(nullptr) {} + ~FileType() = default; + Glib::ustring name; + Glib::ustring pattern; + Inkscape::Extension::Extension *extension; +}; + +/*######################################################################### +### F I L E D I A L O G B A S E C L A S S +#########################################################################*/ + +/** + * This class is the base implementation for the others. This + * reduces redundancies and bugs. + */ +class FileDialogBaseGtk : public Gtk::FileChooserDialog +{ +public: + + /** + * + */ + FileDialogBaseGtk(Gtk::Window& parentWindow, const Glib::ustring &title, + Gtk::FileChooserAction dialogType, FileDialogType type, gchar const* preferenceBase) : + Gtk::FileChooserDialog(parentWindow, title, dialogType), + preferenceBase(preferenceBase ? preferenceBase : "unknown"), + _dialogType(type) + { + internalSetup(); + } + + /** + * + */ + FileDialogBaseGtk(Gtk::Window& parentWindow, const char *title, + Gtk::FileChooserAction dialogType, FileDialogType type, gchar const* preferenceBase) : + Gtk::FileChooserDialog(parentWindow, title, dialogType), + preferenceBase(preferenceBase ? preferenceBase : "unknown"), + _dialogType(type) + { + internalSetup(); + } + + /** + * + */ + ~FileDialogBaseGtk() override + = default; + +protected: + void cleanup( bool showConfirmed ); + + Glib::ustring const preferenceBase; + /** + * What type of 'open' are we? (open, import, place, etc) + */ + FileDialogType _dialogType; + + /** + * Our svg preview widget + */ + SVGPreview svgPreview; + + /** + * Child widgets + */ + Gtk::CheckButton previewCheckbox; + Gtk::CheckButton svgexportCheckbox; + +private: + void internalSetup(); + + /** + * Callback for seeing if the preview needs to be drawn + */ + void _updatePreviewCallback(); + + /** + * Callback to for SVG 2 to SVG 1.1 export. + */ + void _svgexportEnabledCB(); +}; + + + + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +/** + * Our implementation class for the FileOpenDialog interface.. + */ +class FileOpenDialogImplGtk : public FileOpenDialog, public FileDialogBaseGtk +{ +public: + + FileOpenDialogImplGtk(Gtk::Window& parentWindow, + const Glib::ustring &dir, + FileDialogType fileTypes, + const Glib::ustring &title); + + ~FileOpenDialogImplGtk() override; + + bool show() override; + + Inkscape::Extension::Extension *getSelectionType() override; + + Glib::ustring getFilename(); + + std::vector<Glib::ustring> getFilenames() override; + + Glib::ustring getCurrentDirectory() override; + + /// Add a custom file filter menu item + /// @param name - Name of the filter (such as "Javscript") + /// @param pattern - File filtering pattern (such as "*.js") + /// Use the FileDialogType::CUSTOM_TYPE in constructor to not include other file types + void addFilterMenu(Glib::ustring name, Glib::ustring pattern) override; + +private: + + /** + * Create a filter menu for this type of dialog + */ + void createFilterMenu(); + + + /** + * Filter name->extension lookup + */ + std::map<Glib::ustring, Inkscape::Extension::Extension *> extensionMap; + + /** + * The extension to use to write this file + */ + Inkscape::Extension::Extension *extension; + +}; + + + +//######################################################################## +//# F I L E S A V E +//######################################################################## + +/** + * Our implementation of the FileSaveDialog interface. + */ +class FileSaveDialogImplGtk : public FileSaveDialog, public FileDialogBaseGtk +{ + +public: + FileSaveDialogImplGtk(Gtk::Window &parentWindow, + const Glib::ustring &dir, + FileDialogType fileTypes, + const Glib::ustring &title, + const Glib::ustring &default_key, + const gchar* docTitle, + const Inkscape::Extension::FileSaveMethod save_method); + + ~FileSaveDialogImplGtk() override; + + bool show() override; + + Inkscape::Extension::Extension *getSelectionType() override; + void setSelectionType( Inkscape::Extension::Extension * key ) override; + + Glib::ustring getCurrentDirectory() override; + void addFileType(Glib::ustring name, Glib::ustring pattern) override; + +private: + //void change_title(const Glib::ustring& title); + void change_path(const Glib::ustring& path); + void updateNameAndExtension(); + + /** + * The file save method (essentially whether the dialog was invoked by "Save as ..." or "Save a + * copy ..."), which is used to determine file extensions and save paths. + */ + Inkscape::Extension::FileSaveMethod save_method; + + /** + * Fix to allow the user to type the file name + */ + Gtk::Entry *fileNameEntry; + + + /** + * Allow the specification of the output file type + */ + Gtk::ComboBoxText fileTypeComboBox; + + + /** + * Data mirror of the combo box + */ + std::vector<FileType> fileTypes; + + //# Child widgets + Gtk::Box childBox; + Gtk::Box checksBox; + + Gtk::CheckButton fileTypeCheckbox; + + /** + * Callback for user input into fileNameEntry + */ + void fileTypeChangedCallback(); + + /** + * Create a filter menu for this type of dialog + */ + void createFilterMenu(); + + /** + * The extension to use to write this file + */ + Inkscape::Extension::Extension *extension; + + /** + * Callback for user input into fileNameEntry + */ + void fileNameEntryChangedCallback(); + void fileNameChanged(); + bool fromCB; +}; + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif /*__FILE_DIALOGIMPL_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/ui/dialog/filedialogimpl-win32.cpp b/src/ui/dialog/filedialogimpl-win32.cpp new file mode 100644 index 0000000..b44d90c --- /dev/null +++ b/src/ui/dialog/filedialogimpl-win32.cpp @@ -0,0 +1,1929 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Implementation of native file dialogs for Win32. + */ +/* Authors: + * Joel Holdsworth + * The Inkscape Organization + * Abhishek Sharma + * + * Copyright (C) 2004-2008 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifdef _WIN32 + +#include "filedialogimpl-win32.h" +// General includes +#include <cairomm/win32_surface.h> +#include <gdk/gdkwin32.h> +#include <gdkmm/general.h> +#include <glibmm/fileutils.h> +#include <glibmm/i18n.h> +#include <list> +#include <thread> +#include <vector> + +//Inkscape includes +#include "display/cairo-utils.h" +#include "document.h" +#include "extension/db.h" +#include "extension/input.h" +#include "extension/output.h" +#include "filedialog.h" +#include "helper/pixbuf-ops.h" +#include "preferences.h" +#include "util/units.h" + + +using namespace Glib; +using namespace Cairo; +using namespace Gdk::Cairo; + +namespace Inkscape +{ +namespace UI +{ +namespace Dialog +{ + +const int PREVIEW_WIDENING = 150; +const int WINDOW_WIDTH_MINIMUM = 32; +const int WINDOW_WIDTH_FALLBACK = 450; +const int WINDOW_HEIGHT_MINIMUM = 32; +const int WINDOW_HEIGHT_FALLBACK = 360; +const char PreviewWindowClassName[] = "PreviewWnd"; +const unsigned long MaxPreviewFileSize = 10240; // kB + +#define IDC_SHOW_PREVIEW 1000 + +struct Filter +{ + gunichar2* name; + glong name_length; + gunichar2* filter; + glong filter_length; + Inkscape::Extension::Extension* mod; +}; + +ustring utf16_to_ustring(const wchar_t *utf16string, int utf16length = -1) +{ + gchar *utf8string = g_utf16_to_utf8((const gunichar2*)utf16string, + utf16length, NULL, NULL, NULL); + ustring result(utf8string); + g_free(utf8string); + + return result; +} + +namespace { + +int sanitizeWindowSizeParam( int size, int delta, int minimum, int fallback ) +{ + int result = size; + if ( size < minimum ) { + g_warning( "Window size %d is less than cutoff.", size ); + result = fallback - delta; + } + result += delta; + return result; +} + +} // namespace + +/*######################################################################### +### F I L E D I A L O G B A S E C L A S S +#########################################################################*/ + +FileDialogBaseWin32::FileDialogBaseWin32(Gtk::Window &parent, + const Glib::ustring &dir, const gchar *title, + FileDialogType type, gchar const* /*preferenceBase*/) : + dialogType(type), + parent(parent), + _current_directory(dir) +{ + _main_loop = NULL; + + _filter_index = 1; + _filter_count = 0; + + _title = (wchar_t*)g_utf8_to_utf16(title, -1, NULL, NULL, NULL); + g_assert(_title != NULL); + + Glib::RefPtr<const Gdk::Window> parentWindow = parent.get_window(); + g_assert(parentWindow->gobj() != NULL); + _ownerHwnd = (HWND)gdk_win32_window_get_handle((GdkWindow*)parentWindow->gobj()); +} + +FileDialogBaseWin32::~FileDialogBaseWin32() +{ + g_free(_title); +} + +Inkscape::Extension::Extension *FileDialogBaseWin32::getSelectionType() +{ + return _extension; +} + +Glib::ustring FileDialogBaseWin32::getCurrentDirectory() +{ + return _current_directory; +} + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +bool FileOpenDialogImplWin32::_show_preview = true; + +/** + * Constructor. Not called directly. Use the factory. + */ +FileOpenDialogImplWin32::FileOpenDialogImplWin32(Gtk::Window &parent, + const Glib::ustring &dir, + FileDialogType fileTypes, + const gchar *title) : + FileDialogBaseWin32(parent, dir, title, fileTypes, "dialogs.open") +{ + // Initialize to Autodetect + _extension = NULL; + + // Set our dialog type (open, import, etc...) + dialogType = fileTypes; + + _show_preview_button_bitmap = NULL; + _preview_wnd = NULL; + _file_dialog_wnd = NULL; + _base_window_proc = NULL; + + _preview_file_size = 0; + _preview_bitmap = NULL; + _preview_file_icon = NULL; + _preview_document_width = 0; + _preview_document_height = 0; + _preview_image_width = 0; + _preview_image_height = 0; + _preview_emf_image = false; + + _mutex = NULL; + + if (dialogType != CUSTOM_TYPE) + createFilterMenu(); +} + + +/** + * Destructor + */ +FileOpenDialogImplWin32::~FileOpenDialogImplWin32() +{ + if(_filter != NULL) + delete[] _filter; + if(_extension_map != NULL) + delete[] _extension_map; +} + +void FileOpenDialogImplWin32::addFilterMenu(Glib::ustring name, Glib::ustring pattern) +{ + std::list<Filter> filter_list; + + Filter all_exe_files; + + const gchar *all_exe_files_filter_name = name.data(); + const gchar *all_exe_files_filter = pattern.data(); + + // Calculate the amount of memory required + int filter_count = 1; + int filter_length = 1; + + int extension_index = 0; + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + // Filter Executable Files + all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name, + -1, NULL, &all_exe_files.name_length, NULL); + all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter, + -1, NULL, &all_exe_files.filter_length, NULL); + all_exe_files.mod = NULL; + filter_list.push_front(all_exe_files); + + filter_length = all_exe_files.name_length + all_exe_files.filter_length + 3; // Add 3 for two \0s and a * + + _filter = new wchar_t[filter_length]; + wchar_t *filterptr = _filter; + + for(std::list<Filter>::iterator filter_iterator = filter_list.begin(); + filter_iterator != filter_list.end(); ++filter_iterator) + { + const Filter &filter = *filter_iterator; + + wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length); + filterptr += filter.name_length; + g_free(filter.name); + + *(filterptr++) = L'\0'; + *(filterptr++) = L'*'; + + if(filter.filter != NULL) + { + wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length); + filterptr += filter.filter_length; + g_free(filter.filter); + } + + *(filterptr++) = L'\0'; + + // Associate this input extension with the file type name + _extension_map[extension_index++] = filter.mod; + } + *(filterptr++) = L'\0'; + + _filter_count = extension_index; + _filter_index = 1; // Select the 1st filter in the list +} + +void FileOpenDialogImplWin32::createFilterMenu() +{ + std::list<Filter> filter_list; + + int extension_index = 0; + int filter_length = 1; + + if (dialogType == CUSTOM_TYPE) { + return; + } + + if (dialogType != EXE_TYPES) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _show_preview = prefs->getBool("/dialogs/open/enable_preview", true); + + // Compose the filter string + Inkscape::Extension::DB::InputList extension_list; + Inkscape::Extension::db.get_input_list(extension_list); + + ustring all_inkscape_files_filter, all_image_files_filter, all_vectors_filter, all_bitmaps_filter; + Filter all_files, all_inkscape_files, all_image_files, all_vectors, all_bitmaps; + + const gchar *all_files_filter_name = _("All Files"); + const gchar *all_inkscape_files_filter_name = _("All Inkscape Files"); + const gchar *all_image_files_filter_name = _("All Images"); + const gchar *all_vectors_filter_name = _("All Vectors"); + const gchar *all_bitmaps_filter_name = _("All Bitmaps"); + + // Calculate the amount of memory required + int filter_count = 5; // 5 - one for each filter type + + for (Inkscape::Extension::DB::InputList::iterator current_item = extension_list.begin(); + current_item != extension_list.end(); ++current_item) + { + Filter filter; + + Inkscape::Extension::Input *imod = *current_item; + if (imod->deactivated()) continue; + + // Type + filter.name = g_utf8_to_utf16(imod->get_filetypename(true), -1, NULL, &filter.name_length, NULL); + + // Extension + const gchar *file_extension_name = imod->get_extension(); + filter.filter = g_utf8_to_utf16(file_extension_name, -1, NULL, &filter.filter_length, NULL); + + filter.mod = imod; + filter_list.push_back(filter); + + filter_length += filter.name_length + filter.filter_length + 3; // Add 3 for two \0s and a * + + // Add to the "All Inkscape Files" Entry + if(all_inkscape_files_filter.length() > 0) + all_inkscape_files_filter += ";*"; + all_inkscape_files_filter += file_extension_name; + if( strncmp("image", imod->get_mimetype(), 5) == 0) + { + // Add to the "All Image Files" Entry + if(all_image_files_filter.length() > 0) + all_image_files_filter += ";*"; + all_image_files_filter += file_extension_name; + } + + // I don't know of any other way to define "bitmap" formats other than by listing them + // if you change it here, do the same change in filedialogimpl-gtkmm + if ( + strncmp("image/png", imod->get_mimetype(), 9)==0 || + strncmp("image/jpeg", imod->get_mimetype(), 10)==0 || + strncmp("image/gif", imod->get_mimetype(), 9)==0 || + strncmp("image/x-icon", imod->get_mimetype(), 12)==0 || + strncmp("image/x-navi-animation", imod->get_mimetype(), 22)==0 || + strncmp("image/x-cmu-raster", imod->get_mimetype(), 18)==0 || + strncmp("image/x-xpixmap", imod->get_mimetype(), 15)==0 || + strncmp("image/bmp", imod->get_mimetype(), 9)==0 || + strncmp("image/vnd.wap.wbmp", imod->get_mimetype(), 18)==0 || + strncmp("image/tiff", imod->get_mimetype(), 10)==0 || + strncmp("image/x-xbitmap", imod->get_mimetype(), 15)==0 || + strncmp("image/x-tga", imod->get_mimetype(), 11)==0 || + strncmp("image/x-pcx", imod->get_mimetype(), 11)==0 + ) { + if(all_bitmaps_filter.length() > 0) + all_bitmaps_filter += ";*"; + all_bitmaps_filter += file_extension_name; + } else { + if(all_vectors_filter.length() > 0) + all_vectors_filter += ";*"; + all_vectors_filter += file_extension_name; + } + + filter_count++; + } + + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + // Filter bitmap files + all_bitmaps.name = g_utf8_to_utf16(all_bitmaps_filter_name, + -1, NULL, &all_bitmaps.name_length, NULL); + all_bitmaps.filter = g_utf8_to_utf16(all_bitmaps_filter.data(), + -1, NULL, &all_bitmaps.filter_length, NULL); + all_bitmaps.mod = NULL; + filter_list.push_front(all_bitmaps); + + // Filter vector files + all_vectors.name = g_utf8_to_utf16(all_vectors_filter_name, + -1, NULL, &all_vectors.name_length, NULL); + all_vectors.filter = g_utf8_to_utf16(all_vectors_filter.data(), + -1, NULL, &all_vectors.filter_length, NULL); + all_vectors.mod = NULL; + filter_list.push_front(all_vectors); + + // Filter Image Files + all_image_files.name = g_utf8_to_utf16(all_image_files_filter_name, + -1, NULL, &all_image_files.name_length, NULL); + all_image_files.filter = g_utf8_to_utf16(all_image_files_filter.data(), + -1, NULL, &all_image_files.filter_length, NULL); + all_image_files.mod = NULL; + filter_list.push_front(all_image_files); + + // Filter Inkscape Files + all_inkscape_files.name = g_utf8_to_utf16(all_inkscape_files_filter_name, + -1, NULL, &all_inkscape_files.name_length, NULL); + all_inkscape_files.filter = g_utf8_to_utf16(all_inkscape_files_filter.data(), + -1, NULL, &all_inkscape_files.filter_length, NULL); + all_inkscape_files.mod = NULL; + filter_list.push_front(all_inkscape_files); + + // Filter All Files + all_files.name = g_utf8_to_utf16(all_files_filter_name, + -1, NULL, &all_files.name_length, NULL); + all_files.filter = NULL; + all_files.filter_length = 0; + all_files.mod = NULL; + filter_list.push_front(all_files); + + filter_length += all_files.name_length + 3 + + all_inkscape_files.filter_length + + all_inkscape_files.name_length + 3 + + all_image_files.filter_length + + all_image_files.name_length + 3 + + all_vectors.filter_length + + all_vectors.name_length + 3 + + all_bitmaps.filter_length + + all_bitmaps.name_length + 3 + + 1; + // Add 3 for 2*2 \0s and a *, and 1 for a trailing \0 + } else { + // Executables only + ustring all_exe_files_filter = "*.exe;*.bat;*.com"; + Filter all_exe_files, all_files; + + const gchar *all_files_filter_name = _("All Files"); + const gchar *all_exe_files_filter_name = _("All Executable Files"); + + // Calculate the amount of memory required + int filter_count = 2; // 2 - All Files and All Executable Files + + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + // Filter Executable Files + all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name, + -1, NULL, &all_exe_files.name_length, NULL); + all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter.data(), + -1, NULL, &all_exe_files.filter_length, NULL); + all_exe_files.mod = NULL; + filter_list.push_front(all_exe_files); + + // Filter All Files + all_files.name = g_utf8_to_utf16(all_files_filter_name, + -1, NULL, &all_files.name_length, NULL); + all_files.filter = NULL; + all_files.filter_length = 0; + all_files.mod = NULL; + filter_list.push_front(all_files); + + filter_length += all_files.name_length + 3 + + all_exe_files.filter_length + + all_exe_files.name_length + 3 + + 1; + // Add 3 for 2*2 \0s and a *, and 1 for a trailing \0 + } + + _filter = new wchar_t[filter_length]; + wchar_t *filterptr = _filter; + + for(std::list<Filter>::iterator filter_iterator = filter_list.begin(); + filter_iterator != filter_list.end(); ++filter_iterator) + { + const Filter &filter = *filter_iterator; + + wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length); + filterptr += filter.name_length; + g_free(filter.name); + + *(filterptr++) = L'\0'; + *(filterptr++) = L'*'; + + if(filter.filter != NULL) + { + wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length); + filterptr += filter.filter_length; + g_free(filter.filter); + } + + *(filterptr++) = L'\0'; + + // Associate this input extension with the file type name + _extension_map[extension_index++] = filter.mod; + } + *(filterptr++) = L'\0'; + + _filter_count = extension_index; + _filter_index = 2; // Select the 2nd filter in the list - 2 is NOT the 3rd +} + +void FileOpenDialogImplWin32::GetOpenFileName_thread() +{ + OPENFILENAMEW ofn; + + g_assert(_mutex != NULL); + + WCHAR* current_directory_string = (WCHAR*)g_utf8_to_utf16( + _current_directory.data(), _current_directory.length(), + NULL, NULL, NULL); + + memset(&ofn, 0, sizeof(ofn)); + + // Copy the selected file name, converting from UTF-8 to UTF-16 + memset(_path_string, 0, sizeof(_path_string)); + gunichar2* utf16_path_string = g_utf8_to_utf16( + myFilename.data(), -1, NULL, NULL, NULL); + wcsncpy(_path_string, (wchar_t*)utf16_path_string, _MAX_PATH); + g_free(utf16_path_string); + + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = _ownerHwnd; + ofn.lpstrFile = _path_string; + ofn.nMaxFile = _MAX_PATH; + ofn.lpstrFileTitle = NULL; + ofn.nMaxFileTitle = 0; + ofn.lpstrInitialDir = current_directory_string; + ofn.lpstrTitle = _title; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER | OFN_ENABLEHOOK | OFN_HIDEREADONLY | OFN_ENABLESIZING; + ofn.lpstrFilter = _filter; + ofn.nFilterIndex = _filter_index; + ofn.lpfnHook = GetOpenFileName_hookproc; + ofn.lCustData = (LPARAM)this; + + _result = GetOpenFileNameW(&ofn) != 0; + + g_assert(ofn.nFilterIndex >= 1 && ofn.nFilterIndex <= _filter_count); + _filter_index = ofn.nFilterIndex; + _extension = _extension_map[ofn.nFilterIndex - 1]; + + // Copy the selected file name, converting from UTF-16 to UTF-8 + myFilename = utf16_to_ustring(_path_string, _MAX_PATH); + + // Tidy up + g_free(current_directory_string); + + _mutex->lock(); + _finished = true; + _mutex->unlock(); +} + +void FileOpenDialogImplWin32::register_preview_wnd_class() +{ + HINSTANCE hInstance = GetModuleHandle(NULL); + const WNDCLASSA PreviewWndClass = + { + CS_HREDRAW | CS_VREDRAW, + preview_wnd_proc, + 0, + 0, + hInstance, + NULL, + LoadCursor(hInstance, IDC_ARROW), + (HBRUSH)(COLOR_BTNFACE + 1), + NULL, + PreviewWindowClassName + }; + + RegisterClassA(&PreviewWndClass); +} + +UINT_PTR CALLBACK FileOpenDialogImplWin32::GetOpenFileName_hookproc( + HWND hdlg, UINT uiMsg, WPARAM, LPARAM lParam) +{ + FileOpenDialogImplWin32 *pImpl = reinterpret_cast<FileOpenDialogImplWin32*> + (GetWindowLongPtr(hdlg, GWLP_USERDATA)); + + switch(uiMsg) + { + case WM_INITDIALOG: + { + HWND hParentWnd = GetParent(hdlg); + HINSTANCE hInstance = GetModuleHandle(NULL); + + // Set the pointer to the object + OPENFILENAMEW *ofn = reinterpret_cast<OPENFILENAMEW*>(lParam); + SetWindowLongPtr(hdlg, GWLP_USERDATA, ofn->lCustData); + SetWindowLongPtr(hParentWnd, GWLP_USERDATA, ofn->lCustData); + pImpl = reinterpret_cast<FileOpenDialogImplWin32*>(ofn->lCustData); + + // Make the window a bit wider + RECT rcRect; + GetWindowRect(hParentWnd, &rcRect); + + // Don't show the preview when opening executable files + if ( pImpl->dialogType == EXE_TYPES) { + MoveWindow(hParentWnd, rcRect.left, rcRect.top, + rcRect.right - rcRect.left, + rcRect.bottom - rcRect.top, + FALSE); + } else { + MoveWindow(hParentWnd, rcRect.left, rcRect.top, + rcRect.right - rcRect.left + PREVIEW_WIDENING, + rcRect.bottom - rcRect.top, + FALSE); + } + + // Subclass the parent + pImpl->_base_window_proc = (WNDPROC)GetWindowLongPtr(hParentWnd, GWLP_WNDPROC); + SetWindowLongPtr(hParentWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(file_dialog_subclass_proc)); + + if ( pImpl->dialogType != EXE_TYPES) { + // Add a button to the toolbar + pImpl->_toolbar_wnd = FindWindowEx(hParentWnd, NULL, "ToolbarWindow32", NULL); + + pImpl->_show_preview_button_bitmap = LoadBitmap( + hInstance, MAKEINTRESOURCE(IDC_SHOW_PREVIEW)); + TBADDBITMAP tbAddBitmap = {NULL, reinterpret_cast<UINT_PTR>(pImpl->_show_preview_button_bitmap)}; + const int iBitmapIndex = SendMessage(pImpl->_toolbar_wnd, + TB_ADDBITMAP, 1, (LPARAM)&tbAddBitmap); + + + TBBUTTON tbButton; + memset(&tbButton, 0, sizeof(TBBUTTON)); + tbButton.iBitmap = iBitmapIndex; + tbButton.idCommand = IDC_SHOW_PREVIEW; + tbButton.fsState = (pImpl->_show_preview ? TBSTATE_CHECKED : 0) + | TBSTATE_ENABLED; + tbButton.fsStyle = TBSTYLE_CHECK; + tbButton.iString = (INT_PTR)_("Show Preview"); + SendMessage(pImpl->_toolbar_wnd, TB_ADDBUTTONS, 1, (LPARAM)&tbButton); + + // Create preview pane + register_preview_wnd_class(); + } + + pImpl->_mutex->lock(); + + pImpl->_file_dialog_wnd = hParentWnd; + + if ( pImpl->dialogType != EXE_TYPES) { + pImpl->_preview_wnd = + CreateWindowA(PreviewWindowClassName, "", + WS_CHILD | WS_VISIBLE, + 0, 0, 100, 100, hParentWnd, NULL, hInstance, NULL); + SetWindowLongPtr(pImpl->_preview_wnd, GWLP_USERDATA, ofn->lCustData); + } + + pImpl->_mutex->unlock(); + + pImpl->layout_dialog(); + } + break; + + case WM_NOTIFY: + { + + OFNOTIFY *pOFNotify = reinterpret_cast<OFNOTIFY*>(lParam); + switch(pOFNotify->hdr.code) + { + case CDN_SELCHANGE: + { + if(pImpl != NULL) + { + // Get the file name + pImpl->_mutex->lock(); + + SendMessage(pOFNotify->hdr.hwndFrom, CDM_GETFILEPATH, + sizeof(pImpl->_path_string) / sizeof(wchar_t), + (LPARAM)pImpl->_path_string); + + pImpl->_file_selected = true; + + pImpl->_mutex->unlock(); + } + } + break; + } + } + break; + + case WM_CLOSE: + pImpl->_mutex->lock(); + pImpl->_preview_file_size = 0; + + pImpl->_file_dialog_wnd = NULL; + DestroyWindow(pImpl->_preview_wnd); + pImpl->_preview_wnd = NULL; + DeleteObject(pImpl->_show_preview_button_bitmap); + pImpl->_show_preview_button_bitmap = NULL; + pImpl->_mutex->unlock(); + + break; + } + + // Use default dialog behaviour + return 0; +} + +LRESULT CALLBACK FileOpenDialogImplWin32::file_dialog_subclass_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + FileOpenDialogImplWin32 *pImpl = reinterpret_cast<FileOpenDialogImplWin32*> + (GetWindowLongPtr(hwnd, GWLP_USERDATA)); + + LRESULT lResult = CallWindowProc(pImpl->_base_window_proc, hwnd, uMsg, wParam, lParam); + + switch(uMsg) + { + case WM_SHOWWINDOW: + if(wParam != 0) + pImpl->layout_dialog(); + break; + + case WM_SIZE: + pImpl->layout_dialog(); + break; + + case WM_COMMAND: + if(wParam == IDC_SHOW_PREVIEW) + { + const bool enable = SendMessage(pImpl->_toolbar_wnd, + TB_ISBUTTONCHECKED, IDC_SHOW_PREVIEW, 0) != 0; + pImpl->enable_preview(enable); + } + break; + } + + return lResult; +} + +LRESULT CALLBACK FileOpenDialogImplWin32::preview_wnd_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + const int CaptionPadding = 4; + const int IconSize = 32; + + FileOpenDialogImplWin32 *pImpl = reinterpret_cast<FileOpenDialogImplWin32*> + (GetWindowLongPtr(hwnd, GWLP_USERDATA)); + + LRESULT lResult = 0; + + switch(uMsg) + { + case WM_ERASEBKGND: + // Do nothing to erase the background + // - otherwise there'll be flicker + lResult = 1; + break; + + case WM_PAINT: + { + // Get the client rect + RECT rcClient; + GetClientRect(hwnd, &rcClient); + + // Prepare to paint + PAINTSTRUCT paint_struct; + HDC dc = BeginPaint(hwnd, &paint_struct); + + HFONT hCaptionFont = reinterpret_cast<HFONT>(SendMessage(GetParent(hwnd), + WM_GETFONT, 0, 0)); + HFONT hOldFont = static_cast<HFONT>(SelectObject(dc, hCaptionFont)); + SetBkMode(dc, TRANSPARENT); + + pImpl->_mutex->lock(); + + if(pImpl->_path_string[0] == 0) + { + WCHAR* noFileText=(WCHAR*)g_utf8_to_utf16(_("No file selected"), + -1, NULL, NULL, NULL); + FillRect(dc, &rcClient, reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1)); + DrawTextW(dc, noFileText, -1, &rcClient, + DT_CENTER | DT_VCENTER | DT_NOPREFIX); + g_free(noFileText); + } + else if(pImpl->_preview_bitmap != NULL) + { + BITMAP bitmap; + GetObject(pImpl->_preview_bitmap, sizeof(bitmap), &bitmap); + const int destX = (rcClient.right - bitmap.bmWidth) / 2; + + // Render the image + HDC hSrcDC = CreateCompatibleDC(dc); + HBITMAP hOldBitmap = (HBITMAP)SelectObject(hSrcDC, pImpl->_preview_bitmap); + + BitBlt(dc, destX, 0, bitmap.bmWidth, bitmap.bmHeight, + hSrcDC, 0, 0, SRCCOPY); + + SelectObject(hSrcDC, hOldBitmap); + DeleteDC(hSrcDC); + + // Fill in the background area + HRGN hEraseRgn = CreateRectRgn(rcClient.left, rcClient.top, + rcClient.right, rcClient.bottom); + HRGN hImageRgn = CreateRectRgn(destX, 0, + destX + bitmap.bmWidth, bitmap.bmHeight); + CombineRgn(hEraseRgn, hEraseRgn, hImageRgn, RGN_DIFF); + + FillRgn(dc, hEraseRgn, GetSysColorBrush(COLOR_3DFACE)); + + DeleteObject(hImageRgn); + DeleteObject(hEraseRgn); + + // Draw the caption on + RECT rcCaptionRect = {rcClient.left, + rcClient.top + bitmap.bmHeight + CaptionPadding, + rcClient.right, rcClient.bottom}; + + WCHAR szCaption[_MAX_FNAME + 32]; + const int iLength = pImpl->format_caption( + szCaption, sizeof(szCaption) / sizeof(WCHAR)); + + DrawTextW(dc, szCaption, iLength, &rcCaptionRect, + DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS); + } + else if(pImpl->_preview_file_icon != NULL) + { + FillRect(dc, &rcClient, reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1)); + + // Draw the files icon + const int destX = (rcClient.right - IconSize) / 2; + DrawIconEx(dc, destX, 0, pImpl->_preview_file_icon, + IconSize, IconSize, 0, NULL, + DI_NORMAL | DI_COMPAT); + + // Draw the caption on + RECT rcCaptionRect = {rcClient.left, + rcClient.top + IconSize + CaptionPadding, + rcClient.right, rcClient.bottom}; + + WCHAR szFileName[_MAX_FNAME], szCaption[_MAX_FNAME + 32]; + _wsplitpath(pImpl->_path_string, NULL, NULL, szFileName, NULL); + + const int iLength = snwprintf(szCaption, + sizeof(szCaption), L"%ls\n%d kB", + szFileName, pImpl->_preview_file_size); + + DrawTextW(dc, szCaption, iLength, &rcCaptionRect, + DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS); + } + else + { + // Can't show anything! + FillRect(dc, &rcClient, reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1)); + } + + pImpl->_mutex->unlock(); + + // Finish painting + SelectObject(dc, hOldFont); + EndPaint(hwnd, &paint_struct); + } + + break; + + case WM_DESTROY: + pImpl->free_preview(); + break; + + default: + lResult = DefWindowProc(hwnd, uMsg, wParam, lParam); + break; + } + + return lResult; +} + +void FileOpenDialogImplWin32::enable_preview(bool enable) +{ + if (_show_preview != enable) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/open/enable_preview", enable); + } + _show_preview = enable; + + // Relayout the dialog + ShowWindow(_preview_wnd, enable ? SW_SHOW : SW_HIDE); + layout_dialog(); + + // Load or unload the preview + if(enable) + { + _mutex->lock(); + _file_selected = true; + _mutex->unlock(); + } + else free_preview(); +} + +void FileOpenDialogImplWin32::layout_dialog() +{ + union RECTPOINTS + { + RECT r; + POINT p[2]; + }; + + const float MaxExtentScale = 2.0f / 3.0f; + + RECT rcClient; + GetClientRect(_file_dialog_wnd, &rcClient); + + // Re-layout the dialog + HWND hFileListWnd = GetDlgItem(_file_dialog_wnd, lst2); + HWND hFolderComboWnd = GetDlgItem(_file_dialog_wnd, cmb2); + + + RECT rcFolderComboRect; + RECTPOINTS rcFileList; + GetWindowRect(hFileListWnd, &rcFileList.r); + GetWindowRect(hFolderComboWnd, &rcFolderComboRect); + const int iPadding = rcFileList.r.top - rcFolderComboRect.bottom; + MapWindowPoints(NULL, _file_dialog_wnd, rcFileList.p, 2); + + RECT rcPreview; + RECT rcBody = {rcFileList.r.left, rcFileList.r.top, + rcClient.right - iPadding, rcFileList.r.bottom}; + rcFileList.r.right = rcBody.right; + + if(_show_preview && dialogType != EXE_TYPES) + { + rcPreview.top = rcBody.top; + rcPreview.left = rcClient.right - (rcBody.bottom - rcBody.top); + const int iMaxExtent = (int)(MaxExtentScale * (float)(rcBody.left + rcBody.right)) + iPadding / 2; + if(rcPreview.left < iMaxExtent) rcPreview.left = iMaxExtent; + rcPreview.bottom = rcBody.bottom; + rcPreview.right = rcBody.right; + + // Re-layout the preview box + _mutex->lock(); + + _preview_width = rcPreview.right - rcPreview.left; + _preview_height = rcPreview.bottom - rcPreview.top; + + _mutex->unlock(); + + render_preview(); + + MoveWindow(_preview_wnd, rcPreview.left, rcPreview.top, + _preview_width, _preview_height, TRUE); + + rcFileList.r.right = rcPreview.left - iPadding; + } + + // Re-layout the file list box + MoveWindow(hFileListWnd, rcFileList.r.left, rcFileList.r.top, + rcFileList.r.right - rcFileList.r.left, + rcFileList.r.bottom - rcFileList.r.top, TRUE); + + // Re-layout the toolbar + RECTPOINTS rcToolBar; + GetWindowRect(_toolbar_wnd, &rcToolBar.r); + MapWindowPoints(NULL, _file_dialog_wnd, rcToolBar.p, 2); + MoveWindow(_toolbar_wnd, rcToolBar.r.left, rcToolBar.r.top, + rcToolBar.r.right - rcToolBar.r.left, rcToolBar.r.bottom - rcToolBar.r.top, TRUE); +} + +void FileOpenDialogImplWin32::file_selected() +{ + // Destroy any previous previews + free_preview(); + + + // Determine if the file exists + DWORD attributes = GetFileAttributesW(_path_string); + if(attributes == 0xFFFFFFFF || + attributes == FILE_ATTRIBUTE_DIRECTORY) + { + InvalidateRect(_preview_wnd, NULL, FALSE); + return; + } + + // Check the file exists and get the file size + HANDLE file_handle = CreateFileW(_path_string, GENERIC_READ, + FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if(file_handle == INVALID_HANDLE_VALUE) return; + const DWORD file_size = GetFileSize(file_handle, NULL); + if (file_size == INVALID_FILE_SIZE) return; + _preview_file_size = file_size / 1024; + CloseHandle(file_handle); + + if(_show_preview) load_preview(); +} + +void FileOpenDialogImplWin32::load_preview() +{ + // Destroy any previous previews + free_preview(); + + // Try to get the file icon + SHFILEINFOW fileInfo; + if(SUCCEEDED(SHGetFileInfoW(_path_string, 0, &fileInfo, + sizeof(fileInfo), SHGFI_ICON | SHGFI_LARGEICON))) + _preview_file_icon = fileInfo.hIcon; + + // Will this file be too big? + if(_preview_file_size > MaxPreviewFileSize) + { + InvalidateRect(_preview_wnd, NULL, FALSE); + return; + } + + // Prepare to render a preview + const Glib::ustring svg = ".svg"; + const Glib::ustring svgz = ".svgz"; + const Glib::ustring emf = ".emf"; + const Glib::ustring wmf = ".wmf"; + const Glib::ustring path = utf16_to_ustring(_path_string); + + bool success = false; + + _preview_document_width = _preview_document_height = 0; + + if ((dialogType == SVG_TYPES || dialogType == IMPORT_TYPES) && + (hasSuffix(path, svg) || hasSuffix(path, svgz))) + success = set_svg_preview(); + else if (hasSuffix(path, emf) || hasSuffix(path, wmf)) + success = set_emf_preview(); + else if (isValidImageFile(path)) + success = set_image_preview(); + else { + // Show no preview + } + + if(success) render_preview(); + + InvalidateRect(_preview_wnd, NULL, FALSE); +} + +void FileOpenDialogImplWin32::free_preview() +{ + _mutex->lock(); + if(_preview_bitmap != NULL) + DeleteObject(_preview_bitmap); + _preview_bitmap = NULL; + + if(_preview_file_icon != NULL) + DestroyIcon(_preview_file_icon); + _preview_file_icon = NULL; + + _preview_bitmap_image.reset(); + _preview_emf_image = false; + _mutex->unlock(); +} + +bool FileOpenDialogImplWin32::set_svg_preview() +{ + const int PreviewSize = 512; + + gchar *utf8string = g_utf16_to_utf8((const gunichar2*)_path_string, + _MAX_PATH, NULL, NULL, NULL); + std::unique_ptr<SPDocument> svgDoc(SPDocument::createNewDoc(utf8string, 0)); + g_free(utf8string); + + // Check the document loaded properly + if (svgDoc == NULL) { + return false; + } + if (svgDoc->getRoot() == NULL) + { + return false; + } + + // Get the size of the document + Inkscape::Util::Quantity svgWidth = svgDoc->getWidth(); + Inkscape::Util::Quantity svgHeight = svgDoc->getHeight(); + const double svgWidth_px = svgWidth.value("px"); + const double svgHeight_px = svgHeight.value("px"); + + // Find the minimum scale to fit the image inside the preview area + const double scaleFactorX = PreviewSize / svgWidth_px; + const double scaleFactorY = PreviewSize / svgHeight_px; + const double scaleFactor = (scaleFactorX > scaleFactorY) ? scaleFactorY : scaleFactorX; + + const double dpi = 96 * scaleFactor; + Geom::Rect area(0, 0, svgWidth_px, svgHeight_px); + Inkscape::Pixbuf *pixbuf = + sp_generate_internal_bitmap(svgDoc.get(), area, dpi); + + // Tidy up + if (pixbuf == NULL) { + return false; + } + + // Create the GDK pixbuf + _mutex->lock(); + _preview_bitmap_image = Glib::wrap(pixbuf->getPixbufRaw(), /* ref */ true); + delete pixbuf; + _preview_document_width = svgWidth_px; + _preview_document_height = svgHeight_px; + _preview_image_width = round(scaleFactor * svgWidth_px); + _preview_image_height = round(scaleFactor * svgHeight_px); + _mutex->unlock(); + + return true; +} + +void FileOpenDialogImplWin32::destroy_svg_rendering(const guint8 *buffer) +{ + g_assert(buffer != NULL); + g_free((void*)buffer); +} + +bool FileOpenDialogImplWin32::set_image_preview() +{ + const Glib::ustring path = utf16_to_ustring(_path_string, _MAX_PATH); + + bool successful = false; + + _mutex->lock(); + + try { + _preview_bitmap_image = Gdk::Pixbuf::create_from_file(path); + if (_preview_bitmap_image) { + _preview_image_width = _preview_bitmap_image->get_width(); + _preview_document_width = _preview_image_width; + _preview_image_height = _preview_bitmap_image->get_height(); + _preview_document_height = _preview_image_height; + successful = true; + } + } + catch (const Gdk::PixbufError&) {} + catch (const Glib::FileError&) {} + + _mutex->unlock(); + + return successful; +} + +// Aldus Placeable Header =================================================== +// Since we are a 32bit app, we have to be sure this structure compiles to +// be identical to a 16 bit app's version. To do this, we use the #pragma +// to adjust packing, we use a WORD for the hmf handle, and a SMALL_RECT +// for the bbox rectangle. +#pragma pack( push ) +#pragma pack( 2 ) +struct APMHEADER +{ + DWORD dwKey; + WORD hmf; + SMALL_RECT bbox; + WORD wInch; + DWORD dwReserved; + WORD wCheckSum; +}; +using PAPMHEADER = APMHEADER *; +#pragma pack( pop ) + + +static HENHMETAFILE +MyGetEnhMetaFileW( const WCHAR *filename ) +{ + // Try open as Enhanced Metafile + HENHMETAFILE hemf = GetEnhMetaFileW(filename); + + if (!hemf) { + // Try open as Windows Metafile + HMETAFILE hmf = GetMetaFileW(filename); + + METAFILEPICT mp; + HDC hDC; + + if (!hmf) { + WCHAR szTemp[MAX_PATH]; + + DWORD dw = GetShortPathNameW( filename, szTemp, MAX_PATH ); + if (dw) { + hmf = GetMetaFileW( szTemp ); + } + } + + if (hmf) { + // Convert Windows Metafile to Enhanced Metafile + DWORD nSize = GetMetaFileBitsEx( hmf, 0, NULL ); + + if (nSize) { + BYTE *lpvData = new BYTE[nSize]; + if (lpvData) { + DWORD dw = GetMetaFileBitsEx( hmf, nSize, lpvData ); + if (dw) { + // Fill out a METAFILEPICT structure + mp.mm = MM_ANISOTROPIC; + mp.xExt = 1000; + mp.yExt = 1000; + mp.hMF = NULL; + // Get a reference DC + hDC = GetDC( NULL ); + // Make an enhanced metafile from the windows metafile + hemf = SetWinMetaFileBits( nSize, lpvData, hDC, &mp ); + // Clean up + ReleaseDC( NULL, hDC ); + DeleteMetaFile( hmf ); + } + delete[] lpvData; + } + else { + DeleteMetaFile( hmf ); + } + } + else { + DeleteMetaFile( hmf ); + } + } + else { + // Try open as Aldus Placeable Metafile + HANDLE hFile; + hFile = CreateFileW( filename, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); + + if (hFile != INVALID_HANDLE_VALUE) { + DWORD nSize = GetFileSize( hFile, NULL ); + if (nSize) { + BYTE *lpvData = new BYTE[nSize]; + if (lpvData) { + DWORD dw = ReadFile( hFile, lpvData, nSize, &nSize, NULL ); + if (dw) { + if ( ((PAPMHEADER)lpvData)->dwKey == 0x9ac6cdd7l ) { + // Fill out a METAFILEPICT structure + mp.mm = MM_ANISOTROPIC; + mp.xExt = ((PAPMHEADER)lpvData)->bbox.Right - ((PAPMHEADER)lpvData)->bbox.Left; + mp.xExt = ( mp.xExt * 2540l ) / (DWORD)(((PAPMHEADER)lpvData)->wInch); + mp.yExt = ((PAPMHEADER)lpvData)->bbox.Bottom - ((PAPMHEADER)lpvData)->bbox.Top; + mp.yExt = ( mp.yExt * 2540l ) / (DWORD)(((PAPMHEADER)lpvData)->wInch); + mp.hMF = NULL; + // Get a reference DC + hDC = GetDC( NULL ); + // Create an enhanced metafile from the bits + hemf = SetWinMetaFileBits( nSize, lpvData+sizeof(APMHEADER), hDC, &mp ); + // Clean up + ReleaseDC( NULL, hDC ); + } + } + delete[] lpvData; + } + } + CloseHandle( hFile ); + } + } + } + + return hemf; +} + + +bool FileOpenDialogImplWin32::set_emf_preview() +{ + _mutex->lock(); + + BOOL ok = FALSE; + + DWORD w = 0; + DWORD h = 0; + + HENHMETAFILE hemf = MyGetEnhMetaFileW( _path_string ); + + if (hemf) + { + ENHMETAHEADER emh; + ZeroMemory(&emh, sizeof(emh)); + ok = GetEnhMetaFileHeader(hemf, sizeof(emh), &emh) != 0; + + w = (emh.rclFrame.right - emh.rclFrame.left); + h = (emh.rclFrame.bottom - emh.rclFrame.top); + + DeleteEnhMetaFile(hemf); + } + + if (ok) + { + const int PreviewSize = 512; + + // Get the size of the document + const double emfWidth = w; + const double emfHeight = h; + + _preview_document_width = emfWidth / 2540 * 96; // width is in units of 0.01 mm + _preview_document_height = emfHeight / 2540 * 96; // height is in units of 0.01 mm + _preview_image_width = emfWidth; + _preview_image_height = emfHeight; + + _preview_emf_image = true; + } + + _mutex->unlock(); + + return ok; +} + +void FileOpenDialogImplWin32::render_preview() +{ + double x, y; + const double blurRadius = 8; + const double halfBlurRadius = blurRadius / 2; + const int shaddowOffsetX = 0; + const int shaddowOffsetY = 2; + const int pagePadding = 5; + const double shaddowAlpha = 0.75; + + // Is the preview showing? + if(!_show_preview) + return; + + // Do we have anything to render? + _mutex->lock(); + + if(!_preview_bitmap_image && !_preview_emf_image) + { + _mutex->unlock(); + return; + } + + // Tidy up any previous bitmap renderings + if(_preview_bitmap != NULL) + DeleteObject(_preview_bitmap); + _preview_bitmap = NULL; + + // Calculate the size of the caption + int captionHeight = 0; + + if(_preview_wnd != NULL) + { + RECT rcCaptionRect; + WCHAR szCaption[_MAX_FNAME + 32]; + const int iLength = format_caption(szCaption, + sizeof(szCaption) / sizeof(WCHAR)); + + HDC dc = GetDC(_preview_wnd); + DrawTextW(dc, szCaption, iLength, &rcCaptionRect, + DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS | DT_CALCRECT); + ReleaseDC(_preview_wnd, dc); + + captionHeight = rcCaptionRect.bottom - rcCaptionRect.top; + } + + // Find the minimum scale to fit the image inside the preview area + const double scaleFactorX = ((double)_preview_width - pagePadding * 2 - blurRadius) / _preview_image_width; + const double scaleFactorY = ((double)_preview_height - pagePadding * 2 - shaddowOffsetY - halfBlurRadius - captionHeight) / _preview_image_height; + const double scaleFactor = (scaleFactorX > scaleFactorY) ? scaleFactorY : scaleFactorX; + + // Now get the resized values + const double scaledSvgWidth = scaleFactor * _preview_image_width; + const double scaledSvgHeight = scaleFactor * _preview_image_height; + + const int svgX = pagePadding + halfBlurRadius; + const int svgY = pagePadding; + + const int frameX = svgX - pagePadding; + const int frameY = svgY - pagePadding; + const int frameWidth = scaledSvgWidth + pagePadding * 2; + const int frameHeight = scaledSvgHeight + pagePadding * 2; + + const int totalWidth = (int)ceil(frameWidth + blurRadius); + const int totalHeight = (int)ceil(frameHeight + blurRadius); + + // Prepare the drawing surface + HDC hDC = GetDC(_preview_wnd); + HDC hMemDC = CreateCompatibleDC(hDC); + _preview_bitmap = CreateCompatibleBitmap(hDC, totalWidth, totalHeight); + HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemDC, _preview_bitmap); + Cairo::RefPtr<Win32Surface> surface = Win32Surface::create(hMemDC); + Cairo::RefPtr<Context> context = Context::create(surface); + + // Paint the background to match the dialog colour + const COLORREF background = GetSysColor(COLOR_3DFACE); + context->set_source_rgb( + GetRValue(background) / 255.0, + GetGValue(background) / 255.0, + GetBValue(background) / 255.0); + context->paint(); + + //----- Draw the drop shadow -----// + + // Left Edge + x = frameX + shaddowOffsetX - halfBlurRadius; + Cairo::RefPtr<LinearGradient> leftEdgeFade = LinearGradient::create( + x, 0.0, x + blurRadius, 0.0); + leftEdgeFade->add_color_stop_rgba (0, 0, 0, 0, 0); + leftEdgeFade->add_color_stop_rgba (1, 0, 0, 0, shaddowAlpha); + context->set_source(leftEdgeFade); + context->rectangle (x, frameY + shaddowOffsetY + halfBlurRadius, + blurRadius, frameHeight - blurRadius); + context->fill(); + + // Right Edge + x = frameX + frameWidth + shaddowOffsetX - halfBlurRadius; + Cairo::RefPtr<LinearGradient> rightEdgeFade = LinearGradient::create( + x, 0.0, x + blurRadius, 0.0); + rightEdgeFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + rightEdgeFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(rightEdgeFade); + context->rectangle (frameX + frameWidth + shaddowOffsetX - halfBlurRadius, + frameY + shaddowOffsetY + halfBlurRadius, + blurRadius, frameHeight - blurRadius); + context->fill(); + + // Top Edge + y = frameY + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr<LinearGradient> topEdgeFade = LinearGradient::create( + 0.0, y, 0.0, y + blurRadius); + topEdgeFade->add_color_stop_rgba (0, 0, 0, 0, 0); + topEdgeFade->add_color_stop_rgba (1, 0, 0, 0, shaddowAlpha); + context->set_source(topEdgeFade); + context->rectangle (frameX + shaddowOffsetX + halfBlurRadius, y, + frameWidth - blurRadius, blurRadius); + context->fill(); + + // Bottom Edge + y = frameY + frameHeight + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr<LinearGradient> bottomEdgeFade = LinearGradient::create( + 0.0, y, 0.0, y + blurRadius); + bottomEdgeFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + bottomEdgeFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(bottomEdgeFade); + context->rectangle (frameX + shaddowOffsetX + halfBlurRadius, y, + frameWidth - blurRadius, blurRadius); + context->fill(); + + // Top Left Corner + x = frameX + shaddowOffsetX - halfBlurRadius; + y = frameY + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr<RadialGradient> topLeftCornerFade = RadialGradient::create( + x + blurRadius, y + blurRadius, 0, x + blurRadius, y + blurRadius, blurRadius); + topLeftCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + topLeftCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(topLeftCornerFade); + context->rectangle (x, y, blurRadius, blurRadius); + context->fill(); + + // Top Right Corner + x = frameX + frameWidth + shaddowOffsetX - halfBlurRadius; + y = frameY + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr<RadialGradient> topRightCornerFade = RadialGradient::create( + x, y + blurRadius, 0, x, y + blurRadius, blurRadius); + topRightCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + topRightCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(topRightCornerFade); + context->rectangle (x, y, blurRadius, blurRadius); + context->fill(); + + // Bottom Left Corner + x = frameX + shaddowOffsetX - halfBlurRadius; + y = frameY + frameHeight + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr<RadialGradient> bottomLeftCornerFade = RadialGradient::create( + x + blurRadius, y, 0, x + blurRadius, y, blurRadius); + bottomLeftCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + bottomLeftCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(bottomLeftCornerFade); + context->rectangle (x, y, blurRadius, blurRadius); + context->fill(); + + // Bottom Right Corner + x = frameX + frameWidth + shaddowOffsetX - halfBlurRadius; + y = frameY + frameHeight + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr<RadialGradient> bottomRightCornerFade = RadialGradient::create( + x, y, 0, x, y, blurRadius); + bottomRightCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + bottomRightCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(bottomRightCornerFade); + context->rectangle (frameX + frameWidth + shaddowOffsetX - halfBlurRadius, + frameY + frameHeight + shaddowOffsetY - halfBlurRadius, + blurRadius, blurRadius); + context->fill(); + + // Draw the frame + context->set_line_width(1); + context->rectangle (frameX, frameY, frameWidth, frameHeight); + + context->set_source_rgb(1.0, 1.0, 1.0); + context->fill_preserve(); + context->set_source_rgb(0.25, 0.25, 0.25); + context->stroke_preserve(); + + // Draw the image + if(_preview_bitmap_image) // Is the image a pixbuf? + { + // Set the transformation + const Cairo::Matrix matrix( + scaleFactor, 0, + 0, scaleFactor, + svgX, svgY); + context->set_matrix (matrix); + + // Render the image + set_source_pixbuf (context, _preview_bitmap_image, 0, 0); + context->paint(); + + // Reset the transformation + context->set_identity_matrix(); + } + + // Draw the inner frame + context->set_source_rgb(0.75, 0.75, 0.75); + context->rectangle (svgX, svgY, scaledSvgWidth, scaledSvgHeight); + context->stroke(); + + _mutex->unlock(); + + // Finish drawing + surface->finish(); + + if (_preview_emf_image) { + HENHMETAFILE hemf = MyGetEnhMetaFileW(_path_string); + if (hemf) { + RECT rc; + rc.top = svgY+2; + rc.left = svgX+2; + rc.bottom = scaledSvgHeight-2; + rc.right = scaledSvgWidth-2; + PlayEnhMetaFile(hMemDC, hemf, &rc); + DeleteEnhMetaFile(hemf); + } + } + + SelectObject(hMemDC, hOldBitmap) ; + DeleteDC(hMemDC); + + // Refresh the preview pane + InvalidateRect(_preview_wnd, NULL, FALSE); +} + +int FileOpenDialogImplWin32::format_caption(wchar_t *caption, int caption_size) +{ + wchar_t szFileName[_MAX_FNAME]; + _wsplitpath(_path_string, NULL, NULL, szFileName, NULL); + + return snwprintf(caption, caption_size, + L"%ls\n%d\u2009kB\n%d\u2009px \xD7 %d\u2009px", szFileName, _preview_file_size, + (int)_preview_document_width, (int)_preview_document_height); +} + +/** + * Show this dialog modally. Return true if user hits [OK] + */ +bool +FileOpenDialogImplWin32::show() +{ + // We can only run one worker thread at a time + if(_mutex != NULL) return false; + + _result = false; + _finished = false; + _file_selected = false; + _main_loop = g_main_loop_new(g_main_context_default(), FALSE); + + _mutex = std::make_unique<std::mutex>(); + + std::thread thethread([this] { GetOpenFileName_thread(); }); + + { + while(1) + { + g_main_context_iteration(g_main_context_default(), FALSE); + + if(_mutex->try_lock()) + { + // Read mutexed data + const bool finished = _finished; + const bool is_file_selected = _file_selected; + _file_selected = false; + _mutex->unlock(); + + if(finished) break; + if(is_file_selected) file_selected(); + } + + Sleep(10); + } + } + + thethread.join(); + + // Tidy up + _mutex = NULL; + + return _result; +} + +/** + * To Get Multiple filenames selected at-once. + */ +std::vector<Glib::ustring>FileOpenDialogImplWin32::getFilenames() +{ + std::vector<Glib::ustring> result; + result.push_back(getFilename()); + return result; +} + + +/*######################################################################### +### F I L E S A V E +#########################################################################*/ + +/** + * Constructor + */ +FileSaveDialogImplWin32::FileSaveDialogImplWin32(Gtk::Window &parent, + const Glib::ustring &dir, + FileDialogType fileTypes, + const char *title, + const Glib::ustring &/*default_key*/, + const char *docTitle, + const Inkscape::Extension::FileSaveMethod save_method) + : FileDialogBaseWin32(parent, dir, title, fileTypes, + (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) ? "dialogs.save_copy" + : "dialogs.save_as") + , save_method(save_method) + , _title_label(NULL) + , _title_edit(NULL) +{ + FileSaveDialog::myDocTitle = docTitle; + + if (dialogType != CUSTOM_TYPE) + createFilterMenu(); + + /* The code below sets the default file name */ + myFilename = ""; + if (dir.size() > 0) { + Glib::ustring udir(dir); + Glib::ustring::size_type len = udir.length(); + // leaving a trailing backslash on the directory name leads to the infamous + // double-directory bug on win32 + if (len != 0 && udir[len - 1] == '\\') udir.erase(len - 1); + + // Remove the extension: remove everything past the last period found past the last slash + // (not for CUSTOM_TYPE as we can not automatically add a file extension in that case yet) + if (dialogType == CUSTOM_TYPE) { + myFilename = udir; + } else { + size_t last_slash_index = udir.find_last_of( '\\' ); + size_t last_period_index = udir.find_last_of( '.' ); + if (last_period_index > last_slash_index) { + myFilename = udir.substr(0, last_period_index ); + } + } + + // remove one slash if double + if (1 + myFilename.find("\\\\",2)) { + myFilename.replace(myFilename.find("\\\\",2), 1, ""); + } + } +} + +FileSaveDialogImplWin32::~FileSaveDialogImplWin32() +{ +} + +void FileSaveDialogImplWin32::createFilterMenu() +{ + std::list<Filter> filter_list; + + knownExtensions.clear(); + + // Compose the filter string + Glib::ustring all_inkscape_files_filter, all_image_files_filter; + Inkscape::Extension::DB::OutputList extension_list; + Inkscape::Extension::db.get_output_list(extension_list); + + int filter_count = 0; + int filter_length = 1; + bool is_raster = dialogType == RASTER_TYPES; + + for (auto omod : extension_list) { + // FIXME: would be nice to grey them out instead of not listing them + if (omod->deactivated() || (omod->is_raster() != is_raster)) + continue; + + // This extension is limited to save copy only. + if (omod->savecopy_only() && save_method != Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) + continue; + + filter_count++; + + Filter filter; + + // Extension + const gchar *filter_extension = omod->get_extension(); + filter.filter = g_utf8_to_utf16( + filter_extension, -1, NULL, &filter.filter_length, NULL); + knownExtensions.insert(std::pair<Glib::ustring, Inkscape::Extension::Output*>(Glib::ustring(filter_extension).casefold(), omod)); + + // Type + filter.name = g_utf8_to_utf16(omod->get_filetypename(true), -1, NULL, &filter.name_length, NULL); + + filter.mod = omod; + + filter_length += filter.name_length + + filter.filter_length + 3; // Add 3 for two \0s and a * + + filter_list.push_back(filter); + } + + int extension_index = 0; + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + _filter = new wchar_t[filter_length]; + wchar_t *filterptr = _filter; + + for(std::list<Filter>::iterator filter_iterator = filter_list.begin(); + filter_iterator != filter_list.end(); ++filter_iterator) + { + const Filter &filter = *filter_iterator; + + wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length); + filterptr += filter.name_length; + g_free(filter.name); + + *(filterptr++) = L'\0'; + *(filterptr++) = L'*'; + + wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length); + filterptr += filter.filter_length; + g_free(filter.filter); + + *(filterptr++) = L'\0'; + + // Associate this input extension with the file type name + _extension_map[extension_index++] = filter.mod; + } + *(filterptr++) = 0; + + _filter_count = extension_index; + _filter_index = 1; // A value of 1 selects the 1st filter - NOT the 2nd +} + + +void FileSaveDialogImplWin32::addFileType(Glib::ustring name, Glib::ustring pattern) +{ + std::list<Filter> filter_list; + + knownExtensions.clear(); + + Filter all_exe_files; + + const gchar *all_exe_files_filter_name = name.data(); + const gchar *all_exe_files_filter = pattern.data(); + + // Calculate the amount of memory required + int filter_count = 1; + int filter_length = 1; + + // Filter Executable Files + all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name, + -1, NULL, &all_exe_files.name_length, NULL); + all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter, + -1, NULL, &all_exe_files.filter_length, NULL); + all_exe_files.mod = NULL; + filter_list.push_front(all_exe_files); + + filter_length = all_exe_files.name_length + all_exe_files.filter_length + 3; // Add 3 for two \0s and a * + + knownExtensions.insert(std::pair<Glib::ustring, Inkscape::Extension::Output*>(Glib::ustring(all_exe_files_filter).casefold(), NULL)); + + int extension_index = 0; + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + _filter = new wchar_t[filter_length]; + wchar_t *filterptr = _filter; + + for(std::list<Filter>::iterator filter_iterator = filter_list.begin(); + filter_iterator != filter_list.end(); ++filter_iterator) + { + const Filter &filter = *filter_iterator; + + wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length); + filterptr += filter.name_length; + g_free(filter.name); + + *(filterptr++) = L'\0'; + *(filterptr++) = L'*'; + + if(filter.filter != NULL) + { + wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length); + filterptr += filter.filter_length; + g_free(filter.filter); + } + + *(filterptr++) = L'\0'; + + // Associate this input extension with the file type name + _extension_map[extension_index++] = filter.mod; + } + *(filterptr++) = L'\0'; + + _filter_count = extension_index; + _filter_index = 1; // Select the 1st filter in the list + + +} + +void FileSaveDialogImplWin32::GetSaveFileName_thread() +{ + OPENFILENAMEW ofn; + + g_assert(_main_loop != NULL); + + WCHAR* current_directory_string = (WCHAR*)g_utf8_to_utf16( + _current_directory.data(), _current_directory.length(), + NULL, NULL, NULL); + + // Copy the selected file name, converting from UTF-8 to UTF-16 + memset(_path_string, 0, sizeof(_path_string)); + gunichar2* utf16_path_string = g_utf8_to_utf16( + myFilename.data(), -1, NULL, NULL, NULL); + wcsncpy(_path_string, (wchar_t*)utf16_path_string, _MAX_PATH); + g_free(utf16_path_string); + + ZeroMemory(&ofn, sizeof(ofn)); + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = _ownerHwnd; + ofn.lpstrFile = _path_string; + ofn.nMaxFile = _MAX_PATH; + ofn.nFilterIndex = _filter_index; + ofn.lpstrFileTitle = NULL; + ofn.nMaxFileTitle = 0; + ofn.lpstrInitialDir = current_directory_string; + ofn.lpstrTitle = _title; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER | OFN_ENABLEHOOK | OFN_ENABLESIZING; + ofn.lpstrFilter = _filter; + ofn.nFilterIndex = _filter_index; + ofn.lpfnHook = GetSaveFileName_hookproc; + ofn.lpstrDefExt = L"svg\0"; + ofn.lCustData = (LPARAM)this; + _result = GetSaveFileNameW(&ofn) != 0; + + g_assert(ofn.nFilterIndex >= 1 && ofn.nFilterIndex <= _filter_count); + _filter_index = ofn.nFilterIndex; + _extension = _extension_map[ofn.nFilterIndex - 1]; + + // Copy the selected file name, converting from UTF-16 to UTF-8 + myFilename = utf16_to_ustring(_path_string, _MAX_PATH); + + // Tidy up + g_free(current_directory_string); + + g_main_loop_quit(_main_loop); +} + +/** + * Show this dialog modally. Return true if user hits [OK] + */ +bool +FileSaveDialogImplWin32::show() +{ + _result = false; + _main_loop = g_main_loop_new(g_main_context_default(), FALSE); + + if(_main_loop != NULL) + { + std::thread thethread([this] { GetSaveFileName_thread(); }); + g_main_loop_run(_main_loop); + + if(_result && _extension) + appendExtension(myFilename, (Inkscape::Extension::Output*)_extension); + + thethread.join(); + } + + return _result; +} + +void FileSaveDialogImplWin32::setSelectionType( Inkscape::Extension::Extension * /*key*/ ) +{ + // If no pointer to extension is passed in, look up based on filename extension. + +} + + +UINT_PTR CALLBACK FileSaveDialogImplWin32::GetSaveFileName_hookproc( + HWND hdlg, UINT uiMsg, WPARAM, LPARAM lParam) +{ + FileSaveDialogImplWin32 *pImpl = reinterpret_cast<FileSaveDialogImplWin32*> + (GetWindowLongPtr(hdlg, GWLP_USERDATA)); + + switch(uiMsg) + { + case WM_INITDIALOG: + { + HWND hParentWnd = GetParent(hdlg); + HINSTANCE hInstance = GetModuleHandle(NULL); + + // get size/pos of typical combo box + RECT rEDT1, rCB1, rROOT, rST; + GetWindowRect(GetDlgItem(hParentWnd, cmb1), &rCB1); + GetWindowRect(GetDlgItem(hParentWnd, cmb13), &rEDT1); + GetWindowRect(GetDlgItem(hParentWnd, stc2), &rST); + GetWindowRect(hdlg, &rROOT); + int ydelta = rCB1.top - rEDT1.top; + if ( ydelta < 0 ) { + g_warning("Negative dialog ydelta"); + ydelta = 0; + } + + // Make the window a bit longer + // Note: we have a width delta of 1 because there is a suspicion that MoveWindow() to the same size causes zero-width results. + RECT rcRect; + GetWindowRect(hParentWnd, &rcRect); + MoveWindow(hParentWnd, rcRect.left, rcRect.top, + sanitizeWindowSizeParam( rcRect.right - rcRect.left, 1, WINDOW_WIDTH_MINIMUM, WINDOW_WIDTH_FALLBACK ), + sanitizeWindowSizeParam( rcRect.bottom - rcRect.top, ydelta, WINDOW_HEIGHT_MINIMUM, WINDOW_HEIGHT_FALLBACK ), + FALSE); + + // It is not necessary to delete stock objects by calling DeleteObject + HGDIOBJ dlgFont = GetStockObject(DEFAULT_GUI_FONT); + + // Set the pointer to the object + OPENFILENAMEW *ofn = reinterpret_cast<OPENFILENAMEW*>(lParam); + SetWindowLongPtr(hdlg, GWLP_USERDATA, ofn->lCustData); + SetWindowLongPtr(hParentWnd, GWLP_USERDATA, ofn->lCustData); + pImpl = reinterpret_cast<FileSaveDialogImplWin32*>(ofn->lCustData); + + // Create the Title label and edit control + wchar_t *title_label_str = (wchar_t *)g_utf8_to_utf16(_("Title:"), -1, NULL, NULL, NULL); + pImpl->_title_label = CreateWindowExW(0, L"STATIC", title_label_str, + WS_VISIBLE|WS_CHILD, + CW_USEDEFAULT, CW_USEDEFAULT, rCB1.left-rST.left, rST.bottom-rST.top, + hParentWnd, NULL, hInstance, NULL); + g_free(title_label_str); + + if(pImpl->_title_label) { + if(dlgFont) SendMessage(pImpl->_title_label, WM_SETFONT, (WPARAM)dlgFont, MAKELPARAM(FALSE, 0)); + SetWindowPos(pImpl->_title_label, NULL, rST.left-rROOT.left, rST.top+ydelta-rROOT.top, + rCB1.left-rST.left, rST.bottom-rST.top, SWP_SHOWWINDOW|SWP_NOZORDER); + } + + pImpl->_title_edit = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "", + WS_VISIBLE|WS_CHILD|WS_TABSTOP|ES_AUTOHSCROLL, + CW_USEDEFAULT, CW_USEDEFAULT, rCB1.right-rCB1.left, rCB1.bottom-rCB1.top, + hParentWnd, NULL, hInstance, NULL); + if(pImpl->_title_edit) { + if(dlgFont) SendMessage(pImpl->_title_edit, WM_SETFONT, (WPARAM)dlgFont, MAKELPARAM(FALSE, 0)); + SetWindowPos(pImpl->_title_edit, NULL, rCB1.left-rROOT.left, rCB1.top+ydelta-rROOT.top, + rCB1.right-rCB1.left, rCB1.bottom-rCB1.top, SWP_SHOWWINDOW|SWP_NOZORDER); + SetWindowTextW(pImpl->_title_edit, + (const wchar_t*)g_utf8_to_utf16(pImpl->myDocTitle.c_str(), -1, NULL, NULL, NULL)); + } + } + break; + case WM_DESTROY: + { + if(pImpl->_title_edit) { + int length = GetWindowTextLengthW(pImpl->_title_edit)+1; + wchar_t* temp_title = new wchar_t[length]; + GetWindowTextW(pImpl->_title_edit, temp_title, length); + pImpl->myDocTitle = g_utf16_to_utf8((gunichar2*)temp_title, -1, NULL, NULL, NULL); + delete[] temp_title; + DestroyWindow(pImpl->_title_label); + pImpl->_title_label = NULL; + DestroyWindow(pImpl->_title_edit); + pImpl->_title_edit = NULL; + } + } + break; + } + + // Use default dialog behaviour + return 0; +} + +} } } // namespace Dialog, UI, Inkscape + +#endif // ifdef _WIN32 + +/* + 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/ui/dialog/filedialogimpl-win32.h b/src/ui/dialog/filedialogimpl-win32.h new file mode 100644 index 0000000..b115c61 --- /dev/null +++ b/src/ui/dialog/filedialogimpl-win32.h @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Implementation of native file dialogs for Win32 + */ +/* Authors: + * Joel Holdsworth + * The Inkscape Organization + * + * Copyright (C) 2004-2008 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm.h> + +#ifdef _WIN32 + +#include "filedialogimpl-gtkmm.h" + +#include "inkgc/gc-core.h" + +#include <memory> +#include <mutex> +#include <windows.h> + + +namespace Inkscape +{ +namespace UI +{ +namespace Dialog +{ + +/*######################################################################### +### F I L E D I A L O G B A S E C L A S S +#########################################################################*/ + +/// This class is the base implementation of a MS Windows +/// file dialog. +class FileDialogBaseWin32 +{ +protected: + /// Abstract Constructor + /// @param parent The parent window for the dialog + /// @param dir The directory to begin browsing from + /// @param title The title caption for the dialog in UTF-8 + /// @param type The dialog type + /// @param preferenceBase The preferences key + FileDialogBaseWin32(Gtk::Window &parent, const Glib::ustring &dir, + const char *title, FileDialogType type, + gchar const *preferenceBase); + + /// Destructor + ~FileDialogBaseWin32(); + +public: + + /// Gets the currently selected extension. Valid after an [OK] + /// @return Returns a pointer to the selected extension, or NULL + /// if the selected filter requires an automatic type detection + Inkscape::Extension::Extension* getSelectionType(); + + /// Get the path of the current directory + Glib::ustring getCurrentDirectory(); + + +protected: + /// The dialog type + FileDialogType dialogType; + + /// A pointer to the GTK main-loop context object. This + /// is used to keep the rest of the inkscape UI running + /// while the file dialog is displayed + GMainLoop *_main_loop; + + /// The result of the call to GetOpenFileName. If true + /// the user clicked OK, if false the user clicked cancel + bool _result; + + /// The parent window + Gtk::Window &parent; + + /// The windows handle of the parent window + HWND _ownerHwnd; + + /// The path of the directory that is currently being + /// browsed + Glib::ustring _current_directory; + + /// The title of the dialog in UTF-16 + wchar_t *_title; + + /// The path of the currently selected file in UTF-16 + wchar_t _path_string[_MAX_PATH]; + + /// The filter string for GetOpenFileName in UTF-16 + wchar_t *_filter; + + /// The index of the currently selected filter. + /// This value must be greater than or equal to 1, + /// and less than or equal to _filter_count. + unsigned int _filter_index; + + /// The number of filters registered + unsigned int _filter_count; + + /// An array of the extensions associated with the + /// file types of each filter. So the Nth entry of + /// this array corresponds to the extension of the Nth + /// filter in the list. NULL if no specific extension is + /// specified/ + Inkscape::Extension::Extension **_extension_map; + + /// The currently selected extension. Valid after an [OK] + Inkscape::Extension::Extension *_extension; +}; + + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +/// An Inkscape compatible wrapper around MS Windows GetOpenFileName API +class FileOpenDialogImplWin32 : public FileOpenDialog, public FileDialogBaseWin32 +{ +public: + /// Constructor + /// @param parent The parent window for the dialog + /// @param dir The directory to begin browsing from + /// @param title The title caption for the dialog in UTF-8 + /// @param type The dialog type + FileOpenDialogImplWin32(Gtk::Window &parent, + const Glib::ustring &dir, + FileDialogType fileTypes, + const char *title); + + /// Destructor + virtual ~FileOpenDialogImplWin32(); + + /// Shows the file dialog, and blocks until a file + /// has been selected. + /// @return Returns true if the user selected a + /// file, or false if the user pressed cancel. + bool show(); + + /// Gets a list of the selected file names + /// @return Returns an STL vector filled with the + /// GTK names of the selected files + std::vector<Glib::ustring> getFilenames(); + + /// Get the path of the current directory + virtual Glib::ustring getCurrentDirectory() + { return FileDialogBaseWin32::getCurrentDirectory(); } + + /// Gets the currently selected extension. Valid after an [OK] + /// @return Returns a pointer to the selected extension, or NULL + /// if the selected filter requires an automatic type detection + virtual Inkscape::Extension::Extension* getSelectionType() + { return FileDialogBaseWin32::getSelectionType(); } + + + /// Add a custom file filter menu item + /// @param name - Name of the filter (such as "Javscript") + /// @param pattern - File filtering pattern (such as "*.js") + /// Use the FileDialogType::CUSTOM_TYPE in constructor to not include other file types + virtual void addFilterMenu(Glib::ustring name, Glib::ustring pattern); + +private: + + /// Create filter menu for this type of dialog + void createFilterMenu(); + + /// The handle of the preview pane window + HWND _preview_wnd; + + /// The handle of the file dialog window + HWND _file_dialog_wnd; + + /// A pointer to the standard window proc of the + /// unhooked file dialog + WNDPROC _base_window_proc; + + /// The handle of the bitmap of the "show preview" + /// toggle button + HBITMAP _show_preview_button_bitmap; + + /// The handle of the toolbar's window + HWND _toolbar_wnd; + + /// This flag is set true when the preview should be + /// shown, or false when it should be hidden + static bool _show_preview; + + + /// The current width of the preview pane in pixels + int _preview_width; + + /// The current height of the preview pane in pixels + int _preview_height; + + /// The handle of the windows to display within the + /// preview pane, or NULL if no image should be displayed + HBITMAP _preview_bitmap; + + /// The windows shell icon for the selected file + HICON _preview_file_icon; + + /// The size of the preview file in kilobytes + unsigned long _preview_file_size; + + + /// The width of the document to be shown in the preview panel + double _preview_document_width; + + /// The width of the document to be shown in the preview panel + double _preview_document_height; + + /// The width of the rendered preview image in pixels + int _preview_image_width; + + /// The height of the rendered preview image in pixels + int _preview_image_height; + + /// A GDK Pixbuf of the rendered preview to be displayed + Glib::RefPtr<Gdk::Pixbuf> _preview_bitmap_image; + + /// This flag is set true if a file has been selected + bool _file_selected; + + /// This flag is set true when the GetOpenFileName call + /// has returned + bool _finished; + + /// This mutex is used to ensure that the worker thread + /// that calls GetOpenFileName cannot collide with the + /// main Inkscape thread + std::unique_ptr<std::mutex> _mutex; + + /// The controller function for the thread which calls + /// GetOpenFileName + void GetOpenFileName_thread(); + + /// Registers the Windows Class of the preview panel window + static void register_preview_wnd_class(); + + /// A message proc which is called by the standard dialog + /// proc + static UINT_PTR CALLBACK GetOpenFileName_hookproc(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam); + + /// A message proc which wraps the standard dialog proc, + /// but intercepts some calls + static LRESULT CALLBACK file_dialog_subclass_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + + /// The message proc for the preview panel window + static LRESULT CALLBACK preview_wnd_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + + /// Lays out the controls in the file dialog given it's + /// current size + /// GetOpenFileName thread only. + void layout_dialog(); + + /// Enables or disables the file preview. + /// GetOpenFileName thread only. + void enable_preview(bool enable); + + /// This function is called in the App thread when a file had + /// been selected + void file_selected(); + + /// Loads and renders the unshrunk preview image. + /// Main app thread only. + void load_preview(); + + /// Frees all the allocated objects associated with the file + /// currently being previewed + /// Main app thread only. + void free_preview(); + + /// Loads preview for an SVG or SVGZ file. + /// Main app thread only. + /// @return Returns true if the SVG loaded successfully + bool set_svg_preview(); + + /// A callback to allow this class to dispose of the + /// memory block of the rendered SVG bitmap + /// @buffer buffer The buffer to free + static void destroy_svg_rendering(const guint8 *buffer); + + /// Loads the preview for a raster image + /// Main app thread only. + /// @return Returns true if the image loaded successfully + bool set_image_preview(); + + /// Loads the preview for a meta file + /// Main app thread only. + /// @return Returns true if the image loaded successfully + bool set_emf_preview(); + + /// This flag is set true when a meta file is previewed + bool _preview_emf_image; + + /// Renders the unshrunk preview image to a windows HTBITMAP + /// which can be painted in the preview pain. + /// Main app thread only. + void render_preview(); + + /// Formats the caption in UTF-16 for the preview image + /// @param caption The buffer to format the caption string into + /// @param caption_size The number of wchar_ts in the caption buffer + /// @return Returns the number of characters in caption string + int format_caption(wchar_t *caption, int caption_size); +}; + + +/*######################################################################### +### F I L E S A V E +#########################################################################*/ + +/// An Inkscape compatible wrapper around MS Windows GetSaveFileName API +class FileSaveDialogImplWin32 : public FileSaveDialog, public FileDialogBaseWin32 +{ + +public: + FileSaveDialogImplWin32(Gtk::Window &parent, + const Glib::ustring &dir, + FileDialogType fileTypes, + const char *title, + const Glib::ustring &default_key, + const char *docTitle, + const Inkscape::Extension::FileSaveMethod save_method); + + /// Destructor + virtual ~FileSaveDialogImplWin32(); + + /// Shows the file dialog, and blocks until a file + /// has been selected. + /// @return Returns true if the user selected a + /// file, or false if the user pressed cancel. + bool show(); + + /// Get the path of the current directory + virtual Glib::ustring getCurrentDirectory() + { return FileDialogBaseWin32::getCurrentDirectory(); } + + /// Gets the currently selected extension. Valid after an [OK] + /// @return Returns a pointer to the selected extension, or NULL + /// if the selected filter requires an automatic type detection + virtual Inkscape::Extension::Extension* getSelectionType() + { return FileDialogBaseWin32::getSelectionType(); } + + virtual void setSelectionType( Inkscape::Extension::Extension *key ); + + virtual void addFileType(Glib::ustring name, Glib::ustring pattern); + +private: + /// A handle to the title label and edit box + HWND _title_label; + HWND _title_edit; + + /// Create a filter menu for this type of dialog + void createFilterMenu(); + + // SaveAs or SaveAsCopy + Inkscape::Extension::FileSaveMethod save_method; + + /// The controller function for the thread which calls + /// GetSaveFileName + void GetSaveFileName_thread(); + + /// A message proc which is called by the standard dialog + /// proc + static UINT_PTR CALLBACK GetSaveFileName_hookproc(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam); + +}; + + +} +} +} + +#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/ui/dialog/fill-and-stroke.cpp b/src/ui/dialog/fill-and-stroke.cpp new file mode 100644 index 0000000..66b3dcc --- /dev/null +++ b/src/ui/dialog/fill-and-stroke.cpp @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Fill and Stroke dialog - implementation. + * + * Based on the old sp_object_properties_dialog. + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Gustav Broberg <broberg@kth.se> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2004--2007 Authors + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "desktop-style.h" +#include "document.h" +#include "fill-and-stroke.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "preferences.h" + +#include "svg/css-ostringstream.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" + +#include "ui/widget/fill-style.h" +#include "ui/widget/stroke-style.h" +#include "ui/widget/notebook-page.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +FillAndStroke::FillAndStroke() + : DialogBase("/dialogs/fillstroke", "FillStroke") + , _page_fill(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true))) + , _page_stroke_paint(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true))) + , _page_stroke_style(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true))) + , _composite_settings(INKSCAPE_ICON("dialog-fill-and-stroke"), + "fillstroke", + UI::Widget::SimpleFilterModifier::ISOLATION | + UI::Widget::SimpleFilterModifier::BLEND | + UI::Widget::SimpleFilterModifier::BLUR | + UI::Widget::SimpleFilterModifier::OPACITY) + , fillWdgt(nullptr) + , strokeWdgt(nullptr) +{ + set_spacing(2); + pack_start(_notebook, true, true); + + _notebook.append_page(*_page_fill, _createPageTabLabel(_("_Fill"), INKSCAPE_ICON("object-fill"))); + _notebook.append_page(*_page_stroke_paint, _createPageTabLabel(_("Stroke _paint"), INKSCAPE_ICON("object-stroke"))); + _notebook.append_page(*_page_stroke_style, _createPageTabLabel(_("Stroke st_yle"), INKSCAPE_ICON("object-stroke-style"))); + _notebook.set_vexpand(true); + + _notebook.signal_switch_page().connect(sigc::mem_fun(this, &FillAndStroke::_onSwitchPage)); + + _layoutPageFill(); + _layoutPageStrokePaint(); + _layoutPageStrokeStyle(); + + pack_end(_composite_settings, Gtk::PACK_SHRINK); + + show_all_children(); + + _composite_settings.setSubject(&_subject); +} + +FillAndStroke::~FillAndStroke() +{ + // Disconnect signals from composite settings + _composite_settings.setSubject(nullptr); + fillWdgt->setDesktop(nullptr); + strokeWdgt->setDesktop(nullptr); + strokeStyleWdgt->setDesktop(nullptr); + _subject.setDesktop(nullptr); +} + +void FillAndStroke::selectionChanged(Selection *selection) +{ + if (fillWdgt) { + fillWdgt->performUpdate(); + } + if (strokeWdgt) { + strokeWdgt->performUpdate(); + } + if (strokeStyleWdgt) { + strokeStyleWdgt->selectionChangedCB(); + } +} +void FillAndStroke::selectionModified(Selection *selection, guint flags) +{ + if (fillWdgt) { + fillWdgt->selectionModifiedCB(flags); + } + if (strokeWdgt) { + strokeWdgt->selectionModifiedCB(flags); + } + if (strokeStyleWdgt) { + strokeStyleWdgt->selectionModifiedCB(flags); + } +} + +void FillAndStroke::desktopReplaced() +{ + if (fillWdgt) { + fillWdgt->setDesktop(getDesktop()); + } + if (strokeWdgt) { + strokeWdgt->setDesktop(getDesktop()); + } + if (strokeStyleWdgt) { + strokeStyleWdgt->setDesktop(getDesktop()); + } + _subject.setDesktop(getDesktop()); +} + +void FillAndStroke::_onSwitchPage(Gtk::Widget * /*page*/, guint pagenum) +{ + _savePagePref(pagenum); +} + +void +FillAndStroke::_savePagePref(guint page_num) +{ + // remember the current page + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/fillstroke/page", page_num); +} + +void +FillAndStroke::_layoutPageFill() +{ + fillWdgt = Gtk::manage(new UI::Widget::FillNStroke(FILL)); + _page_fill->table().attach(*fillWdgt, 0, 0, 1, 1); +} + +void +FillAndStroke::_layoutPageStrokePaint() +{ + strokeWdgt = Gtk::manage(new UI::Widget::FillNStroke(STROKE)); + _page_stroke_paint->table().attach(*strokeWdgt, 0, 0, 1, 1); +} + +void +FillAndStroke::_layoutPageStrokeStyle() +{ + strokeStyleWdgt = Gtk::manage(new UI::Widget::StrokeStyle()); + strokeStyleWdgt->set_hexpand(); + strokeStyleWdgt->set_halign(Gtk::ALIGN_START); + _page_stroke_style->table().attach(*strokeStyleWdgt, 0, 0, 1, 1); +} + +void +FillAndStroke::showPageFill() +{ + blink(); + _notebook.set_current_page(0); + _savePagePref(0); + +} + +void +FillAndStroke::showPageStrokePaint() +{ + blink(); + _notebook.set_current_page(1); + _savePagePref(1); +} + +void +FillAndStroke::showPageStrokeStyle() +{ + blink(); + _notebook.set_current_page(2); + _savePagePref(2); + +} + +Gtk::Box& +FillAndStroke::_createPageTabLabel(const Glib::ustring& label, const char *label_image) +{ + Gtk::Box *_tab_label_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4)); + + auto img = Gtk::manage(sp_get_icon_image(label_image, Gtk::ICON_SIZE_MENU)); + _tab_label_box->pack_start(*img); + + Gtk::Label *_tab_label = Gtk::manage(new Gtk::Label(label, true)); + _tab_label_box->pack_start(*_tab_label); + _tab_label_box->show_all(); + + return *_tab_label_box; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/fill-and-stroke.h b/src/ui/dialog/fill-and-stroke.h new file mode 100644 index 0000000..f5c9bad --- /dev/null +++ b/src/ui/dialog/fill-and-stroke.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Fill and Stroke dialog + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Gustav Broberg <broberg@kth.se> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2004--2007 Authors + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_FILL_AND_STROKE_H +#define INKSCAPE_UI_DIALOG_FILL_AND_STROKE_H + +#include <gtkmm/notebook.h> + +#include "ui/dialog/dialog-base.h" +#include "ui/widget/object-composite-settings.h" +#include "ui/widget/style-subject.h" + +namespace Inkscape { +namespace UI { + +namespace Widget { +class FillNStroke; +class NotebookPage; +class StrokeStyle; +} + +namespace Dialog { + +class FillAndStroke : public DialogBase +{ +public: + FillAndStroke(); + ~FillAndStroke() override; + + static FillAndStroke &getInstance() { return *new FillAndStroke(); } + + void desktopReplaced() override; + + void showPageFill(); + void showPageStrokePaint(); + void showPageStrokeStyle(); + +protected: + Gtk::Notebook _notebook; + + UI::Widget::NotebookPage *_page_fill; + UI::Widget::NotebookPage *_page_stroke_paint; + UI::Widget::NotebookPage *_page_stroke_style; + + UI::Widget::StyleSubject::Selection _subject; + UI::Widget::ObjectCompositeSettings _composite_settings; + + Gtk::Box &_createPageTabLabel(const Glib::ustring &label, + const char *label_image); + + void _layoutPageFill(); + void _layoutPageStrokePaint(); + void _layoutPageStrokeStyle(); + void _savePagePref(guint page_num); + void _onSwitchPage(Gtk::Widget *page, guint pagenum); + +private: + void selectionChanged(Selection *selection) override; + void selectionModified(Selection *selection, guint flags) override; + FillAndStroke(FillAndStroke const &d) = delete; + FillAndStroke& operator=(FillAndStroke const &d) = delete; + + UI::Widget::FillNStroke *fillWdgt; + UI::Widget::FillNStroke *strokeWdgt; + UI::Widget::StrokeStyle *strokeStyleWdgt; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + +#endif // INKSCAPE_UI_DIALOG_FILL_AND_STROKE_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/ui/dialog/filter-effects-dialog.cpp b/src/ui/dialog/filter-effects-dialog.cpp new file mode 100644 index 0000000..7bb4fe6 --- /dev/null +++ b/src/ui/dialog/filter-effects-dialog.cpp @@ -0,0 +1,3124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Filter Effects dialog. + */ +/* Authors: + * Nicholas Bishop <nicholasbishop@gmail.org> + * Rodrigo Kumpera <kumpera@gmail.com> + * Felipe C. da S. Sanches <juca@members.fsf.org> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * insaner + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/imagemenuitem.h> + +#include <gdkmm/display.h> +#include <gdkmm/general.h> +#include <gdkmm/seat.h> + +#include <gtkmm/checkbutton.h> +#include <gtkmm/colorbutton.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/sizegroup.h> + +#include <glibmm/i18n.h> +#include <glibmm/stringutils.h> +#include <glibmm/main.h> +#include <glibmm/convert.h> + +#include <utility> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "filter-effects-dialog.h" +#include "filter-enums.h" +#include "inkscape.h" +#include "layer-manager.h" +#include "selection-chemistry.h" +#include "style.h" + +#include "include/gtkmm_version.h" + +#include "object/filters/blend.h" +#include "object/filters/colormatrix.h" +#include "object/filters/componenttransfer.h" +#include "object/filters/componenttransfer-funcnode.h" +#include "object/filters/convolvematrix.h" +#include "object/filters/distantlight.h" +#include "object/filters/merge.h" +#include "object/filters/mergenode.h" +#include "object/filters/pointlight.h" +#include "object/filters/spotlight.h" + +#include "svg/svg-color.h" + +#include "ui/dialog/filedialog.h" +#include "ui/icon-names.h" +#include "ui/util.h" +#include "ui/widget/filter-effect-chooser.h" +#include "ui/widget/spinbutton.h" + +#include "io/sys.h" + + +using namespace Inkscape::Filters; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +using Inkscape::UI::Widget::AttrWidget; +using Inkscape::UI::Widget::ComboBoxEnum; +using Inkscape::UI::Widget::DualSpinScale; +using Inkscape::UI::Widget::SpinScale; + + +// Returns the number of inputs available for the filter primitive type +static int input_count(const SPFilterPrimitive* prim) +{ + if(!prim) + return 0; + else if(SP_IS_FEBLEND(prim) || SP_IS_FECOMPOSITE(prim) || SP_IS_FEDISPLACEMENTMAP(prim)) + return 2; + else if(SP_IS_FEMERGE(prim)) { + // Return the number of feMergeNode connections plus an extra + return (int) (prim->children.size() + 1); + } + else + return 1; +} + +class CheckButtonAttr : public Gtk::CheckButton, public AttrWidget +{ +public: + CheckButtonAttr(bool def, const Glib::ustring& label, + Glib::ustring tv, Glib::ustring fv, + const SPAttr a, char* tip_text) + : Gtk::CheckButton(label), + AttrWidget(a, def), + _true_val(std::move(tv)), _false_val(std::move(fv)) + { + signal_toggled().connect(signal_attr_changed().make_slot()); + if (tip_text) { + set_tooltip_text(tip_text); + } + } + + Glib::ustring get_as_attribute() const override + { + return get_active() ? _true_val : _false_val; + } + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + if(val) { + if(_true_val == val) + set_active(true); + else if(_false_val == val) + set_active(false); + } else { + set_active(get_default()->as_bool()); + } + } +private: + const Glib::ustring _true_val, _false_val; +}; + +class SpinButtonAttr : public Inkscape::UI::Widget::SpinButton, public AttrWidget +{ +public: + SpinButtonAttr(double lower, double upper, double step_inc, + double climb_rate, int digits, const SPAttr a, double def, char* tip_text) + : Inkscape::UI::Widget::SpinButton(climb_rate, digits), + AttrWidget(a, def) + { + if (tip_text) { + set_tooltip_text(tip_text); + } + set_range(lower, upper); + set_increments(step_inc, 0); + + signal_value_changed().connect(signal_attr_changed().make_slot()); + } + + Glib::ustring get_as_attribute() const override + { + const double val = get_value(); + + if(get_digits() == 0) + return Glib::Ascii::dtostr((int)val); + else + return Glib::Ascii::dtostr(val); + } + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + if(val){ + set_value(Glib::Ascii::strtod(val)); + } else { + set_value(get_default()->as_double()); + } + } +}; + +template< typename T> class ComboWithTooltip : public Gtk::EventBox +{ +public: + ComboWithTooltip<T>(T default_value, const Util::EnumDataConverter<T>& c, const SPAttr a = SPAttr::INVALID, char* tip_text = nullptr) + { + if (tip_text) { + set_tooltip_text(tip_text); + } + combo = new ComboBoxEnum<T>(default_value, c, a, false); + add(*combo); + show_all(); + } + + ~ComboWithTooltip() override + { + delete combo; + } + + ComboBoxEnum<T>* get_attrwidget() + { + return combo; + } +private: + ComboBoxEnum<T>* combo; +}; + +// Contains an arbitrary number of spin buttons that use separate attributes +class MultiSpinButton : public Gtk::Box +{ +public: + MultiSpinButton(double lower, double upper, double step_inc, + double climb_rate, int digits, std::vector<SPAttr> attrs, std::vector<double> default_values, std::vector<char*> tip_text) + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) + { + g_assert(attrs.size()==default_values.size()); + g_assert(attrs.size()==tip_text.size()); + set_spacing(4); + for(unsigned i = 0; i < attrs.size(); ++i) { + unsigned index = attrs.size() - 1 - i; + _spins.push_back(new SpinButtonAttr(lower, upper, step_inc, climb_rate, digits, attrs[index], default_values[index], tip_text[index])); + pack_end(*_spins.back(), false, false); + } + } + + ~MultiSpinButton() override + { + for(auto & _spin : _spins) + delete _spin; + } + + std::vector<SpinButtonAttr*>& get_spinbuttons() + { + return _spins; + } +private: + std::vector<SpinButtonAttr*> _spins; +}; + +// Contains two spinbuttons that describe a NumberOptNumber +class DualSpinButton : public Gtk::Box, public AttrWidget +{ +public: + DualSpinButton(char* def, double lower, double upper, double step_inc, + double climb_rate, int digits, const SPAttr a, char* tt1, char* tt2) + : AttrWidget(a, def), //TO-DO: receive default num-opt-num as parameter in the constructor + Gtk::Box(Gtk::ORIENTATION_HORIZONTAL), + _s1(climb_rate, digits), _s2(climb_rate, digits) + { + if (tt1) { + _s1.set_tooltip_text(tt1); + } + if (tt2) { + _s2.set_tooltip_text(tt2); + } + _s1.set_range(lower, upper); + _s2.set_range(lower, upper); + _s1.set_increments(step_inc, 0); + _s2.set_increments(step_inc, 0); + + _s1.signal_value_changed().connect(signal_attr_changed().make_slot()); + _s2.signal_value_changed().connect(signal_attr_changed().make_slot()); + + set_spacing(4); + pack_end(_s2, false, false); + pack_end(_s1, false, false); + } + + Inkscape::UI::Widget::SpinButton& get_spinbutton1() + { + return _s1; + } + + Inkscape::UI::Widget::SpinButton& get_spinbutton2() + { + return _s2; + } + + Glib::ustring get_as_attribute() const override + { + double v1 = _s1.get_value(); + double v2 = _s2.get_value(); + + if(_s1.get_digits() == 0) { + v1 = (int)v1; + v2 = (int)v2; + } + + return Glib::Ascii::dtostr(v1) + " " + Glib::Ascii::dtostr(v2); + } + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + NumberOptNumber n; + if(val) { + n.set(val); + } else { + n.set(get_default()->as_charptr()); + } + _s1.set_value(n.getNumber()); + _s2.set_value(n.getOptNumber()); + + } +private: + Inkscape::UI::Widget::SpinButton _s1, _s2; +}; + +class ColorButton : public Gtk::ColorButton, public AttrWidget +{ +public: + ColorButton(unsigned int def, const SPAttr a, char* tip_text) + : AttrWidget(a, def) + { + signal_color_set().connect(signal_attr_changed().make_slot()); + if (tip_text) { + set_tooltip_text(tip_text); + } + + Gdk::RGBA col; + col.set_rgba_u(65535, 65535, 65535); + set_rgba(col); + } + + // Returns the color in 'rgb(r,g,b)' form. + Glib::ustring get_as_attribute() const override + { + // no doubles here, so we can use the standard string stream. + std::ostringstream os; + + const auto c = get_rgba(); + const int r = c.get_red_u() / 257, g = c.get_green_u() / 257, b = c.get_blue_u() / 257;//TO-DO: verify this. This sounds a lot strange! shouldn't it be 256? + os << "rgb(" << r << "," << g << "," << b << ")"; + return os.str(); + } + + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + guint32 i = 0; + if(val) { + i = sp_svg_read_color(val, 0xFFFFFFFF); + } else { + i = (guint32) get_default()->as_uint(); + } + const int r = SP_RGBA32_R_U(i), g = SP_RGBA32_G_U(i), b = SP_RGBA32_B_U(i); + + Gdk::RGBA col; + col.set_rgba_u(r * 256, g * 256, b * 256); + set_rgba(col); + } +}; + +// Used for tableValue in feComponentTransfer +class EntryAttr : public Gtk::Entry, public AttrWidget +{ +public: + EntryAttr(const SPAttr a, char* tip_text) + : AttrWidget(a) + { + signal_changed().connect(signal_attr_changed().make_slot()); + if (tip_text) { + set_tooltip_text(tip_text); + } + } + + // No validity checking is done + Glib::ustring get_as_attribute() const override + { + return get_text(); + } + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + if(val) { + set_text( val ); + } else { + set_text( "" ); + } + } +}; + +/* Displays/Edits the matrix for feConvolveMatrix or feColorMatrix */ +class FilterEffectsDialog::MatrixAttr : public Gtk::Frame, public AttrWidget +{ +public: + MatrixAttr(const SPAttr a, char* tip_text = nullptr) + : AttrWidget(a), _locked(false) + { + _model = Gtk::ListStore::create(_columns); + _tree.set_model(_model); + _tree.set_headers_visible(false); + _tree.show(); + add(_tree); + set_shadow_type(Gtk::SHADOW_IN); + if (tip_text) { + _tree.set_tooltip_text(tip_text); + } + } + + std::vector<double> get_values() const + { + std::vector<double> vec; + for(const auto & iter : _model->children()) { + for(unsigned c = 0; c < _tree.get_columns().size(); ++c) + vec.push_back(iter[_columns.cols[c]]); + } + return vec; + } + + void set_values(const std::vector<double>& v) + { + unsigned i = 0; + for(const auto & iter : _model->children()) { + for(unsigned c = 0; c < _tree.get_columns().size(); ++c) { + if(i >= v.size()) + return; + iter[_columns.cols[c]] = v[i]; + ++i; + } + } + } + + Glib::ustring get_as_attribute() const override + { + // use SVGOStringStream to output SVG-compatible doubles + Inkscape::SVGOStringStream os; + + for(const auto & iter : _model->children()) { + for(unsigned c = 0; c < _tree.get_columns().size(); ++c) { + os << iter[_columns.cols[c]] << " "; + } + } + + return os.str(); + } + + void set_from_attribute(SPObject* o) override + { + if(o) { + if(SP_IS_FECONVOLVEMATRIX(o)) { + SPFeConvolveMatrix* conv = SP_FECONVOLVEMATRIX(o); + int cols, rows; + cols = (int)conv->order.getNumber(); + if(cols > 5) + cols = 5; + rows = conv->order.optNumber_set ? (int)conv->order.getOptNumber() : cols; + update(o, rows, cols); + } + else if(SP_IS_FECOLORMATRIX(o)) + update(o, 4, 5); + } + } +private: + class MatrixColumns : public Gtk::TreeModel::ColumnRecord + { + public: + MatrixColumns() + { + cols.resize(5); + for(auto & col : cols) + add(col); + } + std::vector<Gtk::TreeModelColumn<double> > cols; + }; + + void update(SPObject* o, const int rows, const int cols) + { + if(_locked) + return; + + _model->clear(); + + _tree.remove_all_columns(); + + std::vector<gdouble>* values = nullptr; + if(SP_IS_FECOLORMATRIX(o)) + values = &SP_FECOLORMATRIX(o)->values; + else if(SP_IS_FECONVOLVEMATRIX(o)) + values = &SP_FECONVOLVEMATRIX(o)->kernelMatrix; + else + return; + + if(o) { + int ndx = 0; + + for(int i = 0; i < cols; ++i) { + _tree.append_column_numeric_editable("", _columns.cols[i], "%.2f"); + static_cast<Gtk::CellRendererText*>( + _tree.get_column_cell_renderer(i))->signal_edited().connect( + sigc::mem_fun(*this, &MatrixAttr::rebind)); + } + + for(int r = 0; r < rows; ++r) { + Gtk::TreeRow row = *(_model->append()); + // Default to identity matrix + for(int c = 0; c < cols; ++c, ++ndx) + row[_columns.cols[c]] = ndx < (int)values->size() ? (*values)[ndx] : (r == c ? 1 : 0); + } + } + } + + void rebind(const Glib::ustring&, const Glib::ustring&) + { + _locked = true; + signal_attr_changed()(); + _locked = false; + } + + bool _locked; + Gtk::TreeView _tree; + Glib::RefPtr<Gtk::ListStore> _model; + MatrixColumns _columns; +}; + +// Displays a matrix or a slider for feColorMatrix +class FilterEffectsDialog::ColorMatrixValues : public Gtk::Frame, public AttrWidget +{ +public: + ColorMatrixValues() + : AttrWidget(SPAttr::VALUES), + // TRANSLATORS: this dialog is accessible via menu Filters - Filter editor + _matrix(SPAttr::VALUES, _("This matrix determines a linear transform on color space. Each line affects one of the color components. Each column determines how much of each color component from the input is passed to the output. The last column does not depend on input colors, so can be used to adjust a constant component value.")), + _saturation("", 1, 0, 1, 0.1, 0.01, 2, SPAttr::VALUES), + _angle("", 0, 0, 360, 0.1, 0.01, 1, SPAttr::VALUES), + _label(C_("Label", "None"), Gtk::ALIGN_START), + _use_stored(false), + _saturation_store(1.0), + _angle_store(0) + { + _matrix.signal_attr_changed().connect(signal_attr_changed().make_slot()); + _saturation.signal_attr_changed().connect(signal_attr_changed().make_slot()); + _angle.signal_attr_changed().connect(signal_attr_changed().make_slot()); + signal_attr_changed().connect(sigc::mem_fun(*this, &ColorMatrixValues::update_store)); + + _matrix.show(); + _saturation.show(); + _angle.show(); + _label.show(); + _label.set_sensitive(false); + + set_shadow_type(Gtk::SHADOW_NONE); + } + + void set_from_attribute(SPObject* o) override + { + std::string values_string; + if(SP_IS_FECOLORMATRIX(o)) { + SPFeColorMatrix* col = SP_FECOLORMATRIX(o); + remove(); + switch(col->type) { + case COLORMATRIX_SATURATE: + add(_saturation); + if(_use_stored) + _saturation.set_value(_saturation_store); + else + _saturation.set_from_attribute(o); + values_string = Glib::Ascii::dtostr(_saturation.get_value()); + break; + case COLORMATRIX_HUEROTATE: + add(_angle); + if(_use_stored) + _angle.set_value(_angle_store); + else + _angle.set_from_attribute(o); + values_string = Glib::Ascii::dtostr(_angle.get_value()); + break; + case COLORMATRIX_LUMINANCETOALPHA: + add(_label); + break; + case COLORMATRIX_MATRIX: + default: + add(_matrix); + if(_use_stored) + _matrix.set_values(_matrix_store); + else + _matrix.set_from_attribute(o); + for (auto v : _matrix.get_values()) { + values_string += Glib::Ascii::dtostr(v) + " "; + } + values_string.pop_back(); + break; + } + + // The filter effects widgets derived from AttrWidget automatically update the + // attribute on use. In this case, however, we must also update "values" whenever + // "type" is changed. + auto repr = o->getRepr(); + if (values_string.empty()) { + repr->removeAttribute("values"); + } else { + repr->setAttribute("values", values_string); + } + + _use_stored = true; + } + } + + Glib::ustring get_as_attribute() const override + { + const Widget* w = get_child(); + if(w == &_label) + return ""; + if (auto attrw = dynamic_cast<const AttrWidget *>(w)) + return attrw->get_as_attribute(); + g_assert_not_reached(); + return ""; + } + + void clear_store() + { + _use_stored = false; + } +private: + void update_store() + { + const Widget* w = get_child(); + if(w == &_matrix) + _matrix_store = _matrix.get_values(); + else if(w == &_saturation) + _saturation_store = _saturation.get_value(); + else if(w == &_angle) + _angle_store = _angle.get_value(); + } + + MatrixAttr _matrix; + SpinScale _saturation; + SpinScale _angle; + Gtk::Label _label; + + // Store separate values for the different color modes + bool _use_stored; + std::vector<double> _matrix_store; + double _saturation_store; + double _angle_store; +}; + +static Inkscape::UI::Dialog::FileOpenDialog * selectFeImageFileInstance = nullptr; + +//Displays a chooser for feImage input +//It may be a filename or the id for an SVG Element +//described in xlink:href syntax +class FileOrElementChooser : public Gtk::Box, public AttrWidget +{ +public: + FileOrElementChooser(FilterEffectsDialog& d, const SPAttr a) + : AttrWidget(a) + , _dialog(d) + , Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) + { + pack_start(_entry, false, false); + pack_start(_fromFile, false, false); + pack_start(_fromSVGElement, false, false); + + _fromFile.set_label(_("Image File")); + _fromFile.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_file)); + + _fromSVGElement.set_label(_("Selected SVG Element")); + _fromSVGElement.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_svg_element)); + + _entry.signal_changed().connect(signal_attr_changed().make_slot()); + + show_all(); + + } + + // Returns the element in xlink:href form. + Glib::ustring get_as_attribute() const override + { + return _entry.get_text(); + } + + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + if(val) { + _entry.set_text(val); + } else { + _entry.set_text(""); + } + } + +private: + void select_svg_element() { + Inkscape::Selection* sel = _dialog.getDesktop()->getSelection(); + if (sel->isEmpty()) return; + Inkscape::XML::Node* node = sel->xmlNodes().front(); + if (!node || !node->matchAttributeName("id")) return; + + std::ostringstream xlikhref; + xlikhref << "#" << node->attribute("id"); + _entry.set_text(xlikhref.str()); + } + + void select_file(){ + + //# Get the current directory for finding files + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring open_path; + Glib::ustring attr = prefs->getString("/dialogs/open/path"); + if (!attr.empty()) + open_path = attr; + + //# Test if the open_path directory exists + if (!Inkscape::IO::file_test(open_path.c_str(), + (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) + open_path = ""; + + //# If no open path, default to our home directory + if (open_path.size() < 1) + { + open_path = g_get_home_dir(); + open_path.append(G_DIR_SEPARATOR_S); + } + + //# Create a dialog if we don't already have one + if (!selectFeImageFileInstance) { + selectFeImageFileInstance = + Inkscape::UI::Dialog::FileOpenDialog::create( + *_dialog.getDesktop()->getToplevel(), + open_path, + Inkscape::UI::Dialog::SVG_TYPES,/*TODO: any image, not just svg*/ + (char const *)_("Select an image to be used as input.")); + } + + //# Show the dialog + bool const success = selectFeImageFileInstance->show(); + if (!success) + return; + + //# User selected something. Get name and type + Glib::ustring fileName = selectFeImageFileInstance->getFilename(); + + if (fileName.size() > 0) { + + Glib::ustring newFileName = Glib::filename_to_utf8(fileName); + + if ( newFileName.size() > 0) + fileName = newFileName; + else + g_warning( "ERROR CONVERTING OPEN FILENAME TO UTF-8" ); + + open_path = fileName; + open_path.append(G_DIR_SEPARATOR_S); + prefs->setString("/dialogs/open/path", open_path); + + _entry.set_text(fileName); + } + return; + } + + Gtk::Entry _entry; + Gtk::Button _fromFile; + Gtk::Button _fromSVGElement; + FilterEffectsDialog &_dialog; +}; + +class FilterEffectsDialog::Settings +{ +public: + typedef sigc::slot<void, const AttrWidget*> SetAttrSlot; + + Settings(FilterEffectsDialog& d, Gtk::Box& b, SetAttrSlot slot, const int maxtypes) + : _dialog(d), _set_attr_slot(std::move(slot)), _current_type(-1), _max_types(maxtypes) + { + _groups.resize(_max_types); + _attrwidgets.resize(_max_types); + _size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + + for(int i = 0; i < _max_types; ++i) { + _groups[i] = new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 3); + b.set_spacing(4); + b.pack_start(*_groups[i], Gtk::PACK_SHRINK); + } + //_current_type = 0; If set to 0 then update_and_show() fails to update properly. + } + + ~Settings() + { + for(int i = 0; i < _max_types; ++i) { + delete _groups[i]; + for(auto & j : _attrwidgets[i]) + delete j; + } + } + + // Show the active settings group and update all the AttrWidgets with new values + void show_and_update(const int t, SPObject* ob) + { + if(t != _current_type) { + type(t); + for(auto & _group : _groups) + _group->hide(); + } + if(t >= 0) { + _groups[t]->show(); // Do not use show_all(), it shows children than should be hidden + } + _dialog.set_attrs_locked(true); + for(auto & i : _attrwidgets[_current_type]) + i->set_from_attribute(ob); + _dialog.set_attrs_locked(false); + } + + int get_current_type() const + { + return _current_type; + } + + void type(const int t) + { + _current_type = t; + } + + void add_no_params() + { + Gtk::Label* lbl = Gtk::manage(new Gtk::Label(_("This SVG filter effect does not require any parameters."))); + add_widget(lbl, ""); + } + + // LightSource + LightSourceControl* add_lightsource(); + + // Component Transfer Values + ComponentTransferValues* add_componenttransfervalues(const Glib::ustring& label, SPFeFuncNode::Channel channel); + + // CheckButton + CheckButtonAttr* add_checkbutton(bool def, const SPAttr attr, const Glib::ustring& label, + const Glib::ustring& tv, const Glib::ustring& fv, char* tip_text = nullptr) + { + CheckButtonAttr* cb = new CheckButtonAttr(def, label, tv, fv, attr, tip_text); + add_widget(cb, ""); + add_attr_widget(cb); + return cb; + } + + // ColorButton + ColorButton* add_color(unsigned int def, const SPAttr attr, const Glib::ustring& label, char* tip_text = nullptr) + { + ColorButton* col = new ColorButton(def, attr, tip_text); + add_widget(col, label); + add_attr_widget(col); + return col; + } + + // Matrix + MatrixAttr* add_matrix(const SPAttr attr, const Glib::ustring& label, char* tip_text) + { + MatrixAttr* conv = new MatrixAttr(attr, tip_text); + add_widget(conv, label); + add_attr_widget(conv); + return conv; + } + + // ColorMatrixValues + ColorMatrixValues* add_colormatrixvalues(const Glib::ustring& label) + { + ColorMatrixValues* cmv = new ColorMatrixValues(); + add_widget(cmv, label); + add_attr_widget(cmv); + return cmv; + } + + // SpinScale + SpinScale* add_spinscale(double def, const SPAttr attr, const Glib::ustring& label, + const double lo, const double hi, const double step_inc, const double climb, const int digits, char* tip_text = nullptr) + { + Glib::ustring tip_text2; + if (tip_text) + tip_text2 = tip_text; + SpinScale* spinslider = new SpinScale("", def, lo, hi, step_inc, climb, digits, attr, tip_text2); + add_widget(spinslider, label); + add_attr_widget(spinslider); + return spinslider; + } + + // DualSpinScale + DualSpinScale* add_dualspinscale(const SPAttr attr, const Glib::ustring& label, + const double lo, const double hi, const double step_inc, + const double climb, const int digits, + const Glib::ustring tip_text1 = "", + const Glib::ustring tip_text2 = "") + { + DualSpinScale* dss = new DualSpinScale("", "", lo, lo, hi, step_inc, climb, digits, attr, tip_text1, tip_text2); + add_widget(dss, label); + add_attr_widget(dss); + return dss; + } + + // SpinButton + SpinButtonAttr* add_spinbutton(double defalt_value, const SPAttr attr, const Glib::ustring& label, + const double lo, const double hi, const double step_inc, + const double climb, const int digits, char* tip = nullptr) + { + SpinButtonAttr* sb = new SpinButtonAttr(lo, hi, step_inc, climb, digits, attr, defalt_value, tip); + add_widget(sb, label); + add_attr_widget(sb); + return sb; + } + + // DualSpinButton + DualSpinButton* add_dualspinbutton(char* defalt_value, const SPAttr attr, const Glib::ustring& label, + const double lo, const double hi, const double step_inc, + const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr) + { + DualSpinButton* dsb = new DualSpinButton(defalt_value, lo, hi, step_inc, climb, digits, attr, tip1, tip2); + add_widget(dsb, label); + add_attr_widget(dsb); + return dsb; + } + + // MultiSpinButton + MultiSpinButton* add_multispinbutton(double def1, double def2, const SPAttr attr1, const SPAttr attr2, + const Glib::ustring& label, const double lo, const double hi, + const double step_inc, const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr) + { + std::vector<SPAttr> attrs; + attrs.push_back(attr1); + attrs.push_back(attr2); + + std::vector<double> default_values; + default_values.push_back(def1); + default_values.push_back(def2); + + std::vector<char*> tips; + tips.push_back(tip1); + tips.push_back(tip2); + + MultiSpinButton* msb = new MultiSpinButton(lo, hi, step_inc, climb, digits, attrs, default_values, tips); + add_widget(msb, label); + for(auto & i : msb->get_spinbuttons()) + add_attr_widget(i); + return msb; + } + MultiSpinButton* add_multispinbutton(double def1, double def2, double def3, const SPAttr attr1, const SPAttr attr2, + const SPAttr attr3, const Glib::ustring& label, const double lo, + const double hi, const double step_inc, const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr, char* tip3 = nullptr) + { + std::vector<SPAttr> attrs; + attrs.push_back(attr1); + attrs.push_back(attr2); + attrs.push_back(attr3); + + std::vector<double> default_values; + default_values.push_back(def1); + default_values.push_back(def2); + default_values.push_back(def3); + + std::vector<char*> tips; + tips.push_back(tip1); + tips.push_back(tip2); + tips.push_back(tip3); + + MultiSpinButton* msb = new MultiSpinButton(lo, hi, step_inc, climb, digits, attrs, default_values, tips); + add_widget(msb, label); + for(auto & i : msb->get_spinbuttons()) + add_attr_widget(i); + return msb; + } + + // FileOrElementChooser + FileOrElementChooser* add_fileorelement(const SPAttr attr, const Glib::ustring& label) + { + FileOrElementChooser* foech = new FileOrElementChooser(_dialog, attr); + add_widget(foech, label); + add_attr_widget(foech); + return foech; + } + + // ComboBoxEnum + template<typename T> ComboBoxEnum<T>* add_combo(T default_value, const SPAttr attr, + const Glib::ustring& label, + const Util::EnumDataConverter<T>& conv, char* tip_text = nullptr) + { + ComboWithTooltip<T>* combo = new ComboWithTooltip<T>(default_value, conv, attr, tip_text); + add_widget(combo, label); + add_attr_widget(combo->get_attrwidget()); + return combo->get_attrwidget(); + } + + // Entry + EntryAttr* add_entry(const SPAttr attr, + const Glib::ustring& label, + char* tip_text = nullptr) + { + EntryAttr* entry = new EntryAttr(attr, tip_text); + add_widget(entry, label); + add_attr_widget(entry); + return entry; + } + + Glib::RefPtr<Gtk::SizeGroup> _size_group; +private: + void add_attr_widget(AttrWidget* a) + { + _attrwidgets[_current_type].push_back(a); + a->signal_attr_changed().connect(sigc::bind(_set_attr_slot, a)); + } + + /* Adds a new settings widget using the specified label. The label will be formatted with a colon + and all widgets within the setting group are aligned automatically. */ + void add_widget(Gtk::Widget* w, const Glib::ustring& label) + { + Gtk::Box *hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + hb->set_spacing(12); + + if (label != "") { + Gtk::Label *lbl = Gtk::manage(new Gtk::Label(label)); + lbl->set_xalign(0.0); + hb->pack_start(*lbl, Gtk::PACK_SHRINK); + _size_group->add_widget(*lbl); + } + + hb->pack_start(*w, Gtk::PACK_EXPAND_WIDGET); + _groups[_current_type]->pack_start(*hb, Gtk::PACK_EXPAND_WIDGET); + hb->show_all(); + } + + std::vector<Gtk::Box*> _groups; + FilterEffectsDialog& _dialog; + SetAttrSlot _set_attr_slot; + std::vector<std::vector< AttrWidget*> > _attrwidgets; + int _current_type, _max_types; +}; + +// Displays sliders and/or tables for feComponentTransfer +class FilterEffectsDialog::ComponentTransferValues : public Gtk::Frame, public AttrWidget +{ +public: + ComponentTransferValues(FilterEffectsDialog& d, SPFeFuncNode::Channel channel) + : AttrWidget(SPAttr::INVALID), + _dialog(d), + _settings(d, _box, sigc::mem_fun(*this, &ComponentTransferValues::set_func_attr), COMPONENTTRANSFER_TYPE_ERROR), + _type(ComponentTransferTypeConverter, SPAttr::TYPE, false), + _channel(channel), + _funcNode(nullptr), + _box(Gtk::ORIENTATION_VERTICAL) + { + set_shadow_type(Gtk::SHADOW_IN); + add(_box); + _box.add(_type); + _box.reorder_child(_type, 0); + _type.signal_changed().connect(sigc::mem_fun(*this, &ComponentTransferValues::on_type_changed)); + + _settings.type(COMPONENTTRANSFER_TYPE_LINEAR); + _settings.add_spinscale(1, SPAttr::SLOPE, _("Slope"), -10, 10, 0.1, 0.01, 2); + _settings.add_spinscale(0, SPAttr::INTERCEPT, _("Intercept"), -10, 10, 0.1, 0.01, 2); + + _settings.type(COMPONENTTRANSFER_TYPE_GAMMA); + _settings.add_spinscale(1, SPAttr::AMPLITUDE, _("Amplitude"), 0, 10, 0.1, 0.01, 2); + _settings.add_spinscale(1, SPAttr::EXPONENT, _("Exponent"), 0, 10, 0.1, 0.01, 2); + _settings.add_spinscale(0, SPAttr::OFFSET, _("Offset"), -10, 10, 0.1, 0.01, 2); + + _settings.type(COMPONENTTRANSFER_TYPE_TABLE); + _settings.add_entry(SPAttr::TABLEVALUES, _("Table")); + + _settings.type(COMPONENTTRANSFER_TYPE_DISCRETE); + _settings.add_entry(SPAttr::TABLEVALUES, _("Discrete")); + + //_settings.type(COMPONENTTRANSFER_TYPE_IDENTITY); + _settings.type(-1); // Force update_and_show() to show/hide windows correctly + } + + // FuncNode can be in any order so we must search to find correct one. + SPFeFuncNode* find_node(SPFeComponentTransfer* ct) + { + SPFeFuncNode* funcNode = nullptr; + bool found = false; + for(auto& node: ct->children) { + funcNode = SP_FEFUNCNODE(&node); + if( funcNode->channel == _channel ) { + found = true; + break; + } + } + if( !found ) + funcNode = nullptr; + + return funcNode; + } + + void set_func_attr(const AttrWidget* input) + { + _dialog.set_attr( _funcNode, input->get_attribute(), input->get_as_attribute().c_str()); + } + + // Set new type and update widget visibility + void set_from_attribute(SPObject* o) override + { + // See componenttransfer.cpp + if(SP_IS_FECOMPONENTTRANSFER(o)) { + SPFeComponentTransfer* ct = SP_FECOMPONENTTRANSFER(o); + + _funcNode = find_node(ct); + if( _funcNode ) { + _type.set_from_attribute( _funcNode ); + } else { + // Create <funcNode> + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim) { + Inkscape::XML::Document *xml_doc = prim->document->getReprDoc(); + Inkscape::XML::Node *repr = nullptr; + switch(_channel) { + case SPFeFuncNode::R: + repr = xml_doc->createElement("svg:feFuncR"); + break; + case SPFeFuncNode::G: + repr = xml_doc->createElement("svg:feFuncG"); + break; + case SPFeFuncNode::B: + repr = xml_doc->createElement("svg:feFuncB"); + break; + case SPFeFuncNode::A: + repr = xml_doc->createElement("svg:feFuncA"); + break; + } + + //XML Tree being used directly here while it shouldn't be. + prim->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + + // Now we should find it! + _funcNode = find_node(ct); + if( _funcNode ) { + _funcNode->setAttribute( "type", "identity" ); + } else { + //std::cout << "ERROR ERROR: feFuncX not found!" << std::endl; + } + } + } + + update(); + } + } + +private: + void on_type_changed() + { + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim) { + + _funcNode->setAttributeOrRemoveIfEmpty("type", _type.get_as_attribute()); + + SPFilter* filter = _dialog._filter_modifier.get_selected_filter(); + filter->requestModified(SP_OBJECT_MODIFIED_FLAG); + + DocumentUndo::done(prim->document, _("New transfer function type"), INKSCAPE_ICON("dialog-filters")); + update(); + } + } + + void update() + { + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim && _funcNode) { + _settings.show_and_update(_type.get_active_data()->id, _funcNode); + } + } + +public: + Glib::ustring get_as_attribute() const override + { + return ""; + } + + FilterEffectsDialog& _dialog; + Gtk::Box _box; + Settings _settings; + ComboBoxEnum<FilterComponentTransferType> _type; + SPFeFuncNode::Channel _channel; // RGBA + SPFeFuncNode* _funcNode; +}; + +// Settings for the three light source objects +class FilterEffectsDialog::LightSourceControl : public AttrWidget +{ +public: + LightSourceControl(FilterEffectsDialog& d) + : AttrWidget(SPAttr::INVALID), + _dialog(d), + _settings(d, _box, sigc::mem_fun(_dialog, &FilterEffectsDialog::set_child_attr_direct), LIGHT_ENDSOURCE), + _light_label(_("Light Source:")), + _light_source(LightSourceConverter), + _locked(false), + _box(Gtk::ORIENTATION_VERTICAL), + _light_box(Gtk::ORIENTATION_HORIZONTAL) + { + _light_label.set_xalign(0.0); + _settings._size_group->add_widget(_light_label); + _light_box.pack_start(_light_label, Gtk::PACK_SHRINK); + _light_box.pack_start(_light_source, Gtk::PACK_EXPAND_WIDGET); + _light_box.show_all(); + _light_box.set_spacing(12); + + _box.add(_light_box); + _box.reorder_child(_light_box, 0); + _light_source.signal_changed().connect(sigc::mem_fun(*this, &LightSourceControl::on_source_changed)); + + // FIXME: these range values are complete crap + + _settings.type(LIGHT_DISTANT); + _settings.add_spinscale(0, SPAttr::AZIMUTH, _("Azimuth:"), 0, 360, 1, 1, 0, _("Direction angle for the light source on the XY plane, in degrees")); + _settings.add_spinscale(0, SPAttr::ELEVATION, _("Elevation:"), 0, 360, 1, 1, 0, _("Direction angle for the light source on the YZ plane, in degrees")); + + _settings.type(LIGHT_POINT); + _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0, SPAttr::X, SPAttr::Y, SPAttr::Z, _("Location:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate")); + + _settings.type(LIGHT_SPOT); + _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0, SPAttr::X, SPAttr::Y, SPAttr::Z, _("Location:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate")); + _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0, + SPAttr::POINTSATX, SPAttr::POINTSATY, SPAttr::POINTSATZ, + _("Points at:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate")); + _settings.add_spinscale(1, SPAttr::SPECULAREXPONENT, _("Specular Exponent:"), 1, 100, 1, 1, 0, _("Exponent value controlling the focus for the light source")); + //TODO: here I have used 100 degrees as default value. But spec says that if not specified, no limiting cone is applied. So, there should be a way for the user to set a "no limiting cone" option. + _settings.add_spinscale(100, SPAttr::LIMITINGCONEANGLE, _("Cone Angle:"), 1, 100, 1, 1, 0, _("This is the angle between the spot light axis (i.e. the axis between the light source and the point to which it is pointing at) and the spot light cone. No light is projected outside this cone.")); + + _settings.type(-1); // Force update_and_show() to show/hide windows correctly + + } + + Gtk::Box& get_box() + { + return _box; + } +protected: + Glib::ustring get_as_attribute() const override + { + return ""; + } + void set_from_attribute(SPObject* o) override + { + if(_locked) + return; + + _locked = true; + + SPObject* child = o->firstChild(); + + if(SP_IS_FEDISTANTLIGHT(child)) + _light_source.set_active(0); + else if(SP_IS_FEPOINTLIGHT(child)) + _light_source.set_active(1); + else if(SP_IS_FESPOTLIGHT(child)) + _light_source.set_active(2); + else + _light_source.set_active(-1); + + update(); + + _locked = false; + } +private: + void on_source_changed() + { + if(_locked) + return; + + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim) { + _locked = true; + + SPObject* child = prim->firstChild(); + const int ls = _light_source.get_active_row_number(); + // Check if the light source type has changed + if(!(ls == -1 && !child) && + !(ls == 0 && SP_IS_FEDISTANTLIGHT(child)) && + !(ls == 1 && SP_IS_FEPOINTLIGHT(child)) && + !(ls == 2 && SP_IS_FESPOTLIGHT(child))) { + if(child) + //XML Tree being used directly here while it shouldn't be. + sp_repr_unparent(child->getRepr()); + + if(ls != -1) { + Inkscape::XML::Document *xml_doc = prim->document->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement(_light_source.get_active_data()->key.c_str()); + //XML Tree being used directly here while it shouldn't be. + prim->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + } + + DocumentUndo::done(prim->document, _("New light source"), INKSCAPE_ICON("dialog-filters")); + update(); + } + + _locked = false; + } + } + + void update() + { + _box.hide(); + _box.show(); + _light_box.show_all(); + + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim && prim->firstChild()) + _settings.show_and_update(_light_source.get_active_data()->id, prim->firstChild()); + } + + FilterEffectsDialog& _dialog; + Gtk::Box _box; + Settings _settings; + Gtk::Box _light_box; + Gtk::Label _light_label; + ComboBoxEnum<LightSource> _light_source; + bool _locked; +}; + + // ComponentTransferValues +FilterEffectsDialog::ComponentTransferValues* FilterEffectsDialog::Settings::add_componenttransfervalues(const Glib::ustring& label, SPFeFuncNode::Channel channel) + { + ComponentTransferValues* ct = new ComponentTransferValues(_dialog, channel); + add_widget(ct, label); + add_attr_widget(ct); + return ct; + } + + +FilterEffectsDialog::LightSourceControl* FilterEffectsDialog::Settings::add_lightsource() +{ + LightSourceControl* ls = new LightSourceControl(_dialog); + add_attr_widget(ls); + add_widget(&ls->get_box(), ""); + return ls; +} + +static Gtk::Menu * create_popup_menu(Gtk::Widget& parent, + sigc::slot<void> dup, + sigc::slot<void> rem) +{ + auto menu = Gtk::manage(new Gtk::Menu); + + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Duplicate"),true)); + mi->signal_activate().connect(dup); + mi->show(); + menu->append(*mi); + + mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + menu->append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + menu->accelerate(parent); + + return menu; +} + +/*** FilterModifier ***/ +FilterEffectsDialog::FilterModifier::FilterModifier(FilterEffectsDialog& d) + : Gtk::Box(Gtk::ORIENTATION_VERTICAL), + _dialog(d), + _add(_("_New"), true), + _observer(new Inkscape::XML::SignalObserver) +{ + Gtk::ScrolledWindow* sw = Gtk::manage(new Gtk::ScrolledWindow); + pack_start(*sw); + pack_start(_add, false, false); + sw->add(_list); + + _model = Gtk::ListStore::create(_columns); + _list.set_model(_model); + _cell_toggle.set_active(true); + const int selcol = _list.append_column("", _cell_toggle); + Gtk::TreeViewColumn* col = _list.get_column(selcol - 1); + if(col) + col->add_attribute(_cell_toggle.property_active(), _columns.sel); + _list.append_column_editable(_("_Filter"), _columns.label); + ((Gtk::CellRendererText*)_list.get_column(1)->get_first_cell())-> + signal_edited().connect(sigc::mem_fun(*this, &FilterEffectsDialog::FilterModifier::on_name_edited)); + + _list.append_column("#", _columns.count); + _list.get_column(2)->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE); + _list.get_column(2)->set_expand(false); + _list.get_column(2)->set_reorderable(true); + + sw->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _list.get_column(1)->set_resizable(true); + _list.get_column(1)->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE); + _list.get_column(1)->set_expand(true); + + _list.set_reorderable(true); + _list.enable_model_drag_dest (Gdk::ACTION_MOVE); + + _list.signal_drag_drop().connect( sigc::mem_fun(*this, &FilterModifier::on_filter_move), false ); + + sw->set_shadow_type(Gtk::SHADOW_IN); + show_all_children(); + _add.signal_clicked().connect(sigc::mem_fun(*this, &FilterModifier::add_filter)); + _cell_toggle.signal_toggled().connect(sigc::mem_fun(*this, &FilterModifier::on_selection_toggled)); + _list.signal_button_release_event().connect_notify( + sigc::mem_fun(*this, &FilterModifier::filter_list_button_release)); + + _menu = create_popup_menu(*this, + sigc::mem_fun(*this, &FilterModifier::duplicate_filter), + sigc::mem_fun(*this, &FilterModifier::remove_filter)); + + Gtk::MenuItem *rename_item = Gtk::manage(new Gtk::MenuItem(_("R_ename"), true)); + rename_item->signal_activate().connect(sigc::mem_fun(*this, &FilterModifier::rename_filter)); + rename_item->show(); + _menu->append(*rename_item); + _menu->accelerate(*this); + + Gtk::MenuItem *select_item = Gtk::manage(new Gtk::MenuItem(_("Select"), true)); + select_item->signal_activate().connect(sigc::mem_fun(*this, &FilterModifier::select_filter_elements)); + select_item->show(); + _menu->append(*select_item); + _menu->accelerate(*this); + + _list.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FilterModifier::on_filter_selection_changed)); + _observer->signal_changed().connect(signal_filter_changed().make_slot()); +} + +// Update each filter's sel property based on the current object selection; +// If the filter is not used by any selected object, sel = 0, +// otherwise sel is set to the total number of filters in use by selected objects +// If only one filter is in use, it is selected +void FilterEffectsDialog::FilterModifier::update_selection(Selection *sel) +{ + if (!sel) { + return; + } + + std::set<SPFilter*> used; + auto itemlist= sel->items(); + for(auto i=itemlist.begin(); itemlist.end() != i; ++i) { + SPObject *obj = *i; + SPStyle *style = obj->style; + if (!style || !SP_IS_ITEM(obj)) { + continue; + } + + if (style->filter.set && style->getFilter()) { + SP_ITEM(obj)->bbox_valid = FALSE; + used.insert(style->getFilter()); + } else { + used.insert(nullptr); + } + } + + const int size = used.size(); + + for (Gtk::TreeIter iter = _model->children().begin(); iter != _model->children().end(); ++iter) { + if (used.find((*iter)[_columns.filter]) != used.end()) { + // If only one filter is in use by the selection, select it + if (size == 1) { + _list.get_selection()->select(iter); + } + (*iter)[_columns.sel] = size; + } else { + (*iter)[_columns.sel] = 0; + } + } + update_counts(); +} + +void FilterEffectsDialog::FilterModifier::on_filter_selection_changed() +{ + _observer->set(get_selected_filter()); + signal_filter_changed()(); +} + +void FilterEffectsDialog::FilterModifier::on_name_edited(const Glib::ustring& path, const Glib::ustring& text) +{ + Gtk::TreeModel::iterator iter = _model->get_iter(path); + + if(iter) { + SPFilter* filter = (*iter)[_columns.filter]; + filter->setLabel(text.c_str()); + DocumentUndo::done(filter->document, _("Rename filter"), INKSCAPE_ICON("dialog-filters")); + if(iter) + (*iter)[_columns.label] = text; + } +} + +bool FilterEffectsDialog::FilterModifier::on_filter_move(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int /*x*/, int /*y*/, guint /*time*/) { + +//const Gtk::TreeModel::Path& /*path*/) { +/* The code below is bugged. Use of "object->getRepr()->setPosition(0)" is dangerous! + Writing back the reordered list to XML (reordering XML nodes) should be implemented differently. + Note that the dialog does also not update its list of filters when the order is manually changed + using the XML dialog + for(Gtk::TreeModel::iterator i = _model->children().begin(); i != _model->children().end(); ++i) { + SPObject* object = (*i)[_columns.filter]; + if(object && object->getRepr()) ; + object->getRepr()->setPosition(0); + } +*/ + return false; +} + +void FilterEffectsDialog::FilterModifier::on_selection_toggled(const Glib::ustring& path) +{ + Gtk::TreeIter iter = _model->get_iter(path); + + if(iter) { + SPDesktop *desktop = _dialog.getDesktop(); + SPDocument *doc = desktop->getDocument(); + Inkscape::Selection *sel = desktop->getSelection(); + SPFilter* filter = (*iter)[_columns.filter]; + + /* If this filter is the only one used in the selection, unset it */ + if((*iter)[_columns.sel] == 1) + filter = nullptr; + + for (auto item : sel->items()) { + SPStyle *style = item->style; + g_assert(style != nullptr); + + if (filter && filter->valid_for(item)) { + sp_style_set_property_url(item, "filter", filter, false); + } else { + ::remove_filter(item, false); + } + + item->requestDisplayUpdate((SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG )); + } + + update_selection(sel); + DocumentUndo::done(doc, _("Apply filter"), INKSCAPE_ICON("dialog-filters")); + } +} + + +void FilterEffectsDialog::FilterModifier::update_counts() +{ + for(const auto & i : _model->children()) { + SPFilter* f = i[_columns.filter]; + i[_columns.count] = f->getRefCount(); + } +} + +/* Add all filters in the document to the combobox. + Keeps the same selection if possible, otherwise selects the first element */ +void FilterEffectsDialog::FilterModifier::update_filters() +{ + std::vector<SPObject *> filters = _dialog.getDocument()->getResourceList("filter"); + + _model->clear(); + + for (auto filter : filters) { + Gtk::TreeModel::Row row = *_model->append(); + SPFilter* f = SP_FILTER(filter); + row[_columns.filter] = f; + const gchar* lbl = f->label(); + const gchar* id = f->getId(); + row[_columns.label] = lbl ? lbl : (id ? id : "filter"); + } + + update_selection(_dialog.getSelection()); + _dialog.update_filter_general_settings_view(); +} + +SPFilter* FilterEffectsDialog::FilterModifier::get_selected_filter() +{ + if(_list.get_selection()) { + Gtk::TreeModel::iterator i = _list.get_selection()->get_selected(); + + if(i) + return (*i)[_columns.filter]; + } + + return nullptr; +} + +void FilterEffectsDialog::FilterModifier::select_filter(const SPFilter* filter) +{ + if(filter) { + for(Gtk::TreeModel::iterator i = _model->children().begin(); + i != _model->children().end(); ++i) { + if((*i)[_columns.filter] == filter) { + _list.get_selection()->select(i); + break; + } + } + } +} + +void FilterEffectsDialog::FilterModifier::filter_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + const bool sensitive = get_selected_filter() != nullptr; + auto items = _menu->get_children(); + items[0]->set_sensitive(sensitive); + items[1]->set_sensitive(sensitive); + items[3]->set_sensitive(sensitive); + + _menu->popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void FilterEffectsDialog::FilterModifier::add_filter() +{ + SPDocument* doc = _dialog.getDocument(); + SPFilter* filter = new_filter(doc); + + const int count = _model->children().size(); + std::ostringstream os; + os << _("filter") << count; + filter->setLabel(os.str().c_str()); + + update_filters(); + + select_filter(filter); + + DocumentUndo::done(doc, _("Add filter"), INKSCAPE_ICON("dialog-filters")); +} + +void FilterEffectsDialog::FilterModifier::remove_filter() +{ + SPFilter *filter = get_selected_filter(); + + if(filter) { + auto desktop = _dialog.getDesktop(); + SPDocument* doc = filter->document; + + // Delete all references to this filter + std::vector<SPItem*> x,y; + std::vector<SPItem*> all = get_all_items(x, desktop->layerManager().currentRoot(), desktop, false, false, true, y); + for(std::vector<SPItem*>::const_iterator i=all.begin(); all.end() != i; ++i) { + if (!SP_IS_ITEM(*i)) { + continue; + } + SPItem *item = *i; + if (!item->style) { + continue; + } + + const SPIFilter *ifilter = &(item->style->filter); + if (ifilter && ifilter->href) { + const SPObject *obj = ifilter->href->getObject(); + if (obj && obj == (SPObject *)filter) { + ::remove_filter(item, false); + } + } + } + + //XML Tree being used directly here while it shouldn't be. + sp_repr_unparent(filter->getRepr()); + + DocumentUndo::done(doc, _("Remove filter"), INKSCAPE_ICON("dialog-filters")); + + update_filters(); + } +} + +void FilterEffectsDialog::FilterModifier::duplicate_filter() +{ + SPFilter* filter = get_selected_filter(); + + if (filter) { + Inkscape::XML::Node *repr = filter->getRepr(); + Inkscape::XML::Node *parent = repr->parent(); + repr = repr->duplicate(repr->document()); + parent->appendChild(repr); + + DocumentUndo::done(filter->document, _("Duplicate filter"), INKSCAPE_ICON("dialog-filters")); + + update_filters(); + } +} + +void FilterEffectsDialog::FilterModifier::rename_filter() +{ + _list.set_cursor(_model->get_path(_list.get_selection()->get_selected()), *_list.get_column(1), true); +} + +void FilterEffectsDialog::FilterModifier::select_filter_elements() +{ + SPFilter *filter = get_selected_filter(); + auto desktop = _dialog.getDesktop(); + + if(!filter) + return; + + std::vector<SPItem*> x,y; + std::vector<SPItem*> items; + std::vector<SPItem*> all = get_all_items(x, desktop->layerManager().currentRoot(), desktop, false, false, true, y); + for(SPItem *item: all) { + if (!item->style) { + continue; + } + + SPIFilter const &ifilter = item->style->filter; + if (ifilter.href) { + const SPObject *obj = ifilter.href->getObject(); + if (obj && obj == (SPObject *)filter) { + items.push_back(item); + } + } + } + desktop->getSelection()->setList(items); +} + +FilterEffectsDialog::CellRendererConnection::CellRendererConnection() + : Glib::ObjectBase(typeid(CellRendererConnection)) + , _primitive(*this, "primitive", nullptr) +{} + +Glib::PropertyProxy<void*> FilterEffectsDialog::CellRendererConnection::property_primitive() +{ + return _primitive.get_proxy(); +} + +void FilterEffectsDialog::CellRendererConnection::get_preferred_width_vfunc(Gtk::Widget& widget, + int& minimum_width, + int& natural_width) const +{ + PrimitiveList& primlist = dynamic_cast<PrimitiveList&>(widget); + minimum_width = natural_width = size * primlist.primitive_count() + primlist.get_input_type_width() * 6; +} + +void FilterEffectsDialog::CellRendererConnection::get_preferred_width_for_height_vfunc(Gtk::Widget& widget, + int /* height */, + int& minimum_width, + int& natural_width) const +{ + get_preferred_width(widget, minimum_width, natural_width); +} + +void FilterEffectsDialog::CellRendererConnection::get_preferred_height_vfunc(Gtk::Widget& widget, + int& minimum_height, + int& natural_height) const +{ + // Scale the height depending on the number of inputs, unless it's + // the first primitive, in which case there are no connections + SPFilterPrimitive* prim = SP_FILTER_PRIMITIVE(_primitive.get_value()); + minimum_height = natural_height = size * input_count(prim); +} + +void FilterEffectsDialog::CellRendererConnection::get_preferred_height_for_width_vfunc(Gtk::Widget& widget, + int /* width */, + int& minimum_height, + int& natural_height) const +{ + get_preferred_height(widget, minimum_height, natural_height); +} + +/*** PrimitiveList ***/ +FilterEffectsDialog::PrimitiveList::PrimitiveList(FilterEffectsDialog& d) + : _dialog(d), + _in_drag(0), + _observer(new Inkscape::XML::SignalObserver) +{ + signal_draw().connect(sigc::mem_fun(*this, &PrimitiveList::on_draw_signal)); + + add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK); + + _model = Gtk::ListStore::create(_columns); + + set_reorderable(true); + + set_model(_model); + append_column(_("_Effect"), _columns.type); + get_column(0)->set_resizable(true); + set_headers_visible(); + + _observer->signal_changed().connect(signal_primitive_changed().make_slot()); + get_selection()->signal_changed().connect(sigc::mem_fun(*this, &PrimitiveList::on_primitive_selection_changed)); + signal_primitive_changed().connect(sigc::mem_fun(*this, &PrimitiveList::queue_draw)); + + init_text(); + + int cols_count = append_column(_("Connections"), _connection_cell); + Gtk::TreeViewColumn* col = get_column(cols_count - 1); + if(col) + col->add_attribute(_connection_cell.property_primitive(), _columns.primitive); +} + +// Sets up a vertical Pango context/layout, and returns the largest +// width needed to render the FilterPrimitiveInput labels. +void FilterEffectsDialog::PrimitiveList::init_text() +{ + // Set up a vertical context+layout + Glib::RefPtr<Pango::Context> context = create_pango_context(); + const Pango::Matrix matrix = {0, -1, 1, 0, 0, 0}; + context->set_matrix(matrix); + _vertical_layout = Pango::Layout::create(context); + + // Store the maximum height and width of the an input type label + // for later use in drawing and measuring. + _input_type_height = _input_type_width = 0; + for(unsigned int i = 0; i < FPInputConverter._length; ++i) { + _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str())); + int fontw, fonth; + _vertical_layout->get_pixel_size(fontw, fonth); + if(fonth > _input_type_width) + _input_type_width = fonth; + if (fontw > _input_type_height) + _input_type_height = fontw; + } +} + +sigc::signal<void>& FilterEffectsDialog::PrimitiveList::signal_primitive_changed() +{ + return _signal_primitive_changed; +} + +void FilterEffectsDialog::PrimitiveList::on_primitive_selection_changed() +{ + _observer->set(get_selected()); + signal_primitive_changed()(); + _dialog._color_matrix_values->clear_store(); +} + +/* Add all filter primitives in the current to the list. + Keeps the same selection if possible, otherwise selects the first element */ +void FilterEffectsDialog::PrimitiveList::update() +{ + SPFilter* f = _dialog._filter_modifier.get_selected_filter(); + const SPFilterPrimitive* active_prim = get_selected(); + _model->clear(); + + if(f) { + bool active_found = false; + _dialog._primitive_box->set_sensitive(true); + _dialog.update_filter_general_settings_view(); + for(auto& prim_obj: f->children) { + SPFilterPrimitive *prim = SP_FILTER_PRIMITIVE(&prim_obj); + if(!prim) { + break; + } + Gtk::TreeModel::Row row = *_model->append(); + row[_columns.primitive] = prim; + + //XML Tree being used directly here while it shouldn't be. + row[_columns.type_id] = FPConverter.get_id_from_key(prim->getRepr()->name()); + row[_columns.type] = _(FPConverter.get_label(row[_columns.type_id]).c_str()); + + if (prim->getId()) { + row[_columns.id] = Glib::ustring(prim->getId()); + } + + if(prim == active_prim) { + get_selection()->select(row); + active_found = true; + } + } + + if(!active_found && _model->children().begin()) + get_selection()->select(_model->children().begin()); + + columns_autosize(); + + int width, height; + get_size_request(width, height); + if (height == -1) { + // Need to account for the height of the input type text (rotated text) as well as the + // column headers. Input type text height determined in init_text() by measuring longest + // string. Column header height determined by mapping y coordinate of visible + // rectangle to widget coordinates. + Gdk::Rectangle vis; + int vis_x, vis_y; + get_visible_rect(vis); + convert_tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y); + set_size_request(width, _input_type_height + 2 + vis_y); + } + } + else { + _dialog._primitive_box->set_sensitive(false); + set_size_request(-1, -1); + } +} + +void FilterEffectsDialog::PrimitiveList::set_menu(Gtk::Widget& parent, + sigc::slot<void> dup, + sigc::slot<void> rem) +{ + _primitive_menu = create_popup_menu(parent, dup, rem); +} + +SPFilterPrimitive* FilterEffectsDialog::PrimitiveList::get_selected() +{ + if(_dialog._filter_modifier.get_selected_filter()) { + Gtk::TreeModel::iterator i = get_selection()->get_selected(); + if(i) + return (*i)[_columns.primitive]; + } + + return nullptr; +} + +void FilterEffectsDialog::PrimitiveList::select(SPFilterPrimitive* prim) +{ + for(Gtk::TreeIter i = _model->children().begin(); + i != _model->children().end(); ++i) { + if((*i)[_columns.primitive] == prim) + get_selection()->select(i); + } +} + +void FilterEffectsDialog::PrimitiveList::remove_selected() +{ + SPFilterPrimitive* prim = get_selected(); + + if(prim) { + _observer->set(nullptr); + _model->erase(get_selection()->get_selected()); + + //XML Tree being used directly here while it shouldn't be. + sp_repr_unparent(prim->getRepr()); + + DocumentUndo::done(_dialog.getDocument(), _("Remove filter primitive"), INKSCAPE_ICON("dialog-filters")); + + update(); + } +} + +bool FilterEffectsDialog::PrimitiveList::on_draw_signal(const Cairo::RefPtr<Cairo::Context> & cr) +{ + cr->set_line_width(1.0); + // In GTK+ 3, the draw function receives the widget window, not the + // bin_window (i.e., just the area under the column headers). We + // therefore translate the origin of our coordinate system to account for this + int x_origin, y_origin; + convert_bin_window_to_widget_coords(0,0,x_origin,y_origin); + cr->translate(x_origin, y_origin); + + auto sc = get_style_context(); + + // TODO: In Gtk+ 4, the state is not used in get_color + auto state = sc->get_state(); + auto bg_color = get_background_color(sc, state); + auto orig_color = sc->get_color(state); + Gdk::RGBA fg_color; + Gdk::RGBA mid_color; + { + auto lerp = [](double v0, double v1, double t){ return (1.0 - t) * v0 + t * v1; }; + fg_color.set_rgba( + lerp(bg_color.get_red(), orig_color.get_red(), 0.95), + lerp(bg_color.get_green(), orig_color.get_green(), 0.95), + lerp(bg_color.get_blue(), orig_color.get_blue(), 0.95), + orig_color.get_alpha()); + bg_color.set_rgba( + lerp(bg_color.get_red(), orig_color.get_red(), 0.05), + lerp(bg_color.get_green(), orig_color.get_green(), 0.05), + lerp(bg_color.get_blue(), orig_color.get_blue(), 0.05), + bg_color.get_alpha()); + mid_color.set_rgba( + lerp(bg_color.get_red(), fg_color.get_red(), 0.65), + lerp(bg_color.get_green(), fg_color.get_green(), 0.65), + lerp(bg_color.get_blue(), fg_color.get_blue(), 0.65), + fg_color.get_alpha()); + } + + SPFilterPrimitive* prim = get_selected(); + int row_count = get_model()->children().size(); + + int fheight = CellRendererConnection::size; + Gdk::Rectangle rct, vis; + Gtk::TreeIter row = get_model()->children().begin(); + int text_start_x = 0; + if(row) { + get_cell_area(get_model()->get_path(row), *get_column(1), rct); + get_visible_rect(vis); + text_start_x = rct.get_x() + rct.get_width() - get_input_type_width() * FPInputConverter._length + 1; + + for(unsigned int i = 0; i < FPInputConverter._length; ++i) { + _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str())); + const int x = text_start_x + get_input_type_width() * i; + cr->save(); + Gdk::Cairo::set_source_rgba(cr, bg_color); + cr->rectangle(x, 0, get_input_type_width(), vis.get_height()); + cr->fill_preserve(); + + Gdk::Cairo::set_source_rgba(cr, fg_color); + cr->move_to(x + get_input_type_width(), 5); + cr->rotate_degrees(90); + _vertical_layout->show_in_cairo_context(cr); + + Gdk::Cairo::set_source_rgba(cr, mid_color); + cr->move_to(x, 0); + cr->line_to(x, vis.get_height()); + cr->stroke(); + cr->restore(); + } + cr->rectangle(vis.get_x(), 0, vis.get_width(), vis.get_height()); + cr->clip(); + } + + int row_index = 0; + for(; row != get_model()->children().end(); ++row, ++row_index) { + get_cell_area(get_model()->get_path(row), *get_column(1), rct); + const int x = rct.get_x(), y = rct.get_y(), h = rct.get_height(); + + // Check mouse state + int mx, my; + Gdk::ModifierType mask; + + auto display = get_bin_window()->get_display(); + auto seat = display->get_default_seat(); + auto device = seat->get_pointer(); + cr->set_line_width(0.5); + get_bin_window()->get_device_position(device, mx, my, mask); + + // Outline the bottom of the connection area + const int outline_x = x + fheight * (row_count - row_index); + cr->save(); + + Gdk::Cairo::set_source_rgba(cr, mid_color); + + cr->move_to(vis.get_x(), y + h); + cr->line_to(outline_x, y + h); + // Side outline + cr->line_to(outline_x, y - 1); + + cr->stroke(); + cr->restore(); + + std::vector<Gdk::Point> con_poly; + int con_drag_y = 0; + int con_drag_x = 0; + bool inside; + const SPFilterPrimitive* row_prim = (*row)[_columns.primitive]; + const int inputs = input_count(row_prim); + + if(SP_IS_FEMERGE(row_prim)) { + for(int i = 0; i < inputs; ++i) { + inside = do_connection_node(row, i, con_poly, mx, my); + + cr->save(); + + Gdk::Cairo::set_source_rgba(cr, inside ? mid_color : fg_color); + draw_connection_node(cr, con_poly, inside); + + cr->restore(); + + if(_in_drag == (i + 1)) { + con_drag_y = con_poly[2].get_y(); + con_drag_x = con_poly[2].get_x(); + } + + if(_in_drag != (i + 1) || row_prim != prim) { + draw_connection(cr, row, SPAttr::INVALID, text_start_x, outline_x, con_poly[2].get_y(), row_count, i, fg_color, mid_color); + } + } + } + else { + // Draw "in" shape + inside = do_connection_node(row, 0, con_poly, mx, my); + con_drag_y = con_poly[2].get_y(); + con_drag_x = con_poly[2].get_x(); + + cr->save(); + + Gdk::Cairo::set_source_rgba(cr, inside ? mid_color : fg_color); + draw_connection_node(cr, con_poly, inside); + + cr->restore(); + + // Draw "in" connection + if(_in_drag != 1 || row_prim != prim) { + draw_connection(cr, row, SPAttr::IN_, text_start_x, outline_x, con_poly[2].get_y(), row_count, -1, fg_color, mid_color); + } + + if(inputs == 2) { + // Draw "in2" shape + inside = do_connection_node(row, 1, con_poly, mx, my); + if(_in_drag == 2) { + con_drag_y = con_poly[2].get_y(); + con_drag_x = con_poly[2].get_x(); + } + + cr->save(); + + Gdk::Cairo::set_source_rgba(cr, inside ? mid_color : fg_color); + draw_connection_node(cr, con_poly, inside); + + cr->restore(); + + // Draw "in2" connection + if(_in_drag != 2 || row_prim != prim) { + draw_connection(cr, row, SPAttr::IN2, text_start_x, outline_x, con_poly[2].get_y(), row_count, -1, fg_color, mid_color); + } + } + } + + // Draw drag connection + if(row_prim == prim && _in_drag) { + cr->save(); + Gdk::Cairo::set_source_rgba(cr, orig_color); + cr->move_to(con_drag_x, con_drag_y); + cr->line_to(mx, con_drag_y); + cr->line_to(mx, my); + cr->stroke(); + cr->restore(); + } + } + + return true; +} + +void FilterEffectsDialog::PrimitiveList::draw_connection(const Cairo::RefPtr<Cairo::Context>& cr, + const Gtk::TreeIter& input, const SPAttr attr, + const int text_start_x, const int x1, const int y1, + const int row_count, const int pos, + const Gdk::RGBA fg_color, const Gdk::RGBA mid_color) +{ + cr->save(); + + int src_id = 0; + Gtk::TreeIter res = find_result(input, attr, src_id, pos); + + const bool is_first = input == get_model()->children().begin(); + const bool is_merge = SP_IS_FEMERGE((SPFilterPrimitive*)(*input)[_columns.primitive]); + const bool use_default = !res && !is_merge; + + if(res == input || (use_default && is_first)) { + // Draw straight connection to a standard input + // Draw a lighter line for an implicit connection to a standard input + const int tw = get_input_type_width(); + gint end_x = text_start_x + tw * src_id + (int)(tw * 0.5f) + 1; + + if(use_default && is_first) { + Gdk::Cairo::set_source_rgba(cr, mid_color); + } else { + Gdk::Cairo::set_source_rgba(cr, fg_color); + } + + cr->rectangle(end_x-2, y1-2, 5, 5); + cr->fill_preserve(); + cr->move_to(x1, y1); + cr->line_to(end_x, y1); + cr->stroke(); + } + else { + // Draw an 'L'-shaped connection to another filter primitive + // If no connection is specified, draw a light connection to the previous primitive + if(use_default) { + res = input; + --res; + } + + if(res) { + Gdk::Rectangle rct; + + get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct); + const int fheight = CellRendererConnection::size; + + get_cell_area(get_model()->get_path(res), *get_column(1), rct); + const int row_index = find_index(res); + const int x2 = rct.get_x() + fheight * (row_count - row_index) - fheight / 2; + const int y2 = rct.get_y() + rct.get_height(); + + // Draw a bevelled 'L'-shaped connection + Gdk::Cairo::set_source_rgba(cr, fg_color); + cr->move_to(x1, y1); + cr->line_to(x2-fheight/4, y1); + cr->line_to(x2, y1-fheight/4); + cr->line_to(x2, y2); + cr->stroke(); + } + } + cr->restore(); +} + +// Draw the triangular outline of the connection node, and fill it +// if desired +void FilterEffectsDialog::PrimitiveList::draw_connection_node(const Cairo::RefPtr<Cairo::Context>& cr, + const std::vector<Gdk::Point>& points, + const bool fill) +{ + cr->save(); + cr->move_to(points[0].get_x()+0.5, points[0].get_y()+0.5); + cr->line_to(points[1].get_x()+0.5, points[1].get_y()+0.5); + cr->line_to(points[2].get_x()+0.5, points[2].get_y()+0.5); + cr->line_to(points[0].get_x()+0.5, points[0].get_y()+0.5); + + if(fill) cr->fill(); + else cr->stroke(); + + cr->restore(); +} + +// Creates a triangle outline of the connection node and returns true if (x,y) is inside the node +bool FilterEffectsDialog::PrimitiveList::do_connection_node(const Gtk::TreeIter& row, const int input, + std::vector<Gdk::Point>& points, + const int ix, const int iy) +{ + Gdk::Rectangle rct; + const int icnt = input_count((*row)[_columns.primitive]); + + get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct); + const int fheight = CellRendererConnection::size; + + get_cell_area(_model->get_path(row), *get_column(1), rct); + const float h = rct.get_height() / icnt; + + const int x = rct.get_x() + fheight * (_model->children().size() - find_index(row)); + const int con_w = (int)(fheight * 0.35f); + const int con_y = (int)(rct.get_y() + (h / 2) - con_w + (input * h)); + points.clear(); + points.emplace_back(x, con_y); + points.emplace_back(x, con_y + con_w * 2); + points.emplace_back(x - con_w, con_y + con_w); + + return ix >= x - h && iy >= con_y && ix <= x && iy <= points[1].get_y(); +} + +const Gtk::TreeIter FilterEffectsDialog::PrimitiveList::find_result(const Gtk::TreeIter& start, + const SPAttr attr, int& src_id, + const int pos) +{ + SPFilterPrimitive* prim = (*start)[_columns.primitive]; + Gtk::TreeIter target = _model->children().end(); + int image = 0; + + if(SP_IS_FEMERGE(prim)) { + int c = 0; + bool found = false; + for (auto& o: prim->children) { + if(c == pos && SP_IS_FEMERGENODE(&o)) { + image = SP_FEMERGENODE(&o)->input; + found = true; + } + ++c; + } + if(!found) + return target; + } + else { + if(attr == SPAttr::IN_) + image = prim->image_in; + else if(attr == SPAttr::IN2) { + if(SP_IS_FEBLEND(prim)) + image = SP_FEBLEND(prim)->in2; + else if(SP_IS_FECOMPOSITE(prim)) + image = SP_FECOMPOSITE(prim)->in2; + else if(SP_IS_FEDISPLACEMENTMAP(prim)) + image = SP_FEDISPLACEMENTMAP(prim)->in2; + else + return target; + } + else + return target; + } + + if(image >= 0) { + for(Gtk::TreeIter i = _model->children().begin(); + i != start; ++i) { + if(((SPFilterPrimitive*)(*i)[_columns.primitive])->image_out == image) + target = i; + } + return target; + } + else if(image < -1) { + src_id = -(image + 2); + return start; + } + + return target; +} + +int FilterEffectsDialog::PrimitiveList::find_index(const Gtk::TreeIter& target) +{ + int i = 0; + for(Gtk::TreeIter iter = _model->children().begin(); + iter != target; ++iter, ++i){}; + return i; +} + +bool FilterEffectsDialog::PrimitiveList::on_button_press_event(GdkEventButton* e) +{ + Gtk::TreePath path; + Gtk::TreeViewColumn* col; + const int x = (int)e->x, y = (int)e->y; + int cx, cy; + + _drag_prim = nullptr; + + if(get_path_at_pos(x, y, path, col, cx, cy)) { + Gtk::TreeIter iter = _model->get_iter(path); + std::vector<Gdk::Point> points; + + _drag_prim = (*iter)[_columns.primitive]; + const int icnt = input_count(_drag_prim); + + for(int i = 0; i < icnt; ++i) { + if(do_connection_node(_model->get_iter(path), i, points, x, y)) { + _in_drag = i + 1; + break; + } + } + + queue_draw(); + } + + if(_in_drag) { + _scroll_connection = Glib::signal_timeout().connect(sigc::mem_fun(*this, &PrimitiveList::on_scroll_timeout), 150); + _autoscroll_x = 0; + _autoscroll_y = 0; + get_selection()->select(path); + return true; + } + else + return Gtk::TreeView::on_button_press_event(e); +} + +bool FilterEffectsDialog::PrimitiveList::on_motion_notify_event(GdkEventMotion* e) +{ + const int speed = 10; + const int limit = 15; + + Gdk::Rectangle vis; + get_visible_rect(vis); + int vis_x, vis_y; + + int vis_x2, vis_y2; + convert_widget_to_tree_coords(vis.get_x(), vis.get_y(), vis_x2, vis_y2); + + convert_tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y); + const int top = vis_y + vis.get_height(); + const int right_edge = vis_x + vis.get_width(); + + // When autoscrolling during a connection drag, set the speed based on + // where the mouse is in relation to the edges. + if(e->y < vis_y) + _autoscroll_y = -(int)(speed + (vis_y - e->y) / 5); + else if(e->y < vis_y + limit) + _autoscroll_y = -speed; + else if(e->y > top) + _autoscroll_y = (int)(speed + (e->y - top) / 5); + else if(e->y > top - limit) + _autoscroll_y = speed; + else + _autoscroll_y = 0; + + double e2 = ( e->x - vis_x2/2); + // horizontal scrolling + if(e2 < vis_x) + _autoscroll_x = -(int)(speed + (vis_x - e2) / 5); + else if(e2 < vis_x + limit) + _autoscroll_x = -speed; + else if(e2 > right_edge) + _autoscroll_x = (int)(speed + (e2 - right_edge) / 5); + else if(e2 > right_edge - limit) + _autoscroll_x = speed; + else + _autoscroll_x = 0; + + + + queue_draw(); + + return Gtk::TreeView::on_motion_notify_event(e); +} + +bool FilterEffectsDialog::PrimitiveList::on_button_release_event(GdkEventButton* e) +{ + SPFilterPrimitive *prim = get_selected(), *target; + + _scroll_connection.disconnect(); + + if(_in_drag && prim) { + Gtk::TreePath path; + Gtk::TreeViewColumn* col; + int cx, cy; + + if(get_path_at_pos((int)e->x, (int)e->y, path, col, cx, cy)) { + const gchar *in_val = nullptr; + Glib::ustring result; + Gtk::TreeIter target_iter = _model->get_iter(path); + target = (*target_iter)[_columns.primitive]; + col = get_column(1); + + Gdk::Rectangle rct; + get_cell_area(path, *col, rct); + const int twidth = get_input_type_width(); + const int sources_x = rct.get_width() - twidth * FPInputConverter._length; + if(cx > sources_x) { + int src = (cx - sources_x) / twidth; + if (src < 0) { + src = 0; + } else if(src >= static_cast<int>(FPInputConverter._length)) { + src = FPInputConverter._length - 1; + } + result = FPInputConverter.get_key((FilterPrimitiveInput)src); + in_val = result.c_str(); + } + else { + // Ensure that the target comes before the selected primitive + for(Gtk::TreeIter iter = _model->children().begin(); + iter != get_selection()->get_selected(); ++iter) { + if(iter == target_iter) { + Inkscape::XML::Node *repr = target->getRepr(); + // Make sure the target has a result + const gchar *gres = repr->attribute("result"); + if(!gres) { + result = SP_FILTER(prim->parent)->get_new_result_name(); + repr->setAttributeOrRemoveIfEmpty("result", result); + in_val = result.c_str(); + } + else + in_val = gres; + break; + } + } + } + + if(SP_IS_FEMERGE(prim)) { + int c = 1; + bool handled = false; + for (auto& o: prim->children) { + if(c == _in_drag && SP_IS_FEMERGENODE(&o)) { + // If input is null, delete it + if(!in_val) { + + //XML Tree being used directly here while it shouldn't be. + sp_repr_unparent(o.getRepr()); + DocumentUndo::done(prim->document, _("Remove merge node"), INKSCAPE_ICON("dialog-filters")); + (*get_selection()->get_selected())[_columns.primitive] = prim; + } else { + _dialog.set_attr(&o, SPAttr::IN_, in_val); + } + handled = true; + break; + } + ++c; + } + // Add new input? + if(!handled && c == _in_drag && in_val) { + Inkscape::XML::Document *xml_doc = prim->document->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:feMergeNode"); + repr->setAttribute("inkscape:collect", "always"); + + //XML Tree being used directly here while it shouldn't be. + prim->getRepr()->appendChild(repr); + SPFeMergeNode *node = SP_FEMERGENODE(prim->document->getObjectByRepr(repr)); + Inkscape::GC::release(repr); + _dialog.set_attr(node, SPAttr::IN_, in_val); + (*get_selection()->get_selected())[_columns.primitive] = prim; + } + } + else { + if(_in_drag == 1) + _dialog.set_attr(prim, SPAttr::IN_, in_val); + else if(_in_drag == 2) + _dialog.set_attr(prim, SPAttr::IN2, in_val); + } + } + + _in_drag = 0; + queue_draw(); + + _dialog.update_settings_view(); + } + + if((e->type == GDK_BUTTON_RELEASE) && (e->button == 3)) { + const bool sensitive = get_selected() != nullptr; + auto items = _primitive_menu->get_children(); + items[0]->set_sensitive(sensitive); + items[1]->set_sensitive(sensitive); + + _primitive_menu->popup_at_pointer(reinterpret_cast<GdkEvent *>(e)); + + return true; + } + else + return Gtk::TreeView::on_button_release_event(e); +} + +// Checks all of prim's inputs, removes any that use result +static void check_single_connection(SPFilterPrimitive* prim, const int result) +{ + if (prim && (result >= 0)) { + if (prim->image_in == result) { + prim->removeAttribute("in"); + } + + if (SP_IS_FEBLEND(prim)) { + if (SP_FEBLEND(prim)->in2 == result) { + prim->removeAttribute("in2"); + } + } else if (SP_IS_FECOMPOSITE(prim)) { + if (SP_FECOMPOSITE(prim)->in2 == result) { + prim->removeAttribute("in2"); + } + } else if (SP_IS_FEDISPLACEMENTMAP(prim)) { + if (SP_FEDISPLACEMENTMAP(prim)->in2 == result) { + prim->removeAttribute("in2"); + } + } + } +} + +// Remove any connections going to/from prim_iter that forward-reference other primitives +void FilterEffectsDialog::PrimitiveList::sanitize_connections(const Gtk::TreeIter& prim_iter) +{ + SPFilterPrimitive *prim = (*prim_iter)[_columns.primitive]; + bool before = true; + + for(Gtk::TreeIter iter = _model->children().begin(); + iter != _model->children().end(); ++iter) { + if(iter == prim_iter) + before = false; + else { + SPFilterPrimitive* cur_prim = (*iter)[_columns.primitive]; + if(before) + check_single_connection(cur_prim, prim->image_out); + else + check_single_connection(prim, cur_prim->image_out); + } + } +} + +// Reorder the filter primitives to match the list order +void FilterEffectsDialog::PrimitiveList::on_drag_end(const Glib::RefPtr<Gdk::DragContext>& /*dc*/) +{ + SPFilter* filter = _dialog._filter_modifier.get_selected_filter(); + int ndx = 0; + + for (Gtk::TreeModel::iterator iter = _model->children().begin(); + iter != _model->children().end(); ++iter, ++ndx) { + SPFilterPrimitive* prim = (*iter)[_columns.primitive]; + if (prim && prim == _drag_prim) { + prim->getRepr()->setPosition(ndx); + break; + } + } + + for (Gtk::TreeModel::iterator iter = _model->children().begin(); + iter != _model->children().end(); ++iter, ++ndx) { + SPFilterPrimitive* prim = (*iter)[_columns.primitive]; + if (prim && prim == _drag_prim) { + sanitize_connections(iter); + get_selection()->select(iter); + break; + } + } + + filter->requestModified(SP_OBJECT_MODIFIED_FLAG); + + DocumentUndo::done(filter->document, _("Reorder filter primitive"), INKSCAPE_ICON("dialog-filters")); +} + +// If a connection is dragged towards the top or bottom of the list, the list should scroll to follow. +bool FilterEffectsDialog::PrimitiveList::on_scroll_timeout() +{ + if(_autoscroll_y) { + auto a = static_cast<Gtk::ScrolledWindow*>(get_parent())->get_vadjustment(); + double v = a->get_value() + _autoscroll_y; + + if(v < 0) + v = 0; + if(v > a->get_upper() - a->get_page_size()) + v = a->get_upper() - a->get_page_size(); + + a->set_value(v); + + queue_draw(); + } + + + if(_autoscroll_x) { + auto a_h = static_cast<Gtk::ScrolledWindow*>(get_parent())->get_hadjustment(); + double h = a_h->get_value() + _autoscroll_x; + + if(h < 0) + h = 0; + if(h > a_h->get_upper() - a_h->get_page_size()) + h = a_h->get_upper() - a_h->get_page_size(); + + a_h->set_value(h); + + queue_draw(); + } + + return true; +} + +int FilterEffectsDialog::PrimitiveList::primitive_count() const +{ + return _model->children().size(); +} + +int FilterEffectsDialog::PrimitiveList::get_input_type_width() const +{ + // Maximum font height calculated in initText() and stored in _input_type_width. + // Add 2 to font height to account for rectangle around text. + return _input_type_width + 2; +} + +/*** FilterEffectsDialog ***/ + +FilterEffectsDialog::FilterEffectsDialog() + : DialogBase("/dialogs/filtereffects", "FilterEffects") + , _add_primitive_type(FPConverter) + , _add_primitive(_("Add Effect:")) + , _empty_settings(_("No effect selected"), Gtk::ALIGN_START) + , _no_filter_selected(_("No filter selected"), Gtk::ALIGN_START) + , _settings_initialized(false) + , _locked(false) + , _attr_lock(false) + , _filter_modifier(*this) + , _primitive_list(*this) + , _settings_tab1(Gtk::ORIENTATION_VERTICAL) + , _settings_tab2(Gtk::ORIENTATION_VERTICAL) +{ + _settings = new Settings(*this, _settings_tab1, sigc::mem_fun(*this, &FilterEffectsDialog::set_attr_direct), + NR_FILTER_ENDPRIMITIVETYPE); + _filter_general_settings = new Settings(*this, _settings_tab2, sigc::mem_fun(*this, &FilterEffectsDialog::set_filternode_attr), + 1); + + // Initialize widget hierarchy + auto hpaned = Gtk::manage(new Gtk::Paned()); + _primitive_box = Gtk::manage(new Gtk::Paned(Gtk::ORIENTATION_VERTICAL)); + + _sw_infobox = Gtk::manage(new Gtk::ScrolledWindow); + Gtk::ScrolledWindow* sw_prims = Gtk::manage(new Gtk::ScrolledWindow); + Gtk::Box* infobox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, /*spacing:*/4)); + Gtk::Box* hb_prims = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + Gtk::Box* vb_prims = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + Gtk::Box* vb_desc = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + + Gtk::Box* prim_vbox_p = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + Gtk::Box* prim_vbox_i = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + + sw_prims->add(_primitive_list); + + prim_vbox_p->pack_start(*sw_prims, true, true); + prim_vbox_i->pack_start(*vb_prims, true, true); + + _primitive_box->pack1(*prim_vbox_p); + _primitive_box->pack2(*prim_vbox_i, false, false); + + hpaned->pack1(_filter_modifier); + hpaned->pack2(*_primitive_box); + pack_start(*hpaned, true, true); + + _infobox_icon.set_halign(Gtk::ALIGN_START); + _infobox_icon.set_valign(Gtk::ALIGN_START); + _infobox_desc.set_halign(Gtk::ALIGN_START); + _infobox_desc.set_valign(Gtk::ALIGN_START); + _infobox_desc.set_justify(Gtk::JUSTIFY_LEFT); + _infobox_desc.set_line_wrap(true); + + vb_desc->pack_start(_infobox_desc, true, true); + + infobox->pack_start(_infobox_icon, false, false); + infobox->pack_start(*vb_desc, true, true); + + _sw_infobox->add(*infobox); + + vb_prims->pack_start(*hb_prims, false, false); + vb_prims->pack_start(*_sw_infobox, true, true); + + hb_prims->pack_start(_add_primitive, false, false); + hb_prims->pack_start(_add_primitive_type, true, true); + pack_start(_settings_tabs, false, false); + _settings_tabs.append_page(_settings_tab1, _("Effect parameters")); + _settings_tabs.append_page(_settings_tab2, _("Filter General Settings")); + + _primitive_list.signal_primitive_changed().connect( + sigc::mem_fun(*this, &FilterEffectsDialog::update_settings_view)); + _filter_modifier.signal_filter_changed().connect( + sigc::mem_fun(_primitive_list, &PrimitiveList::update)); + + _add_primitive_type.signal_changed().connect( + sigc::mem_fun(*this, &FilterEffectsDialog::update_primitive_infobox)); + + sw_prims->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + sw_prims->set_shadow_type(Gtk::SHADOW_IN); + _sw_infobox->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + +// al_settings->set_padding(0, 0, 12, 0); +// fr_settings->set_shadow_type(Gtk::SHADOW_NONE); +// ((Gtk::Label*)fr_settings->get_label_widget())->set_use_markup(); + _add_primitive.signal_clicked().connect(sigc::mem_fun(*this, &FilterEffectsDialog::add_primitive)); + _primitive_list.set_menu(*this, sigc::mem_fun(*this, &FilterEffectsDialog::duplicate_primitive), + sigc::mem_fun(_primitive_list, &PrimitiveList::remove_selected)); + + init_settings_widgets(); + _primitive_list.update(); + update_primitive_infobox(); + + show(); + show_all_children(); + update(); +} + +FilterEffectsDialog::~FilterEffectsDialog() +{ + delete _settings; + delete _filter_general_settings; +} + +void FilterEffectsDialog::documentReplaced() +{ + _resource_changed.disconnect(); + if (auto document = getDocument()) { + _resource_changed = document->connectResourcesChanged("filter", sigc::mem_fun(_filter_modifier, &FilterModifier::update_filters)); + _filter_modifier.update_filters(); + } +} + +void FilterEffectsDialog::selectionChanged(Inkscape::Selection *selection) +{ + if (selection) { + _filter_modifier.update_selection(selection); + } +} + +void FilterEffectsDialog::selectionModified(Inkscape::Selection *selection, guint flags) +{ + if (flags & ( SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_PARENT_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG) ) { + _filter_modifier.update_selection(selection); + } +} + +void FilterEffectsDialog::set_attrs_locked(const bool l) +{ + _locked = l; +} + +void FilterEffectsDialog::show_all_vfunc() +{ + update_settings_view(); +} + +void FilterEffectsDialog::init_settings_widgets() +{ + // TODO: Find better range/climb-rate/digits values for the SpinScales, + // most of the current values are complete guesses! + + _settings_tab1.set_border_width(4); + _settings_tab2.set_border_width(4); + + _empty_settings.set_sensitive(false); + _settings_tab1.pack_start(_empty_settings); + + _no_filter_selected.set_sensitive(false); + _settings_tab2.pack_start(_no_filter_selected); + _settings_initialized = true; + + _filter_general_settings->type(0); + auto _region_auto = _filter_general_settings->add_checkbutton(true, SPAttr::AUTO_REGION, _("Automatic Region"), "true", "false", _("If unset, the coordinates and dimensions won't be updated automatically.")); + _region_pos = _filter_general_settings->add_multispinbutton(/*default x:*/ (double) -0.1, /*default y:*/ (double) -0.1, SPAttr::X, SPAttr::Y, _("Coordinates:"), -100, 100, 0.01, 0.1, 2, _("X coordinate of the left corners of filter effects region"), _("Y coordinate of the upper corners of filter effects region")); + _region_size = _filter_general_settings->add_multispinbutton(/*default width:*/ (double) 1.2, /*default height:*/ (double) 1.2, SPAttr::WIDTH, SPAttr::HEIGHT, _("Dimensions:"), 0, 1000, 0.01, 0.1, 2, _("Width of filter effects region"), _("Height of filter effects region")); + _region_auto->signal_attr_changed().connect( sigc::bind(sigc::mem_fun(*this, &FilterEffectsDialog::update_automatic_region), _region_auto)); + + _settings->type(NR_FILTER_BLEND); + _settings->add_combo(SP_CSS_BLEND_NORMAL, SPAttr::MODE, _("Mode:"), SPBlendModeConverter); + + _settings->type(NR_FILTER_COLORMATRIX); + ComboBoxEnum<FilterColorMatrixType>* colmat = _settings->add_combo(COLORMATRIX_MATRIX, SPAttr::TYPE, _("Type:"), ColorMatrixTypeConverter, _("Indicates the type of matrix operation. The keyword 'matrix' indicates that a full 5x4 matrix of values will be provided. The other keywords represent convenience shortcuts to allow commonly used color operations to be performed without specifying a complete matrix.")); + _color_matrix_values = _settings->add_colormatrixvalues(_("Value(s):")); + colmat->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::update_color_matrix)); + + _settings->type(NR_FILTER_COMPONENTTRANSFER); + // TRANSLATORS: Abbreviation for red color channel in RGBA + _settings->add_componenttransfervalues(C_("color", "R:"), SPFeFuncNode::R); + // TRANSLATORS: Abbreviation for green color channel in RGBA + _settings->add_componenttransfervalues(C_("color", "G:"), SPFeFuncNode::G); + // TRANSLATORS: Abbreviation for blue color channel in RGBA + _settings->add_componenttransfervalues(C_("color", "B:"), SPFeFuncNode::B); + // TRANSLATORS: Abbreviation for alpha channel in RGBA + _settings->add_componenttransfervalues(C_("color", "A:"), SPFeFuncNode::A); + + _settings->type(NR_FILTER_COMPOSITE); + _settings->add_combo(COMPOSITE_OVER, SPAttr::OPERATOR, _("Operator:"), CompositeOperatorConverter); + _k1 = _settings->add_spinscale(0, SPAttr::K1, _("K1:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively.")); + _k2 = _settings->add_spinscale(0, SPAttr::K2, _("K2:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively.")); + _k3 = _settings->add_spinscale(0, SPAttr::K3, _("K3:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively.")); + _k4 = _settings->add_spinscale(0, SPAttr::K4, _("K4:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively.")); + + _settings->type(NR_FILTER_CONVOLVEMATRIX); + _convolve_order = _settings->add_dualspinbutton((char*)"3", SPAttr::ORDER, _("Size:"), 1, 5, 1, 1, 0, _("width of the convolve matrix"), _("height of the convolve matrix")); + _convolve_target = _settings->add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, SPAttr::TARGETX, SPAttr::TARGETY, _("Target:"), 0, 4, 1, 1, 0, _("X coordinate of the target point in the convolve matrix. The convolution is applied to pixels around this point."), _("Y coordinate of the target point in the convolve matrix. The convolution is applied to pixels around this point.")); + //TRANSLATORS: for info on "Kernel", see http://en.wikipedia.org/wiki/Kernel_(matrix) + _convolve_matrix = _settings->add_matrix(SPAttr::KERNELMATRIX, _("Kernel:"), _("This matrix describes the convolve operation that is applied to the input image in order to calculate the pixel colors at the output. Different arrangements of values in this matrix result in various possible visual effects. An identity matrix would lead to a motion blur effect (parallel to the matrix diagonal) while a matrix filled with a constant non-zero value would lead to a common blur effect.")); + _convolve_order->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::convolve_order_changed)); + _settings->add_spinscale(0, SPAttr::DIVISOR, _("Divisor:"), 0, 1000, 1, 0.1, 2, _("After applying the kernelMatrix to the input image to yield a number, that number is divided by divisor to yield the final destination color value. A divisor that is the sum of all the matrix values tends to have an evening effect on the overall color intensity of the result.")); + _settings->add_spinscale(0, SPAttr::BIAS, _("Bias:"), -10, 10, 1, 0.01, 1, _("This value is added to each component. This is useful to define a constant value as the zero response of the filter.")); + _settings->add_combo(CONVOLVEMATRIX_EDGEMODE_DUPLICATE, SPAttr::EDGEMODE, _("Edge Mode:"), ConvolveMatrixEdgeModeConverter, _("Determines how to extend the input image as necessary with color values so that the matrix operations can be applied when the kernel is positioned at or near the edge of the input image.")); + _settings->add_checkbutton(false, SPAttr::PRESERVEALPHA, _("Preserve Alpha"), "true", "false", _("If set, the alpha channel won't be altered by this filter primitive.")); + + _settings->type(NR_FILTER_DIFFUSELIGHTING); + _settings->add_color(/*default: white*/ 0xffffffff, SPAttr::LIGHTING_COLOR, _("Diffuse Color:"), _("Defines the color of the light source")); + _settings->add_spinscale(1, SPAttr::SURFACESCALE, _("Surface Scale:"), -5, 5, 0.01, 0.001, 3, _("This value amplifies the heights of the bump map defined by the input alpha channel")); + _settings->add_spinscale(1, SPAttr::DIFFUSECONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model.")); + _settings->add_dualspinscale(SPAttr::KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1); + _settings->add_lightsource(); + + _settings->type(NR_FILTER_DISPLACEMENTMAP); + _settings->add_spinscale(0, SPAttr::SCALE, _("Scale:"), 0, 100, 1, 0.01, 1, _("This defines the intensity of the displacement effect.")); + _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SPAttr::XCHANNELSELECTOR, _("X displacement:"), DisplacementMapChannelConverter, _("Color component that controls the displacement in the X direction")); + _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SPAttr::YCHANNELSELECTOR, _("Y displacement:"), DisplacementMapChannelConverter, _("Color component that controls the displacement in the Y direction")); + + _settings->type(NR_FILTER_FLOOD); + _settings->add_color(/*default: black*/ 0, SPAttr::FLOOD_COLOR, _("Flood Color:"), _("The whole filter region will be filled with this color.")); + _settings->add_spinscale(1, SPAttr::FLOOD_OPACITY, _("Opacity:"), 0, 1, 0.1, 0.01, 2); + + _settings->type(NR_FILTER_GAUSSIANBLUR); + _settings->add_dualspinscale(SPAttr::STDDEVIATION, _("Standard Deviation:"), 0, 100, 1, 0.01, 2, _("The standard deviation for the blur operation.")); + + _settings->type(NR_FILTER_MERGE); + _settings->add_no_params(); + + _settings->type(NR_FILTER_MORPHOLOGY); + _settings->add_combo(MORPHOLOGY_OPERATOR_ERODE, SPAttr::OPERATOR, _("Operator:"), MorphologyOperatorConverter, _("Erode: performs \"thinning\" of input image.\nDilate: performs \"fattening\" of input image.")); + _settings->add_dualspinscale(SPAttr::RADIUS, _("Radius:"), 0, 100, 1, 0.01, 1); + + _settings->type(NR_FILTER_IMAGE); + _settings->add_fileorelement(SPAttr::XLINK_HREF, _("Source of Image:")); + _image_x = _settings->add_entry(SPAttr::X,_("X"),_("X")); + _image_x->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_x_changed)); + //This is commented out because we want the default empty value of X or Y and couldn't get it from SpinButton + //_image_y = _settings->add_spinbutton(0, SPAttr::Y, _("Y:"), -DBL_MAX, DBL_MAX, 1, 1, 5, _("Y")); + _image_y = _settings->add_entry(SPAttr::Y,_("Y"),_("Y")); + _image_y->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_y_changed)); + _settings->type(NR_FILTER_OFFSET); + _settings->add_checkbutton(false, SPAttr::PRESERVEALPHA, _("Preserve Alpha"), "true", "false", _("If set, the alpha channel won't be altered by this filter primitive.")); + _settings->add_spinscale(0, SPAttr::DX, _("Delta X:"), -100, 100, 1, 0.01, 2, _("This is how far the input image gets shifted to the right")); + _settings->add_spinscale(0, SPAttr::DY, _("Delta Y:"), -100, 100, 1, 0.01, 2, _("This is how far the input image gets shifted downwards")); + + _settings->type(NR_FILTER_SPECULARLIGHTING); + _settings->add_color(/*default: white*/ 0xffffffff, SPAttr::LIGHTING_COLOR, _("Specular Color:"), _("Defines the color of the light source")); + _settings->add_spinscale(1, SPAttr::SURFACESCALE, _("Surface Scale:"), -5, 5, 0.1, 0.01, 2, _("This value amplifies the heights of the bump map defined by the input alpha channel")); + _settings->add_spinscale(1, SPAttr::SPECULARCONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model.")); + _settings->add_spinscale(1, SPAttr::SPECULAREXPONENT, _("Exponent:"), 1, 50, 1, 0.01, 1, _("Exponent for specular term, larger is more \"shiny\".")); + _settings->add_dualspinscale(SPAttr::KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1); + _settings->add_lightsource(); + + _settings->type(NR_FILTER_TILE); + _settings->add_no_params(); + + _settings->type(NR_FILTER_TURBULENCE); +// _settings->add_checkbutton(false, SPAttr::STITCHTILES, _("Stitch Tiles"), "stitch", "noStitch"); + _settings->add_combo(TURBULENCE_TURBULENCE, SPAttr::TYPE, _("Type:"), TurbulenceTypeConverter, _("Indicates whether the filter primitive should perform a noise or turbulence function.")); + _settings->add_dualspinscale(SPAttr::BASEFREQUENCY, _("Base Frequency:"), 0, 100, 0.1, 2, 2); + _settings->add_spinscale(1, SPAttr::NUMOCTAVES, _("Octaves:"), 1, 10, 1, 1, 0); + _settings->add_spinscale(0, SPAttr::SEED, _("Seed:"), 0, 1000, 1, 1, 0, _("The starting number for the pseudo random number generator.")); +} + +void FilterEffectsDialog::add_primitive() +{ + SPFilter* filter = _filter_modifier.get_selected_filter(); + + if(filter) { + SPFilterPrimitive* prim = filter_add_primitive(filter, _add_primitive_type.get_active_data()->id); + + _primitive_list.select(prim); + + DocumentUndo::done(filter->document, _("Add filter primitive"), INKSCAPE_ICON("dialog-filters")); + } +} + +void FilterEffectsDialog::update_primitive_infobox() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/showfiltersinfobox/value", true)){ + _sw_infobox->show(); + } else { + _sw_infobox->hide(); + } + switch(_add_primitive_type.get_active_data()->id){ + case(NR_FILTER_BLEND): + _infobox_icon.set_from_icon_name("feBlend-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Provides image blending modes, such as screen, multiply, darken and lighten.")); + break; + case(NR_FILTER_COLORMATRIX): + _infobox_icon.set_from_icon_name("feColorMatrix-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Modifies pixel colors based on a transformation matrix. Useful for adjusting color hue and saturation.")); + break; + case(NR_FILTER_COMPONENTTRANSFER): + _infobox_icon.set_from_icon_name("feComponentTransfer-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Manipulates color components according to particular transfer functions. Useful for brightness and contrast adjustment, color balance, and thresholding.")); + break; + case(NR_FILTER_COMPOSITE): + _infobox_icon.set_from_icon_name("feComposite-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Composites two images using one of the Porter-Duff blending modes or the arithmetic mode described in SVG standard.")); + break; + case(NR_FILTER_CONVOLVEMATRIX): + _infobox_icon.set_from_icon_name("feConvolveMatrix-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Performs a convolution on the input image enabling effects like blur, sharpening, embossing and edge detection.")); + break; + case(NR_FILTER_DIFFUSELIGHTING): + _infobox_icon.set_from_icon_name("feDiffuseLighting-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Creates \"embossed\" shadings. The input's alpha channel is used to provide depth information: higher opacity areas are raised toward the viewer and lower opacity areas recede away from the viewer.")); + break; + case(NR_FILTER_DISPLACEMENTMAP): + _infobox_icon.set_from_icon_name("feDisplacementMap-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Displaces pixels from the first input using the second as a map of displacement intensity. Classical examples are whirl and pinch effects.")); + break; + case(NR_FILTER_FLOOD): + _infobox_icon.set_from_icon_name("feFlood-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Fills the region with a given color and opacity. Often used as input to other filters to apply color to a graphic.")); + break; + case(NR_FILTER_GAUSSIANBLUR): + _infobox_icon.set_from_icon_name("feGaussianBlur-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Uniformly blurs its input. Commonly used together with Offset to create a drop shadow effect.")); + break; + case(NR_FILTER_IMAGE): + _infobox_icon.set_from_icon_name("feImage-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Fills the region with graphics from an external file or from another portion of the document.")); + break; + case(NR_FILTER_MERGE): + _infobox_icon.set_from_icon_name("feMerge-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Merges multiple inputs using normal alpha compositing. Equivalent to using several Blend primitives in 'normal' mode or several Composite primitives in 'over' mode.")); + break; + case(NR_FILTER_MORPHOLOGY): + _infobox_icon.set_from_icon_name("feMorphology-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Provides erode and dilate effects. For single-color objects erode makes the object thinner and dilate makes it thicker.")); + break; + case(NR_FILTER_OFFSET): + _infobox_icon.set_from_icon_name("feOffset-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Offsets the input by an user-defined amount. Commonly used for drop shadow effects.")); + break; + case(NR_FILTER_SPECULARLIGHTING): + _infobox_icon.set_from_icon_name("feSpecularLighting-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Creates \"embossed\" shadings. The input's alpha channel is used to provide depth information: higher opacity areas are raised toward the viewer and lower opacity areas recede away from the viewer.")); + break; + case(NR_FILTER_TILE): + _infobox_icon.set_from_icon_name("feTile-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Tiles a region with an input graphic. The source tile is defined by the filter primitive subregion of the input.")); + break; + case(NR_FILTER_TURBULENCE): + _infobox_icon.set_from_icon_name("feTurbulence-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("Renders Perlin noise, which is useful to generate textures such as clouds, fire, smoke, marble or granite.")); + break; + default: + g_assert(false); + break; + } + //_infobox_icon.set_pixel_size(96); + _infobox_icon.set_pixel_size(64); +} + +void FilterEffectsDialog::duplicate_primitive() +{ + SPFilter* filter = _filter_modifier.get_selected_filter(); + SPFilterPrimitive* origprim = _primitive_list.get_selected(); + + if (filter && origprim) { + Inkscape::XML::Node *repr; + repr = origprim->getRepr()->duplicate(origprim->getRepr()->document()); + filter->getRepr()->appendChild(repr); + + DocumentUndo::done(filter->document, _("Duplicate filter primitive"), INKSCAPE_ICON("dialog-filters")); + + _primitive_list.update(); + } +} + +void FilterEffectsDialog::convolve_order_changed() +{ + _convolve_matrix->set_from_attribute(_primitive_list.get_selected()); + _convolve_target->get_spinbuttons()[0]->get_adjustment()->set_upper(_convolve_order->get_spinbutton1().get_value() - 1); + _convolve_target->get_spinbuttons()[1]->get_adjustment()->set_upper(_convolve_order->get_spinbutton2().get_value() - 1); +} + +bool number_or_empy(const Glib::ustring& text) { + if (text.empty()) { + return true; + } + double n = g_strtod(text.c_str(), nullptr); + if (n == 0.0 && strcmp(text.c_str(), "0") != 0 && strcmp(text.c_str(), "0.0") != 0) { + return false; + } + else { + return true; + } +} + +void FilterEffectsDialog::image_x_changed() +{ + if (number_or_empy(_image_x->get_text())) { + _image_x->set_from_attribute(_primitive_list.get_selected()); + } +} + +void FilterEffectsDialog::image_y_changed() +{ + if (number_or_empy(_image_y->get_text())) { + _image_y->set_from_attribute(_primitive_list.get_selected()); + } +} + +void FilterEffectsDialog::set_attr_direct(const AttrWidget* input) +{ + set_attr(_primitive_list.get_selected(), input->get_attribute(), input->get_as_attribute().c_str()); +} + +void FilterEffectsDialog::set_filternode_attr(const AttrWidget* input) +{ + if(!_locked) { + _attr_lock = true; + SPFilter *filter = _filter_modifier.get_selected_filter(); + const gchar* name = (const gchar*)sp_attribute_name(input->get_attribute()); + if (filter && name && filter->getRepr()){ + filter->setAttributeOrRemoveIfEmpty(name, input->get_as_attribute()); + filter->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + _attr_lock = false; + } +} + +void FilterEffectsDialog::set_child_attr_direct(const AttrWidget* input) +{ + set_attr(_primitive_list.get_selected()->firstChild(), input->get_attribute(), input->get_as_attribute().c_str()); +} + +void FilterEffectsDialog::set_attr(SPObject* o, const SPAttr attr, const gchar* val) +{ + if(!_locked) { + _attr_lock = true; + + SPFilter *filter = _filter_modifier.get_selected_filter(); + const gchar* name = (const gchar*)sp_attribute_name(attr); + if(filter && name && o) { + update_settings_sensitivity(); + + o->setAttribute(name, val); + filter->requestModified(SP_OBJECT_MODIFIED_FLAG); + + Glib::ustring undokey = "filtereffects:"; + undokey += name; + DocumentUndo::maybeDone(filter->document, undokey.c_str(), _("Set filter primitive attribute"), INKSCAPE_ICON("dialog-filters")); + } + + _attr_lock = false; + } +} + +void FilterEffectsDialog::update_filter_general_settings_view() +{ + if(_settings_initialized != true) return; + + if(!_locked) { + _attr_lock = true; + + SPFilter* filter = _filter_modifier.get_selected_filter(); + + if(filter) { + _filter_general_settings->show_and_update(0, filter); + _no_filter_selected.hide(); + } + else { + std::vector<Gtk::Widget*> vect = _settings_tab2.get_children(); + vect[0]->hide(); + _no_filter_selected.show(); + } + + _attr_lock = false; + } +} + +void FilterEffectsDialog::update_settings_view() +{ + update_settings_sensitivity(); + + if(_attr_lock) + return; + +//First Tab + + std::vector<Gtk::Widget*> vect1 = _settings_tab1.get_children(); + for(auto & i : vect1) + i->hide(); + _empty_settings.show(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/showfiltersinfobox/value", true)){ + _sw_infobox->show(); + } else { + _sw_infobox->hide(); + } + + SPFilterPrimitive* prim = _primitive_list.get_selected(); + + if(prim && prim->getRepr()) { + + //XML Tree being used directly here while it shouldn't be. + _settings->show_and_update(FPConverter.get_id_from_key(prim->getRepr()->name()), prim); + _empty_settings.hide(); + } + +//Second Tab + + std::vector<Gtk::Widget*> vect2 = _settings_tab2.get_children(); + vect2[0]->hide(); + _no_filter_selected.show(); + + SPFilter* filter = _filter_modifier.get_selected_filter(); + + if(filter) { + _filter_general_settings->show_and_update(0, filter); + _no_filter_selected.hide(); + } + +} + +void FilterEffectsDialog::update_settings_sensitivity() +{ + SPFilterPrimitive* prim = _primitive_list.get_selected(); + const bool use_k = SP_IS_FECOMPOSITE(prim) && SP_FECOMPOSITE(prim)->composite_operator == COMPOSITE_ARITHMETIC; + _k1->set_sensitive(use_k); + _k2->set_sensitive(use_k); + _k3->set_sensitive(use_k); + _k4->set_sensitive(use_k); + +} + +void FilterEffectsDialog::update_color_matrix() +{ + _color_matrix_values->set_from_attribute(_primitive_list.get_selected()); +} + +void FilterEffectsDialog::update_automatic_region(Gtk::CheckButton *btn) +{ + bool automatic = btn->get_active(); + _region_pos->set_sensitive(!automatic); + _region_size->set_sensitive(!automatic); + +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/filter-effects-dialog.h b/src/ui/dialog/filter-effects-dialog.h new file mode 100644 index 0000000..3970c43 --- /dev/null +++ b/src/ui/dialog/filter-effects-dialog.h @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Filter Effects dialog + */ +/* Authors: + * Nicholas Bishop <nicholasbishop@gmail.com> + * Rodrigo Kumpera <kumpera@gmail.com> + * insaner + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_FILTER_EFFECTS_H +#define INKSCAPE_UI_DIALOG_FILTER_EFFECTS_H + +#include <glibmm/property.h> +#include <glibmm/propertyproxy.h> +#include <gtkmm/notebook.h> +#include <gtkmm/paned.h> +#include <gtkmm/scrolledwindow.h> +#include <memory> + +#include "attributes.h" +#include "display/nr-filter-types.h" +#include "helper/auto-connection.h" +#include "ui/dialog/dialog-base.h" +#include "ui/widget/combo-enums.h" +#include "ui/widget/spin-scale.h" +#include "xml/helper-observer.h" + +class SPFilter; +class SPFilterPrimitive; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class EntryAttr; +class FileOrElementChooser; +class DualSpinButton; +class MultiSpinButton; + +class FilterEffectsDialog : public DialogBase +{ +public: + FilterEffectsDialog(); + ~FilterEffectsDialog() override; + + static FilterEffectsDialog &getInstance() { return *new FilterEffectsDialog(); } + + void set_attrs_locked(const bool); +protected: + void show_all_vfunc() override; +private: + + void documentReplaced() override; + void selectionChanged(Inkscape::Selection *selection) override; + void selectionModified(Inkscape::Selection *selection, guint flags) override; + + Inkscape::auto_connection _resource_changed; + + friend class FileOrElementChooser; + + class FilterModifier : public Gtk::Box + { + public: + FilterModifier(FilterEffectsDialog&); + + void update_filters(); + void update_selection(Selection *); + + SPFilter* get_selected_filter(); + void select_filter(const SPFilter*); + + sigc::signal<void>& signal_filter_changed() + { + return _signal_filter_changed; + } + + private: + class Columns : public Gtk::TreeModel::ColumnRecord + { + public: + Columns() + { + add(filter); + add(label); + add(sel); + add(count); + } + + Gtk::TreeModelColumn<SPFilter*> filter; + Gtk::TreeModelColumn<Glib::ustring> label; + Gtk::TreeModelColumn<int> sel; + Gtk::TreeModelColumn<int> count; + }; + + void on_filter_selection_changed(); + void on_name_edited(const Glib::ustring&, const Glib::ustring&); + bool on_filter_move(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int x, int y, guint /*time*/); + void on_selection_toggled(const Glib::ustring&); + + void update_counts(); + void filter_list_button_release(GdkEventButton*); + void add_filter(); + void remove_filter(); + void duplicate_filter(); + void rename_filter(); + void select_filter_elements(); + + FilterEffectsDialog& _dialog; + Gtk::TreeView _list; + Glib::RefPtr<Gtk::ListStore> _model; + Columns _columns; + Gtk::CellRendererToggle _cell_toggle; + Gtk::Button _add; + Gtk::Menu *_menu; + sigc::signal<void> _signal_filter_changed; + std::unique_ptr<Inkscape::XML::SignalObserver> _observer; + }; + + class PrimitiveColumns : public Gtk::TreeModel::ColumnRecord + { + public: + PrimitiveColumns() + { + add(primitive); + add(type_id); + add(type); + add(id); + } + + Gtk::TreeModelColumn<SPFilterPrimitive*> primitive; + Gtk::TreeModelColumn<Inkscape::Filters::FilterPrimitiveType> type_id; + Gtk::TreeModelColumn<Glib::ustring> type; + Gtk::TreeModelColumn<Glib::ustring> id; + }; + + class CellRendererConnection : public Gtk::CellRenderer + { + public: + CellRendererConnection(); + Glib::PropertyProxy<void*> property_primitive(); + + static const int size = 24; + + protected: + void get_preferred_width_vfunc(Gtk::Widget& widget, + int& minimum_width, + int& natural_width) const override; + + void get_preferred_width_for_height_vfunc(Gtk::Widget& widget, + int height, + int& minimum_width, + int& natural_width) const override; + + void get_preferred_height_vfunc(Gtk::Widget& widget, + int& minimum_height, + int& natural_height) const override; + + void get_preferred_height_for_width_vfunc(Gtk::Widget& widget, + int width, + int& minimum_height, + int& natural_height) const override; + private: + // void* should be SPFilterPrimitive*, some weirdness with properties prevents this + Glib::Property<void*> _primitive; + }; + + class PrimitiveList : public Gtk::TreeView + { + public: + PrimitiveList(FilterEffectsDialog&); + + sigc::signal<void>& signal_primitive_changed(); + + void update(); + void set_menu(Gtk::Widget &parent, + sigc::slot<void> dup, + sigc::slot<void> rem); + + SPFilterPrimitive* get_selected(); + void select(SPFilterPrimitive *prim); + void remove_selected(); + + int primitive_count() const; + int get_input_type_width() const; + + protected: + bool on_draw_signal(const Cairo::RefPtr<Cairo::Context> &cr); + + + bool on_button_press_event(GdkEventButton*) override; + bool on_motion_notify_event(GdkEventMotion*) override; + bool on_button_release_event(GdkEventButton*) override; + void on_drag_end(const Glib::RefPtr<Gdk::DragContext>&) override; + private: + void init_text(); + + void draw_connection_node(const Cairo::RefPtr<Cairo::Context>& cr, + const std::vector<Gdk::Point>& points, + const bool fill); + + bool do_connection_node(const Gtk::TreeIter& row, const int input, std::vector<Gdk::Point>& points, + const int ix, const int iy); + + const Gtk::TreeIter find_result(const Gtk::TreeIter& start, const SPAttr attr, int& src_id, const int pos); + int find_index(const Gtk::TreeIter& target); + void draw_connection(const Cairo::RefPtr<Cairo::Context>& cr, + const Gtk::TreeIter&, const SPAttr attr, const int text_start_x, + const int x1, const int y1, const int row_count, const int pos, + const Gdk::RGBA fg_color, const Gdk::RGBA mid_color); + void sanitize_connections(const Gtk::TreeIter& prim_iter); + void on_primitive_selection_changed(); + bool on_scroll_timeout(); + + FilterEffectsDialog& _dialog; + Glib::RefPtr<Gtk::ListStore> _model; + PrimitiveColumns _columns; + CellRendererConnection _connection_cell; + Gtk::Menu *_primitive_menu; + Glib::RefPtr<Pango::Layout> _vertical_layout; + int _in_drag; + SPFilterPrimitive* _drag_prim; + sigc::signal<void> _signal_primitive_changed; + sigc::connection _scroll_connection; + int _autoscroll_y; + int _autoscroll_x; + std::unique_ptr<Inkscape::XML::SignalObserver> _observer; + int _input_type_width; + int _input_type_height; + }; + + void init_settings_widgets(); + + // Handlers + void add_primitive(); + void remove_primitive(); + void duplicate_primitive(); + void convolve_order_changed(); + void image_x_changed(); + void image_y_changed(); + + void set_attr_direct(const UI::Widget::AttrWidget*); + void set_child_attr_direct(const UI::Widget::AttrWidget*); + void set_filternode_attr(const UI::Widget::AttrWidget*); + void set_attr(SPObject*, const SPAttr, const gchar* val); + void update_settings_view(); + void update_filter_general_settings_view(); + void update_settings_sensitivity(); + void update_color_matrix(); + void update_automatic_region(Gtk::CheckButton *btn); + void update_primitive_infobox(); + + // Primitives Info Box + Gtk::Label _infobox_desc; + Gtk::Image _infobox_icon; + Gtk::ScrolledWindow* _sw_infobox; + + // View/add primitives + Gtk::Paned* _primitive_box; + + UI::Widget::ComboBoxEnum<Inkscape::Filters::FilterPrimitiveType> _add_primitive_type; + Gtk::Button _add_primitive; + + // Bottom pane (filter effect primitive settings) + Gtk::Notebook _settings_tabs; + Gtk::Box _settings_tab2; + Gtk::Box _settings_tab1; + Gtk::Label _empty_settings; + Gtk::Label _no_filter_selected; + bool _settings_initialized; + + class Settings; + class MatrixAttr; + class ColorMatrixValues; + class ComponentTransferValues; + class LightSourceControl; + Settings* _settings; + Settings* _filter_general_settings; + + // General settings + MultiSpinButton *_region_pos, *_region_size; + + // Color Matrix + ColorMatrixValues* _color_matrix_values; + + // Component Transfer + ComponentTransferValues* _component_transfer_values; + + // Convolve Matrix + MatrixAttr* _convolve_matrix; + DualSpinButton* _convolve_order; + MultiSpinButton* _convolve_target; + + // Image + EntryAttr* _image_x; + EntryAttr* _image_y; + + // For controlling setting sensitivity + Gtk::Widget* _k1, *_k2, *_k3, *_k4; + + // To prevent unwanted signals + bool _locked; + bool _attr_lock; + + // These go last since they depend on the prior initialization of + // other FilterEffectsDialog members + FilterModifier _filter_modifier; + PrimitiveList _primitive_list; + + FilterEffectsDialog(FilterEffectsDialog const &d); + FilterEffectsDialog& operator=(FilterEffectsDialog const &d); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_FILTER_EFFECTS_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/ui/dialog/find.cpp b/src/ui/dialog/find.cpp new file mode 100644 index 0000000..a2bc355 --- /dev/null +++ b/src/ui/dialog/find.cpp @@ -0,0 +1,1132 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Johan Engelen <goejendaagh@zonnet.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "find.h" + +#include <gtkmm/entry.h> +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "layer-manager.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "text-editing.h" + +#include "object/sp-defs.h" +#include "object/sp-ellipse.h" +#include "object/sp-flowdiv.h" +#include "object/sp-flowtext.h" +#include "object/sp-image.h" +#include "object/sp-line.h" +#include "object/sp-offset.h" +#include "object/sp-path.h" +#include "object/sp-polyline.h" +#include "object/sp-rect.h" +#include "object/sp-root.h" +#include "object/sp-spiral.h" +#include "object/sp-star.h" +#include "object/sp-text.h" +#include "object/sp-tref.h" +#include "object/sp-tspan.h" +#include "object/sp-use.h" + +#include "ui/icon-names.h" +#include "ui/dialog-events.h" + +#include "xml/attribute-record.h" +#include "xml/node-iterators.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +Find::Find() + : DialogBase("/dialogs/find", "Find"), + + entry_find(_("F_ind:"), _("Find objects by their content or properties (exact or partial match)")), + entry_replace(_("R_eplace:"), _("Replace match with this value")), + + check_scope_all(_("_All")), + check_scope_layer(_("Current _layer")), + check_scope_selection(_("Sele_ction")), + check_searchin_text(_("_Text")), + check_searchin_property(_("_Properties")), + frame_searchin(_("Search in")), + frame_scope(_("Scope")), + + check_case_sensitive(_("Case sensiti_ve")), + check_exact_match(_("E_xact match")), + check_include_hidden(_("Include _hidden")), + check_include_locked(_("Include loc_ked")), + expander_options(_("Options")), + frame_options(_("General")), + + check_ids(_("_ID")), + check_attributename(_("Attribute _name")), + check_attributevalue(_("Attri_bute value")), + check_style(_("_Style")), + check_font(_("F_ont")), + check_desc(_("_Desc")), + check_title(_("Title")), + frame_properties(_("Properties")), + + check_alltypes(_("All types")), + check_rects(_("Rectangles")), + check_ellipses(_("Ellipses")), + check_stars(_("Stars")), + check_spirals(_("Spirals")), + check_paths(_("Paths")), + check_texts(_("Texts")), + check_groups(_("Groups")), + check_clones( + //TRANSLATORS: "Clones" is a noun indicating type of object to find + C_("Find dialog", "Clones")), + + check_images(_("Images")), + check_offsets(_("Offsets")), + frame_types(_("Object types")), + + _left_size_group(Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL)), + _right_size_group(Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL)), + + status(""), + button_find(_("_Find")), + button_replace(_("_Replace All")), + _action_replace(false), + blocked(false), + + hbox_searchin(Gtk::ORIENTATION_HORIZONTAL), + vbox_scope(Gtk::ORIENTATION_VERTICAL), + vbox_searchin(Gtk::ORIENTATION_VERTICAL), + vbox_options1(Gtk::ORIENTATION_VERTICAL), + vbox_options2(Gtk::ORIENTATION_VERTICAL), + hbox_options(Gtk::ORIENTATION_HORIZONTAL), + vbox_expander(Gtk::ORIENTATION_VERTICAL), + hbox_properties(Gtk::ORIENTATION_HORIZONTAL), + vbox_properties1(Gtk::ORIENTATION_VERTICAL), + vbox_properties2(Gtk::ORIENTATION_VERTICAL), + vbox_types1(Gtk::ORIENTATION_VERTICAL), + vbox_types2(Gtk::ORIENTATION_VERTICAL), + hbox_types(Gtk::ORIENTATION_HORIZONTAL), + hboxbutton_row(Gtk::ORIENTATION_HORIZONTAL) + +{ + button_find.set_use_underline(); + button_find.set_tooltip_text(_("Select all objects matching the selection criteria")); + button_replace.set_use_underline(); + button_replace.set_tooltip_text(_("Replace all matches")); + check_scope_all.set_use_underline(); + check_scope_all.set_tooltip_text(_("Search in all layers")); + check_scope_layer.set_use_underline(); + check_scope_layer.set_tooltip_text(_("Limit search to the current layer")); + check_scope_selection.set_use_underline(); + check_scope_selection.set_tooltip_text(_("Limit search to the current selection")); + check_searchin_text.set_use_underline(); + check_searchin_text.set_tooltip_text(_("Search in text objects")); + check_searchin_property.set_use_underline(); + check_searchin_property.set_tooltip_text(_("Search in object properties, styles, attributes and IDs")); + check_case_sensitive.set_use_underline(); + check_case_sensitive.set_tooltip_text(_("Match upper/lower case")); + check_case_sensitive.set_active(false); + check_exact_match.set_use_underline(); + check_exact_match.set_tooltip_text(_("Match whole objects only")); + check_exact_match.set_active(false); + check_include_hidden.set_use_underline(); + check_include_hidden.set_tooltip_text(_("Include hidden objects in search")); + check_include_hidden.set_active(false); + check_include_locked.set_use_underline(); + check_include_locked.set_tooltip_text(_("Include locked objects in search")); + check_include_locked.set_active(false); + check_ids.set_use_underline(); + check_ids.set_tooltip_text(_("Search ID name")); + check_ids.set_active(true); + check_attributename.set_use_underline(); + check_attributename.set_tooltip_text(_("Search attribute name")); + check_attributename.set_active(false); + check_attributevalue.set_use_underline(); + check_attributevalue.set_tooltip_text(_("Search attribute value")); + check_attributevalue.set_active(true); + check_style.set_use_underline(); + check_style.set_tooltip_text(_("Search style")); + check_style.set_active(true); + check_font.set_use_underline(); + check_font.set_tooltip_text(_("Search fonts")); + check_font.set_active(false); + check_desc.set_use_underline(); + check_desc.set_tooltip_text(_("Search description")); + check_desc.set_active(false); + check_title.set_use_underline(); + check_title.set_tooltip_text(_("Search title")); + check_title.set_active(false); + check_alltypes.set_use_underline(); + check_alltypes.set_tooltip_text(_("Search all object types")); + check_alltypes.set_active(true); + check_rects.set_use_underline(); + check_rects.set_tooltip_text(_("Search rectangles")); + check_rects.set_active(false); + check_ellipses.set_use_underline(); + check_ellipses.set_tooltip_text(_("Search ellipses, arcs, circles")); + check_ellipses.set_active(false); + check_stars.set_use_underline(); + check_stars.set_tooltip_text(_("Search stars and polygons")); + check_stars.set_active(false); + check_spirals.set_use_underline(); + check_spirals.set_tooltip_text(_("Search spirals")); + check_spirals.set_active(false); + check_paths.set_use_underline(); + check_paths.set_tooltip_text(_("Search paths, lines, polylines")); + check_paths.set_active(false); + check_texts.set_use_underline(); + check_texts.set_tooltip_text(_("Search text objects")); + check_texts.set_active(false); + check_groups.set_use_underline(); + check_groups.set_tooltip_text(_("Search groups")); + check_groups.set_active(false), + check_clones.set_use_underline(); + check_clones.set_tooltip_text(_("Search clones")); + check_clones.set_active(false); + check_images.set_use_underline(); + check_images.set_tooltip_text(_("Search images")); + check_images.set_active(false); + check_offsets.set_use_underline(); + check_offsets.set_tooltip_text(_("Search offset objects")); + check_offsets.set_active(false); + + entry_find.getEntry()->set_width_chars(25); + entry_find.child_property_fill(*entry_find.getEntry()) = true; + entry_find.child_property_expand(*entry_find.getEntry()) = true; + entry_replace.getEntry()->set_width_chars(25); + entry_replace.child_property_fill(*entry_replace.getEntry()) = true; + entry_replace.child_property_expand(*entry_replace.getEntry()) = true; + + Gtk::RadioButtonGroup grp_searchin = check_searchin_text.get_group(); + check_searchin_property.set_group(grp_searchin); + vbox_searchin.pack_start(check_searchin_text, Gtk::PACK_SHRINK); + vbox_searchin.pack_start(check_searchin_property, Gtk::PACK_SHRINK); + frame_searchin.add(vbox_searchin); + + Gtk::RadioButtonGroup grp_scope = check_scope_all.get_group(); + check_scope_layer.set_group(grp_scope); + check_scope_selection.set_group(grp_scope); + vbox_scope.pack_start(check_scope_all, Gtk::PACK_SHRINK); + vbox_scope.pack_start(check_scope_layer, Gtk::PACK_SHRINK); + vbox_scope.pack_start(check_scope_selection, Gtk::PACK_SHRINK); + hbox_searchin.set_spacing(12); + hbox_searchin.pack_start(frame_searchin, Gtk::PACK_SHRINK); + hbox_searchin.pack_start(frame_scope, Gtk::PACK_SHRINK); + frame_scope.add(vbox_scope); + + vbox_options1.pack_start(check_case_sensitive, Gtk::PACK_SHRINK); + vbox_options1.pack_start(check_include_hidden, Gtk::PACK_SHRINK); + vbox_options2.pack_start(check_exact_match, Gtk::PACK_SHRINK); + vbox_options2.pack_start(check_include_locked, Gtk::PACK_SHRINK); + _left_size_group->add_widget(check_case_sensitive); + _left_size_group->add_widget(check_include_hidden); + _right_size_group->add_widget(check_exact_match); + _right_size_group->add_widget(check_include_locked); + hbox_options.set_spacing(4); + hbox_options.pack_start(vbox_options1, Gtk::PACK_SHRINK); + hbox_options.pack_start(vbox_options2, Gtk::PACK_SHRINK); + frame_options.add(hbox_options); + + vbox_properties1.pack_start(check_ids, Gtk::PACK_SHRINK); + vbox_properties1.pack_start(check_style, Gtk::PACK_SHRINK); + vbox_properties1.pack_start(check_font, Gtk::PACK_SHRINK); + vbox_properties1.pack_start(check_desc, Gtk::PACK_SHRINK); + vbox_properties1.pack_start(check_title, Gtk::PACK_SHRINK); + vbox_properties2.pack_start(check_attributevalue, Gtk::PACK_SHRINK); + vbox_properties2.pack_start(check_attributename, Gtk::PACK_SHRINK); + vbox_properties2.set_valign(Gtk::ALIGN_START); + _left_size_group->add_widget(check_ids); + _left_size_group->add_widget(check_style); + _left_size_group->add_widget(check_font); + _left_size_group->add_widget(check_desc); + _left_size_group->add_widget(check_title); + _right_size_group->add_widget(check_attributevalue); + _right_size_group->add_widget(check_attributename); + hbox_properties.set_spacing(4); + hbox_properties.pack_start(vbox_properties1, Gtk::PACK_SHRINK); + hbox_properties.pack_start(vbox_properties2, Gtk::PACK_SHRINK); + frame_properties.add(hbox_properties); + + vbox_types1.pack_start(check_alltypes, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_paths, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_texts, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_groups, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_clones, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_images, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_offsets, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_rects, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_ellipses, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_stars, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_spirals, Gtk::PACK_SHRINK); + vbox_types2.set_valign(Gtk::ALIGN_END); + _left_size_group->add_widget(check_alltypes); + _left_size_group->add_widget(check_paths); + _left_size_group->add_widget(check_texts); + _left_size_group->add_widget(check_groups); + _left_size_group->add_widget(check_clones); + _left_size_group->add_widget(check_images); + _right_size_group->add_widget(check_offsets); + _right_size_group->add_widget(check_rects); + _right_size_group->add_widget(check_ellipses); + _right_size_group->add_widget(check_stars); + _right_size_group->add_widget(check_spirals); + hbox_types.set_spacing(4); + hbox_types.pack_start(vbox_types1, Gtk::PACK_SHRINK); + hbox_types.pack_start(vbox_types2, Gtk::PACK_SHRINK); + frame_types.add(hbox_types); + + vbox_expander.set_spacing(4); + vbox_expander.pack_start(frame_options, true, true); + vbox_expander.pack_start(frame_properties, true, true); + vbox_expander.pack_start(frame_types, true, true); + + expander_options.set_use_underline(); + expander_options.add(vbox_expander); + + box_buttons.set_layout(Gtk::BUTTONBOX_END); + box_buttons.set_spacing(6); + box_buttons.pack_start(button_find, true, true); + box_buttons.pack_start(button_replace, true, true); + hboxbutton_row.set_spacing(6); + hboxbutton_row.pack_start(status, true, true); + hboxbutton_row.pack_end(box_buttons, true, true); + + set_spacing(6); + pack_start(entry_find, false, false); + pack_start(entry_replace, false, false); + pack_start(hbox_searchin, false, false); + pack_start(expander_options, false, false); + pack_end(hboxbutton_row, false, false); + + checkProperties.push_back(&check_ids); + checkProperties.push_back(&check_style); + checkProperties.push_back(&check_font); + checkProperties.push_back(&check_desc); + checkProperties.push_back(&check_title); + checkProperties.push_back(&check_attributevalue); + checkProperties.push_back(&check_attributename); + + checkTypes.push_back(&check_paths); + checkTypes.push_back(&check_texts); + checkTypes.push_back(&check_groups); + checkTypes.push_back(&check_clones); + checkTypes.push_back(&check_images); + checkTypes.push_back(&check_offsets); + checkTypes.push_back(&check_rects); + checkTypes.push_back(&check_ellipses); + checkTypes.push_back(&check_stars); + checkTypes.push_back(&check_spirals); + + // set signals to handle clicks + expander_options.property_expanded().signal_changed().connect(sigc::mem_fun(*this, &Find::onExpander)); + button_find.signal_clicked().connect(sigc::mem_fun(*this, &Find::onFind)); + button_replace.signal_clicked().connect(sigc::mem_fun(*this, &Find::onReplace)); + check_searchin_text.signal_clicked().connect(sigc::mem_fun(*this, &Find::onSearchinText)); + check_searchin_property.signal_clicked().connect(sigc::mem_fun(*this, &Find::onSearchinProperty)); + check_alltypes.signal_clicked().connect(sigc::mem_fun(*this, &Find::onToggleAlltypes)); + + for (auto & checkProperty : checkProperties) { + checkProperty->signal_clicked().connect(sigc::mem_fun(*this, &Find::onToggleCheck)); + } + + for (auto & checkType : checkTypes) { + checkType->signal_clicked().connect(sigc::mem_fun(*this, &Find::onToggleCheck)); + } + + onSearchinText(); + onToggleAlltypes(); + + show_all_children(); + + button_find.set_can_default(); + //button_find.grab_default(); // activatable by Enter + entry_find.getEntry()->grab_focus(); +} + +void Find::desktopReplaced() +{ + if (auto selection = getSelection()) { + SPItem *item = selection->singleItem(); + if (item && entry_find.getEntry()->get_text_length() == 0) { + Glib::ustring str = sp_te_get_string_multiline(item); + if (!str.empty()) { + entry_find.getEntry()->set_text(str); + } + } + } +} + +void Find::selectionChanged(Selection *selection) +{ + if (!blocked) { + status.set_text(""); + } +} + +/*######################################################################## +# FIND helper functions +########################################################################*/ + +Glib::ustring Find::find_replace(const gchar *str, const gchar *find, const gchar *replace, bool exact, bool casematch, bool replaceall) +{ + Glib::ustring ustr = str; + Glib::ustring ufind = find; + gsize replace_length = Glib::ustring(replace).length(); + if (!casematch) { + ufind = ufind.lowercase(); + } + gsize n = find_strcmp_pos(ustr.c_str(), ufind.c_str(), exact, casematch); + while (n != std::string::npos) { + ustr.replace(n, ufind.length(), replace); + if (!replaceall) { + return ustr; + } + // Start the next search after the last replace character to avoid infinite loops (replace "a" with "aaa" etc) + n = find_strcmp_pos(ustr.c_str(), ufind.c_str(), exact, casematch, n + replace_length); + } + return ustr; +} + +gsize Find::find_strcmp_pos(const gchar *str, const gchar *find, bool exact, bool casematch, gsize start/*=0*/) +{ + Glib::ustring ustr = str ? str : ""; + Glib::ustring ufind = find; + + if (!casematch) { + ustr = ustr.lowercase(); + ufind = ufind.lowercase(); + } + + gsize pos = std::string::npos; + if (exact) { + if (ustr == ufind) { + pos = 0; + } + } else { + pos = ustr.find(ufind, start); + } + + return pos; +} + + +bool Find::find_strcmp(const gchar *str, const gchar *find, bool exact, bool casematch) +{ + return (std::string::npos != find_strcmp_pos(str, find, exact, casematch)); +} + +bool Find::item_desc_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace) +{ + gchar* desc = item->desc(); + bool found = find_strcmp(desc, text, exact, casematch); + if (found && replace) { + Glib::ustring r = find_replace(desc, text, entry_replace.getEntry()->get_text().c_str(), exact, casematch, replace); + item->setDesc(r.c_str()); + } + g_free(desc); + return found; +} + +bool Find::item_title_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace) +{ + gchar* title = item->title(); + bool found = find_strcmp(title, text, exact, casematch); + if (found && replace) { + Glib::ustring r = find_replace(title, text, entry_replace.getEntry()->get_text().c_str(), exact, casematch, replace); + item->setTitle(r.c_str()); + } + g_free(title); + return found; +} + +bool Find::item_text_match (SPItem *item, const gchar *find, bool exact, bool casematch, bool replace/*=false*/) +{ + if (item->getRepr() == nullptr) { + return false; + } + + Glib::ustring item_text = sp_te_get_string_multiline(item); + + if (!item_text.empty()) { + bool found = find_strcmp(item_text.c_str(), find, exact, casematch); + + if (found && replace) { + Glib::ustring ufind = find; + if (!casematch) { + ufind = ufind.lowercase(); + } + + Inkscape::Text::Layout const *layout = te_get_layout (item); + if (!layout) { + return found; + } + + Glib::ustring replace = entry_replace.getEntry()->get_text(); + gsize n = find_strcmp_pos(item_text.c_str(), ufind.c_str(), exact, casematch); + static Inkscape::Text::Layout::iterator _begin_w; + static Inkscape::Text::Layout::iterator _end_w; + while (n != std::string::npos) { + _begin_w = layout->charIndexToIterator(n); + _end_w = layout->charIndexToIterator(n + ufind.length()); + sp_te_replace(item, _begin_w, _end_w, replace.c_str()); + item_text = sp_te_get_string_multiline (item); + n = find_strcmp_pos(item_text.c_str(), ufind.c_str(), exact, casematch, n + replace.length()); + } + } + + return found; + } + return false; +} + + +bool Find::item_id_match (SPItem *item, const gchar *id, bool exact, bool casematch, bool replace/*=false*/) +{ + if (item->getRepr() == nullptr) { + return false; + } + + if (dynamic_cast<SPString *>(item)) { // SPStrings have "on demand" ids which are useless for searching + return false; + } + + const gchar *item_id = item->getRepr()->attribute("id"); + if (item_id == nullptr) { + return false; + } + + bool found = find_strcmp(item_id, id, exact, casematch); + + if (found && replace) { + gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + Glib::ustring new_item_style = find_replace(item_id, id, replace_text , exact, casematch, true); + if (new_item_style != item_id) { + item->setAttribute("id", new_item_style); + } + g_free(replace_text); + } + + return found; +} + +bool Find::item_style_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace/*=false*/) +{ + if (item->getRepr() == nullptr) { + return false; + } + + gchar *item_style = g_strdup(item->getRepr()->attribute("style")); + if (item_style == nullptr) { + return false; + } + + bool found = find_strcmp(item_style, text, exact, casematch); + + if (found && replace) { + gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + Glib::ustring new_item_style = find_replace(item_style, text, replace_text , exact, casematch, true); + if (new_item_style != item_style) { + item->setAttribute("style", new_item_style); + } + g_free(replace_text); + } + + g_free(item_style); + return found; +} + +bool Find::item_attr_match(SPItem *item, const gchar *text, bool exact, bool /*casematch*/, bool replace/*=false*/) +{ + bool found = false; + + if (item->getRepr() == nullptr) { + return false; + } + + gchar *attr_value = g_strdup(item->getRepr()->attribute(text)); + if (exact) { + found = (attr_value != nullptr); + } else { + found = item->getRepr()->matchAttributeName(text); + } + g_free(attr_value); + + // TODO - Rename attribute name ? + if (found && replace) { + found = false; + } + + return found; +} + +bool Find::item_attrvalue_match(SPItem *item, const gchar *text, bool exact, bool casematch, bool replace/*=false*/) +{ + bool ret = false; + + if (item->getRepr() == nullptr) { + return false; + } + + for (const auto & iter:item->getRepr()->attributeList()) { + const gchar* key = g_quark_to_string(iter.key); + gchar *attr_value = g_strdup(item->getRepr()->attribute(key)); + bool found = find_strcmp(attr_value, text, exact, casematch); + if (found) { + ret = true; + } + + if (found && replace) { + gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + Glib::ustring new_item_style = find_replace(attr_value, text, replace_text , exact, casematch, true); + if (new_item_style != attr_value) { + item->setAttribute(key, new_item_style); + } + } + + g_free(attr_value); + } + + return ret; +} + + +bool Find::item_font_match(SPItem *item, const gchar *text, bool exact, bool casematch, bool /*replace*/ /*=false*/) +{ + bool ret = false; + + if (item->getRepr() == nullptr) { + return false; + } + + const gchar *item_style = item->getRepr()->attribute("style"); + if (item_style == nullptr) { + return false; + } + + std::vector<Glib::ustring> vFontTokenNames; + vFontTokenNames.emplace_back("font-family:"); + vFontTokenNames.emplace_back("-inkscape-font-specification:"); + + std::vector<Glib::ustring> vStyleTokens = Glib::Regex::split_simple(";", item_style); + for (auto & vStyleToken : vStyleTokens) { + Glib::ustring token = vStyleToken; + for (const auto & vFontTokenName : vFontTokenNames) { + if ( token.find(vFontTokenName) != std::string::npos) { + Glib::ustring font1 = Glib::ustring(vFontTokenName).append(text); + bool found = find_strcmp(token.c_str(), font1.c_str(), exact, casematch); + if (found) { + ret = true; + if (_action_replace) { + gchar *replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + gchar *orig_str = g_strdup(token.c_str()); + // Exact match fails since the "font-family:" is in the token, since the find was exact it still works with false below + Glib::ustring new_item_style = find_replace(orig_str, text, replace_text , false /*exact*/, casematch, true); + if (new_item_style != orig_str) { + vStyleToken = new_item_style; + } + g_free(orig_str); + g_free(replace_text); + } + } + } + } + } + + if (ret && _action_replace) { + Glib::ustring new_item_style; + for (const auto & vStyleToken : vStyleTokens) { + new_item_style.append(vStyleToken).append(";"); + } + new_item_style.erase(new_item_style.size()-1); + item->setAttribute("style", new_item_style); + } + + return ret; +} + + +std::vector<SPItem*> Find::filter_fields (std::vector<SPItem*> &l, bool exact, bool casematch) +{ + Glib::ustring tmp = entry_find.getEntry()->get_text(); + if (tmp.empty()) { + return l; + } + gchar* text = g_strdup(tmp.c_str()); + + std::vector<SPItem*> in = l; + std::vector<SPItem*> out; + + if (check_searchin_text.get_active()) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_text_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)) { + out.push_back(*i); + if (_action_replace) { + item_text_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + else if (check_searchin_property.get_active()) { + + bool ids = check_ids.get_active(); + bool style = check_style.get_active(); + bool font = check_font.get_active(); + bool desc = check_desc.get_active(); + bool title = check_title.get_active(); + bool attrname = check_attributename.get_active(); + bool attrvalue = check_attributevalue.get_active(); + + if (ids) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + if (item_id_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)) { + out.push_back(*i); + if (_action_replace) { + item_id_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + + if (style) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_style_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)){ + out.push_back(*i); + if (_action_replace) { + item_style_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + + if (attrname) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_attr_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)) { + out.push_back(*i); + if (_action_replace) { + item_attr_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + + if (attrvalue) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_attrvalue_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)) { + out.push_back(*i); + if (_action_replace) { + item_attrvalue_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + + if (font) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_font_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(),*i)) { + out.push_back(*i); + if (_action_replace) { + item_font_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + if (desc) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_desc_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(),*i)) { + out.push_back(*i); + if (_action_replace) { + item_desc_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + if (title) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_title_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(),*i)) { + out.push_back(*i); + if (_action_replace) { + item_title_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + } + + g_free(text); + + return out; +} + + +bool Find::item_type_match (SPItem *item) +{ + bool all =check_alltypes.get_active(); + + if ( dynamic_cast<SPRect *>(item)) { + return ( all ||check_rects.get_active()); + + } else if (dynamic_cast<SPGenericEllipse *>(item)) { + return ( all || check_ellipses.get_active()); + + } else if (dynamic_cast<SPStar *>(item) || dynamic_cast<SPPolygon *>(item)) { + return ( all || check_stars.get_active()); + + } else if (dynamic_cast<SPSpiral *>(item)) { + return ( all || check_spirals.get_active()); + + } else if (dynamic_cast<SPPath *>(item) || dynamic_cast<SPLine *>(item) || dynamic_cast<SPPolyLine *>(item)) { + return (all || check_paths.get_active()); + + } else if (dynamic_cast<SPText *>(item) || dynamic_cast<SPTSpan *>(item) || + dynamic_cast<SPTRef *>(item) || dynamic_cast<SPString *>(item) || + dynamic_cast<SPFlowtext *>(item) || dynamic_cast<SPFlowdiv *>(item) || + dynamic_cast<SPFlowtspan *>(item) || dynamic_cast<SPFlowpara *>(item)) { + return (all || check_texts.get_active()); + + } else if (dynamic_cast<SPGroup *>(item) && + !getDesktop()->layerManager().isLayer(item)) { // never select layers! + return (all || check_groups.get_active()); + + } else if (dynamic_cast<SPUse *>(item)) { + return (all || check_clones.get_active()); + + } else if (dynamic_cast<SPImage *>(item)) { + return (all || check_images.get_active()); + + } else if (dynamic_cast<SPOffset *>(item)) { + return (all || check_offsets.get_active()); + } + + return false; +} + +std::vector<SPItem*> Find::filter_types (std::vector<SPItem*> &l) +{ + std::vector<SPItem*> n; + for (std::vector<SPItem*>::const_reverse_iterator i=l.rbegin(); l.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_type_match(item)) { + n.push_back(*i); + } + } + return n; +} + + +std::vector<SPItem*> &Find::filter_list (std::vector<SPItem*> &l, bool exact, bool casematch) +{ + l = filter_types (l); + l = filter_fields (l, exact, casematch); + return l; +} + +std::vector<SPItem*> &Find::all_items (SPObject *r, std::vector<SPItem*> &l, bool hidden, bool locked) +{ + if (dynamic_cast<SPDefs *>(r)) { + return l; // we're not interested in items in defs + } + + if (!strcmp(r->getRepr()->name(), "svg:metadata")) { + return l; // we're not interested in metadata + } + + auto desktop = getDesktop(); + for (auto& child: r->children) { + SPItem *item = dynamic_cast<SPItem *>(&child); + if (item && !child.cloned && !desktop->layerManager().isLayer(item)) { + if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) { + l.insert(l.begin(),(SPItem*)&child); + } + } + l = all_items (&child, l, hidden, locked); + } + return l; +} + +std::vector<SPItem*> &Find::all_selection_items (Inkscape::Selection *s, std::vector<SPItem*> &l, SPObject *ancestor, bool hidden, bool locked) +{ + auto desktop = getDesktop(); + auto itemlist = s->items(); + for (auto i=boost::rbegin(itemlist); boost::rend(itemlist) != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item && !item->cloned && !desktop->layerManager().isLayer(item)) { + if (!ancestor || ancestor->isAncestorOf(item)) { + if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) { + l.push_back(*i); + } + } + } + if (!ancestor || ancestor->isAncestorOf(item)) { + l = all_items(item, l, hidden, locked); + } + } + return l; +} + + + +/*######################################################################## +# BUTTON CLICK HANDLERS (callbacks) +########################################################################*/ + +void Find::onFind() +{ + _action_replace = false; + onAction(); + + // Return focus to the find entry + entry_find.getEntry()->grab_focus(); +} + +void Find::onReplace() +{ + if (entry_find.getEntry()->get_text().length() < 1) { + status.set_text(_("Nothing to replace")); + return; + } + _action_replace = true; + onAction(); + + // Return focus to the find entry + entry_find.getEntry()->grab_focus(); +} + +void Find::onAction() +{ + auto desktop = getDesktop(); + bool hidden = check_include_hidden.get_active(); + bool locked = check_include_locked.get_active(); + bool exact = check_exact_match.get_active(); + bool casematch = check_case_sensitive.get_active(); + blocked = true; + + std::vector<SPItem*> l; + if (check_scope_selection.get_active()) { + if (check_scope_layer.get_active()) { + l = all_selection_items (desktop->selection, l, desktop->layerManager().currentLayer(), hidden, locked); + } else { + l = all_selection_items (desktop->selection, l, nullptr, hidden, locked); + } + } else { + if (check_scope_layer.get_active()) { + l = all_items (desktop->layerManager().currentLayer(), l, hidden, locked); + } else { + l = all_items(desktop->getDocument()->getRoot(), l, hidden, locked); + } + } + guint all = l.size(); + + std::vector<SPItem*> n = filter_list (l, exact, casematch); + + if (!n.empty()) { + int count = n.size(); + desktop->messageStack()->flashF(Inkscape::NORMAL_MESSAGE, + // TRANSLATORS: "%s" is replaced with "exact" or "partial" when this string is displayed + ngettext("<b>%d</b> object found (out of <b>%d</b>), %s match.", + "<b>%d</b> objects found (out of <b>%d</b>), %s match.", + count), + count, all, exact? _("exact") : _("partial")); + if (_action_replace){ + // TRANSLATORS: "%1" is replaced with the number of matches + status.set_text(Glib::ustring::compose(ngettext("%1 match replaced","%1 matches replaced",count), count)); + } + else { + // TRANSLATORS: "%1" is replaced with the number of matches + status.set_text(Glib::ustring::compose(ngettext("%1 object found","%1 objects found",count), count)); + bool attributenameyok = !check_attributename.get_active(); + button_replace.set_sensitive(attributenameyok); + } + + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + selection->setList(n); + SPObject *obj = n[0]; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + scroll_to_show_item(desktop, item); + + if (_action_replace) { + DocumentUndo::done(desktop->getDocument(), _("Replace text or property"), INKSCAPE_ICON("draw-text")); + } + + } else { + status.set_text(_("Nothing found")); + if (!check_scope_selection.get_active()) { + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + } + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("No objects found")); + } + blocked = false; + +} + +void Find::onToggleCheck () +{ + bool objectok = false; + status.set_text(""); + + if (check_alltypes.get_active()) { + objectok = true; + } + for (auto & checkType : checkTypes) { + if (checkType->get_active()) { + objectok = true; + } + } + + if (!objectok) { + status.set_text(_("Select an object type")); + } + + + bool propertyok = false; + + if (!check_searchin_property.get_active()) { + propertyok = true; + } else { + + for (auto & checkProperty : checkProperties) { + if (checkProperty->get_active()) { + propertyok = true; + } + } + } + + if (!propertyok) { + status.set_text(_("Select a property")); + } + + // Can't replace attribute names + // bool attributenameyok = !check_attributename.get_active(); + + button_find.set_sensitive(objectok && propertyok); + // button_replace.set_sensitive(objectok && propertyok && attributenameyok); + button_replace.set_sensitive(false); +} + +void Find::onToggleAlltypes () +{ + bool all =check_alltypes.get_active(); + for (auto & checkType : checkTypes) { + checkType->set_sensitive(!all); + } + + onToggleCheck(); +} + +void Find::onSearchinText () +{ + searchinToggle(false); + onToggleCheck(); +} + +void Find::onSearchinProperty () +{ + searchinToggle(true); + onToggleCheck(); +} + +void Find::searchinToggle(bool on) +{ + for (auto & checkProperty : checkProperties) { + checkProperty->set_sensitive(on); + } +} + +void Find::onExpander () +{ + if (!expander_options.get_expanded()) + squeeze_window(); +} + +/*######################################################################## +# UTILITY +########################################################################*/ + +void Find::squeeze_window() +{ + // TODO: resize dialog window when the expander is closed + // set_size_request(-1, -1); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/find.h b/src/ui/dialog/find.h new file mode 100644 index 0000000..14ee6b8 --- /dev/null +++ b/src/ui/dialog/find.h @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Find dialog + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_FIND_H +#define INKSCAPE_UI_DIALOG_FIND_H + +#include <gtkmm/box.h> +#include <gtkmm/buttonbox.h> +#include <gtkmm/expander.h> +#include <gtkmm/label.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/sizegroup.h> + +#include "ui/dialog/dialog-base.h" +#include "ui/widget/entry.h" +#include "ui/widget/frame.h" + +class SPItem; +class SPObject; + +namespace Inkscape { +class Selection; + +namespace UI { +namespace Dialog { + +/** + * The Find class defines the Find and replace dialog. + * + * The Find and replace dialog allows you to search within the + * current document for specific text or properties of items. + * Matches items are highlighted and can be replaced as well. + * Scope can be limited to the entire document, current layer or selected items. + * Other options allow searching on specific object types and properties. + */ + +class Find : public DialogBase +{ +public: + Find(); + ~Find() override {}; + + void desktopReplaced() override; + void selectionChanged(Selection *selection) override; + /** + * Helper function which returns a new instance of the dialog. + * getInstance is needed by the dialog manager (Inkscape::UI::Dialog::DialogManager). + */ + static Find &getInstance() { return *new Find(); } + +protected: + + + /** + * Callbacks for pressing the dialog buttons. + */ + void onFind(); + void onReplace(); + void onExpander(); + void onAction(); + void onToggleAlltypes(); + void onToggleCheck(); + void onSearchinText(); + void onSearchinProperty(); + + /** + * Toggle all the properties checkboxes + */ + void searchinToggle(bool on); + + /** + * Returns true if the SPItem 'item' has the same id + * + * @param item the SPItem to check + * @param id the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_id_match (SPItem *item, const gchar *id, bool exact, bool casematch, bool replace=false); + /** + * Returns true if the SPItem 'item' has the same text content + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + * + */ + bool item_text_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false); + /** + * Returns true if the SPItem 'item' has the same text in the style attribute + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_style_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false); + /** + * Returns true if the SPItem 'item' has a <title> or <desc> child that + * matches + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_desc_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false); + bool item_title_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false); + /** + * Returns true if found the SPItem 'item' has the same attribute name + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_attr_match (SPItem *item, const gchar *name, bool exact, bool casematch, bool replace=false); + /** + * Returns true if the SPItem 'item' has the same attribute value + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_attrvalue_match (SPItem *item, const gchar *name, bool exact, bool casematch, bool replace=false); + /** + * Returns true if the SPItem 'item' has the same font values + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_font_match (SPItem *item, const gchar *name, bool exact, bool casematch, bool replace=false); + /** + * Function to filter a list of items based on the item type by calling each item_XXX_match function + */ + std::vector<SPItem*> filter_fields (std::vector<SPItem*> &l, bool exact, bool casematch); + bool item_type_match (SPItem *item); + std::vector<SPItem*> filter_types (std::vector<SPItem*> &l); + std::vector<SPItem*> & filter_list (std::vector<SPItem*> &l, bool exact, bool casematch); + + /** + * Find a string within a string and returns true if found with options for exact and casematching + */ + bool find_strcmp(const gchar *str, const gchar *find, bool exact, bool casematch); + + /** + * Find a string within a string and return the position with options for exact, casematching and search start location + */ + gsize find_strcmp_pos(const gchar *str, const gchar *find, bool exact, bool casematch, gsize start=0); + + /** + * Replace a string with another string with options for exact and casematching and replace once/all + */ + Glib::ustring find_replace(const gchar *str, const gchar *find, const gchar *replace, bool exact, bool casematch, bool replaceall); + + /** + * recursive function to return a list of all the items in the SPObject tree + * + */ + std::vector<SPItem*> & all_items (SPObject *r, std::vector<SPItem*> &l, bool hidden, bool locked); + /** + * to return a list of all the selected items + * + */ + std::vector<SPItem*> & all_selection_items (Inkscape::Selection *s, std::vector<SPItem*> &l, SPObject *ancestor, bool hidden, bool locked); + + /** + * Shrink the dialog size when the expander widget is closed + * Currently not working, no known way to do this + */ + void squeeze_window(); + +private: + Find(Find const &d) = delete; + Find& operator=(Find const &d) = delete; + + /* + * Find and replace combo box widgets + */ + UI::Widget::Entry entry_find; + UI::Widget::Entry entry_replace; + + /** + * Scope and search in widgets + */ + Gtk::RadioButton check_scope_all; + Gtk::RadioButton check_scope_layer; + Gtk::RadioButton check_scope_selection; + Gtk::RadioButton check_searchin_text; + Gtk::RadioButton check_searchin_property; + Gtk::Box hbox_searchin; + Gtk::Box vbox_scope; + Gtk::Box vbox_searchin; + UI::Widget::Frame frame_searchin; + UI::Widget::Frame frame_scope; + + /** + * General option widgets + */ + Gtk::CheckButton check_case_sensitive; + Gtk::CheckButton check_exact_match; + Gtk::CheckButton check_include_hidden; + Gtk::CheckButton check_include_locked; + Gtk::Box vbox_options1; + Gtk::Box vbox_options2; + Gtk::Box hbox_options; + Gtk::Box vbox_expander; + Gtk::Expander expander_options; + UI::Widget::Frame frame_options; + + /** + * Property type widgets + */ + Gtk::CheckButton check_ids; + Gtk::CheckButton check_attributename; + Gtk::CheckButton check_attributevalue; + Gtk::CheckButton check_style; + Gtk::CheckButton check_font; + Gtk::CheckButton check_desc; + Gtk::CheckButton check_title; + Gtk::Box hbox_properties; + Gtk::Box vbox_properties1; + Gtk::Box vbox_properties2; + UI::Widget::Frame frame_properties; + + /** + * A vector of all the properties widgets for easy processing + */ + std::vector<Gtk::CheckButton *> checkProperties; + + /** + * Object type widgets + */ + Gtk::CheckButton check_alltypes; + Gtk::CheckButton check_rects; + Gtk::CheckButton check_ellipses; + Gtk::CheckButton check_stars; + Gtk::CheckButton check_spirals; + Gtk::CheckButton check_paths; + Gtk::CheckButton check_texts; + Gtk::CheckButton check_groups; + Gtk::CheckButton check_clones; + Gtk::CheckButton check_images; + Gtk::CheckButton check_offsets; + Gtk::Box vbox_types1; + Gtk::Box vbox_types2; + Gtk::Box hbox_types; + UI::Widget::Frame frame_types; + + Glib::RefPtr<Gtk::SizeGroup> _left_size_group; + Glib::RefPtr<Gtk::SizeGroup> _right_size_group; + + /** + * A vector of all the check option widgets for easy processing + */ + std::vector<Gtk::CheckButton *> checkTypes; + + //Gtk::Box hbox_text; + + /** + * Action Buttons and status + */ + Gtk::Label status; + Gtk::Button button_find; + Gtk::Button button_replace; + Gtk::ButtonBox box_buttons; + Gtk::Box hboxbutton_row; + + /** + * Finding or replacing + */ + bool _action_replace; + bool blocked; + + sigc::connection selectChangedConn; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_FIND_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/ui/dialog/font-substitution.cpp b/src/ui/dialog/font-substitution.cpp new file mode 100644 index 0000000..bf91259 --- /dev/null +++ b/src/ui/dialog/font-substitution.cpp @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <set> + +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include <gtkmm/messagedialog.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/textview.h> + +#include "font-substitution.h" + +#include "desktop.h" +#include "document.h" +#include "inkscape.h" +#include "selection-chemistry.h" +#include "text-editing.h" + +#include "object/sp-root.h" +#include "object/sp-text.h" +#include "object/sp-textpath.h" +#include "object/sp-flowtext.h" +#include "object/sp-flowdiv.h" +#include "object/sp-tspan.h" + +#include "libnrtype/FontFactory.h" +#include "libnrtype/font-instance.h" + +#include "ui/dialog-events.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +FontSubstitution::FontSubstitution() += default; + +FontSubstitution::~FontSubstitution() += default; + +void +FontSubstitution::checkFontSubstitutions(SPDocument* doc) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int show_dlg = prefs->getInt("/options/font/substitutedlg", 0); + if (show_dlg) { + Glib::ustring out; + std::vector<SPItem*> l = getFontReplacedItems(doc, &out); + if (out.length() > 0) { + show(out, l); + } + } +} + +void +FontSubstitution::show(Glib::ustring out, std::vector<SPItem*> &l) +{ + Gtk::MessageDialog warning(_("\nSome fonts are not available and have been substituted."), + false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK, true); + warning.set_resizable(true); + warning.set_title(_("Font substitution")); + + GtkWidget *dlg = GTK_WIDGET(warning.gobj()); + sp_transientize(dlg); + + Gtk::TextView * textview = new Gtk::TextView(); + textview->set_editable(false); + textview->set_wrap_mode(Gtk::WRAP_WORD); + textview->show(); + textview->get_buffer()->set_text(_(out.c_str())); + + Gtk::ScrolledWindow * scrollwindow = new Gtk::ScrolledWindow(); + scrollwindow->add(*textview); + scrollwindow->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + scrollwindow->set_shadow_type(Gtk::SHADOW_IN); + scrollwindow->set_size_request(0, 100); + scrollwindow->show(); + + Gtk::CheckButton *cbSelect = new Gtk::CheckButton(); + cbSelect->set_label(_("Select all the affected items")); + cbSelect->set_active(true); + cbSelect->show(); + + Gtk::CheckButton *cbWarning = new Gtk::CheckButton(); + cbWarning->set_label(_("Don't show this warning again")); + cbWarning->show(); + + auto box = warning.get_content_area(); + box->set_spacing(2); + box->pack_start(*scrollwindow, true, true, 4); + box->pack_start(*cbSelect, false, false, 0); + box->pack_start(*cbWarning, false, false, 0); + + warning.run(); + + if (cbWarning->get_active()) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/options/font/substitutedlg", 0); + } + + if (cbSelect->get_active()) { + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + selection->setList(l); + } + +} + +/* + * Find all the fonts that are in the document but not available on the users system + * and have been substituted for other fonts + * + * Return a list of SPItems where fonts have been substituted. + * + * Walk through all the objects ... + * a. Build up a list of the objects with fonts defined in the style attribute + * b. Build up a list of the objects rendered fonts - taken for the objects layout/spans + * If there are fonts in a. that are not in b. then those fonts have been substituted. + */ +std::vector<SPItem*> FontSubstitution::getFontReplacedItems(SPDocument* doc, Glib::ustring *out) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + std::vector<SPItem*> allList; + std::vector<SPItem*> outList,x,y; + std::set<Glib::ustring> setErrors; + std::set<Glib::ustring> setFontSpans; + std::map<SPItem *, Glib::ustring> mapFontStyles; + + allList = get_all_items(x, doc->getRoot(), desktop, false, false, true, y); + for(auto item : allList){ + SPStyle *style = item->style; + Glib::ustring family = ""; + + if (is_top_level_text_object (item)) { + // Should only need to check the first span, since the others should be covered by TSPAN's etc + family = te_get_layout(item)->getFontFamily(0); + setFontSpans.insert(family); + } + else if (SP_IS_TEXTPATH(item)) { + SPTextPath const *textpath = SP_TEXTPATH(item); + if (textpath->originalPath != nullptr) { + family = SP_TEXT(item->parent)->layout.getFontFamily(0); + setFontSpans.insert(family); + } + } + else if (SP_IS_TSPAN(item) || SP_IS_FLOWTSPAN(item)) { + // is_part_of_text_subtree (item) + // TSPAN layout comes from the parent->layout->_spans + SPObject *parent_text = item; + while (parent_text && !SP_IS_TEXT(parent_text)) { + parent_text = parent_text->parent; + } + if (parent_text != nullptr) { + family = SP_TEXT(parent_text)->layout.getFontFamily(0); + // Add all the spans fonts to the set + for (unsigned int f=0; f < parent_text->children.size(); f++) { + family = SP_TEXT(parent_text)->layout.getFontFamily(f); + setFontSpans.insert(family); + } + } + } + + if (style) { + gchar const *style_font = nullptr; + if (style->font_family.set) + style_font = style->font_family.value(); + else if (style->font_specification.set) + style_font = style->font_specification.value(); + else + style_font = style->font_family.value(); + + if (style_font) { + if (has_visible_text(item)) { + mapFontStyles.insert(std::make_pair (item, style_font)); + } + } + } + } + + // Check if any document styles are not in the actual layout + std::map<SPItem *, Glib::ustring>::const_reverse_iterator mapIter; + for (mapIter = mapFontStyles.rbegin(); mapIter != mapFontStyles.rend(); ++mapIter) { + SPItem *item = mapIter->first; + Glib::ustring fonts = mapIter->second; + + // CSS font fallbacks can have more that one font listed, split the font list + std::vector<Glib::ustring> vFonts = Glib::Regex::split_simple("," , fonts); + bool fontFound = false; + for(auto font : vFonts) { + // trim whitespace + size_t startpos = font.find_first_not_of(" \n\r\t"); + size_t endpos = font.find_last_not_of(" \n\r\t"); + if(( std::string::npos == startpos ) || ( std::string::npos == endpos)) { + continue; // empty font name + } + font = font.substr( startpos, endpos-startpos+1 ); + std::set<Glib::ustring>::const_iterator iter = setFontSpans.find(font); + if (iter != setFontSpans.end() || + font == Glib::ustring("sans-serif") || + font == Glib::ustring("Sans") || + font == Glib::ustring("serif") || + font == Glib::ustring("Serif") || + font == Glib::ustring("monospace") || + font == Glib::ustring("Monospace")) { + fontFound = true; + break; + } + } + if (fontFound == false) { + Glib::ustring subName = getSubstituteFontName(fonts); + Glib::ustring err = Glib::ustring::compose( + _("Font '%1' substituted with '%2'"), fonts.c_str(), subName.c_str()); + setErrors.insert(err); + outList.push_back(item); + } + } + + std::set<Glib::ustring>::const_iterator setIter; + for (setIter = setErrors.begin(); setIter != setErrors.end(); ++setIter) { + Glib::ustring err = (*setIter); + out->append(err + "\n"); + g_warning("%s", err.c_str()); + } + + return outList; +} + + +Glib::ustring FontSubstitution::getSubstituteFontName (Glib::ustring font) +{ + Glib::ustring out = font; + + PangoFontDescription *descr = pango_font_description_new(); + pango_font_description_set_family(descr,font.c_str()); + font_instance *res = (font_factory::Default())->Face(descr); + if (res->pFont) { + PangoFontDescription *nFaceDesc = pango_font_describe(res->pFont); + out = sp_font_description_get_family(nFaceDesc); + } + pango_font_description_free(descr); + + return out; +} + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/font-substitution.h b/src/ui/dialog/font-substitution.h new file mode 100644 index 0000000..6ab01d3 --- /dev/null +++ b/src/ui/dialog/font-substitution.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief FontSubstitution dialog + */ +/* Authors: + * + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_FONT_SUBSTITUTION_H +#define INKSCAPE_UI_FONT_SUBSTITUTION_H + +#include <glibmm/ustring.h> +#include <vector> + +class SPItem; +class SPDocument; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class FontSubstitution { +public: + FontSubstitution(); + virtual ~FontSubstitution(); + void checkFontSubstitutions(SPDocument* doc); + void show(Glib::ustring out, std::vector<SPItem*> &l); + + static FontSubstitution &getInstance() { return *new FontSubstitution(); } + Glib::ustring getSubstituteFontName (Glib::ustring font); + +protected: + std::vector<SPItem*> getFontReplacedItems(SPDocument* doc, Glib::ustring *out); + +private: + FontSubstitution(FontSubstitution const &d) = delete; + FontSubstitution& operator=(FontSubstitution const &d) = delete; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_FONT_SUBSTITUTION_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/ui/dialog/glyphs.cpp b/src/ui/dialog/glyphs.cpp new file mode 100644 index 0000000..f67059a --- /dev/null +++ b/src/ui/dialog/glyphs.cpp @@ -0,0 +1,783 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Jon A. Cruz + * Abhishek Sharma + * Tavmjong Bah + * + * Copyright (C) 2010 Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> + +#include "glyphs.h" + +#include <glibmm/i18n.h> +#include <glibmm/markup.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/grid.h> +#include <gtkmm/iconview.h> +#include <gtkmm/liststore.h> +#include <gtkmm/scrolledwindow.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" // for SPDocumentUndo::done() +#include "selection.h" +#include "text-editing.h" + +#include "libnrtype/font-instance.h" +#include "libnrtype/font-lister.h" + +#include "object/sp-flowtext.h" +#include "object/sp-text.h" + +#include "ui/icon-names.h" +#include "ui/widget/font-selector.h" +#include "ui/widget/scrollprotected.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +GlyphsPanel &GlyphsPanel::getInstance() +{ + return *new GlyphsPanel(); +} + + +static std::map<GUnicodeScript, Glib::ustring> & getScriptToName() +{ + static bool init = false; + static std::map<GUnicodeScript, Glib::ustring> mappings; + if (!init) { + init = true; + mappings[G_UNICODE_SCRIPT_INVALID_CODE] = _("all"); + mappings[G_UNICODE_SCRIPT_COMMON] = _("common"); + mappings[G_UNICODE_SCRIPT_INHERITED] = _("inherited"); + mappings[G_UNICODE_SCRIPT_ARABIC] = _("Arabic"); + mappings[G_UNICODE_SCRIPT_ARMENIAN] = _("Armenian"); + mappings[G_UNICODE_SCRIPT_BENGALI] = _("Bengali"); + mappings[G_UNICODE_SCRIPT_BOPOMOFO] = _("Bopomofo"); + mappings[G_UNICODE_SCRIPT_CHEROKEE] = _("Cherokee"); + mappings[G_UNICODE_SCRIPT_COPTIC] = _("Coptic"); + mappings[G_UNICODE_SCRIPT_CYRILLIC] = _("Cyrillic"); + mappings[G_UNICODE_SCRIPT_DESERET] = _("Deseret"); + mappings[G_UNICODE_SCRIPT_DEVANAGARI] = _("Devanagari"); + mappings[G_UNICODE_SCRIPT_ETHIOPIC] = _("Ethiopic"); + mappings[G_UNICODE_SCRIPT_GEORGIAN] = _("Georgian"); + mappings[G_UNICODE_SCRIPT_GOTHIC] = _("Gothic"); + mappings[G_UNICODE_SCRIPT_GREEK] = _("Greek"); + mappings[G_UNICODE_SCRIPT_GUJARATI] = _("Gujarati"); + mappings[G_UNICODE_SCRIPT_GURMUKHI] = _("Gurmukhi"); + mappings[G_UNICODE_SCRIPT_HAN] = _("Han"); + mappings[G_UNICODE_SCRIPT_HANGUL] = _("Hangul"); + mappings[G_UNICODE_SCRIPT_HEBREW] = _("Hebrew"); + mappings[G_UNICODE_SCRIPT_HIRAGANA] = _("Hiragana"); + mappings[G_UNICODE_SCRIPT_KANNADA] = _("Kannada"); + mappings[G_UNICODE_SCRIPT_KATAKANA] = _("Katakana"); + mappings[G_UNICODE_SCRIPT_KHMER] = _("Khmer"); + mappings[G_UNICODE_SCRIPT_LAO] = _("Lao"); + mappings[G_UNICODE_SCRIPT_LATIN] = _("Latin"); + mappings[G_UNICODE_SCRIPT_MALAYALAM] = _("Malayalam"); + mappings[G_UNICODE_SCRIPT_MONGOLIAN] = _("Mongolian"); + mappings[G_UNICODE_SCRIPT_MYANMAR] = _("Myanmar"); + mappings[G_UNICODE_SCRIPT_OGHAM] = _("Ogham"); + mappings[G_UNICODE_SCRIPT_OLD_ITALIC] = _("Old Italic"); + mappings[G_UNICODE_SCRIPT_ORIYA] = _("Oriya"); + mappings[G_UNICODE_SCRIPT_RUNIC] = _("Runic"); + mappings[G_UNICODE_SCRIPT_SINHALA] = _("Sinhala"); + mappings[G_UNICODE_SCRIPT_SYRIAC] = _("Syriac"); + mappings[G_UNICODE_SCRIPT_TAMIL] = _("Tamil"); + mappings[G_UNICODE_SCRIPT_TELUGU] = _("Telugu"); + mappings[G_UNICODE_SCRIPT_THAANA] = _("Thaana"); + mappings[G_UNICODE_SCRIPT_THAI] = _("Thai"); + mappings[G_UNICODE_SCRIPT_TIBETAN] = _("Tibetan"); + mappings[G_UNICODE_SCRIPT_CANADIAN_ABORIGINAL] = _("Canadian Aboriginal"); + mappings[G_UNICODE_SCRIPT_YI] = _("Yi"); + mappings[G_UNICODE_SCRIPT_TAGALOG] = _("Tagalog"); + mappings[G_UNICODE_SCRIPT_HANUNOO] = _("Hanunoo"); + mappings[G_UNICODE_SCRIPT_BUHID] = _("Buhid"); + mappings[G_UNICODE_SCRIPT_TAGBANWA] = _("Tagbanwa"); + mappings[G_UNICODE_SCRIPT_BRAILLE] = _("Braille"); + mappings[G_UNICODE_SCRIPT_CYPRIOT] = _("Cypriot"); + mappings[G_UNICODE_SCRIPT_LIMBU] = _("Limbu"); + mappings[G_UNICODE_SCRIPT_OSMANYA] = _("Osmanya"); + mappings[G_UNICODE_SCRIPT_SHAVIAN] = _("Shavian"); + mappings[G_UNICODE_SCRIPT_LINEAR_B] = _("Linear B"); + mappings[G_UNICODE_SCRIPT_TAI_LE] = _("Tai Le"); + mappings[G_UNICODE_SCRIPT_UGARITIC] = _("Ugaritic"); + mappings[G_UNICODE_SCRIPT_NEW_TAI_LUE] = _("New Tai Lue"); + mappings[G_UNICODE_SCRIPT_BUGINESE] = _("Buginese"); + mappings[G_UNICODE_SCRIPT_GLAGOLITIC] = _("Glagolitic"); + mappings[G_UNICODE_SCRIPT_TIFINAGH] = _("Tifinagh"); + mappings[G_UNICODE_SCRIPT_SYLOTI_NAGRI] = _("Syloti Nagri"); + mappings[G_UNICODE_SCRIPT_OLD_PERSIAN] = _("Old Persian"); + mappings[G_UNICODE_SCRIPT_KHAROSHTHI] = _("Kharoshthi"); + mappings[G_UNICODE_SCRIPT_UNKNOWN] = _("unassigned"); + mappings[G_UNICODE_SCRIPT_BALINESE] = _("Balinese"); + mappings[G_UNICODE_SCRIPT_CUNEIFORM] = _("Cuneiform"); + mappings[G_UNICODE_SCRIPT_PHOENICIAN] = _("Phoenician"); + mappings[G_UNICODE_SCRIPT_PHAGS_PA] = _("Phags-pa"); + mappings[G_UNICODE_SCRIPT_NKO] = _("N'Ko"); + mappings[G_UNICODE_SCRIPT_KAYAH_LI] = _("Kayah Li"); + mappings[G_UNICODE_SCRIPT_LEPCHA] = _("Lepcha"); + mappings[G_UNICODE_SCRIPT_REJANG] = _("Rejang"); + mappings[G_UNICODE_SCRIPT_SUNDANESE] = _("Sundanese"); + mappings[G_UNICODE_SCRIPT_SAURASHTRA] = _("Saurashtra"); + mappings[G_UNICODE_SCRIPT_CHAM] = _("Cham"); + mappings[G_UNICODE_SCRIPT_OL_CHIKI] = _("Ol Chiki"); + mappings[G_UNICODE_SCRIPT_VAI] = _("Vai"); + mappings[G_UNICODE_SCRIPT_CARIAN] = _("Carian"); + mappings[G_UNICODE_SCRIPT_LYCIAN] = _("Lycian"); + mappings[G_UNICODE_SCRIPT_LYDIAN] = _("Lydian"); + mappings[G_UNICODE_SCRIPT_AVESTAN] = _("Avestan"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_BAMUM] = _("Bamum"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_EGYPTIAN_HIEROGLYPHS] = _("Egyptian Hieroglpyhs"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_IMPERIAL_ARAMAIC] = _("Imperial Aramaic"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_INSCRIPTIONAL_PAHLAVI]= _("Inscriptional Pahlavi"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_INSCRIPTIONAL_PARTHIAN]= _("Inscriptional Parthian"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_JAVANESE] = _("Javanese"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_KAITHI] = _("Kaithi"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_LISU] = _("Lisu"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_MEETEI_MAYEK] = _("Meetei Mayek"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_OLD_SOUTH_ARABIAN] = _("Old South Arabian"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_OLD_TURKIC] = _("Old Turkic"); // Since: 2.28 + mappings[G_UNICODE_SCRIPT_SAMARITAN] = _("Samaritan"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_TAI_THAM] = _("Tai Tham"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_TAI_VIET] = _("Tai Viet"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_BATAK] = _("Batak"); // Since: 2.28 + mappings[G_UNICODE_SCRIPT_BRAHMI] = _("Brahmi"); // Since: 2.28 + mappings[G_UNICODE_SCRIPT_MANDAIC] = _("Mandaic"); // Since: 2.28 + mappings[G_UNICODE_SCRIPT_CHAKMA] = _("Chakma"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_MEROITIC_CURSIVE] = _("Meroitic Cursive"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_MEROITIC_HIEROGLYPHS] = _("Meroitic Hieroglyphs"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_MIAO] = _("Miao"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_SHARADA] = _("Sharada"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_SORA_SOMPENG] = _("Sora Sompeng"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_TAKRI] = _("Takri"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_BASSA_VAH] = _("Bassa"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_CAUCASIAN_ALBANIAN] = _("Caucasian Albanian"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_DUPLOYAN] = _("Duployan"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_ELBASAN] = _("Elbasan"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_GRANTHA] = _("Grantha"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_KHOJKI] = _("Khojki"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_KHUDAWADI] = _("Khudawadi, Sindhi"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_LINEAR_A] = _("Linear A"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MAHAJANI] = _("Mahajani"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MANICHAEAN] = _("Manichaean"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MENDE_KIKAKUI] = _("Mende Kikakui"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MODI] = _("Modi"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MRO] = _("Mro"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_NABATAEAN] = _("Nabataean"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_OLD_NORTH_ARABIAN] = _("Old North Arabian"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_OLD_PERMIC] = _("Old Permic"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_PAHAWH_HMONG] = _("Pahawh Hmong"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_PALMYRENE] = _("Palmyrene"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_PAU_CIN_HAU] = _("Pau Cin Hau"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_PSALTER_PAHLAVI] = _("Psalter Pahlavi"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_SIDDHAM] = _("Siddham"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_TIRHUTA] = _("Tirhuta"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_WARANG_CITI] = _("Warang Citi"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_AHOM] = _("Ahom"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_ANATOLIAN_HIEROGLYPHS]= _("Anatolian Hieroglyphs"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_HATRAN] = _("Hatran"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_MULTANI] = _("Multani"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_OLD_HUNGARIAN] = _("Old Hungarian"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_SIGNWRITING] = _("Signwriting"); // Since: 2.48 +/* + mappings[G_UNICODE_SCRIPT_ADLAM] = _("Adlam"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_BHAIKSUKI] = _("Bhaiksuki"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_MARCHEN] = _("Marchen"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_NEWA] = _("Newa"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_OSAGE] = _("Osage"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_TANGUT] = _("Tangut"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_MASARAM_GONDI] = _("Masaram Gondi"); // Since: 2.54 + mappings[G_UNICODE_SCRIPT_NUSHU] = _("Nushu"); // Since: 2.54 + mappings[G_UNICODE_SCRIPT_SOYOMBO] = _("Soyombo"); // Since: 2.54 + mappings[G_UNICODE_SCRIPT_ZANABAZAR_SQUARE] = _("Zanabazar Square"); // Since: 2.54 + mappings[G_UNICODE_SCRIPT_DOGRA] = _("Dogra"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_GUNJALA_GONDI] = _("Gunjala Gondi"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_HANIFI_ROHINGYA] = _("Hanifi Rohingya"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_MAKASAR] = _("Makasar"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_MEDEFAIDRIN] = _("Medefaidrin"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_OLD_SOGDIAN] = _("Old Sogdian"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_SOGDIAN] = _("Sogdian"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_ELYMAIC] = _("Elym"); // Since: 2.62 + mappings[G_UNICODE_SCRIPT_NANDINAGARI] = _("Nand"); // Since: 2.62 + mappings[G_UNICODE_SCRIPT_NYIAKENG_PUACHUE_HMONG]= _("Rohg"); // Since: 2.62 + mappings[G_UNICODE_SCRIPT_WANCHO] = _("Wcho"); // Since: 2.62 +*/ + } + return mappings; +} + +typedef std::pair<gunichar, gunichar> Range; +typedef std::pair<Range, Glib::ustring> NamedRange; + +static std::vector<NamedRange> & getRanges() +{ + static bool init = false; + static std::vector<NamedRange> ranges; + if (!init) { + init = true; + ranges.emplace_back(std::make_pair(0x00000, 0x2FFFF), _("all")); + ranges.emplace_back(std::make_pair(0x00000, 0x0FFFF), _("Basic Plane")); + ranges.emplace_back(std::make_pair(0x10000, 0x1FFFF), _("Extended Multilingual Plane")); + ranges.emplace_back(std::make_pair(0x20000, 0x2FFFF), _("Supplementary Ideographic Plane")); + + ranges.emplace_back(std::make_pair(0x0000, 0x007F), _("Basic Latin")); + ranges.emplace_back(std::make_pair(0x0080, 0x00FF), _("Latin-1 Supplement")); + ranges.emplace_back(std::make_pair(0x0100, 0x017F), _("Latin Extended-A")); + ranges.emplace_back(std::make_pair(0x0180, 0x024F), _("Latin Extended-B")); + ranges.emplace_back(std::make_pair(0x0250, 0x02AF), _("IPA Extensions")); + ranges.emplace_back(std::make_pair(0x02B0, 0x02FF), _("Spacing Modifier Letters")); + ranges.emplace_back(std::make_pair(0x0300, 0x036F), _("Combining Diacritical Marks")); + ranges.emplace_back(std::make_pair(0x0370, 0x03FF), _("Greek and Coptic")); + ranges.emplace_back(std::make_pair(0x0400, 0x04FF), _("Cyrillic")); + ranges.emplace_back(std::make_pair(0x0500, 0x052F), _("Cyrillic Supplement")); + ranges.emplace_back(std::make_pair(0x0530, 0x058F), _("Armenian")); + ranges.emplace_back(std::make_pair(0x0590, 0x05FF), _("Hebrew")); + ranges.emplace_back(std::make_pair(0x0600, 0x06FF), _("Arabic")); + ranges.emplace_back(std::make_pair(0x0700, 0x074F), _("Syriac")); + ranges.emplace_back(std::make_pair(0x0750, 0x077F), _("Arabic Supplement")); + ranges.emplace_back(std::make_pair(0x0780, 0x07BF), _("Thaana")); + ranges.emplace_back(std::make_pair(0x07C0, 0x07FF), _("NKo")); + ranges.emplace_back(std::make_pair(0x0800, 0x083F), _("Samaritan")); + ranges.emplace_back(std::make_pair(0x0900, 0x097F), _("Devanagari")); + ranges.emplace_back(std::make_pair(0x0980, 0x09FF), _("Bengali")); + ranges.emplace_back(std::make_pair(0x0A00, 0x0A7F), _("Gurmukhi")); + ranges.emplace_back(std::make_pair(0x0A80, 0x0AFF), _("Gujarati")); + ranges.emplace_back(std::make_pair(0x0B00, 0x0B7F), _("Oriya")); + ranges.emplace_back(std::make_pair(0x0B80, 0x0BFF), _("Tamil")); + ranges.emplace_back(std::make_pair(0x0C00, 0x0C7F), _("Telugu")); + ranges.emplace_back(std::make_pair(0x0C80, 0x0CFF), _("Kannada")); + ranges.emplace_back(std::make_pair(0x0D00, 0x0D7F), _("Malayalam")); + ranges.emplace_back(std::make_pair(0x0D80, 0x0DFF), _("Sinhala")); + ranges.emplace_back(std::make_pair(0x0E00, 0x0E7F), _("Thai")); + ranges.emplace_back(std::make_pair(0x0E80, 0x0EFF), _("Lao")); + ranges.emplace_back(std::make_pair(0x0F00, 0x0FFF), _("Tibetan")); + ranges.emplace_back(std::make_pair(0x1000, 0x109F), _("Myanmar")); + ranges.emplace_back(std::make_pair(0x10A0, 0x10FF), _("Georgian")); + ranges.emplace_back(std::make_pair(0x1100, 0x11FF), _("Hangul Jamo")); + ranges.emplace_back(std::make_pair(0x1200, 0x137F), _("Ethiopic")); + ranges.emplace_back(std::make_pair(0x1380, 0x139F), _("Ethiopic Supplement")); + ranges.emplace_back(std::make_pair(0x13A0, 0x13FF), _("Cherokee")); + ranges.emplace_back(std::make_pair(0x1400, 0x167F), _("Unified Canadian Aboriginal Syllabics")); + ranges.emplace_back(std::make_pair(0x1680, 0x169F), _("Ogham")); + ranges.emplace_back(std::make_pair(0x16A0, 0x16FF), _("Runic")); + ranges.emplace_back(std::make_pair(0x1700, 0x171F), _("Tagalog")); + ranges.emplace_back(std::make_pair(0x1720, 0x173F), _("Hanunoo")); + ranges.emplace_back(std::make_pair(0x1740, 0x175F), _("Buhid")); + ranges.emplace_back(std::make_pair(0x1760, 0x177F), _("Tagbanwa")); + ranges.emplace_back(std::make_pair(0x1780, 0x17FF), _("Khmer")); + ranges.emplace_back(std::make_pair(0x1800, 0x18AF), _("Mongolian")); + ranges.emplace_back(std::make_pair(0x18B0, 0x18FF), _("Unified Canadian Aboriginal Syllabics Extended")); + ranges.emplace_back(std::make_pair(0x1900, 0x194F), _("Limbu")); + ranges.emplace_back(std::make_pair(0x1950, 0x197F), _("Tai Le")); + ranges.emplace_back(std::make_pair(0x1980, 0x19DF), _("New Tai Lue")); + ranges.emplace_back(std::make_pair(0x19E0, 0x19FF), _("Khmer Symbols")); + ranges.emplace_back(std::make_pair(0x1A00, 0x1A1F), _("Buginese")); + ranges.emplace_back(std::make_pair(0x1A20, 0x1AAF), _("Tai Tham")); + ranges.emplace_back(std::make_pair(0x1B00, 0x1B7F), _("Balinese")); + ranges.emplace_back(std::make_pair(0x1B80, 0x1BBF), _("Sundanese")); + ranges.emplace_back(std::make_pair(0x1C00, 0x1C4F), _("Lepcha")); + ranges.emplace_back(std::make_pair(0x1C50, 0x1C7F), _("Ol Chiki")); + ranges.emplace_back(std::make_pair(0x1CD0, 0x1CFF), _("Vedic Extensions")); + ranges.emplace_back(std::make_pair(0x1D00, 0x1D7F), _("Phonetic Extensions")); + ranges.emplace_back(std::make_pair(0x1D80, 0x1DBF), _("Phonetic Extensions Supplement")); + ranges.emplace_back(std::make_pair(0x1DC0, 0x1DFF), _("Combining Diacritical Marks Supplement")); + ranges.emplace_back(std::make_pair(0x1E00, 0x1EFF), _("Latin Extended Additional")); + ranges.emplace_back(std::make_pair(0x1F00, 0x1FFF), _("Greek Extended")); + ranges.emplace_back(std::make_pair(0x2000, 0x206F), _("General Punctuation")); + ranges.emplace_back(std::make_pair(0x2070, 0x209F), _("Superscripts and Subscripts")); + ranges.emplace_back(std::make_pair(0x20A0, 0x20CF), _("Currency Symbols")); + ranges.emplace_back(std::make_pair(0x20D0, 0x20FF), _("Combining Diacritical Marks for Symbols")); + ranges.emplace_back(std::make_pair(0x2100, 0x214F), _("Letterlike Symbols")); + ranges.emplace_back(std::make_pair(0x2150, 0x218F), _("Number Forms")); + ranges.emplace_back(std::make_pair(0x2190, 0x21FF), _("Arrows")); + ranges.emplace_back(std::make_pair(0x2200, 0x22FF), _("Mathematical Operators")); + ranges.emplace_back(std::make_pair(0x2300, 0x23FF), _("Miscellaneous Technical")); + ranges.emplace_back(std::make_pair(0x2400, 0x243F), _("Control Pictures")); + ranges.emplace_back(std::make_pair(0x2440, 0x245F), _("Optical Character Recognition")); + ranges.emplace_back(std::make_pair(0x2460, 0x24FF), _("Enclosed Alphanumerics")); + ranges.emplace_back(std::make_pair(0x2500, 0x257F), _("Box Drawing")); + ranges.emplace_back(std::make_pair(0x2580, 0x259F), _("Block Elements")); + ranges.emplace_back(std::make_pair(0x25A0, 0x25FF), _("Geometric Shapes")); + ranges.emplace_back(std::make_pair(0x2600, 0x26FF), _("Miscellaneous Symbols")); + ranges.emplace_back(std::make_pair(0x2700, 0x27BF), _("Dingbats")); + ranges.emplace_back(std::make_pair(0x27C0, 0x27EF), _("Miscellaneous Mathematical Symbols-A")); + ranges.emplace_back(std::make_pair(0x27F0, 0x27FF), _("Supplemental Arrows-A")); + ranges.emplace_back(std::make_pair(0x2800, 0x28FF), _("Braille Patterns")); + ranges.emplace_back(std::make_pair(0x2900, 0x297F), _("Supplemental Arrows-B")); + ranges.emplace_back(std::make_pair(0x2980, 0x29FF), _("Miscellaneous Mathematical Symbols-B")); + ranges.emplace_back(std::make_pair(0x2A00, 0x2AFF), _("Supplemental Mathematical Operators")); + ranges.emplace_back(std::make_pair(0x2B00, 0x2BFF), _("Miscellaneous Symbols and Arrows")); + ranges.emplace_back(std::make_pair(0x2C00, 0x2C5F), _("Glagolitic")); + ranges.emplace_back(std::make_pair(0x2C60, 0x2C7F), _("Latin Extended-C")); + ranges.emplace_back(std::make_pair(0x2C80, 0x2CFF), _("Coptic")); + ranges.emplace_back(std::make_pair(0x2D00, 0x2D2F), _("Georgian Supplement")); + ranges.emplace_back(std::make_pair(0x2D30, 0x2D7F), _("Tifinagh")); + ranges.emplace_back(std::make_pair(0x2D80, 0x2DDF), _("Ethiopic Extended")); + ranges.emplace_back(std::make_pair(0x2DE0, 0x2DFF), _("Cyrillic Extended-A")); + ranges.emplace_back(std::make_pair(0x2E00, 0x2E7F), _("Supplemental Punctuation")); + ranges.emplace_back(std::make_pair(0x2E80, 0x2EFF), _("CJK Radicals Supplement")); + ranges.emplace_back(std::make_pair(0x2F00, 0x2FDF), _("Kangxi Radicals")); + ranges.emplace_back(std::make_pair(0x2FF0, 0x2FFF), _("Ideographic Description Characters")); + ranges.emplace_back(std::make_pair(0x3000, 0x303F), _("CJK Symbols and Punctuation")); + ranges.emplace_back(std::make_pair(0x3040, 0x309F), _("Hiragana")); + ranges.emplace_back(std::make_pair(0x30A0, 0x30FF), _("Katakana")); + ranges.emplace_back(std::make_pair(0x3100, 0x312F), _("Bopomofo")); + ranges.emplace_back(std::make_pair(0x3130, 0x318F), _("Hangul Compatibility Jamo")); + ranges.emplace_back(std::make_pair(0x3190, 0x319F), _("Kanbun")); + ranges.emplace_back(std::make_pair(0x31A0, 0x31BF), _("Bopomofo Extended")); + ranges.emplace_back(std::make_pair(0x31C0, 0x31EF), _("CJK Strokes")); + ranges.emplace_back(std::make_pair(0x31F0, 0x31FF), _("Katakana Phonetic Extensions")); + ranges.emplace_back(std::make_pair(0x3200, 0x32FF), _("Enclosed CJK Letters and Months")); + ranges.emplace_back(std::make_pair(0x3300, 0x33FF), _("CJK Compatibility")); + ranges.emplace_back(std::make_pair(0x3400, 0x4DBF), _("CJK Unified Ideographs Extension A")); + ranges.emplace_back(std::make_pair(0x4DC0, 0x4DFF), _("Yijing Hexagram Symbols")); + ranges.emplace_back(std::make_pair(0x4E00, 0x9FFF), _("CJK Unified Ideographs")); + ranges.emplace_back(std::make_pair(0xA000, 0xA48F), _("Yi Syllables")); + ranges.emplace_back(std::make_pair(0xA490, 0xA4CF), _("Yi Radicals")); + ranges.emplace_back(std::make_pair(0xA4D0, 0xA4FF), _("Lisu")); + ranges.emplace_back(std::make_pair(0xA500, 0xA63F), _("Vai")); + ranges.emplace_back(std::make_pair(0xA640, 0xA69F), _("Cyrillic Extended-B")); + ranges.emplace_back(std::make_pair(0xA6A0, 0xA6FF), _("Bamum")); + ranges.emplace_back(std::make_pair(0xA700, 0xA71F), _("Modifier Tone Letters")); + ranges.emplace_back(std::make_pair(0xA720, 0xA7FF), _("Latin Extended-D")); + ranges.emplace_back(std::make_pair(0xA800, 0xA82F), _("Syloti Nagri")); + ranges.emplace_back(std::make_pair(0xA830, 0xA83F), _("Common Indic Number Forms")); + ranges.emplace_back(std::make_pair(0xA840, 0xA87F), _("Phags-pa")); + ranges.emplace_back(std::make_pair(0xA880, 0xA8DF), _("Saurashtra")); + ranges.emplace_back(std::make_pair(0xA8E0, 0xA8FF), _("Devanagari Extended")); + ranges.emplace_back(std::make_pair(0xA900, 0xA92F), _("Kayah Li")); + ranges.emplace_back(std::make_pair(0xA930, 0xA95F), _("Rejang")); + ranges.emplace_back(std::make_pair(0xA960, 0xA97F), _("Hangul Jamo Extended-A")); + ranges.emplace_back(std::make_pair(0xA980, 0xA9DF), _("Javanese")); + ranges.emplace_back(std::make_pair(0xAA00, 0xAA5F), _("Cham")); + ranges.emplace_back(std::make_pair(0xAA60, 0xAA7F), _("Myanmar Extended-A")); + ranges.emplace_back(std::make_pair(0xAA80, 0xAADF), _("Tai Viet")); + ranges.emplace_back(std::make_pair(0xABC0, 0xABFF), _("Meetei Mayek")); + ranges.emplace_back(std::make_pair(0xAC00, 0xD7AF), _("Hangul Syllables")); + ranges.emplace_back(std::make_pair(0xD7B0, 0xD7FF), _("Hangul Jamo Extended-B")); + ranges.emplace_back(std::make_pair(0xD800, 0xDB7F), _("High Surrogates")); + ranges.emplace_back(std::make_pair(0xDB80, 0xDBFF), _("High Private Use Surrogates")); + ranges.emplace_back(std::make_pair(0xDC00, 0xDFFF), _("Low Surrogates")); + ranges.emplace_back(std::make_pair(0xE000, 0xF8FF), _("Private Use Area")); + ranges.emplace_back(std::make_pair(0xF900, 0xFAFF), _("CJK Compatibility Ideographs")); + ranges.emplace_back(std::make_pair(0xFB00, 0xFB4F), _("Alphabetic Presentation Forms")); + ranges.emplace_back(std::make_pair(0xFB50, 0xFDFF), _("Arabic Presentation Forms-A")); + ranges.emplace_back(std::make_pair(0xFE00, 0xFE0F), _("Variation Selectors")); + ranges.emplace_back(std::make_pair(0xFE10, 0xFE1F), _("Vertical Forms")); + ranges.emplace_back(std::make_pair(0xFE20, 0xFE2F), _("Combining Half Marks")); + ranges.emplace_back(std::make_pair(0xFE30, 0xFE4F), _("CJK Compatibility Forms")); + ranges.emplace_back(std::make_pair(0xFE50, 0xFE6F), _("Small Form Variants")); + ranges.emplace_back(std::make_pair(0xFE70, 0xFEFF), _("Arabic Presentation Forms-B")); + ranges.emplace_back(std::make_pair(0xFF00, 0xFFEF), _("Halfwidth and Fullwidth Forms")); + ranges.emplace_back(std::make_pair(0xFFF0, 0xFFFF), _("Specials")); + + // Selected ranges in Extended Multilingual Plane + ranges.emplace_back(std::make_pair(0x1F300, 0x1F5FF), _("Miscellaneous Symbols and Pictographs")); + ranges.emplace_back(std::make_pair(0x1F600, 0x1F64F), _("Emoticons")); + ranges.emplace_back(std::make_pair(0x1F650, 0x1F67F), _("Ornamental Dingbats")); + ranges.emplace_back(std::make_pair(0x1F680, 0x1F6FF), _("Transport and Map Symbols")); + ranges.emplace_back(std::make_pair(0x1F700, 0x1F77F), _("Alchemical Symbols")); + ranges.emplace_back(std::make_pair(0x1F780, 0x1F7FF), _("Geometric Shapes Extended")); + ranges.emplace_back(std::make_pair(0x1F800, 0x1F8FF), _("Supplemental Arrows-C")); + ranges.emplace_back(std::make_pair(0x1F900, 0x1F9FF), _("Supplemental Symbols and Pictographs")); + ranges.emplace_back(std::make_pair(0x1FA00, 0x1FA7F), _("Chess Symbols")); + ranges.emplace_back(std::make_pair(0x1FA80, 0x1FAFF), _("Symbols and Pictographs Extended-A")); + + } + + return ranges; +} + +class GlyphColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + Gtk::TreeModelColumn<gunichar> code; + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<Glib::ustring> tooltip; + + GlyphColumns() + { + add(code); + add(name); + add(tooltip); + } +}; + +GlyphColumns *GlyphsPanel::getColumns() +{ + static GlyphColumns *columns = new GlyphColumns(); + + return columns; +} + +/** + * Constructor + */ +GlyphsPanel::GlyphsPanel() + : DialogBase("/dialogs/glyphs", "Glyphs") + , store(Gtk::ListStore::create(*getColumns())) + , instanceConns() +{ + set_orientation(Gtk::ORIENTATION_VERTICAL); + auto table = new Gtk::Grid(); + table->set_row_spacing(4); + table->set_column_spacing(4); + pack_start(*Gtk::manage(table), Gtk::PACK_EXPAND_WIDGET); + guint row = 0; + +// ------------------------------- + + { + fontSelector = new Inkscape::UI::Widget::FontSelector (false, false); + fontSelector->set_name ("UnicodeCharacters"); + + sigc::connection conn = + fontSelector->connectChanged(sigc::hide(sigc::mem_fun(*this, &GlyphsPanel::rebuild))); + instanceConns.push_back(conn); + + table->attach(*Gtk::manage(fontSelector), 0, row, 3, 1); + row++; + } + +// ------------------------------- + + { + auto label = new Gtk::Label(_("Script: ")); + + table->attach( *Gtk::manage(label), 0, row, 1, 1); + + scriptCombo = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>()); + for (auto & it : getScriptToName()) + { + scriptCombo->append(it.second); + } + + scriptCombo->set_active_text(getScriptToName()[G_UNICODE_SCRIPT_INVALID_CODE]); + sigc::connection conn = scriptCombo->signal_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::rebuild)); + instanceConns.push_back(conn); + + scriptCombo->set_halign(Gtk::ALIGN_START); + scriptCombo->set_valign(Gtk::ALIGN_START); + scriptCombo->set_hexpand(); + table->attach(*scriptCombo, 1, row, 1, 1); + } + + row++; + +// ------------------------------- + + { + auto label = new Gtk::Label(_("Range: ")); + table->attach( *Gtk::manage(label), 0, row, 1, 1); + + rangeCombo = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>()); + for (auto & it : getRanges()) { + rangeCombo->append(it.second); + } + + rangeCombo->set_active_text(getRanges()[4].second); + sigc::connection conn = rangeCombo->signal_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::rebuild)); + instanceConns.push_back(conn); + + rangeCombo->set_halign(Gtk::ALIGN_START); + rangeCombo->set_valign(Gtk::ALIGN_START); + rangeCombo->set_hexpand(); + table->attach(*rangeCombo, 1, row, 1, 1); + } + + row++; + +// ------------------------------- + + GlyphColumns *columns = getColumns(); + + iconView = new Gtk::IconView(static_cast<Glib::RefPtr<Gtk::TreeModel> >(store)); + iconView->set_name("UnicodeIconView"); + iconView->set_markup_column(columns->name); + iconView->set_tooltip_column(2); // Uses Pango markup, must use column number. + iconView->set_margin(0); + iconView->set_item_padding(0); + iconView->set_row_spacing(0); + iconView->set_column_spacing(0); + + sigc::connection conn; + conn = iconView->signal_item_activated().connect(sigc::mem_fun(*this, &GlyphsPanel::glyphActivated)); + instanceConns.push_back(conn); + conn = iconView->signal_selection_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::glyphSelectionChanged)); + instanceConns.push_back(conn); + + + Gtk::ScrolledWindow *scroller = new Gtk::ScrolledWindow(); + scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS); + scroller->add(*Gtk::manage(iconView)); + scroller->set_hexpand(); + scroller->set_vexpand(); + table->attach(*Gtk::manage(scroller), 0, row, 3, 1); + + row++; + +// ------------------------------- + + Gtk::Box *box = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL); + + entry = std::make_shared<Gtk::Entry>(); + conn = entry->signal_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::calcCanInsert)); + instanceConns.push_back(conn); + entry->set_width_chars(18); + box->pack_start(*entry.get(), Gtk::PACK_SHRINK); + + Gtk::Label *pad = new Gtk::Label(" "); + box->pack_start(*Gtk::manage(pad), Gtk::PACK_SHRINK); + + label = std::make_shared<Gtk::Label>(" "); + box->pack_start(*label.get(), Gtk::PACK_SHRINK); + + pad = new Gtk::Label(""); + box->pack_start(*Gtk::manage(pad), Gtk::PACK_EXPAND_WIDGET); + + insertBtn = std::make_shared<Gtk::Button>(_("Append")); + conn = insertBtn->signal_clicked().connect(sigc::mem_fun(*this, &GlyphsPanel::insertText)); + instanceConns.push_back(conn); + insertBtn->set_can_default(); + insertBtn->set_sensitive(false); + + box->pack_end(*insertBtn.get(), Gtk::PACK_SHRINK); + box->set_hexpand(); + table->attach( *Gtk::manage(box), 0, row, 3, 1); + + row++; + +// ------------------------------- + + + show_all_children(); +} + +GlyphsPanel::~GlyphsPanel() +{ + for (auto & instanceConn : instanceConns) { + instanceConn.disconnect(); + } + instanceConns.clear(); +} + +void GlyphsPanel::selectionChanged(Selection *selection) +{ + readSelection(true, true); +} +void GlyphsPanel::selectionModified(Selection *selection, guint flags) +{ + bool style = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG )) != 0 ); + + bool content = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_TEXT_CONTENT_MODIFIED_FLAG )) != 0 ); + + readSelection(style, content); +} + +// Append selected glyphs to selected text +void GlyphsPanel::insertText() +{ + auto selection = getSelection(); + if (!selection) + return; + + SPItem *textItem = nullptr; + auto itemlist = selection->items(); + for(auto i=itemlist.begin(); itemlist.end() != i; ++i) { + if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) { + textItem = *i; + break; + } + } + + if (textItem) { + Glib::ustring glyphs; + if (entry->get_text_length() > 0) { + glyphs = entry->get_text(); + } else { + auto itemArray = iconView->get_selected_items(); + + if (!itemArray.empty()) { + Gtk::TreeModel::Path const & path = *itemArray.begin(); + Gtk::ListStore::iterator row = store->get_iter(path); + gunichar ch = (*row)[getColumns()->code]; + glyphs = ch; + } + } + + if (!glyphs.empty()) { + Glib::ustring combined = sp_te_get_string_multiline(textItem); + combined += glyphs; + sp_te_set_repr_text_multiline(textItem, combined.c_str()); + DocumentUndo::done(getDocument(), _("Append text"), INKSCAPE_ICON("draw-text")); + } + } +} + +void GlyphsPanel::glyphActivated(Gtk::TreeModel::Path const & path) +{ + Gtk::ListStore::iterator row = store->get_iter(path); + gunichar ch = (*row)[getColumns()->code]; + Glib::ustring tmp; + tmp += ch; + + int startPos = 0; + int endPos = 0; + if (entry->get_selection_bounds(startPos, endPos)) { + // there was something selected. + entry->delete_text(startPos, endPos); + } + startPos = entry->get_position(); + entry->insert_text(tmp, -1, startPos); + entry->set_position(startPos); +} + +void GlyphsPanel::glyphSelectionChanged() +{ + auto itemArray = iconView->get_selected_items(); + + if (itemArray.empty()) { + label->set_text(" "); + } else { + Gtk::TreeModel::Path const & path = *itemArray.begin(); + Gtk::ListStore::iterator row = store->get_iter(path); + gunichar ch = (*row)[getColumns()->code]; + + + Glib::ustring scriptName; + GUnicodeScript script = g_unichar_get_script(ch); + std::map<GUnicodeScript, Glib::ustring> mappings = getScriptToName(); + if (mappings.find(script) != mappings.end()) { + scriptName = mappings[script]; + } + gchar * tmp = g_strdup_printf("U+%04X %s", ch, scriptName.c_str()); + label->set_text(tmp); + } + calcCanInsert(); +} + +void GlyphsPanel::calcCanInsert() +{ + auto selection = getSelection(); + if (!selection) + return; + + int items = 0; + auto itemlist = selection->items(); + for(auto i=itemlist.begin(); itemlist.end() != i; ++i) { + if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) { + ++items; + } + } + + bool enable = (items == 1); + if (enable) { + enable &= (!iconView->get_selected_items().empty() + || (entry->get_text_length() > 0)); + } + + if (enable != insertBtn->is_sensitive()) { + insertBtn->set_sensitive(enable); + } +} + +void GlyphsPanel::readSelection( bool updateStyle, bool updateContent ) +{ + calcCanInsert(); + + if (updateStyle) { + Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance(); + + // Update family/style based on selection. + fontlister->selection_update(); + + // Update GUI (based on fontlister values). + fontSelector->update_font (); + } +} + + +void GlyphsPanel::rebuild() +{ + Glib::ustring fontspec = fontSelector->get_fontspec(); + + font_instance* font = nullptr; + if( !fontspec.empty() ) { + font = font_factory::Default()->FaceFromFontSpecification( fontspec.c_str() ); + } + + if (font) { + + GUnicodeScript script = G_UNICODE_SCRIPT_INVALID_CODE; + Glib::ustring scriptName = scriptCombo->get_active_text(); + std::map<GUnicodeScript, Glib::ustring> items = getScriptToName(); + for (auto & item : items) { + if (scriptName == item.second) { + script = item.first; + break; + } + } + + // Disconnect the model while we update it. Simple work-around for 5x+ performance boost. + Glib::RefPtr<Gtk::ListStore> tmp = Gtk::ListStore::create(*getColumns()); + iconView->set_model(tmp); + + gunichar lower = 0x00001; + gunichar upper = 0x2FFFF; + int active = rangeCombo->get_active_row_number(); + if (active >= 0) { + lower = getRanges()[active].first.first; + upper = getRanges()[active].first.second; + } + std::vector<gunichar> present; + for (gunichar ch = lower; ch <= upper; ch++) { + int glyphId = font->MapUnicodeChar(ch); + if (glyphId > 0) { + if ((script == G_UNICODE_SCRIPT_INVALID_CODE) || (script == g_unichar_get_script(ch))) { + present.push_back(ch); + } + } + } + + GlyphColumns *columns = getColumns(); + store->clear(); + for (unsigned int & it : present) + { + Gtk::ListStore::iterator row = store->append(); + Glib::ustring tmp; + tmp += it; + tmp = Glib::Markup::escape_text(tmp); // Escape '&', '<', etc. + (*row)[columns->code] = it; + (*row)[columns->name] = "<span font_desc=\"" + fontspec + "\">" + tmp + "</span>"; + (*row)[columns->tooltip] = "<span font_desc=\"" + fontspec + "\" size=\"42000\">" + tmp + "</span>"; + } + + // Reconnect the model once it has been updated: + iconView->set_model(store); + } +} + + +} // namespace Dialogs +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/glyphs.h b/src/ui/dialog/glyphs.h new file mode 100644 index 0000000..9e22d61 --- /dev/null +++ b/src/ui/dialog/glyphs.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2010 Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_DIALOGS_GLYPHS_H +#define SEEN_DIALOGS_GLYPHS_H + +#include <gtkmm/treemodel.h> + +#include "ui/dialog/dialog-base.h" + +namespace Gtk { +class ComboBoxText; +class Entry; +class IconView; +class Label; +class ListStore; +} + +namespace Inkscape { +namespace UI { + +namespace Widget { +class FontSelector; +} + +namespace Dialog { + +class GlyphColumns; + +/** + * A panel that displays character glyphs. + */ +class GlyphsPanel : public DialogBase +{ +public: + GlyphsPanel(); + ~GlyphsPanel() override; + + static GlyphsPanel& getInstance(); + + void selectionChanged(Selection *selection) override; + void selectionModified(Selection *selection, guint flags) override; + +protected: + +private: + GlyphsPanel(GlyphsPanel const &) = delete; // no copy + GlyphsPanel &operator=(GlyphsPanel const &) = delete; // no assign + + static GlyphColumns *getColumns(); + + void rebuild(); + + void glyphActivated(Gtk::TreeModel::Path const & path); + void glyphSelectionChanged(); + void readSelection( bool updateStyle, bool updateContent ); + void calcCanInsert(); + void insertText(); + + Glib::RefPtr<Gtk::ListStore> store; + Gtk::IconView *iconView; + std::shared_ptr<Gtk::Entry> entry; + std::shared_ptr<Gtk::Label> label; + std::shared_ptr<Gtk::Button> insertBtn; + Gtk::ComboBoxText *scriptCombo; + Gtk::ComboBoxText *rangeCombo; + Inkscape::UI::Widget::FontSelector *fontSelector; + + std::vector<sigc::connection> instanceConns; +}; + + +} // namespace Dialogs +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_DIALOGS_GLYPHS_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/ui/dialog/grid-arrange-tab.cpp b/src/ui/dialog/grid-arrange-tab.cpp new file mode 100644 index 0000000..a0508bc --- /dev/null +++ b/src/ui/dialog/grid-arrange-tab.cpp @@ -0,0 +1,659 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple dialog for creating grid type arrangements of selected objects + * + * Authors: + * Bob Jamison ( based off trace dialog) + * John Cliff + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * Declara Denis + * + * Copyright (C) 2004 Bob Jamison + * Copyright (C) 2004 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +//#define DEBUG_GRID_ARRANGE 1 + +#include "ui/dialog/grid-arrange-tab.h" +#include <numeric> +#include <glibmm/i18n.h> + +#include <gtkmm/grid.h> +#include <gtkmm/sizegroup.h> + +#include <2geom/transforms.h> + +#include "preferences.h" +#include "inkscape.h" + +#include "document.h" +#include "document-undo.h" +#include "desktop.h" + +#include "ui/icon-names.h" +#include "ui/dialog/tile.h" // for Inkscape::UI::Dialog::ArrangeDialog + + +/** + * Sort ObjectSet by an existing grid arrangement + * + * This is based on this paper here: DOI:10.1049/iet-ipr.2015.0126 + * + * @param items - The unsorted object set to sort. + * @returns a new grid-sorted std::vector of SPItems. + */ +static std::vector<SPItem *> grid_item_sort(Inkscape::ObjectSet *items) +{ + std::vector<SPItem *> results; + Inkscape::ObjectSet rest; + + // 1. Find middle Y position of the largest top object. + double box_top = items->visualBounds()->min()[Geom::Y]; + double last_height = 0.0; + double target = box_top; + for (auto item : items->items()) { + if (auto item_box = item->desktopVisualBounds()) { + if (Geom::are_near(item_box->min()[Geom::Y], box_top, 2.0)) { + if (item_box->height() > last_height) { + last_height = item_box->height(); + target = item_box->midpoint()[Geom::Y]; + } + } + } + } + + // 2. Loop through all remaining items + for (auto item : items->items()) { + // Items without visual bounds are completely ignored. + if (auto item_box = item->desktopVisualBounds()) { + auto radius = item_box->height() / 2; + auto min = item_box->midpoint()[Geom::Y] - radius; + auto max = item_box->midpoint()[Geom::Y] + radius; + + if (max > target && min < target) { + // 2a. if the item's radius falls on the Y position above + results.push_back(item); + } else { + // 2b. Save items not in this row for later + rest.add(item); + } + } + } + + // 3. Sort this single row according to the X position + std::sort(results.begin(), results.end(), [](SPItem *a, SPItem *b) { + // These boxes always exist because of the above filtering. + return (a->desktopVisualBounds()->min()[Geom::X] < b->desktopVisualBounds()->min()[Geom::X]); + }); + + if (results.size() == 0) { + g_warning("Bad grid detection when sorting items!"); + } else if (!rest.isEmpty()) { + // 4. If there's any remaining, run this function again. + auto sorted_rest = grid_item_sort(&rest); + results.reserve(items->size()); + results.insert(results.end(), sorted_rest.begin(), sorted_rest.end()); + } + return results; +} + + namespace Inkscape { + namespace UI { + namespace Dialog { + + + //######################################################################### + //## E V E N T S + //######################################################################### + + /* + * + * This arranges the selection in a grid pattern. + * + */ + + void GridArrangeTab::arrange() + { + // check for correct numbers in the row- and col-spinners + on_col_spinbutton_changed(); + on_row_spinbutton_changed(); + + // set padding to manual values + double paddingx = XPadding.getValue("px"); + double paddingy = YPadding.getValue("px"); + int NoOfCols = NoOfColsSpinner.get_value_as_int(); + int NoOfRows = NoOfRowsSpinner.get_value_as_int(); + + SPDesktop *desktop = Parent->getDesktop(); + desktop->getDocument()->ensureUpToDate(); + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + auto sel_box = selection->documentBounds(SPItem::VISUAL_BBOX); + double grid_left = sel_box->min()[Geom::X]; + double grid_top = sel_box->min()[Geom::Y]; + + // require the sorting done before we can calculate row heights etc. + auto sorted = grid_item_sort(selection); + + // Calculate individual Row and Column sizes if necessary + auto row_heights = std::vector<double>(NoOfRows, 0.0); + auto col_widths = std::vector<double>(NoOfCols, 0.0); + for(int i = 0; i < sorted.size(); i++) { + if (Geom::OptRect box = sorted[i]->documentVisualBounds()) { + double width = box->dimensions()[Geom::X]; + double height = box->dimensions()[Geom::Y]; + if (width > col_widths[(i % NoOfCols)]) { + col_widths[(i % NoOfCols)] = width; + } + if (height > row_heights[(i / NoOfCols)]) { + row_heights[(i / NoOfCols)] = height; + } + } + } + + double col_width = *std::max_element(std::begin(col_widths), std::end(col_widths)); + double row_height = *std::max_element(std::begin(row_heights), std::end(row_heights)); + + /// Make sure the top and left of the grid don't move by compensating for align values. + if (RowHeightButton.get_active()){ + grid_top = grid_top - (((row_height - row_heights[0]) / 2)*(VertAlign)); + } + if (ColumnWidthButton.get_active()){ + grid_left = grid_left - (((col_width - col_widths[0]) /2)*(HorizAlign)); + } + + // Calculate total widths and heights, allowing for columns and rows non uniformly sized. + double last_col_padding = 0.0; + double total_col_width = 0.0; + if (ColumnWidthButton.get_active()){ + total_col_width = col_width * NoOfCols; + // Remember the amount of padding the box will lose on the right side + last_col_padding = (col_width - col_widths[NoOfCols-1]) / 2; + std::fill(col_widths.begin(), col_widths.end(), col_width); + } else { + total_col_width = std::accumulate(col_widths.begin(), col_widths.end(), 0); + } + + double last_row_padding = 0.0; + double total_row_height = 0.0; + if (RowHeightButton.get_active()){ + total_row_height = row_height * NoOfRows; + // Remember the amount of padding the box will lose on the bottom side + last_row_padding = (row_height - row_heights[NoOfRows-1]) / 2; + std::fill(row_heights.begin(), row_heights.end(), row_height); + } else { + total_row_height = std::accumulate(row_heights.begin(), row_heights.end(), 0); + } + + // Fit to bbox, calculate padding between rows accordingly. + if (SpaceByBBoxRadioButton.get_active()) { + paddingx = (sel_box->width() - total_col_width + last_col_padding) / (NoOfCols -1); + paddingy = (sel_box->height() - total_row_height + last_row_padding) / (NoOfRows -1); + } + +/* + Horizontal align - Left = 0 + Centre = 1 + Right = 2 + + Vertical align - Top = 0 + Middle = 1 + Bottom = 2 + + X position is calculated by taking the grids left co-ord, adding the distance to the column, + then adding 1/2 the spacing multiplied by the align variable above, + Y position likewise, takes the top of the grid, adds the y to the current row then adds the padding in to align it. + +*/ + + // Calculate row and column x and y coords required to allow for columns and rows which are non uniformly sized. + std::vector<double> col_xs = {0.0}; + for (int col=1; col < NoOfCols; col++) { + col_xs.push_back(col_widths[col-1] + paddingx + col_xs[col-1]); + } + + std::vector<double> row_ys = {0.0}; + for (int row=1; row < NoOfRows; row++) { + row_ys.push_back(row_heights[row-1] + paddingy + row_ys[row-1]); + } + + int cnt = 0; + std::vector<SPItem*>::iterator it = sorted.begin(); + for (int row_cnt=0; ((it != sorted.end()) && (row_cnt<NoOfRows)); ++row_cnt) { + + std::vector<SPItem *> current_row; + int col_cnt = 0; + for(;it!=sorted.end()&&col_cnt<NoOfCols;++it) { + current_row.push_back(*it); + col_cnt++; + } + + for (auto item:current_row) { + auto min = Geom::Point(0, 0); + double width, height = 0; + if (auto vbox = item->documentVisualBounds()) { + width = vbox->dimensions()[Geom::X]; + height = vbox->dimensions()[Geom::Y]; + min = vbox->min(); + } + + int row = cnt / NoOfCols; + int col = cnt % NoOfCols; + + double new_x = grid_left + (((col_widths[col] - width)/2)*HorizAlign) + col_xs[col]; + double new_y = grid_top + (((row_heights[row] - height)/2)*VertAlign) + row_ys[row]; + + Geom::Point move = Geom::Point(new_x, new_y) - min; + Geom::Affine const affine = Geom::Affine(Geom::Translate(move)); + item->set_i2d_affine(item->i2doc_affine() * affine * item->document->doc2dt()); + item->doWriteTransform(item->transform); + item->updateRepr(); + cnt +=1; + } + } + + DocumentUndo::done(desktop->getDocument(), _("Arrange in a grid"), INKSCAPE_ICON("dialog-align-and-distribute")); + +} + + +//######################################################################### +//## E V E N T S +//######################################################################### + +/** + * changed value in # of columns spinbox. + */ +void GridArrangeTab::on_row_spinbutton_changed() +{ + SPDesktop *desktop = Parent->getDesktop(); + Inkscape::Selection *selection = desktop ? desktop->selection : nullptr; + if (!selection) return; + + int selcount = (int) boost::distance(selection->items()); + + double NoOfRows = ceil(selcount / NoOfColsSpinner.get_value()); + NoOfRowsSpinner.set_value(NoOfRows); +} + +/** + * changed value in # of rows spinbox. + */ +void GridArrangeTab::on_col_spinbutton_changed() +{ + SPDesktop *desktop = Parent->getDesktop(); + Inkscape::Selection *selection = desktop ? desktop->selection : nullptr; + if (!selection) return; + + int selcount = (int) boost::distance(selection->items()); + + double NoOfCols = ceil(selcount / NoOfRowsSpinner.get_value()); + NoOfColsSpinner.set_value(NoOfCols); +} + +/** + * changed value in x padding spinbox. + */ +void GridArrangeTab::on_xpad_spinbutton_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/XPad", XPadding.getValue("px")); + +} + +/** + * changed value in y padding spinbox. + */ +void GridArrangeTab::on_ypad_spinbutton_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/YPad", YPadding.getValue("px")); +} + + +/** + * checked/unchecked autosize Rows button. + */ +void GridArrangeTab::on_RowSize_checkbutton_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (RowHeightButton.get_active()) { + prefs->setDouble("/dialogs/gridtiler/AutoRowSize", 20); + } else { + prefs->setDouble("/dialogs/gridtiler/AutoRowSize", -20); + } + RowHeightBox.set_sensitive ( !RowHeightButton.get_active()); +} + +/** + * checked/unchecked autosize Rows button. + */ +void GridArrangeTab::on_ColSize_checkbutton_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (ColumnWidthButton.get_active()) { + prefs->setDouble("/dialogs/gridtiler/AutoColSize", 20); + } else { + prefs->setDouble("/dialogs/gridtiler/AutoColSize", -20); + } + ColumnWidthBox.set_sensitive ( !ColumnWidthButton.get_active()); +} + +/** + * changed value in columns spinbox. + */ +void GridArrangeTab::on_rowSize_spinbutton_changed() +{ + // quit if run by the attr_changed listener + if (updating) { + return; + } + + // in turn, prevent listener from responding + updating = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/RowHeight", RowHeightSpinner.get_value()); + updating=false; + +} + +/** + * changed value in rows spinbox. + */ +void GridArrangeTab::on_colSize_spinbutton_changed() +{ + // quit if run by the attr_changed listener + if (updating) { + return; + } + + // in turn, prevent listener from responding + updating = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/ColWidth", ColumnWidthSpinner.get_value()); + updating=false; + +} + +/** + * changed Radio button in Spacing group. + */ +void GridArrangeTab::Spacing_button_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (SpaceManualRadioButton.get_active()) { + prefs->setDouble("/dialogs/gridtiler/SpacingType", 20); + } else { + prefs->setDouble("/dialogs/gridtiler/SpacingType", -20); + } + + XPadding.set_sensitive ( SpaceManualRadioButton.get_active()); + YPadding.set_sensitive ( SpaceManualRadioButton.get_active()); +} + +/** + * changed Anchor selection widget. + */ +void GridArrangeTab::Align_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + VertAlign = AlignmentSelector.getVerticalAlignment(); + prefs->setInt("/dialogs/gridtiler/VertAlign", VertAlign); + HorizAlign = AlignmentSelector.getHorizontalAlignment(); + prefs->setInt("/dialogs/gridtiler/HorizAlign", HorizAlign); +} + +/** + * Desktop selection changed + */ +void GridArrangeTab::updateSelection() +{ + // quit if run by the attr_changed listener + if (updating) { + return; + } + + // in turn, prevent listener from responding + updating = true; + SPDesktop *desktop = Parent->getDesktop(); + Inkscape::Selection *selection = desktop ? desktop->selection : nullptr; + std::vector<SPItem*> items; + if (selection) { + items.insert(items.end(), selection->items().begin(), selection->items().end()); + } + + if (!items.empty()) { + int selcount = items.size(); + + if (NoOfColsSpinner.get_value() > 1 && NoOfRowsSpinner.get_value() > 1){ + // Update the number of rows assuming number of columns wanted remains same. + double NoOfRows = ceil(selcount / NoOfColsSpinner.get_value()); + NoOfRowsSpinner.set_value(NoOfRows); + + // if the selection has less than the number set for one row, reduce it appropriately + if (selcount < NoOfColsSpinner.get_value()) { + double NoOfCols = ceil(selcount / NoOfRowsSpinner.get_value()); + NoOfColsSpinner.set_value(NoOfCols); + } + } else { + double PerRow = ceil(sqrt(selcount)); + double PerCol = ceil(sqrt(selcount)); + NoOfRowsSpinner.set_value(PerRow); + NoOfColsSpinner.set_value(PerCol); + } + } + + updating = false; +} + +void GridArrangeTab::setDesktop(SPDesktop *desktop) +{ + _selection_changed_connection.disconnect(); + + if (desktop) { + updateSelection(); + + _selection_changed_connection = INKSCAPE.signal_selection_changed.connect( + sigc::hide<0>(sigc::mem_fun(*this, &GridArrangeTab::updateSelection))); + } +} + + +//######################################################################### +//## C O N S T R U C T O R / D E S T R U C T O R +//######################################################################### +/** + * Constructor + */ +GridArrangeTab::GridArrangeTab(ArrangeDialog *parent) + : Parent(parent), + XPadding(_("X:"), _("Horizontal spacing between columns."), UNIT_TYPE_LINEAR, "", "object-columns", &PaddingUnitMenu), + YPadding(_("Y:"), _("Vertical spacing between rows."), XPadding, "", "object-rows"), + PaddingTable(Gtk::manage(new Gtk::Grid())) +{ + // bool used by spin button callbacks to stop loops where they change each other. + updating = false; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + auto _col1 = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + auto _col2 = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + auto _col3 = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + + Gtk::Box *contents = this; + set_valign(Gtk::ALIGN_START); + +#define MARGIN 2 + + //##Set up the panel + + NoOfRowsLabel.set_text_with_mnemonic(_("_Rows:")); + NoOfRowsLabel.set_mnemonic_widget(NoOfRowsSpinner); + NoOfRowsBox.set_orientation(Gtk::ORIENTATION_VERTICAL); + NoOfRowsBox.pack_start(NoOfRowsLabel, false, false, MARGIN); + + NoOfRowsSpinner.set_digits(0); + NoOfRowsSpinner.set_increments(1, 0); + NoOfRowsSpinner.set_range(1.0, 10000.0); + NoOfRowsSpinner.signal_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_col_spinbutton_changed)); + NoOfRowsSpinner.set_tooltip_text(_("Number of rows")); + NoOfRowsBox.pack_start(NoOfRowsSpinner, false, false, MARGIN); + _col1->add_widget(NoOfRowsBox); + + RowHeightButton.set_label(_("Equal _height")); + RowHeightButton.set_use_underline(true); + double AutoRow = prefs->getDouble("/dialogs/gridtiler/AutoRowSize", 15); + if (AutoRow>0) + AutoRowSize=true; + else + AutoRowSize=false; + RowHeightButton.set_active(AutoRowSize); + + NoOfRowsBox.pack_start(RowHeightButton, false, false, MARGIN); + + RowHeightButton.set_tooltip_text(_("If not set, each row has the height of the tallest object in it")); + RowHeightButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::on_RowSize_checkbutton_changed)); + + SpinsHBox.pack_start(NoOfRowsBox, false, false, MARGIN); + + + /*#### Label for X ####*/ + padXByYLabel.set_label(" "); + XByYLabelVBox.set_orientation(Gtk::ORIENTATION_VERTICAL); + XByYLabelVBox.pack_start(padXByYLabel, false, false, MARGIN); + XByYLabel.set_markup(" × "); + XByYLabelVBox.pack_start(XByYLabel, false, false, MARGIN); + SpinsHBox.pack_start(XByYLabelVBox, false, false, MARGIN); + _col2->add_widget(XByYLabelVBox); + + /*#### Number of columns ####*/ + + NoOfColsLabel.set_text_with_mnemonic(_("_Columns:")); + NoOfColsLabel.set_mnemonic_widget(NoOfColsSpinner); + NoOfColsBox.set_orientation(Gtk::ORIENTATION_VERTICAL); + NoOfColsBox.pack_start(NoOfColsLabel, false, false, MARGIN); + + NoOfColsSpinner.set_digits(0); + NoOfColsSpinner.set_increments(1, 0); + NoOfColsSpinner.set_range(1.0, 10000.0); + NoOfColsSpinner.signal_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_row_spinbutton_changed)); + NoOfColsSpinner.set_tooltip_text(_("Number of columns")); + NoOfColsBox.pack_start(NoOfColsSpinner, false, false, MARGIN); + _col3->add_widget(NoOfColsBox); + + ColumnWidthButton.set_label(_("Equal _width")); + ColumnWidthButton.set_use_underline(true); + double AutoCol = prefs->getDouble("/dialogs/gridtiler/AutoColSize", 15); + if (AutoCol>0) + AutoColSize=true; + else + AutoColSize=false; + ColumnWidthButton.set_active(AutoColSize); + NoOfColsBox.pack_start(ColumnWidthButton, false, false, MARGIN); + + ColumnWidthButton.set_tooltip_text(_("If not set, each column has the width of the widest object in it")); + ColumnWidthButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::on_ColSize_checkbutton_changed)); + + SpinsHBox.pack_start(NoOfColsBox, false, false, MARGIN); + + TileBox.set_orientation(Gtk::ORIENTATION_VERTICAL); + TileBox.pack_start(SpinsHBox, false, false, MARGIN); + + VertAlign = prefs->getInt("/dialogs/gridtiler/VertAlign", 1); + HorizAlign = prefs->getInt("/dialogs/gridtiler/HorizAlign", 1); + + // Anchor selection widget + AlignLabel.set_label(_("Alignment:")); + AlignLabel.set_halign(Gtk::ALIGN_START); + AlignLabel.set_valign(Gtk::ALIGN_CENTER); + AlignmentSelector.setAlignment(HorizAlign, VertAlign); + AlignmentSelector.on_selectionChanged().connect(sigc::mem_fun(*this, &GridArrangeTab::Align_changed)); + TileBox.pack_start(AlignLabel, false, false, MARGIN); + TileBox.pack_start(AlignmentSelector, true, false, MARGIN); + + { + /*#### Radio buttons to control spacing manually or to fit selection bbox ####*/ + SpaceByBBoxRadioButton.set_label(_("_Fit into selection box")); + SpaceByBBoxRadioButton.set_use_underline (true); + SpaceByBBoxRadioButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::Spacing_button_changed)); + SpacingGroup = SpaceByBBoxRadioButton.get_group(); + + SpacingVBox.pack_start(SpaceByBBoxRadioButton, false, false, MARGIN); + + SpaceManualRadioButton.set_label(_("_Set spacing:")); + SpaceManualRadioButton.set_use_underline (true); + SpaceManualRadioButton.set_group(SpacingGroup); + SpaceManualRadioButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::Spacing_button_changed)); + SpacingVBox.pack_start(SpaceManualRadioButton, false, false, MARGIN); + + TileBox.pack_start(SpacingVBox, false, false, MARGIN); + } + + { + /*#### Padding ####*/ + PaddingUnitMenu.setUnitType(UNIT_TYPE_LINEAR); + PaddingUnitMenu.setUnit("px"); + + YPadding.setDigits(5); + YPadding.setIncrements(0.2, 0); + YPadding.setRange(-10000, 10000); + double yPad = prefs->getDouble("/dialogs/gridtiler/YPad", 15); + YPadding.setValue(yPad, "px"); + YPadding.signal_value_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_ypad_spinbutton_changed)); + + XPadding.setDigits(5); + XPadding.setIncrements(0.2, 0); + XPadding.setRange(-10000, 10000); + double xPad = prefs->getDouble("/dialogs/gridtiler/XPad", 15); + XPadding.setValue(xPad, "px"); + + XPadding.signal_value_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_xpad_spinbutton_changed)); + } + + PaddingTable->set_border_width(MARGIN); + PaddingTable->set_row_spacing(MARGIN); + PaddingTable->set_column_spacing(MARGIN); + PaddingTable->attach(XPadding, 0, 0, 1, 1); + PaddingTable->attach(PaddingUnitMenu, 1, 0, 1, 1); + PaddingTable->attach(YPadding, 0, 1, 1, 1); + + TileBox.pack_start(*PaddingTable, false, false, MARGIN); + + contents->set_border_width(4); + contents->pack_start(TileBox); + + double SpacingType = prefs->getDouble("/dialogs/gridtiler/SpacingType", 15); + if (SpacingType>0) { + ManualSpacing=true; + } else { + ManualSpacing=false; + } + SpaceManualRadioButton.set_active(ManualSpacing); + SpaceByBBoxRadioButton.set_active(!ManualSpacing); + XPadding.set_sensitive (ManualSpacing); + YPadding.set_sensitive (ManualSpacing); + + show_all_children(); +} + + +GridArrangeTab::~GridArrangeTab() { + setDesktop(nullptr); +} + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/grid-arrange-tab.h b/src/ui/dialog/grid-arrange-tab.h new file mode 100644 index 0000000..6c244b3 --- /dev/null +++ b/src/ui/dialog/grid-arrange-tab.h @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @brief Arranges Objects into a Grid + */ +/* Authors: + * Bob Jamison ( based off trace dialog) + * John Cliff + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * Declara Denis + * + * Copyright (C) 2004 Bob Jamison + * Copyright (C) 2004 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_GRID_ARRANGE_TAB_H +#define INKSCAPE_UI_DIALOG_GRID_ARRANGE_TAB_H + +#include "ui/widget/scalar-unit.h" +#include "ui/dialog/arrange-tab.h" + +#include "ui/widget/anchor-selector.h" +#include "ui/widget/spinbutton.h" + +#include <gtkmm/checkbutton.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/radiobuttongroup.h> + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ArrangeDialog; + +/** + * Dialog for tiling an object + */ +class GridArrangeTab : public ArrangeTab { +public: + GridArrangeTab(ArrangeDialog *parent); + ~GridArrangeTab() override; + + /** + * Do the actual work + */ + void arrange() override; + + /** + * Respond to selection change + */ + void updateSelection(); + + // Callbacks from spinbuttons + void on_row_spinbutton_changed(); + void on_col_spinbutton_changed(); + void on_xpad_spinbutton_changed(); + void on_ypad_spinbutton_changed(); + void on_RowSize_checkbutton_changed(); + void on_ColSize_checkbutton_changed(); + void on_rowSize_spinbutton_changed(); + void on_colSize_spinbutton_changed(); + void Spacing_button_changed(); + void Align_changed(); + + +private: + GridArrangeTab(GridArrangeTab const &d) = delete; // no copy + void operator=(GridArrangeTab const &d) = delete; // no assign + + ArrangeDialog *Parent; + + bool updating; + + Gtk::Box TileBox; + + // Number selected label + Gtk::Label SelectionContentsLabel; + + + Gtk::Box AlignHBox; + Gtk::Box SpinsHBox; + + // Number per Row + Gtk::Box NoOfColsBox; + Gtk::Label NoOfColsLabel; + Inkscape::UI::Widget::SpinButton NoOfColsSpinner; + bool AutoRowSize; + Gtk::CheckButton RowHeightButton; + + Gtk::Box XByYLabelVBox; + Gtk::Label padXByYLabel; + Gtk::Label XByYLabel; + + // Number per Column + Gtk::Box NoOfRowsBox; + Gtk::Label NoOfRowsLabel; + Inkscape::UI::Widget::SpinButton NoOfRowsSpinner; + bool AutoColSize; + Gtk::CheckButton ColumnWidthButton; + + // Alignment + Gtk::Label AlignLabel; + Inkscape::UI::Widget::AnchorSelector AlignmentSelector; + double VertAlign; + double HorizAlign; + + Inkscape::UI::Widget::UnitMenu PaddingUnitMenu; + Inkscape::UI::Widget::ScalarUnit XPadding; + Inkscape::UI::Widget::ScalarUnit YPadding; + Gtk::Grid *PaddingTable; + + // BBox or manual spacing + Gtk::Box SpacingVBox; + Gtk::RadioButtonGroup SpacingGroup; + Gtk::RadioButton SpaceByBBoxRadioButton; + Gtk::RadioButton SpaceManualRadioButton; + bool ManualSpacing; + + // Row height + Gtk::Box RowHeightBox; + Inkscape::UI::Widget::SpinButton RowHeightSpinner; + + // Column width + Gtk::Box ColumnWidthBox; + Inkscape::UI::Widget::SpinButton ColumnWidthSpinner; + + sigc::connection _selection_changed_connection; + + public: + void setDesktop(SPDesktop *); +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif /* INKSCAPE_UI_DIALOG_GRID_ARRANGE_TAB_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/ui/dialog/guides.cpp b/src/ui/dialog/guides.cpp new file mode 100644 index 0000000..9be41a5 --- /dev/null +++ b/src/ui/dialog/guides.cpp @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Simple guideline dialog. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Andrius R. <knutux@gmail.com> + * Johan Engelen + * Abhishek Sharma + * + * Copyright (C) 1999-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "guides.h" + +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "message-context.h" + +#include "include/gtkmm_version.h" + +#include "object/sp-guide.h" +#include "object/sp-namedview.h" + +#include "ui/dialog-events.h" +#include "ui/tools/tool-base.h" +#include "ui/widget/spinbutton.h" + +#include "widgets/desktop-widget.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +GuidelinePropertiesDialog::GuidelinePropertiesDialog(SPGuide *guide, SPDesktop *desktop) +: _desktop(desktop), _guide(guide), + _locked_toggle(_("Lo_cked")), + _relative_toggle(_("Rela_tive change")), + _spin_button_x(C_("Guides", "_X:"), "", UNIT_TYPE_LINEAR, "", "", &_unit_menu), + _spin_button_y(C_("Guides", "_Y:"), "", UNIT_TYPE_LINEAR, "", "", &_unit_menu), + _label_entry(_("_Label:"), _("Optionally give this guideline a name")), + _spin_angle(_("_Angle:"), "", UNIT_TYPE_RADIAL), + _mode(true), _oldpos(0.,0.), _oldangle(0.0) +{ + _locked_toggle.set_use_underline(); + _locked_toggle.set_tooltip_text(_("Lock the movement of guides")); + _relative_toggle.set_use_underline(); + _relative_toggle.set_tooltip_text(_("Move and/or rotate the guide relative to current settings")); +} + +bool GuidelinePropertiesDialog::_relative_toggle_status = false; // initialize relative checkbox status for when this dialog is opened for first time +Glib::ustring GuidelinePropertiesDialog::_angle_unit_status = DEG; // initialize angle unit status + +GuidelinePropertiesDialog::~GuidelinePropertiesDialog() { + // save current status + _relative_toggle_status = _relative_toggle.get_active(); + _angle_unit_status = _spin_angle.getUnit()->abbr; +} + +void GuidelinePropertiesDialog::showDialog(SPGuide *guide, SPDesktop *desktop) { + GuidelinePropertiesDialog dialog(guide, desktop); + dialog._setup(); + dialog.run(); +} + +void GuidelinePropertiesDialog::_modeChanged() +{ + _mode = !_relative_toggle.get_active(); + if (!_mode) { + // relative + _spin_angle.setValue(0); + + _spin_button_y.setValue(0); + _spin_button_x.setValue(0); + } else { + // absolute + _spin_angle.setValueKeepUnit(_oldangle, DEG); + + _spin_button_x.setValueKeepUnit(_oldpos[Geom::X], "px"); + _spin_button_y.setValueKeepUnit(_oldpos[Geom::Y], "px"); + } +} + +void GuidelinePropertiesDialog::_onOK() +{ + this->_onOKimpl(); + DocumentUndo::done(_guide->document, _("Set guide properties"), ""); + +} + +void GuidelinePropertiesDialog::_onOKimpl() +{ + double deg_angle = _spin_angle.getValue(DEG); + if (!_mode) + deg_angle += _oldangle; + Geom::Point normal; + if ( deg_angle == 90. || deg_angle == 270. || deg_angle == -90. || deg_angle == -270.) { + normal = Geom::Point(1.,0.); + } else if ( deg_angle == 0. || deg_angle == 180. || deg_angle == -180.) { + normal = Geom::Point(0.,1.); + } else { + double rad_angle = Geom::rad_from_deg( deg_angle ); + normal = Geom::rot90(Geom::Point::polar(rad_angle, 1.0)); + } + //To allow reposition from dialog + _guide->set_locked(false, false); + + _guide->set_normal(normal, true); + + double const points_x = _spin_button_x.getValue("px"); + double const points_y = _spin_button_y.getValue("px"); + Geom::Point newpos(points_x, points_y); + if (!_mode) + newpos += _oldpos; + + _guide->moveto(newpos, true); + + const gchar* name = g_strdup( _label_entry.getEntry()->get_text().c_str() ); + + _guide->set_label(name, true); + + const bool locked = _locked_toggle.get_active(); + + _guide->set_locked(locked, true); + + g_free((gpointer) name); + + const auto c = _color.get_rgba(); + unsigned r = c.get_red_u()/257, g = c.get_green_u()/257, b = c.get_blue_u()/257; + //TODO: why 257? verify this! + // don't know why, but introduced: 761f7da58cd6d625b88c24eee6fae1b7fa3bfcdd + + _guide->set_color(r, g, b, true); +} + +void GuidelinePropertiesDialog::_onDelete() +{ + SPDocument *doc = _guide->document; + if (_guide->remove(true)) + DocumentUndo::done(doc, _("Delete guide"), ""); +} + +void GuidelinePropertiesDialog::_onDuplicate() +{ + _guide = _guide->duplicate(); + this->_onOKimpl(); + DocumentUndo::done(_guide->document, _("Duplicate guide"), ""); +} + +void GuidelinePropertiesDialog::_response(gint response) +{ + switch (response) { + case Gtk::RESPONSE_OK: + _onOK(); + break; + case -12: + _onDelete(); + break; + case -13: + _onDuplicate(); + break; + case Gtk::RESPONSE_CANCEL: + break; + case Gtk::RESPONSE_DELETE_EVENT: + break; + default: + g_assert_not_reached(); + } +} + +void GuidelinePropertiesDialog::_setup() { + set_title(_("Guideline")); + add_button(_("_OK"), Gtk::RESPONSE_OK); + add_button(_("_Duplicate"), -13); + add_button(_("_Delete"), -12); + add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + + auto mainVBox = get_content_area(); + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + _layout_table.set_border_width(4); + + mainVBox->pack_start(_layout_table, false, false, 0); + + _label_name.set_label("foo0"); + _label_name.set_halign(Gtk::ALIGN_START); + _label_name.set_valign(Gtk::ALIGN_CENTER); + + _label_descr.set_label("foo1"); + _label_descr.set_halign(Gtk::ALIGN_START); + _label_descr.set_valign(Gtk::ALIGN_CENTER); + + _label_name.set_halign(Gtk::ALIGN_FILL); + _label_name.set_valign(Gtk::ALIGN_FILL); + _layout_table.attach(_label_name, 0, 0, 3, 1); + + _label_descr.set_halign(Gtk::ALIGN_FILL); + _label_descr.set_valign(Gtk::ALIGN_FILL); + _layout_table.attach(_label_descr, 0, 1, 3, 1); + + _label_entry.set_halign(Gtk::ALIGN_FILL); + _label_entry.set_valign(Gtk::ALIGN_FILL); + _label_entry.set_hexpand(); + _layout_table.attach(_label_entry, 1, 2, 2, 1); + + _color.set_halign(Gtk::ALIGN_FILL); + _color.set_valign(Gtk::ALIGN_FILL); + _color.set_hexpand(); + _color.set_margin_end(6); + _layout_table.attach(_color, 1, 3, 2, 1); + + // unitmenus + /* fixme: We should allow percents here too, as percents of the canvas size */ + _unit_menu.setUnitType(UNIT_TYPE_LINEAR); + _unit_menu.setUnit("px"); + if (_desktop->namedview->display_units) { + _unit_menu.setUnit( _desktop->namedview->display_units->abbr ); + } + _spin_angle.setUnit(_angle_unit_status); + + // position spinbuttons + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + size_t minimumexponent = std::min(std::abs(prefs->getInt("/options/svgoutput/minimumexponent", -8)), 5); //we limit to 5 to minimize rounding errors + _spin_button_x.setDigits(minimumexponent); + _spin_button_x.setAlignment(1.0); + _spin_button_x.setIncrements(1.0, 10.0); + _spin_button_x.setRange(-1e6, 1e6); + _spin_button_y.setDigits(minimumexponent); + + _spin_button_y.setAlignment(1.0); + _spin_button_y.setIncrements(1.0, 10.0); + _spin_button_y.setRange(-1e6, 1e6); + + _spin_button_x.set_halign(Gtk::ALIGN_FILL); + _spin_button_x.set_valign(Gtk::ALIGN_FILL); + _spin_button_x.set_hexpand(); + _layout_table.attach(_spin_button_x, 1, 4, 1, 1); + + _spin_button_y.set_halign(Gtk::ALIGN_FILL); + _spin_button_y.set_valign(Gtk::ALIGN_FILL); + _spin_button_y.set_hexpand(); + _layout_table.attach(_spin_button_y, 1, 5, 1, 1); + + _unit_menu.set_halign(Gtk::ALIGN_FILL); + _unit_menu.set_valign(Gtk::ALIGN_FILL); + _unit_menu.set_margin_end(6); + _layout_table.attach(_unit_menu, 2, 4, 1, 1); + + // angle spinbutton + _spin_angle.setDigits(3); + _spin_angle.setDigits(minimumexponent); + _spin_angle.setAlignment(1.0); + _spin_angle.setIncrements(1.0, 10.0); + _spin_angle.setRange(-3600., 3600.); + + _spin_angle.set_halign(Gtk::ALIGN_FILL); + _spin_angle.set_valign(Gtk::ALIGN_FILL); + _spin_angle.set_hexpand(); + _layout_table.attach(_spin_angle, 1, 6, 2, 1); + + // mode radio button + _relative_toggle.set_halign(Gtk::ALIGN_FILL); + _relative_toggle.set_valign(Gtk::ALIGN_FILL); + _relative_toggle.set_hexpand(); + _relative_toggle.set_margin_start(6); + _layout_table.attach(_relative_toggle, 1, 7, 2, 1); + + // locked radio button + _locked_toggle.set_halign(Gtk::ALIGN_FILL); + _locked_toggle.set_valign(Gtk::ALIGN_FILL); + _locked_toggle.set_hexpand(); + _locked_toggle.set_margin_start(6); + _layout_table.attach(_locked_toggle, 1, 8, 2, 1); + + _relative_toggle.signal_toggled().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::_modeChanged)); + _relative_toggle.set_active(_relative_toggle_status); + + bool global_guides_lock = _desktop->namedview->getLockGuides(); + if(global_guides_lock){ + _locked_toggle.set_sensitive(false); + } + _locked_toggle.set_active(_guide->getLocked()); + + // This results in the dialog closing when entering a value in one of the spinbuttons and pressing enter (see LP bug 484187) + auto sbx = dynamic_cast<UI::Widget::SpinButton *>(_spin_button_x.getWidget()); + auto sby = dynamic_cast<UI::Widget::SpinButton *>(_spin_button_y.getWidget()); + auto sba = dynamic_cast<UI::Widget::SpinButton *>(_spin_button_y.getWidget()); + + if(sbx) sbx->signal_activate().connect(sigc::mem_fun(this, &GuidelinePropertiesDialog::on_sb_activate)); + if(sby) sby->signal_activate().connect(sigc::mem_fun(this, &GuidelinePropertiesDialog::on_sb_activate)); + if(sba) sba->signal_activate().connect(sigc::mem_fun(this, &GuidelinePropertiesDialog::on_sb_activate)); + + // dialog + set_default_response(Gtk::RESPONSE_OK); + signal_response().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::_response)); + + // initialize dialog + _oldpos = _guide->getPoint(); + if (_guide->isVertical()) { + _oldangle = 90; + } else if (_guide->isHorizontal()) { + _oldangle = 0; + } else { + _oldangle = Geom::deg_from_rad( std::atan2( - _guide->getNormal()[Geom::X], _guide->getNormal()[Geom::Y] ) ); + } + + { + gchar *label = g_strdup_printf(_("Guideline ID: %s"), _guide->getId()); + _label_name.set_label(label); + g_free(label); + } + { + gchar *guide_description = _guide->description(false); + gchar *label = g_strdup_printf(_("Current: %s"), guide_description); + g_free(guide_description); + _label_descr.set_markup(label); + g_free(label); + } + + // init name entry + _label_entry.getEntry()->set_text(_guide->getLabel() ? _guide->getLabel() : ""); + + Gdk::RGBA c; + c.set_rgba(((_guide->getColor()>>24)&0xff) / 255.0, ((_guide->getColor()>>16)&0xff) / 255.0, ((_guide->getColor()>>8)&0xff) / 255.0); + _color.set_rgba(c); + + _modeChanged(); // sets values of spinboxes. + + if ( _oldangle == 90. || _oldangle == 270. || _oldangle == -90. || _oldangle == -270.) { + _spin_button_x.grabFocusAndSelectEntry(); + } else if ( _oldangle == 0. || _oldangle == 180. || _oldangle == -180.) { + _spin_button_y.grabFocusAndSelectEntry(); + } else { + _spin_angle.grabFocusAndSelectEntry(); + } + + set_position(Gtk::WIN_POS_MOUSE); + + show_all_children(); + set_modal(true); + _desktop->setWindowTransient (gobj()); + property_destroy_with_parent() = true; +} + +void +GuidelinePropertiesDialog::on_sb_activate() +{ + activate_default(); +} +} +} +} + +/* + 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/ui/dialog/guides.h b/src/ui/dialog/guides.h new file mode 100644 index 0000000..9c449ce --- /dev/null +++ b/src/ui/dialog/guides.h @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Andrius R. <knutux@gmail.com> + * Johan Engelen + * + * Copyright (C) 2006-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_GUIDELINE_H +#define INKSCAPE_DIALOG_GUIDELINE_H + +#include <gtkmm/checkbutton.h> +#include <gtkmm/colorbutton.h> +#include <gtkmm/dialog.h> +#include <gtkmm/grid.h> +#include <gtkmm/label.h> + +#include "ui/widget/unit-menu.h" +#include "ui/widget/scalar-unit.h" +#include "ui/widget/entry.h" +#include <2geom/point.h> + +class SPGuide; +class SPDesktop; + +namespace Inkscape { +namespace UI { + +namespace Widget { + class UnitMenu; +}; + +namespace Dialogs { + +/** + * Dialog for modifying guidelines. + */ +class GuidelinePropertiesDialog : public Gtk::Dialog { +public: + GuidelinePropertiesDialog(SPGuide *guide, SPDesktop *desktop); + ~GuidelinePropertiesDialog() override; + + Glib::ustring getName() const { return "GuidelinePropertiesDialog"; } + + static void showDialog(SPGuide *guide, SPDesktop *desktop); + +protected: + void _setup(); + + void _onOK(); + void _onOKimpl(); + void _onDelete(); + void _onDuplicate(); + + void _response(gint response); + void _modeChanged(); + +private: + GuidelinePropertiesDialog(GuidelinePropertiesDialog const &) = delete; // no copy + GuidelinePropertiesDialog &operator=(GuidelinePropertiesDialog const &) = delete; // no assign + + SPDesktop *_desktop; + SPGuide *_guide; + + Gtk::Grid _layout_table; + Gtk::Label _label_name; + Gtk::Label _label_descr; + Gtk::CheckButton _locked_toggle; + Gtk::CheckButton _relative_toggle; + static bool _relative_toggle_status; // remember the status of the _relative_toggle_status button across instances + Inkscape::UI::Widget::UnitMenu _unit_menu; + Inkscape::UI::Widget::ScalarUnit _spin_button_x; + Inkscape::UI::Widget::ScalarUnit _spin_button_y; + Inkscape::UI::Widget::Entry _label_entry; + Gtk::ColorButton _color; + + Inkscape::UI::Widget::ScalarUnit _spin_angle; + static Glib::ustring _angle_unit_status; // remember the status of the _relative_toggle_status button across instances + + bool _mode; + Geom::Point _oldpos; + gdouble _oldangle; + + void on_sb_activate(); +}; + +} // namespace +} // namespace +} // namespace + + +#endif // INKSCAPE_DIALOG_GUIDELINE_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/ui/dialog/icon-preview.cpp b/src/ui/dialog/icon-preview.cpp new file mode 100644 index 0000000..f09f6ae --- /dev/null +++ b/src/ui/dialog/icon-preview.cpp @@ -0,0 +1,646 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A simple dialog for previewing icon representation. + */ +/* Authors: + * Jon A. Cruz + * Bob Jamison + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * + * Copyright (C) 2004 Bob Jamison + * Copyright (C) 2005,2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <glibmm/timer.h> +#include <glibmm/main.h> + +#include <gtkmm/buttonbox.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/frame.h> + +#include "desktop.h" +#include "document.h" +#include "inkscape.h" +#include "page-manager.h" + +#include "display/cairo-utils.h" +#include "display/drawing.h" +#include "display/drawing-context.h" + +#include "object/sp-namedview.h" +#include "object/sp-root.h" + +#include "icon-preview.h" + +#include "ui/widget/frame.h" + +extern "C" { +// takes doc, drawing, icon, and icon name to produce pixels +guchar * +sp_icon_doc_icon( SPDocument *doc, Inkscape::Drawing &drawing, + const gchar *name, unsigned int psize, unsigned &stride); +} + +#define noICON_VERBOSE 1 + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +IconPreviewPanel &IconPreviewPanel::getInstance() +{ + IconPreviewPanel *instance = new IconPreviewPanel(); + + instance->refreshPreview(); + + return *instance; +} + +//######################################################################### +//## E V E N T S +//######################################################################### + +void IconPreviewPanel::on_button_clicked(int which) +{ + if ( hot != which ) { + buttons[hot]->set_active( false ); + + hot = which; + updateMagnify(); + queue_draw(); + } +} + + + + +//######################################################################### +//## C O N S T R U C T O R / D E S T R U C T O R +//######################################################################### +/** + * Constructor + */ +IconPreviewPanel::IconPreviewPanel() + : DialogBase("/dialogs/iconpreview", "IconPreview") + , drawing(nullptr) + , drawing_doc(nullptr) + , visionkey(0) + , timer(nullptr) + , renderTimer(nullptr) + , pending(false) + , minDelay(0.1) + , targetId() + , hot(1) + , selectionButton(nullptr) + , docModConn() + , iconBox(Gtk::ORIENTATION_VERTICAL) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + numEntries = 0; + + bool pack = prefs->getBool("/iconpreview/pack", true); + + std::vector<Glib::ustring> pref_sizes = prefs->getAllDirs("/iconpreview/sizes/default"); + std::vector<int> rawSizes; + + for (auto & pref_size : pref_sizes) { + if (prefs->getBool(pref_size + "/show", true)) { + int sizeVal = prefs->getInt(pref_size + "/value", -1); + if (sizeVal > 0) { + rawSizes.push_back(sizeVal); + } + } + } + + if ( !rawSizes.empty() ) { + numEntries = rawSizes.size(); + sizes = new int[numEntries]; + int i = 0; + for ( std::vector<int>::iterator it = rawSizes.begin(); it != rawSizes.end(); ++it, ++i ) { + sizes[i] = *it; + } + } + + if ( numEntries < 1 ) + { + numEntries = 5; + sizes = new int[numEntries]; + sizes[0] = 16; + sizes[1] = 24; + sizes[2] = 32; + sizes[3] = 48; + sizes[4] = 128; + } + + pixMem = new guchar*[numEntries]; + images = new Gtk::Image*[numEntries]; + labels = new Glib::ustring*[numEntries]; + buttons = new Gtk::ToggleToolButton*[numEntries]; + + + for ( int i = 0; i < numEntries; i++ ) { + char *label = g_strdup_printf(_("%d x %d"), sizes[i], sizes[i]); + labels[i] = new Glib::ustring(label); + g_free(label); + pixMem[i] = nullptr; + images[i] = nullptr; + } + + + magLabel.set_label( *labels[hot] ); + + Gtk::Box* magBox = new Gtk::Box(Gtk::ORIENTATION_VERTICAL); + + UI::Widget::Frame *magFrame = Gtk::manage(new UI::Widget::Frame(_("Magnified:"))); + magFrame->add( magnified ); + + magBox->pack_start( *magFrame, Gtk::PACK_EXPAND_WIDGET ); + magBox->pack_start( magLabel, Gtk::PACK_SHRINK ); + + + Gtk::Box *verts = new Gtk::Box(Gtk::ORIENTATION_VERTICAL); + Gtk::Box *horiz = nullptr; + int previous = 0; + int avail = 0; + for ( int i = numEntries - 1; i >= 0; --i ) { + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, sizes[i]); + pixMem[i] = new guchar[sizes[i] * stride]; + memset( pixMem[i], 0x00, sizes[i] * stride ); + + auto pb = Gdk::Pixbuf::create_from_data(pixMem[i], Gdk::COLORSPACE_RGB, true, 8, sizes[i], sizes[i], stride); + images[i] = Gtk::make_managed<Gtk::Image>(pb); + Glib::ustring label(*labels[i]); + buttons[i] = new Gtk::ToggleToolButton(label); + buttons[i]->set_active( i == hot ); + if ( prefs->getBool("/iconpreview/showFrames", true) ) { + Gtk::Frame *frame = new Gtk::Frame(); + frame->set_shadow_type(Gtk::SHADOW_ETCHED_IN); + frame->add(*images[i]); + buttons[i]->set_icon_widget(*Gtk::manage(frame)); + } else { + buttons[i]->set_icon_widget(*images[i]); + } + + buttons[i]->set_tooltip_text(label); + + buttons[i]->signal_clicked().connect( sigc::bind<int>( sigc::mem_fun(*this, &IconPreviewPanel::on_button_clicked), i) ); + + buttons[i]->set_halign(Gtk::ALIGN_CENTER); + buttons[i]->set_valign(Gtk::ALIGN_CENTER); + + if ( !pack || ( (avail == 0) && (previous == 0) ) ) { + verts->pack_end(*(buttons[i]), Gtk::PACK_SHRINK); + previous = sizes[i]; + avail = sizes[i]; + } else { + int pad = 12; + if ((avail < pad) || ((sizes[i] > avail) && (sizes[i] < previous))) { + horiz = nullptr; + } + if ((horiz == nullptr) && (sizes[i] <= previous)) { + avail = previous; + } + if (sizes[i] <= avail) { + if (!horiz) { + horiz = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + avail = previous; + verts->pack_end(*horiz, Gtk::PACK_SHRINK); + } + horiz->pack_start(*(buttons[i]), Gtk::PACK_EXPAND_WIDGET); + avail -= sizes[i]; + avail -= pad; // a little extra for padding + } else { + horiz = nullptr; + verts->pack_end(*(buttons[i]), Gtk::PACK_SHRINK); + } + } + } + + iconBox.pack_start(splitter); + splitter.pack1( *magBox, true, false ); + UI::Widget::Frame *actuals = Gtk::manage(new UI::Widget::Frame (_("Actual Size:"))); + actuals->set_border_width(4); + actuals->add(*verts); + splitter.pack2( *actuals, false, false ); + + + selectionButton = new Gtk::CheckButton(C_("Icon preview window", "Sele_ction"), true);//selectionButton = (Gtk::ToggleButton*) gtk_check_button_new_with_mnemonic(_("_Selection")); // , GTK_RESPONSE_APPLY + magBox->pack_start( *selectionButton, Gtk::PACK_SHRINK ); + selectionButton->set_tooltip_text(_("Selection only or whole document")); + selectionButton->signal_clicked().connect( sigc::mem_fun(*this, &IconPreviewPanel::modeToggled) ); + + gint val = prefs->getBool("/iconpreview/selectionOnly"); + selectionButton->set_active( val != 0 ); + + pack_start(iconBox, Gtk::PACK_SHRINK); + + show_all_children(); +} + +IconPreviewPanel::~IconPreviewPanel() +{ + removeDrawing(); + if (timer) { + timer->stop(); + delete timer; + timer = nullptr; + } + if ( renderTimer ) { + renderTimer->stop(); + delete renderTimer; + renderTimer = nullptr; + } + + docModConn.disconnect(); +} + +//######################################################################### +//## M E T H O D S +//######################################################################### + + +#if ICON_VERBOSE +static Glib::ustring getTimestr() +{ + Glib::ustring str; + gint64 micr = g_get_monotonic_time(); + gint64 mins = ((int)round(micr / 60000000)) % 60; + gdouble dsecs = micr / 1000000; + gchar *ptr = g_strdup_printf(":%02u:%f", mins, dsecs); + str = ptr; + g_free(ptr); + ptr = 0; + return str; +} +#endif // ICON_VERBOSE + +void IconPreviewPanel::selectionModified(Selection *selection, guint flags) +{ + if (getDesktop() && Inkscape::Preferences::get()->getBool("/iconpreview/autoRefresh", true)) { + queueRefresh(); + } +} + +void IconPreviewPanel::documentReplaced() +{ + removeDrawing(); + drawing_doc = getDocument(); + if (drawing_doc) { + drawing = new Inkscape::Drawing(); + visionkey = SPItem::display_key_new(1); + drawing->setRoot(drawing_doc->getRoot()->invoke_show(*drawing, visionkey, SP_ITEM_SHOW_DISPLAY)); + docDesConn = drawing_doc->connectDestroy([=]() { removeDrawing(); }); + queueRefresh(); + } +} + +/// Safely delete the Inkscape::Drawing and references to it. +void IconPreviewPanel::removeDrawing() +{ + docDesConn.disconnect(); + if (!drawing) { + return; + } + drawing_doc->getRoot()->invoke_hide(visionkey); + delete drawing; + drawing = nullptr; + drawing_doc = nullptr; +} + +void IconPreviewPanel::refreshPreview() +{ + auto document = getDocument(); + if (!timer) { + timer = new Glib::Timer(); + } + if (timer->elapsed() < minDelay) { +#if ICON_VERBOSE + g_message( "%s Deferring refresh as too soon. calling queueRefresh()", getTimestr().c_str() ); +#endif //ICON_VERBOSE + // Do not refresh too quickly + queueRefresh(); + } else if (document) { +#if ICON_VERBOSE + g_message( "%s Refreshing preview.", getTimestr().c_str() ); +#endif // ICON_VERBOSE + bool hold = Inkscape::Preferences::get()->getBool("/iconpreview/selectionHold", true); + SPObject *target = nullptr; + if ( selectionButton && selectionButton->get_active() ) + { + target = (hold && !targetId.empty()) ? document->getObjectById( targetId.c_str() ) : nullptr; + if ( !target ) { + targetId.clear(); + if (auto selection = getSelection()) { + for (auto item : selection->items()) { + if (gchar const *id = item->getId()) { + targetId = id; + target = item; + } + } + } + } + } else { + target = getDesktop()->getDocument()->getRoot(); + } + if (target) { + renderPreview(target); + } +#if ICON_VERBOSE + g_message( "%s resetting timer", getTimestr().c_str() ); +#endif // ICON_VERBOSE + timer->reset(); + } +} + +bool IconPreviewPanel::refreshCB() +{ + bool callAgain = true; + if (!timer) { + timer = new Glib::Timer(); + } + if ( timer->elapsed() > minDelay ) { +#if ICON_VERBOSE + g_message( "%s refreshCB() timer has progressed", getTimestr().c_str() ); +#endif // ICON_VERBOSE + callAgain = false; + refreshPreview(); +#if ICON_VERBOSE + g_message( "%s refreshCB() setting pending false", getTimestr().c_str() ); +#endif // ICON_VERBOSE + pending = false; + } + return callAgain; +} + +void IconPreviewPanel::queueRefresh() +{ + if (!pending) { + pending = true; +#if ICON_VERBOSE + g_message( "%s queueRefresh() Setting pending true", getTimestr().c_str() ); +#endif // ICON_VERBOSE + if (!timer) { + timer = new Glib::Timer(); + } + Glib::signal_idle().connect( sigc::mem_fun(this, &IconPreviewPanel::refreshCB), Glib::PRIORITY_DEFAULT_IDLE ); + } +} + +void IconPreviewPanel::modeToggled() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool selectionOnly = (selectionButton && selectionButton->get_active()); + prefs->setBool("/iconpreview/selectionOnly", selectionOnly); + if ( !selectionOnly ) { + targetId.clear(); + } + + refreshPreview(); +} + +void overlayPixels(guchar *px, int width, int height, int stride, + unsigned r, unsigned g, unsigned b) +{ + int bytesPerPixel = 4; + int spacing = 4; + for ( int y = 0; y < height; y += spacing ) { + guchar *ptr = px + y * stride; + for ( int x = 0; x < width; x += spacing ) { + *(ptr++) = r; + *(ptr++) = g; + *(ptr++) = b; + *(ptr++) = 0xff; + + ptr += bytesPerPixel * (spacing - 1); + } + } + + if ( width > 1 && height > 1 ) { + // point at the last pixel + guchar *ptr = px + ((height-1) * stride) + ((width - 1) * bytesPerPixel); + + if ( width > 2 ) { + px[4] = r; + px[5] = g; + px[6] = b; + px[7] = 0xff; + + ptr[-12] = r; + ptr[-11] = g; + ptr[-10] = b; + ptr[-9] = 0xff; + } + + ptr[-4] = r; + ptr[-3] = g; + ptr[-2] = b; + ptr[-1] = 0xff; + + px[0 + stride] = r; + px[1 + stride] = g; + px[2 + stride] = b; + px[3 + stride] = 0xff; + + ptr[0 - stride] = r; + ptr[1 - stride] = g; + ptr[2 - stride] = b; + ptr[3 - stride] = 0xff; + + if ( height > 2 ) { + ptr[0 - stride * 3] = r; + ptr[1 - stride * 3] = g; + ptr[2 - stride * 3] = b; + ptr[3 - stride * 3] = 0xff; + } + } +} + +// takes doc, drawing, icon, and icon name to produce pixels +extern "C" guchar * +sp_icon_doc_icon( SPDocument *doc, Inkscape::Drawing &drawing, + gchar const *name, unsigned psize, + unsigned &stride) +{ + bool const dump = Inkscape::Preferences::get()->getBool("/debug/icons/dumpSvg"); + guchar *px = nullptr; + + if (doc) { + SPObject *object = doc->getObjectById(name); + if (object && SP_IS_ITEM(object)) { + SPItem *item = SP_ITEM(object); + // Find bbox in document + Geom::OptRect dbox = item->documentVisualBounds(); + + if ( object->parent == nullptr ) + { + dbox = Geom::Rect(Geom::Point(0, 0), + Geom::Point(doc->getWidth().value("px"), doc->getHeight().value("px"))); + } + + /* This is in document coordinates, i.e. pixels */ + if ( dbox ) { + /* Update to renderable state */ + double sf = 1.0; + drawing.root()->setTransform(Geom::Scale(sf)); + drawing.update(); + /* Item integer bbox in points */ + // NOTE: previously, each rect coordinate was rounded using floor(c + 0.5) + Geom::IntRect ibox = dbox->roundOutwards(); + + if ( dump ) { + g_message( " box --'%s' (%f,%f)-(%f,%f)", name, (double)ibox.left(), (double)ibox.top(), (double)ibox.right(), (double)ibox.bottom() ); + } + + /* Find button visible area */ + int width = ibox.width(); + int height = ibox.height(); + + if ( dump ) { + g_message( " vis --'%s' (%d,%d)", name, width, height ); + } + + { + int block = std::max(width, height); + if (block != static_cast<int>(psize) ) { + if ( dump ) { + g_message(" resizing" ); + } + sf = (double)psize / (double)block; + + drawing.root()->setTransform(Geom::Scale(sf)); + drawing.update(); + + auto scaled_box = *dbox * Geom::Scale(sf); + ibox = scaled_box.roundOutwards(); + if ( dump ) { + g_message( " box2 --'%s' (%f,%f)-(%f,%f)", name, (double)ibox.left(), (double)ibox.top(), (double)ibox.right(), (double)ibox.bottom() ); + } + + /* Find button visible area */ + width = ibox.width(); + height = ibox.height(); + if ( dump ) { + g_message( " vis2 --'%s' (%d,%d)", name, width, height ); + } + } + } + + Geom::IntPoint pdim(psize, psize); + int dx, dy; + //dx = (psize - width) / 2; + //dy = (psize - height) / 2; + dx=dy=psize; + dx=(dx-width)/2; // watch out for psize, since 'unsigned'-'signed' can cause problems if the result is negative + dy=(dy-height)/2; + Geom::IntRect area = Geom::IntRect::from_xywh(ibox.min() - Geom::IntPoint(dx,dy), pdim); + /* Actual renderable area */ + Geom::IntRect ua = *Geom::intersect(ibox, area); + + if ( dump ) { + g_message( " area --'%s' (%f,%f)-(%f,%f)", name, (double)area.left(), (double)area.top(), (double)area.right(), (double)area.bottom() ); + g_message( " ua --'%s' (%f,%f)-(%f,%f)", name, (double)ua.left(), (double)ua.top(), (double)ua.right(), (double)ua.bottom() ); + } + + stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, psize); + + /* Set up pixblock */ + px = g_new(guchar, stride * psize); + memset(px, 0x00, stride * psize); + + /* Render */ + cairo_surface_t *s = cairo_image_surface_create_for_data(px, + CAIRO_FORMAT_ARGB32, psize, psize, stride); + Inkscape::DrawingContext dc(s, ua.min()); + + auto bg = doc->getPageManager().getDefaultBackgroundColor(); + + cairo_t *cr = cairo_create(s); + cairo_set_source_rgba(cr, bg[0], bg[1], bg[2], bg[3]); + cairo_rectangle(cr, 0, 0, psize, psize); + cairo_fill(cr); + cairo_save(cr); + cairo_destroy(cr); + + drawing.render(dc, ua); + cairo_surface_destroy(s); + + // convert to GdkPixbuf format + convert_pixels_argb32_to_pixbuf(px, psize, psize, stride); + + if ( Inkscape::Preferences::get()->getBool("/debug/icons/overlaySvg") ) { + overlayPixels( px, psize, psize, stride, 0x00, 0x00, 0xff ); + } + } + } + } + + return px; +} // end of sp_icon_doc_icon() + + +void IconPreviewPanel::renderPreview( SPObject* obj ) +{ + SPDocument * doc = obj->document; + gchar const * id = obj->getId(); + if ( !renderTimer ) { + renderTimer = new Glib::Timer(); + } + renderTimer->reset(); + +#if ICON_VERBOSE + g_message("%s setting up to render '%s' as the icon", getTimestr().c_str(), id ); +#endif // ICON_VERBOSE + + for ( int i = 0; i < numEntries; i++ ) { + unsigned unused; + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, sizes[i]); + guchar *px = sp_icon_doc_icon(doc, *drawing, id, sizes[i], unused); +// g_message( " size %d %s", sizes[i], (px ? "worked" : "failed") ); + if ( px ) { + memcpy( pixMem[i], px, sizes[i] * stride ); + g_free( px ); + px = nullptr; + } else { + memset( pixMem[i], 0, sizes[i] * stride ); + } + images[i]->set(images[i]->get_pixbuf()); + // images[i]->queue_draw(); + } + updateMagnify(); + + renderTimer->stop(); + minDelay = std::max( 0.1, renderTimer->elapsed() * 3.0 ); +#if ICON_VERBOSE + g_message(" render took %f seconds.", renderTimer->elapsed()); +#endif // ICON_VERBOSE +} + +void IconPreviewPanel::updateMagnify() +{ + Glib::RefPtr<Gdk::Pixbuf> buf = images[hot]->get_pixbuf()->scale_simple( 128, 128, Gdk::INTERP_NEAREST ); + magLabel.set_label( *labels[hot] ); + magnified.set( buf ); + // magnified.queue_draw(); + // magnified.get_parent()->queue_draw(); +} + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/icon-preview.h b/src/ui/dialog/icon-preview.h new file mode 100644 index 0000000..3ab7624 --- /dev/null +++ b/src/ui/dialog/icon-preview.h @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A simple dialog for previewing icon representation. + */ +/* Authors: + * Jon A. Cruz + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004,2005 The Inkscape Organization + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_ICON_PREVIEW_H +#define SEEN_ICON_PREVIEW_H + +#include <gtkmm/box.h> +#include <gtkmm/button.h> +#include <gtkmm/image.h> +#include <gtkmm/label.h> +#include <gtkmm/paned.h> +#include <gtkmm/togglebutton.h> +#include <gtkmm/toggletoolbutton.h> + +#include "ui/dialog/dialog-base.h" + +class SPObject; +namespace Glib { +class Timer; +} + +namespace Inkscape { +class Drawing; +namespace UI { +namespace Dialog { + + +/** + * A panel that displays an icon preview + */ +class IconPreviewPanel : public DialogBase +{ +public: + IconPreviewPanel(); + //IconPreviewPanel(Glib::ustring const &label); + ~IconPreviewPanel() override; + + static IconPreviewPanel& getInstance(); + void selectionModified(Selection *selection, guint flags) override; + void documentReplaced() override; + + void refreshPreview(); + void modeToggled(); + +private: + IconPreviewPanel(IconPreviewPanel const &) = delete; // no copy + IconPreviewPanel &operator=(IconPreviewPanel const &) = delete; // no assign + + Drawing *drawing; + SPDocument *drawing_doc; + unsigned int visionkey; + Glib::Timer *timer; + Glib::Timer *renderTimer; + bool pending; + gdouble minDelay; + + Gtk::Box iconBox; + Gtk::Paned splitter; + Glib::ustring targetId; + int hot; + int numEntries; + int* sizes; + + Gtk::Image magnified; + Gtk::Label magLabel; + + Gtk::ToggleButton *selectionButton; + + guchar** pixMem; + Gtk::Image** images; + Glib::ustring** labels; + Gtk::ToggleToolButton** buttons; + sigc::connection docModConn; + sigc::connection docDesConn; + + void setDocument( SPDocument *document ); + void removeDrawing(); + void on_button_clicked(int which); + void renderPreview( SPObject* obj ); + void updateMagnify(); + void queueRefresh(); + bool refreshCB(); +}; + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + + +#endif // SEEN_ICON_PREVIEW_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/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp new file mode 100644 index 0000000..fed10b7 --- /dev/null +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -0,0 +1,3707 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkscape Preferences dialog - implementation. + */ +/* Authors: + * Carl Hetherington + * Marco Scholten + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Bruno Dilly <bruno.dilly@gmail.com> + *F + * Copyright (C) 2004-2013 Authors + * + * 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 "inkscape-preferences.h" + +#include <fstream> +#include <strings.h> +#include <sstream> +#include <iomanip> +#include <algorithm> + +#include <gio/gio.h> +#include <gtk/gtk.h> +#include <glibmm/i18n.h> +#include <gtkmm.h> + +#include "auto-save.h" +#include "cms-system.h" +#include "document.h" +#include "enums.h" +#include "inkscape-window.h" +#include "inkscape.h" +#include "message-stack.h" +#include "path-prefix.h" +#include "preferences.h" +#include "selcue.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "style.h" + +#include "display/control/canvas-grid.h" +#include "display/nr-filter-gaussian.h" + +#include "extension/internal/gdkpixbuf-input.h" + +#include "io/resource.h" + +#include "object/color-profile.h" + +#include "svg/svg-color.h" + +#include "ui/desktop/menubar.h" +#include "ui/interface.h" +#include "ui/shortcuts.h" +#include "ui/modifiers.h" +#include "ui/util.h" +#include "ui/widget/style-swatch.h" +#include "ui/widget/canvas.h" +#include "ui/themes.h" + +#include "util/trim.h" + +#include "widgets/desktop-widget.h" +#include "widgets/toolbox.h" +#include "widgets/spw-utilities.h" + +#include <gtkmm/accelgroup.h> + +#if WITH_GSPELL +# include "ui/dialog/spellcheck.h" // for get_available_langs +# ifdef _WIN32 +# include <windows.h> +# endif +#endif + +namespace Inkscape { +namespace UI { +namespace Dialog { + +using Inkscape::UI::Widget::DialogPage; +using Inkscape::UI::Widget::PrefCheckButton; +using Inkscape::UI::Widget::PrefRadioButton; +using Inkscape::UI::Widget::PrefItem; +using Inkscape::UI::Widget::PrefRadioButtons; +using Inkscape::UI::Widget::PrefSpinButton; +using Inkscape::UI::Widget::StyleSwatch; +using Inkscape::CMSSystem; +using Inkscape::IO::Resource::get_filename; +using Inkscape::IO::Resource::UIS; + +std::function<Gtk::Image*()> reset_icon = []() { + auto image = Gtk::make_managed<Gtk::Image>(); + image->set_from_icon_name("reset", Gtk::ICON_SIZE_BUTTON); + image->set_opacity(0.6); + image->set_tooltip_text(_("Requires restart to take effect")); + return image; +}; + +/** + * Case-insensitive and unicode normalized search of `pattern` in `string`. + * + * @param pattern Text to find + * @param string Text to search in + * @param[out] score Match score between 0.0 (not found) and 1.0 (pattern == string) + * @return True if `pattern` is a substring of `string`. + */ +static bool fuzzy_search(Glib::ustring const &pattern, Glib::ustring const &string, float &score) +{ + Glib::ustring norm_patt = pattern.lowercase().normalize(); + Glib::ustring norm_str = string.lowercase().normalize(); + bool found = (norm_str.find(norm_patt) != Glib::ustring::npos); + score = found ? (float)pattern.size() / (float)string.size() : 0; + return score > 0.0 ? true : false; +} + +/** + * Case-insensitive and unicode normalized search of `pattern` in `string`. + * + * @param pattern Text to find + * @param string Text to search in + * @return True if `pattern` is a substring of `string`. + */ +static bool fuzzy_search(Glib::ustring const &pattern, Glib::ustring const &string) +{ + float score; + return fuzzy_search(pattern, string, score); +} + +/** + * Get number of child Labels that match a key in a widget + * + * @param key Text to find + * @param widget Gtk::Widget to search in + * @return Number of matches found + */ +static int get_num_matches(Glib::ustring const &key, Gtk::Widget *widget) +{ + int matches = 0; + if (auto label = dynamic_cast<Gtk::Label *>(widget)) { + if (fuzzy_search(key, label->get_text().lowercase())) { + // set score + ++matches; + } + } + std::vector<Gtk::Widget *> children; + if (auto container = dynamic_cast<Gtk::Container *>(widget)) { + children = container->get_children(); + } else { + children = widget->list_mnemonic_labels(); + } + for (auto *child : children) { + if (int child_matches = get_num_matches(key, child)) { + matches += child_matches; + } + } + return matches; +} + +/** + * Add CSS-based highlight-class and pango highlight to a Gtk::Label + * + * @param label Label to add highlight to + * @param key Text to add pango highlight + */ +void InkscapePreferences::add_highlight(Gtk::Label *label, Glib::ustring const &key) +{ + Glib::ustring text = label->get_text(); + Glib::ustring const n_text = text.lowercase().normalize(); + Glib::ustring const n_key = key.lowercase().normalize(); + label->get_style_context()->add_class("highlight"); + auto const pos = n_text.find(n_key); + auto const len = n_key.size(); + text = Glib::Markup::escape_text(text.substr(0, pos)) + "<span weight=\"bold\" underline=\"single\">" + + Glib::Markup::escape_text(text.substr(pos, len)) + "</span>" + + Glib::Markup::escape_text(text.substr(pos + len)); + label->set_markup(text); +} + +/** + * Remove CSS-based highlight-class and pango highlight from a Gtk::Label + * + * @param label Label to remove highlight from + * @param key Text to remove pango highlight from + */ +void InkscapePreferences::remove_highlight(Gtk::Label *label) +{ + if (label->get_use_markup()) { + Glib::ustring text = label->get_text(); + label->set_text(text); + label->get_style_context()->remove_class("highlight"); + } +} + +InkscapePreferences::InkscapePreferences() + : DialogBase("/dialogs/preferences", "Preferences"), + _minimum_width(0), + _minimum_height(0), + _natural_width(900), + _natural_height(700), + _current_page(nullptr), + _init(true) +{ + //get the width of a spinbutton + Inkscape::UI::Widget::SpinButton* sb = new Inkscape::UI::Widget::SpinButton; + sb->set_width_chars(6); + add(*sb); + show_all_children(); + Gtk::Requisition sreq; + Gtk::Requisition sreq_natural; + sb->get_preferred_size(sreq_natural, sreq); + _sb_width = sreq.width; + remove(*sb); + delete sb; + + //Main HBox + auto hbox_list_page = Gtk::manage(new Gtk::Box()); + hbox_list_page->set_border_width(12); + hbox_list_page->set_spacing(12); + add(*hbox_list_page); + + //Pagelist + auto list_box = Gtk::manage(new Gtk::Box(Gtk::Orientation::ORIENTATION_VERTICAL, 3)); + Gtk::ScrolledWindow* scrolled_window = Gtk::manage(new Gtk::ScrolledWindow()); + _search.set_valign(Gtk::Align::ALIGN_START); + list_box->pack_start(_search, false, true, 0); + list_box->pack_start(*scrolled_window, false, true, 0); + hbox_list_page->pack_start(*list_box, false, true, 0); + _page_list.set_headers_visible(false); + scrolled_window->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + scrolled_window->set_valign(Gtk::Align::ALIGN_FILL); + scrolled_window->set_propagate_natural_width(); + scrolled_window->set_propagate_natural_height(); + scrolled_window->add(_page_list); + scrolled_window->set_vexpand_set(true); + scrolled_window->set_vexpand(true); + scrolled_window->set_shadow_type(Gtk::SHADOW_IN); + _page_list_model = Gtk::TreeStore::create(_page_list_columns); + _page_list_model_filter = Gtk::TreeModelFilter::create(_page_list_model); + _page_list_model_sort = Gtk::TreeModelSort::create(_page_list_model_filter); + _page_list_model_sort->set_sort_column(_page_list_columns._col_name, Gtk::SortType::SORT_ASCENDING); + _page_list.set_enable_search(false); + _page_list.set_model(_page_list_model_sort); + _page_list.append_column("name",_page_list_columns._col_name); + Glib::RefPtr<Gtk::TreeSelection> page_list_selection = _page_list.get_selection(); + page_list_selection->signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::on_pagelist_selection_changed)); + page_list_selection->set_mode(Gtk::SELECTION_BROWSE); + + // Search + _page_list.set_search_column(-1); // this disables pop-up search! + _search.signal_search_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::on_search_changed)); + _search.set_tooltip_text("Search"); + _page_list_model_sort->set_sort_func( + _page_list_columns._col_name, [=](Gtk::TreeModel::iterator const &a, Gtk::TreeModel::iterator const &b) -> int { + float score_a, score_b; + Glib::ustring key = _search.get_text().lowercase(); + if (key == "") { + return -1; + } + Glib::ustring label_a = a->get_value(_page_list_columns._col_name).lowercase(); + Glib::ustring label_b = b->get_value(_page_list_columns._col_name).lowercase(); + auto *grid_a = a->get_value(_page_list_columns._col_page); + auto *grid_b = b->get_value(_page_list_columns._col_page); + int num_res_a = num_widgets_in_grid(key, grid_a); + int num_res_b = num_widgets_in_grid(key, grid_b); + fuzzy_search(key, label_a, score_a); + fuzzy_search(key, label_b, score_b); + if (score_a > score_b) { + return -1; + } else if (score_a < score_b) { + return 1; + } else if (num_res_a >= num_res_b) { + return -1; + } else if (num_res_a < num_res_b) { + return 1; + } else { + return a->get_value(_page_list_columns._col_id) > b->get_value(_page_list_columns._col_id) ? -1 : 1; + } + }); + + _search.signal_next_match().connect([=]() { + if (_search_results.size() > 0) { + Gtk::TreeIter curr = _page_list.get_selection()->get_selected(); + auto _page_list_selection = _page_list.get_selection(); + auto next = get_next_result(curr); + if (next) { + _page_list.get_model()->get_iter(next); + _page_list.scroll_to_cell(next, *_page_list.get_column(0)); + _page_list.set_cursor(next); + } + } + }); + + _search.signal_previous_match().connect([=]() { + if (_search_results.size() > 0) { + Gtk::TreeIter curr = _page_list.get_selection()->get_selected(); + auto _page_list_selection = _page_list.get_selection(); + auto prev = get_prev_result(curr); + if (prev) { + _page_list.get_model()->get_iter(prev); + _page_list.scroll_to_cell(prev, *_page_list.get_column(0)); + _page_list.set_cursor(prev); + } + } + }); + + _search.signal_key_press_event().connect(sigc::mem_fun(*this, &InkscapePreferences::on_navigate_key_press)); + + _page_list_model_filter->set_visible_func([=](Gtk::TreeModel::const_iterator const &row) -> bool { + auto key_lower = _search.get_text().lowercase(); + return recursive_filter(key_lower, row); + }); + + //Pages + auto vbox_page = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + Gtk::Frame* title_frame = Gtk::manage(new Gtk::Frame()); + + Gtk::ScrolledWindow* pageScroller = Gtk::manage(new Gtk::ScrolledWindow()); + pageScroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + pageScroller->set_propagate_natural_width(); + pageScroller->set_propagate_natural_height(); + pageScroller->add(*vbox_page); + hbox_list_page->pack_start(*pageScroller, true, true, 0); + + title_frame->add(_page_title); + vbox_page->pack_start(*title_frame, false, false, 0); + vbox_page->pack_start(_page_frame, true, true, 0); + _page_frame.set_shadow_type(Gtk::SHADOW_NONE); + title_frame->set_shadow_type(Gtk::SHADOW_NONE); + + initPageTools(); + initPageUI(); + initPageBehavior(); + initPageIO(); + + initPageSystem(); + initPageBitmaps(); + initPageRendering(); + initPageSpellcheck(); + + signal_map().connect(sigc::mem_fun(*this, &InkscapePreferences::showPage)); + + //calculate the size request for this dialog + _page_list.expand_all(); + _page_list_model->foreach_iter(sigc::mem_fun(*this, &InkscapePreferences::GetSizeRequest)); + _page_list.collapse_all(); + + // Set Custom theme + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _theme_oberver = prefs->createObserver("/theme/", [=]() { + prefs->setString("/options/boot/theme", "custom"); + }); +} + +InkscapePreferences::~InkscapePreferences() += default; + +/** + * Get child Labels that match a key in a widget grid + * and add the results to global _search_results vector + * + * @param key Text to find + * @param widget Gtk::Widget to search in + */ +void InkscapePreferences::get_widgets_in_grid(Glib::ustring const &key, Gtk::Widget *widget) +{ + if (auto label = dynamic_cast<Gtk::Label *>(widget)) { + if (fuzzy_search(key, label->get_text())) { + _search_results.push_back(widget); + } + } + std::vector<Gtk::Widget *> children; + if (auto container = dynamic_cast<Gtk::Container *>(widget)) { + children = container->get_children(); + } else { + children = widget->list_mnemonic_labels(); + } + for (auto *child : children) { + get_widgets_in_grid(key, child); + } +} + +/** + * Get number of child Labels that match a key in a widget grid + * and add the results to global _search_results vector + * + * @param key Text to find + * @param widget Gtk::Widget to search in + * @return Number of results found + */ +int InkscapePreferences::num_widgets_in_grid(Glib::ustring const &key, Gtk::Widget *widget) +{ + int results = 0; + if (auto label = dynamic_cast<Gtk::Label *>(widget)) { + float score; + if (fuzzy_search(key, label->get_text(), score)) { + ++results; + } + } + std::vector<Gtk::Widget *> children; + if (auto container = dynamic_cast<Gtk::Container *>(widget)) { + children = container->get_children(); + } else { + children = widget->list_mnemonic_labels(); + } + for (auto *child : children) { + int num_child = num_widgets_in_grid(key, child); + results += num_child; + } + return results; +} + + +bool InkscapePreferences::on_outline_overlay_changed(GdkEventFocus * /* focus_event */) +{ + if (auto *desktop = SP_ACTIVE_DESKTOP) { + desktop->getCanvas()->redraw_all(); + } + return false; + +} + +/** + * Implementation of the search functionality executes each time + * search entry is changed + */ +void InkscapePreferences::on_search_changed() +{ + _num_results = 0; + if (_search_results.size() > 0) { + for (auto *result : _search_results) { + remove_highlight(static_cast<Gtk::Label *>(result)); + } + _search_results.clear(); + } + auto key = _search.get_text(); + _page_list_model_filter->refilter(); + // get first iter + Gtk::TreeModel::Children children = _page_list.get_model()->children(); + Gtk::TreeModel::iterator iter = children.begin(); + + highlight_results(key, iter); + goto_first_result(); + if (key == "") { + Gtk::TreeModel::Children children = _page_list.get_model()->children(); + Gtk::TreeModel::iterator iter = children.begin(); + _page_list.scroll_to_cell(Gtk::TreePath(iter), *_page_list.get_column(0)); + _page_list.set_cursor(Gtk::TreePath(iter)); + } else if (_num_results == 0 && key != "") { + _page_list.set_has_tooltip(false); + _show_all = true; + _page_list_model_filter->refilter(); + _show_all = false; + show_not_found(); + } else { + _page_list.expand_all(); + } +} + +/** + * Select the first row in the tree that has a result + */ +void InkscapePreferences::goto_first_result() +{ + auto key = _search.get_text(); + if (_num_results > 0) { + Gtk::TreeIter curr = _page_list.get_model()->children().begin(); + if (fuzzy_search(key, curr->get_value(_page_list_columns._col_name)) || + get_num_matches(key, curr->get_value(_page_list_columns._col_page)) > 0) { + _page_list.scroll_to_cell(Gtk::TreePath(curr), *_page_list.get_column(0)); + _page_list.set_cursor(Gtk::TreePath(curr)); + } else { + auto next = get_next_result(curr); + if (next) { + _page_list.scroll_to_cell(next, *_page_list.get_column(0)); + _page_list.set_cursor(next); + } + } + } +} + +/** + * Look for the immediate next row in the tree that contains a search result + * + * @param iter Current iter the that is selected in the tree + * @param check_children Bool whether to check if the children of the iter + * contain search result + * @return Immediate next row than contains a search result + */ +Gtk::TreePath InkscapePreferences::get_next_result(Gtk::TreeIter &iter, bool check_children) +{ + auto key = _search.get_text(); + Gtk::TreePath path = Gtk::TreePath(iter); + if (iter->children().begin() && check_children) { // check for search results in children + auto child = iter->children().begin(); + _page_list.expand_row(path, false); + if (fuzzy_search(key, child->get_value(_page_list_columns._col_name)) || + get_num_matches(key, child->get_value(_page_list_columns._col_page)) > 0) { + return Gtk::TreePath(child); + } else { + return get_next_result(child); + } + } else { + ++iter; // go to next row + if (iter) { // if row exists + if (fuzzy_search(key, iter->get_value(_page_list_columns._col_name)) || + get_num_matches(key, iter->get_value(_page_list_columns._col_page))) { + path.next(); + return path; + } else { + return get_next_result(iter); + } + } else if (path.up() && path) { + path.next(); + iter = _page_list.get_model()->get_iter(path); + if (iter) { + if (fuzzy_search(key, iter->get_value(_page_list_columns._col_name)) || + get_num_matches(key, iter->get_value(_page_list_columns._col_page))) { + return Gtk::TreePath(iter); + } else { + return get_next_result(iter); + } + } else { + path.up(); + if (path) { + iter = _page_list.get_model()->get_iter(path); + return get_next_result(iter, false); // dont check for children + } else { + return Gtk::TreePath(_page_list.get_model()->children().begin()); + } + } + } + } + assert(!iter); + return Gtk::TreePath(); +} + +/** + * Look for the immediate previous row in the tree that contains a search result + * + * @param iter Current iter that is selected in the tree + * @param check_children Bool whether to check if the children of the iter + * contain search result + * @return Immediate previous row than contains a search result + */ +Gtk::TreePath InkscapePreferences::get_prev_result(Gtk::TreeIter &iter, bool iterate) +{ + auto key = _search.get_text(); + Gtk::TreePath path = Gtk::TreePath(iter); + if (iterate) { + --iter; + } + if (iter) { + if (iter->children().begin()) { + auto child = iter->children().end(); + --child; + Gtk::TreePath path = Gtk::TreePath(iter); + _page_list.expand_row(path, false); + return get_prev_result(child, false); + } else if (fuzzy_search(key, iter->get_value(_page_list_columns._col_name)) || + get_num_matches(key, iter->get_value(_page_list_columns._col_page))) { + return (Gtk::TreePath(iter)); + } else { + return get_prev_result(iter); + } + } else if (path.up()) { + if (path) { + iter = _page_list.get_model()->get_iter(path); + if (fuzzy_search(key, iter->get_value(_page_list_columns._col_name)) || + get_num_matches(key, iter->get_value(_page_list_columns._col_page))) { + return path; + } else { + return get_prev_result(iter); + } + } else { + auto lastIter = _page_list.get_model()->children().end(); + --lastIter; + return get_prev_result(lastIter, false); + } + } else { + return Gtk::TreePath(iter); + } +} + +/** + * Handle key F3 and Shift+F3 key press events to navigate to the next search + * result + * + * @param evt event object + * @return Always returns False to label the key press event as handled, this + * prevents the search bar from retaining focus for other keyboard event. + */ +bool InkscapePreferences::on_navigate_key_press(GdkEventKey *evt) +{ + if (evt->keyval == GDK_KEY_F3) { + if (_search_results.size() > 0) { + GdkModifierType modmask = gtk_accelerator_get_default_mod_mask(); + if ((evt->state & modmask) == Gdk::SHIFT_MASK) { + Gtk::TreeIter curr = _page_list.get_selection()->get_selected(); + auto _page_list_selection = _page_list.get_selection(); + auto prev = get_prev_result(curr); + if (prev) { + _page_list.scroll_to_cell(prev, *_page_list.get_column(0)); + _page_list.set_cursor(prev); + } + } else { + Gtk::TreeIter curr = _page_list.get_selection()->get_selected(); + auto _page_list_selection = _page_list.get_selection(); + auto next = get_next_result(curr); + if (next) { + _page_list.scroll_to_cell(next, *_page_list.get_column(0)); + _page_list.set_cursor(next); + } + } + } + } + return false; +} + +/** + * Add highlight to all the search results + * + * @param key Text to search + * @param iter pointing to the first row of the tree + */ +void InkscapePreferences::highlight_results(Glib::ustring const &key, Gtk::TreeModel::iterator &iter) +{ + Gtk::TreeModel::Path path; + Glib::ustring Txt; + while (iter) { + Gtk::TreeModel::Row row = *iter; + auto *grid = row->get_value(_page_list_columns._col_page); + get_widgets_in_grid(key, grid); + if (_search_results.size() > 0) { + for (auto *result : _search_results) { + // underline and highlight + add_highlight(static_cast<Gtk::Label *>(result), key); + } + } + if (iter->children()) { + auto children = iter->children(); + auto child_iter = children.begin(); + highlight_results(key, child_iter); + } + iter++; + } +} + +/** + * Filter function for the search functionality to show only matching + * rows or rows with matching search resuls in the tree view + * + * @param key Text to search for + * @param iter iter pointing to the first row of the tree view + * @return True if the row is to be shown else return False + */ +bool InkscapePreferences::recursive_filter(Glib::ustring &key, Gtk::TreeModel::const_iterator const &iter) +{ + if(_show_all) + return true; + auto row_label = iter->get_value(_page_list_columns._col_name).lowercase(); + if (key == "") { + return true; + } + if (fuzzy_search(key, row_label)) { + ++_num_results; + return true; + } + auto *grid = iter->get_value(_page_list_columns._col_page); + int matches = get_num_matches(key, grid); + _num_results += matches; + if (matches) { + return true; + } + auto child = iter->children().begin(); + if (child) { + for (auto inner = child; inner; ++inner) { + if (recursive_filter(key, inner)) { + return true; + } + } + } + return false; +} + +Gtk::TreeModel::iterator InkscapePreferences::AddPage(DialogPage& p, Glib::ustring title, int id) +{ + return AddPage(p, title, Gtk::TreeModel::iterator() , id); +} + +Gtk::TreeModel::iterator InkscapePreferences::AddPage(DialogPage& p, Glib::ustring title, Gtk::TreeModel::iterator parent, int id) +{ + Gtk::TreeModel::iterator iter; + if (parent) + iter = _page_list_model->append((*parent).children()); + else + iter = _page_list_model->append(); + Gtk::TreeModel::Row row = *iter; + row[_page_list_columns._col_name] = title; + row[_page_list_columns._col_id] = id; + row[_page_list_columns._col_page] = &p; + return iter; +} + +void InkscapePreferences::AddSelcueCheckbox(DialogPage &p, Glib::ustring const &prefs_path, bool def_value) +{ + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Show selection cue"), prefs_path + "/selcue", def_value); + p.add_line( false, "", *cb, "", _("Whether selected objects display a selection cue (the same as in selector)")); +} + +void InkscapePreferences::AddGradientCheckbox(DialogPage &p, Glib::ustring const &prefs_path, bool def_value) +{ + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Enable gradient editing"), prefs_path + "/gradientdrag", def_value); + p.add_line( false, "", *cb, "", _("Whether selected objects display gradient editing controls")); +} + +void InkscapePreferences::AddConvertGuidesCheckbox(DialogPage &p, Glib::ustring const &prefs_path, bool def_value) { + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Conversion to guides uses edges instead of bounding box"), prefs_path + "/convertguides", def_value); + p.add_line( false, "", *cb, "", _("Converting an object to guides places these along the object's true edges (imitating the object's shape), not along the bounding box")); +} + +void InkscapePreferences::AddDotSizeSpinbutton(DialogPage &p, Glib::ustring const &prefs_path, double def_value) +{ + PrefSpinButton* sb = Gtk::manage( new PrefSpinButton); + sb->init ( prefs_path + "/dot-size", 0.0, 1000.0, 0.1, 10.0, def_value, false, false); + p.add_line( false, _("Ctrl+click _dot size:"), *sb, _("times current stroke width"), + _("Size of dots created with Ctrl+click (relative to current stroke width)"), + false ); +} + +void InkscapePreferences::AddBaseSimplifySpinbutton(DialogPage &p, Glib::ustring const &prefs_path, double def_value) +{ + PrefSpinButton* sb = Gtk::manage( new PrefSpinButton); + sb->init ( prefs_path + "/base-simplify", 0.0, 100.0, 1.0, 10.0, def_value, false, false); + p.add_line( false, _("Base simplify:"), *sb, _("on dynamic LPE simplify"), + _("Base simplify of dynamic LPE based simplify"), + false ); +} + + +static void StyleFromSelectionToTool(Glib::ustring const &prefs_path, StyleSwatch *swatch) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop == nullptr) + return; + + Inkscape::Selection *selection = desktop->getSelection(); + + if (selection->isEmpty()) { + desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, + _("<b>No objects selected</b> to take the style from.")); + return; + } + SPItem *item = selection->singleItem(); + if (!item) { + /* TODO: If each item in the selection has the same style then don't consider it an error. + * Maybe we should try to handle multiple selections anyway, e.g. the intersection of the + * style attributes for the selected items. */ + desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, + _("<b>More than one object selected.</b> Cannot take style from multiple objects.")); + return; + } + + SPCSSAttr *css = take_style_from_item (item); + + if (!css) return; + + // remove black-listed properties + css = sp_css_attr_unset_blacklist (css); + + // only store text style for the text tool + if (prefs_path != "/tools/text") { + css = sp_css_attr_unset_text (css); + } + + // we cannot store properties with uris - they will be invalid in other documents + css = sp_css_attr_unset_uris (css); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setStyle(prefs_path + "/style", css); + sp_repr_css_attr_unref (css); + + // update the swatch + if (swatch) { + SPCSSAttr *css = prefs->getInheritedStyle(prefs_path + "/style"); + swatch->setStyle (css); + sp_repr_css_attr_unref(css); + } +} + +void InkscapePreferences::AddNewObjectsStyle(DialogPage &p, Glib::ustring const &prefs_path, const gchar *banner) +{ + if (banner) + p.add_group_header(banner); + else + p.add_group_header( _("Style of new objects")); + PrefRadioButton* current = Gtk::manage( new PrefRadioButton); + current->init ( _("Last used style"), prefs_path + "/usecurrent", 1, true, nullptr); + p.add_line( true, "", *current, "", + _("Apply the style you last set on an object")); + + PrefRadioButton* own = Gtk::manage( new PrefRadioButton); + auto hb = Gtk::manage( new Gtk::Box); + own->init ( _("This tool's own style:"), prefs_path + "/usecurrent", 0, false, current); + own->set_halign(Gtk::ALIGN_START); + own->set_valign(Gtk::ALIGN_START); + hb->add(*own); + p.set_tip( *own, _("Each tool may store its own style to apply to the newly created objects. Use the button below to set it.")); + p.add_line( true, "", *hb, "", ""); + + // style swatch + Gtk::Button* button = Gtk::manage( new Gtk::Button(_("Take from selection"), true)); + StyleSwatch *swatch = nullptr; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getInt(prefs_path + "/usecurrent")) { + button->set_sensitive(false); + } + + SPCSSAttr *css = prefs->getStyle(prefs_path + "/style"); + swatch = new StyleSwatch(css, _("This tool's style of new objects")); + hb->add(*swatch); + sp_repr_css_attr_unref(css); + + button->signal_clicked().connect( sigc::bind( sigc::ptr_fun(StyleFromSelectionToTool), prefs_path, swatch) ); + own->changed_signal.connect( sigc::mem_fun(*button, &Gtk::Button::set_sensitive) ); + p.add_line( true, "", *button, "", + _("Remember the style of the (first) selected object as this tool's style")); +} + +void InkscapePreferences::initPageTools() +{ + Gtk::TreeModel::iterator iter_tools = this->AddPage(_page_tools, _("Tools"), PREFS_PAGE_TOOLS); + this->AddPage(_page_selector, _("Selector"), iter_tools, PREFS_PAGE_TOOLS_SELECTOR); + this->AddPage(_page_node, _("Node"), iter_tools, PREFS_PAGE_TOOLS_NODE); + + // shapes + Gtk::TreeModel::iterator iter_shapes = this->AddPage(_page_shapes, _("Shapes"), iter_tools, PREFS_PAGE_TOOLS_SHAPES); + this->AddPage(_page_rectangle, _("Rectangle"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_RECT); + this->AddPage(_page_ellipse, _("Ellipse"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_ELLIPSE); + this->AddPage(_page_star, _("Star"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_STAR); + this->AddPage(_page_3dbox, _("3D Box"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_3DBOX); + this->AddPage(_page_spiral, _("Spiral"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_SPIRAL); + + this->AddPage(_page_pen, _("Pen"), iter_tools, PREFS_PAGE_TOOLS_PEN); + this->AddPage(_page_pencil, _("Pencil"), iter_tools, PREFS_PAGE_TOOLS_PENCIL); + this->AddPage(_page_calligraphy, _("Calligraphy"), iter_tools, PREFS_PAGE_TOOLS_CALLIGRAPHY); + this->AddPage(_page_text, C_("ContextVerb", "Text"), iter_tools, PREFS_PAGE_TOOLS_TEXT); + + this->AddPage(_page_gradient, _("Gradient"), iter_tools, PREFS_PAGE_TOOLS_GRADIENT); + this->AddPage(_page_dropper, _("Dropper"), iter_tools, PREFS_PAGE_TOOLS_DROPPER); + this->AddPage(_page_paintbucket, _("Paint Bucket"), iter_tools, PREFS_PAGE_TOOLS_PAINTBUCKET); + + this->AddPage(_page_tweak, _("Tweak"), iter_tools, PREFS_PAGE_TOOLS_TWEAK); + this->AddPage(_page_spray, _("Spray"), iter_tools, PREFS_PAGE_TOOLS_SPRAY); + this->AddPage(_page_eraser, _("Eraser"), iter_tools, PREFS_PAGE_TOOLS_ERASER); + this->AddPage(_page_connector, _("Connector"), iter_tools, PREFS_PAGE_TOOLS_CONNECTOR); +#ifdef WITH_LPETOOL + this->AddPage(_page_lpetool, _("LPE Tool"), iter_tools, PREFS_PAGE_TOOLS_LPETOOL); +#endif // WITH_LPETOOL + this->AddPage(_page_zoom, _("Zoom"), iter_tools, PREFS_PAGE_TOOLS_ZOOM); + this->AddPage(_page_measure, C_("ContextVerb", "Measure"), iter_tools, PREFS_PAGE_TOOLS_MEASURE); + + _page_tools.add_group_header( _("Bounding box to use")); + _t_bbox_visual.init ( _("Visual bounding box"), "/tools/bounding_box", 0, false, nullptr); // 0 means visual + _page_tools.add_line( true, "", _t_bbox_visual, "", + _("This bounding box includes stroke width, markers, filter margins, etc.")); + _t_bbox_geometric.init ( _("Geometric bounding box"), "/tools/bounding_box", 1, true, &_t_bbox_visual); // 1 means geometric + _page_tools.add_line( true, "", _t_bbox_geometric, "", + _("This bounding box includes only the bare path")); + + _page_tools.add_group_header( _("Conversion to guides")); + _t_cvg_keep_objects.init ( _("Keep objects after conversion to guides"), "/tools/cvg_keep_objects", false); + _page_tools.add_line( true, "", _t_cvg_keep_objects, "", + _("When converting an object to guides, don't delete the object after the conversion")); + _t_cvg_convert_whole_groups.init ( _("Treat groups as a single object"), "/tools/cvg_convert_whole_groups", false); + _page_tools.add_line( true, "", _t_cvg_convert_whole_groups, "", + _("Treat groups as a single object during conversion to guides rather than converting each child separately")); + + _pencil_average_all_sketches.init ( _("Average all sketches"), "/tools/freehand/pencil/average_all_sketches", false); + _calligrapy_keep_selected.init ( _("Select new path"), "/tools/calligraphic/keep_selected", true); + _connector_ignore_text.init( _("Don't attach connectors to text objects"), "/tools/connector/ignoretext", true); + + //Selector + + + AddSelcueCheckbox(_page_selector, "/tools/select", false); + AddGradientCheckbox(_page_selector, "/tools/select", false); + + _page_selector.add_group_header( _("When transforming, show")); + _t_sel_trans_obj.init ( _("Objects"), "/tools/select/show", "content", true, nullptr); + _page_selector.add_line( true, "", _t_sel_trans_obj, "", + _("Show the actual objects when moving or transforming")); + _t_sel_trans_outl.init ( _("Box outline"), "/tools/select/show", "outline", false, &_t_sel_trans_obj); + _page_selector.add_line( true, "", _t_sel_trans_outl, "", + _("Show only a box outline of the objects when moving or transforming")); + _page_selector.add_group_header( _("Per-object selection cue")); + _t_sel_cue_none.init ( C_("Selection cue", "None"), "/options/selcue/value", Inkscape::SelCue::NONE, false, nullptr); + _page_selector.add_line( true, "", _t_sel_cue_none, "", + _("No per-object selection indication")); + _t_sel_cue_mark.init ( _("Mark"), "/options/selcue/value", Inkscape::SelCue::MARK, true, &_t_sel_cue_none); + _page_selector.add_line( true, "", _t_sel_cue_mark, "", + _("Each selected object has a diamond mark in the top left corner")); + _t_sel_cue_box.init ( _("Box"), "/options/selcue/value", Inkscape::SelCue::BBOX, false, &_t_sel_cue_none); + _page_selector.add_line( true, "", _t_sel_cue_box, "", + _("Each selected object displays its bounding box")); + + //Node + AddSelcueCheckbox(_page_node, "/tools/nodes", true); + AddGradientCheckbox(_page_node, "/tools/nodes", true); + _page_node.add_group_header( _("Path outline")); + _t_node_pathoutline_color.init(_("Path outline color"), "/tools/nodes/highlight_color", 0xff0000ff); + _page_node.add_line( false, "", _t_node_pathoutline_color, "", _("Selects the color used for showing the path outline"), false); + _t_node_show_outline.init(_("Always show outline"), "/tools/nodes/show_outline", false); + _page_node.add_line( true, "", _t_node_show_outline, "", _("Show outlines for all paths, not only invisible paths")); + _t_node_live_outline.init(_("Update outline when dragging nodes"), "/tools/nodes/live_outline", false); + _page_node.add_line( true, "", _t_node_live_outline, "", _("Update the outline when dragging or transforming nodes; if this is off, the outline will only update when completing a drag")); + _t_node_live_objects.init(_("Update paths when dragging nodes"), "/tools/nodes/live_objects", false); + _page_node.add_line( true, "", _t_node_live_objects, "", _("Update paths when dragging or transforming nodes; if this is off, paths will only be updated when completing a drag")); + _t_node_show_path_direction.init(_("Show path direction on outlines"), "/tools/nodes/show_path_direction", false); + _page_node.add_line( true, "", _t_node_show_path_direction, "", _("Visualize the direction of selected paths by drawing small arrows in the middle of each outline segment")); + _t_node_pathflash_enabled.init ( _("Show temporary path outline"), "/tools/nodes/pathflash_enabled", false); + _page_node.add_line( true, "", _t_node_pathflash_enabled, "", _("When hovering over a path, briefly flash its outline")); + _t_node_pathflash_selected.init ( _("Show temporary outline for selected paths"), "/tools/nodes/pathflash_selected", false); + _page_node.add_line( true, "", _t_node_pathflash_selected, "", _("Show temporary outline even when a path is selected for editing")); + _t_node_pathflash_timeout.init("/tools/nodes/pathflash_timeout", 0, 10000.0, 100.0, 100.0, 1000.0, true, false); + _page_node.add_line( false, _("_Flash time:"), _t_node_pathflash_timeout, "ms", _("Specifies how long the path outline will be visible after a mouse-over (in milliseconds); specify 0 to have the outline shown until mouse leaves the path"), false); + _page_node.add_group_header(_("Editing preferences")); + _t_node_single_node_transform_handles.init(_("Show transform handles for single nodes"), "/tools/nodes/single_node_transform_handles", false); + _page_node.add_line( true, "", _t_node_single_node_transform_handles, "", _("Show transform handles even when only a single node is selected")); + _t_node_delete_preserves_shape.init(_("Deleting nodes preserves shape"), "/tools/nodes/delete_preserves_shape", true); + _page_node.add_line( true, "", _t_node_delete_preserves_shape, "", _("Move handles next to deleted nodes to resemble original shape; hold Ctrl to get the other behavior")); + + //Tweak + this->AddNewObjectsStyle(_page_tweak, "/tools/tweak", _("Object paint style")); + AddSelcueCheckbox(_page_tweak, "/tools/tweak", true); + AddGradientCheckbox(_page_tweak, "/tools/tweak", false); + + //Zoom + AddSelcueCheckbox(_page_zoom, "/tools/zoom", true); + AddGradientCheckbox(_page_zoom, "/tools/zoom", false); + + //Measure + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Ignore first and last points"), "/tools/measure/ignore_1st_and_last", true); + _page_measure.add_line( false, "", *cb, "", _("The start and end of the measurement tool's control line will not be considered for calculating lengths. Only lengths between actual curve intersections will be displayed.")); + + //Shapes + this->AddSelcueCheckbox(_page_shapes, "/tools/shapes", true); + this->AddGradientCheckbox(_page_shapes, "/tools/shapes", true); + + //Rectangle + this->AddNewObjectsStyle(_page_rectangle, "/tools/shapes/rect"); + this->AddConvertGuidesCheckbox(_page_rectangle, "/tools/shapes/rect", true); + + //3D box + this->AddNewObjectsStyle(_page_3dbox, "/tools/shapes/3dbox"); + this->AddConvertGuidesCheckbox(_page_3dbox, "/tools/shapes/3dbox", true); + + //Ellipse + this->AddNewObjectsStyle(_page_ellipse, "/tools/shapes/arc"); + + //Star + this->AddNewObjectsStyle(_page_star, "/tools/shapes/star"); + + //Spiral + this->AddNewObjectsStyle(_page_spiral, "/tools/shapes/spiral"); + + //Pencil + this->AddSelcueCheckbox(_page_pencil, "/tools/freehand/pencil", true); + this->AddNewObjectsStyle(_page_pencil, "/tools/freehand/pencil"); + this->AddDotSizeSpinbutton(_page_pencil, "/tools/freehand/pencil", 3.0); + this->AddBaseSimplifySpinbutton(_page_pencil, "/tools/freehand/pencil", 25.0); + _page_pencil.add_group_header( _("Sketch mode")); + _page_pencil.add_line( true, "", _pencil_average_all_sketches, "", + _("If on, the sketch result will be the normal average of all sketches made, instead of averaging the old result with the new sketch")); + + //Pen + this->AddSelcueCheckbox(_page_pen, "/tools/freehand/pen", true); + this->AddNewObjectsStyle(_page_pen, "/tools/freehand/pen"); + this->AddDotSizeSpinbutton(_page_pen, "/tools/freehand/pen", 3.0); + + //Calligraphy + this->AddSelcueCheckbox(_page_calligraphy, "/tools/calligraphic", false); + this->AddNewObjectsStyle(_page_calligraphy, "/tools/calligraphic"); + _page_calligraphy.add_line( false, "", _calligrapy_keep_selected, "", + _("If on, each newly created object will be selected (deselecting previous selection)")); + + //Text + this->AddSelcueCheckbox(_page_text, "/tools/text", true); + this->AddGradientCheckbox(_page_text, "/tools/text", true); + { + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Show font samples in the drop-down list"), "/tools/text/show_sample_in_list", true); + _page_text.add_line( false, "", *cb, "", _("Show font samples alongside font names in the drop-down list in Text bar")); + + _font_dialog.init(_("Show font substitution warning dialog"), "/options/font/substitutedlg", false); + _page_text.add_line( false, "", _font_dialog, "", _("Show font substitution warning dialog when requested fonts are not available on the system")); + _font_sample.init("/tools/text/font_sample", true); + _page_text.add_line( false, _("Font sample"), _font_sample, "", _("Change font preview sample text"), true); + + cb = Gtk::manage(new PrefCheckButton); + cb->init ( _("Use SVG2 auto-flowed text"), "/tools/text/use_svg2", true); + _page_text.add_line( false, "", *cb, "", _("Use SVG2 auto-flowed text instead of SVG1.2 auto-flowed text. (Recommended)")); + } + + //_page_text.add_group_header( _("Text units")); + //_font_output_px.init ( _("Always output text size in pixels (px)"), "/options/font/textOutputPx", true); + //_page_text.add_line( true, "", _font_output_px, "", _("Always convert the text size units above into pixels (px) before saving to file")); + + _page_text.add_group_header( _("Font directories")); + _font_fontsdir_system.init( _("Use Inkscape's fonts directory"), "/options/font/use_fontsdir_system", true); + _page_text.add_line( true, "", _font_fontsdir_system, "", _("Load additional fonts from \"fonts\" directory located in Inkscape's global \"share\" directory")); + _font_fontsdir_user.init( _("Use user's fonts directory"), "/options/font/use_fontsdir_user", true); + _page_text.add_line( true, "", _font_fontsdir_user, "", _("Load additional fonts from \"fonts\" directory located in Inkscape's user configuration directory")); + _font_fontdirs_custom.init("/options/font/custom_fontdirs", 50); + _page_text.add_line(true, _("Additional font directories"), _font_fontdirs_custom, "", _("Load additional fonts from custom locations (one path per line)"), true); + + + this->AddNewObjectsStyle(_page_text, "/tools/text"); + + //Spray + AddSelcueCheckbox(_page_spray, "/tools/spray", true); + AddGradientCheckbox(_page_spray, "/tools/spray", false); + + //Eraser + this->AddNewObjectsStyle(_page_eraser, "/tools/eraser"); + + //Paint Bucket + this->AddSelcueCheckbox(_page_paintbucket, "/tools/paintbucket", false); + this->AddNewObjectsStyle(_page_paintbucket, "/tools/paintbucket"); + + //Gradient + this->AddSelcueCheckbox(_page_gradient, "/tools/gradient", true); + _misc_forkvectors.init( _("Prevent sharing of gradient definitions"), "/options/forkgradientvectors/value", true); + _page_gradient.add_line( false, "", _misc_forkvectors, "", + _("When on, shared gradient definitions are automatically forked on change; uncheck to allow sharing of gradient definitions so that editing one object may affect other objects using the same gradient"), true); + + _misc_gradientangle.init("/dialogs/gradienteditor/angle", -359, 359, 1, 90, 0, false, false); + _page_gradient.add_line( false, _("Linear gradient _angle:"), _misc_gradientangle, "", + _("Default angle of new linear gradients in degrees (clockwise from horizontal)"), false); + + _misc_gradient_collect.init(_("Auto-delete unused gradients"), "/option/gradient/auto_collect", true); + _page_gradient.add_line( + false, "", _misc_gradient_collect, "", + _("When enabled, gradients that are not used will be deleted (auto-collected) automatically " + "from the SVG file. When disabled, unused gradients will be preserved in " + "the file for later use. (Note: This setting only affects new gradients.)"), + true); + + //Dropper + this->AddSelcueCheckbox(_page_dropper, "/tools/dropper", true); + this->AddGradientCheckbox(_page_dropper, "/tools/dropper", true); + + //Connector + this->AddSelcueCheckbox(_page_connector, "/tools/connector", true); + _page_connector.add_line(false, "", _connector_ignore_text, "", + _("If on, connector attachment points will not be shown for text objects")); + +#ifdef WITH_LPETOOL + //LPETool + //disabled, because the LPETool is not finished yet. + this->AddNewObjectsStyle(_page_lpetool, "/tools/lpetool"); +#endif // WITH_LPETOOL +} + +void InkscapePreferences::get_highlight_colors(guint32 &colorsetbase, guint32 &colorsetsuccess, + guint32 &colorsetwarning, guint32 &colorseterror) +{ + using namespace Inkscape::IO::Resource; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", "")); + Glib::ustring prefix = ""; + if (prefs->getBool("/theme/darkTheme", false)) { + prefix = ".dark "; + } + auto higlight = get_filename_string(ICONS, (themeiconname + "/highlights.css").c_str(), false, true); + if (!higlight.empty()) { + std::ifstream ifs(higlight); + std::string content((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>())); + Glib::ustring result; + size_t startpos = content.find(prefix + ".base"); + size_t endpos = content.find("}"); + if (startpos != std::string::npos) { + result = content.substr(startpos, endpos - startpos); + size_t startposin = result.find("fill:"); + size_t endposin = result.find(";"); + result = result.substr(startposin + 5, endposin - (startposin + 5)); + Util::trim(result); + Gdk::RGBA base_color = Gdk::RGBA(result); + SPColor base_color_sp(base_color.get_red(), base_color.get_green(), base_color.get_blue()); + colorsetbase = base_color_sp.toRGBA32(base_color.get_alpha()); + } + content.erase(0, endpos + 1); + startpos = content.find(prefix + ".success"); + endpos = content.find("}"); + if (startpos != std::string::npos) { + result = content.substr(startpos, endpos - startpos); + size_t startposin = result.find("fill:"); + size_t endposin = result.find(";"); + result = result.substr(startposin + 5, endposin - (startposin + 5)); + Util::trim(result); + Gdk::RGBA success_color = Gdk::RGBA(result); + SPColor success_color_sp(success_color.get_red(), success_color.get_green(), success_color.get_blue()); + colorsetsuccess = success_color_sp.toRGBA32(success_color.get_alpha()); + } + content.erase(0, endpos + 1); + startpos = content.find(prefix + ".warning"); + endpos = content.find("}"); + if (startpos != std::string::npos) { + result = content.substr(startpos, endpos - startpos); + size_t startposin = result.find("fill:"); + size_t endposin = result.find(";"); + result = result.substr(startposin + 5, endposin - (startposin + 5)); + Util::trim(result); + Gdk::RGBA warning_color = Gdk::RGBA(result); + SPColor warning_color_sp(warning_color.get_red(), warning_color.get_green(), warning_color.get_blue()); + colorsetwarning = warning_color_sp.toRGBA32(warning_color.get_alpha()); + } + content.erase(0, endpos + 1); + startpos = content.find(prefix + ".error"); + endpos = content.find("}"); + if (startpos != std::string::npos) { + result = content.substr(startpos, endpos - startpos); + size_t startposin = result.find("fill:"); + size_t endposin = result.find(";"); + result = result.substr(startposin + 5, endposin - (startposin + 5)); + Util::trim(result); + Gdk::RGBA error_color = Gdk::RGBA(result); + SPColor error_color_sp(error_color.get_red(), error_color.get_green(), error_color.get_blue()); + colorseterror = error_color_sp.toRGBA32(error_color.get_alpha()); + } + } +} + +void InkscapePreferences::resetIconsColors(bool themechange) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", "")); + if (!prefs->getBool("/theme/symbolicIcons", false)) { + _symbolic_base_colors.set_sensitive(false); + _symbolic_highlight_colors.set_sensitive(false); + _symbolic_base_color.setSensitive(false); + _symbolic_success_color.setSensitive(false); + _symbolic_warning_color.setSensitive(false); + _symbolic_error_color.setSensitive(false); + return; + } + if (prefs->getBool("/theme/symbolicDefaultBaseColors", true) || + !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) { + auto const screen = Gdk::Screen::get_default(); + if (INKSCAPE.themecontext->getColorizeProvider()) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider()); + } + // This colors are set on style.css of inkscape + Gdk::RGBA base_color = _symbolic_base_color.get_style_context()->get_color(); + // This is a hack to fix a problematic style which isn't updated fast enough on + // change from dark to bright themes + if (themechange) { + auto sc = _symbolic_base_color.get_style_context(); + base_color = get_background_color(sc); + } + SPColor base_color_sp(base_color.get_red(), base_color.get_green(), base_color.get_blue()); + //we copy highlight to not use + guint32 colorsetbase = base_color_sp.toRGBA32(base_color.get_alpha()); + guint32 colorsetsuccess = colorsetbase; + guint32 colorsetwarning = colorsetbase; + guint32 colorseterror = colorsetbase; + get_highlight_colors(colorsetbase, colorsetsuccess, colorsetwarning, colorseterror); + _symbolic_base_color.setRgba32(colorsetbase); + prefs->setUInt("/theme/" + themeiconname + "/symbolicBaseColor", colorsetbase); + _symbolic_base_color.setSensitive(false); + changeIconsColors(); + } else { + _symbolic_base_color.setSensitive(true); + } + if (prefs->getBool("/theme/symbolicDefaultHighColors", true)) { + auto const screen = Gdk::Screen::get_default(); + if (INKSCAPE.themecontext->getColorizeProvider()) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider()); + } + Gdk::RGBA success_color = _symbolic_success_color.get_style_context()->get_color(); + Gdk::RGBA warning_color = _symbolic_warning_color.get_style_context()->get_color(); + Gdk::RGBA error_color = _symbolic_error_color.get_style_context()->get_color(); + SPColor success_color_sp(success_color.get_red(), success_color.get_green(), success_color.get_blue()); + SPColor warning_color_sp(warning_color.get_red(), warning_color.get_green(), warning_color.get_blue()); + SPColor error_color_sp(error_color.get_red(), error_color.get_green(), error_color.get_blue()); + //we copy base to not use + guint32 colorsetbase = success_color_sp.toRGBA32(success_color.get_alpha()); + guint32 colorsetsuccess = success_color_sp.toRGBA32(success_color.get_alpha()); + guint32 colorsetwarning = warning_color_sp.toRGBA32(warning_color.get_alpha()); + guint32 colorseterror = error_color_sp.toRGBA32(error_color.get_alpha()); + get_highlight_colors(colorsetbase, colorsetsuccess, colorsetwarning, colorseterror); + _symbolic_success_color.setRgba32(colorsetsuccess); + _symbolic_warning_color.setRgba32(colorsetwarning); + _symbolic_error_color.setRgba32(colorseterror); + prefs->setUInt("/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess); + prefs->setUInt("/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning); + prefs->setUInt("/theme/" + themeiconname + "/symbolicErrorColor", colorseterror); + _symbolic_success_color.setSensitive(false); + _symbolic_warning_color.setSensitive(false); + _symbolic_error_color.setSensitive(false); + changeIconsColors(); + } else { + _symbolic_success_color.setSensitive(true); + _symbolic_warning_color.setSensitive(true); + _symbolic_error_color.setSensitive(true); + /* _complementary_colors->get_style_context()->remove_class("disabled"); */ + } +} + +void InkscapePreferences::resetIconsColorsWrapper() { resetIconsColors(false); } + +void InkscapePreferences::changeIconsColors() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", "")); + guint32 colorsetbase = prefs->getUInt("/theme/" + themeiconname + "/symbolicBaseColor", 0x2E3436ff); + guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff); + _symbolic_base_color.setRgba32(colorsetbase); + _symbolic_success_color.setRgba32(colorsetsuccess); + _symbolic_warning_color.setRgba32(colorsetwarning); + _symbolic_error_color.setRgba32(colorseterror); + auto const screen = Gdk::Screen::get_default(); + if (INKSCAPE.themecontext->getColorizeProvider()) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider()); + } + Gtk::CssProvider::create(); + Glib::ustring css_str = ""; + if (prefs->getBool("/theme/symbolicIcons", false)) { + css_str = INKSCAPE.themecontext->get_symbolic_colors(); + } + try { + INKSCAPE.themecontext->getColorizeProvider()->load_from_data(css_str); + } catch (const Gtk::CssProviderError &ex) { + g_critical("CSSProviderError::load_from_data(): failed to load '%s'\n(%s)", css_str.c_str(), ex.what().c_str()); + } + Gtk::StyleContext::add_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider(), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +void InkscapePreferences::toggleSymbolic() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + if (prefs->getBool("/theme/symbolicIcons", false)) { + if (window ) { + window->get_style_context()->add_class("symbolic"); + window->get_style_context()->remove_class("regular"); + } + _symbolic_base_colors.set_sensitive(true); + _symbolic_highlight_colors.set_sensitive(true); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", "")); + if (prefs->getBool("/theme/symbolicDefaultColors", true) || + !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) { + resetIconsColors(); + } else { + changeIconsColors(); + } + } else { + if (window) { + window->get_style_context()->add_class("regular"); + window->get_style_context()->remove_class("symbolic"); + } + auto const screen = Gdk::Screen::get_default(); + if (INKSCAPE.themecontext->getColorizeProvider()) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider()); + } + _symbolic_base_colors.set_sensitive(false); + _symbolic_highlight_colors.set_sensitive(false); + } + INKSCAPE.themecontext->getChangeThemeSignal().emit(); +} + +bool InkscapePreferences::contrastChange(GdkEventButton *button_event) +{ + themeChange(); + return true; +} + +void InkscapePreferences::comboThemeChange() +{ + //we reset theming on combo change + _dark_theme.set_active(false); + _symbolic_base_colors.set_active(true); + if (_contrast_theme.getSpinButton()->get_value() != 10.0){ + _contrast_theme.getSpinButton()->set_value(10.0); + } else { + themeChange(); + } +} +void InkscapePreferences::contrastThemeChange() +{ + //we reset theming on combo change + themeChange(true); +} + +void InkscapePreferences::themeChange(bool contrastslider) +{ + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + if (window) { + auto const screen = Gdk::Screen::get_default(); + if (INKSCAPE.themecontext->getContrastThemeProvider()) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getContrastThemeProvider()); + } + if (INKSCAPE.themecontext->getThemeProvider() ) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getThemeProvider() ); + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring current_theme = prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", "")); + _dark_theme.get_parent()->set_no_show_all(false); + if (dark_themes[current_theme]) { + _dark_theme.get_parent()->show_all(); + } else { + _dark_theme.get_parent()->hide(); + } + + auto settings = Gtk::Settings::get_default(); + settings->property_gtk_theme_name() = current_theme; + bool dark = INKSCAPE.themecontext->isCurrentThemeDark(dynamic_cast<Gtk::Container *>(window)); + bool toggled = prefs->getBool("/theme/darkTheme", false) != dark; + if (dark) { + prefs->setBool("/theme/darkTheme", true); + window->get_style_context()->add_class("dark"); + window->get_style_context()->remove_class("bright"); + } else { + prefs->setBool("/theme/darkTheme", false); + window->get_style_context()->add_class("bright"); + window->get_style_context()->remove_class("dark"); + } + INKSCAPE.themecontext->getChangeThemeSignal().emit(); + INKSCAPE.themecontext->add_gtk_css(true, contrastslider); + resetIconsColors(toggled); + } +} + +void InkscapePreferences::preferDarkThemeChange() +{ + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + if (window) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool dark = INKSCAPE.themecontext->isCurrentThemeDark(dynamic_cast<Gtk::Container *>(window)); + bool toggled = prefs->getBool("/theme/darkTheme", false) != dark; + if (dark) { + prefs->setBool("/theme/darkTheme", true); + window->get_style_context()->add_class("dark"); + window->get_style_context()->remove_class("bright"); + } else { + prefs->setBool("/theme/darkTheme", false); + window->get_style_context()->add_class("bright"); + window->get_style_context()->remove_class("dark"); + } + INKSCAPE.themecontext->getChangeThemeSignal().emit(); + INKSCAPE.themecontext->add_gtk_css(true); + // we avoid switched base colors + if (!_symbolic_base_colors.get_active()) { + prefs->setBool("/theme/symbolicDefaultBaseColors", true); + resetIconsColors(false); + _symbolic_base_colors.set_sensitive(true); + prefs->setBool("/theme/symbolicDefaultBaseColors", false); + } else { + resetIconsColors(toggled); + } + } +} + +void InkscapePreferences::symbolicThemeCheck() +{ + using namespace Inkscape::IO::Resource; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", "")); + bool symbolic = false; + auto settings = Gtk::Settings::get_default(); + if (settings) { + if (themeiconname != "") { + settings->property_gtk_icon_theme_name() = themeiconname; + } + } + // we always show symbolic in default theme (relays in hicolor theme) + if (themeiconname != prefs->getString("/theme/defaultIconTheme", "")) { + + auto folders = get_foldernames(ICONS, { "application" }); + for (auto &folder : folders) { + auto path = folder; + const size_t last_slash_idx = folder.find_last_of("\\/"); + if (std::string::npos != last_slash_idx) { + folder.erase(0, last_slash_idx + 1); + } + if (folder == themeiconname) { +#ifdef _WIN32 + path += g_win32_locale_filename_from_utf8("/symbolic/actions"); +#else + path += "/symbolic/actions"; +#endif + std::vector<Glib::ustring> symbolic_icons = get_filenames(path, { ".svg" }, {}); + if (symbolic_icons.size() > 0) { + symbolic = true; + symbolic_icons.clear(); + } + } + } + } else { + symbolic = true; + } + if (_symbolic_icons.get_parent()) { + if (!symbolic) { + _symbolic_icons.set_active(false); + _symbolic_icons.get_parent()->hide(); + _symbolic_base_colors.get_parent()->hide(); + _symbolic_highlight_colors.get_parent()->hide(); + _symbolic_base_color.get_parent()->get_parent()->hide(); + _symbolic_success_color.get_parent()->get_parent()->hide(); + } else { + _symbolic_icons.get_parent()->show(); + _symbolic_base_colors.get_parent()->show(); + _symbolic_highlight_colors.get_parent()->show(); + _symbolic_base_color.get_parent()->get_parent()->show(); + _symbolic_success_color.get_parent()->get_parent()->show(); + } + } + if (symbolic) { + if (prefs->getBool("/theme/symbolicDefaultHighColors", true) || + prefs->getBool("/theme/symbolicDefaultBaseColors", true) || + !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) { + resetIconsColors(); + } else { + changeIconsColors(); + } + guint32 colorsetbase = prefs->getUInt("/theme/" + themeiconname + "/symbolicBaseColor", 0x2E3436ff); + guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff); + _symbolic_base_color.init(_("Color for symbolic icons:"), "/theme/" + themeiconname + "/symbolicBaseColor", + colorsetbase); + _symbolic_success_color.init(_("Color for symbolic success icons:"), + "/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess); + _symbolic_warning_color.init(_("Color for symbolic warning icons:"), + "/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning); + _symbolic_error_color.init(_("Color for symbolic error icons:"), + "/theme/" + themeiconname + "/symbolicErrorColor", colorseterror); + } +} +/* void sp_mix_colors(cairo_t *ct, int pos, SPColor a, SPColor b) +{ + double arcEnd=2*M_PI; + cairo_set_source_rgba(ct, 1, 1, 1, 1); + cairo_arc(ct,pos,13,12,0,arcEnd); + cairo_fill(ct); + cairo_set_source_rgba(ct, a.v.c[0], a.v.c[1], a.v.c[2], 0.5); + cairo_arc(ct,pos,13,12,0,arcEnd); + cairo_fill(ct); + cairo_set_source_rgba(ct, b.v.c[0], b.v.c[1], b.v.c[2], 0.5); + cairo_arc(ct,pos,13,12,0,arcEnd); + cairo_fill(ct); +} + +Glib::RefPtr< Gdk::Pixbuf > sp_mix_colors() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff); + SPColor success(colorsetsuccess); + SPColor warning(colorsetwarning); + SPColor error(colorseterror); + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 82, 26); + cairo_t *ct = cairo_create(s); + // success + warning + sp_mix_colors(ct, 13, success, warning); + sp_mix_colors(ct, 41, success, error); + sp_mix_colors(ct, 69, warning, error); + cairo_destroy(ct); + cairo_surface_flush(s); + + Cairo::RefPtr<Cairo::Surface> sref = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s)); + Glib::RefPtr<Gdk::Pixbuf> pixbuf = + Gdk::Pixbuf::create(sref, 0, 0, 82, 26); + cairo_surface_destroy(s); + return pixbuf; +} */ + +void InkscapePreferences::changeIconsColor(guint32 /*color*/) +{ + changeIconsColors(); + /* _complementary_colors->set(sp_mix_colors()); */ +} + +void InkscapePreferences::initPageUI() +{ + Gtk::TreeModel::iterator iter_ui = this->AddPage(_page_ui, _("Interface"), PREFS_PAGE_UI); + + Glib::ustring languages[] = {_("System default"), + _("Albanian (sq)"), _("Arabic (ar)"), _("Armenian (hy)"), _("Assamese (as)"), _("Azerbaijani (az)"), + _("Basque (eu)"), _("Belarusian (be)"), _("Bulgarian (bg)"), _("Bengali (bn)"), _("Bengali/Bangladesh (bn_BD)"), _("Bodo (brx)"), _("Breton (br)"), + _("Catalan (ca)"), _("Valencian Catalan (ca@valencia)"), _("Chinese/China (zh_CN)"), _("Chinese/Taiwan (zh_TW)"), _("Croatian (hr)"), _("Czech (cs)"), + _("Danish (da)"), _("Dogri (doi)"), _("Dutch (nl)"), _("Dzongkha (dz)"), + _("German (de)"), _("Greek (el)"), + _("English (en)"), _("English/Australia (en_AU)"), _("English/Canada (en_CA)"), _("English/Great Britain (en_GB)"), _("Esperanto (eo)"), _("Estonian (et)"), + _("Farsi (fa)"), _("Finnish (fi)"), _("French (fr)"), + _("Galician (gl)"), _("Gujarati (gu)"), + _("Hebrew (he)"), _("Hindi (hi)"), _("Hungarian (hu)"), + _("Icelandic (is)"), _("Indonesian (id)"), _("Irish (ga)"), _("Italian (it)"), + _("Japanese (ja)"), + _("Kannada (kn)"), _("Kashmiri in Perso-Arabic script (ks@aran)"), _("Kashmiri in Devanagari script (ks@deva)"), _("Khmer (km)"), _("Kinyarwanda (rw)"), _("Konkani (kok)"), _("Konkani in Latin script (kok@latin)"), _("Korean (ko)"), + _("Latvian (lv)"), _("Lithuanian (lt)"), + _("Macedonian (mk)"), _("Maithili (mai)"), _("Malayalam (ml)"), _("Manipuri (mni)"), _("Manipuri in Bengali script (mni@beng)"), _("Marathi (mr)"), _("Mongolian (mn)"), + _("Nepali (ne)"), _("Norwegian Bokmål (nb)"), _("Norwegian Nynorsk (nn)"), + _("Odia (or)"), + _("Panjabi (pa)"), _("Polish (pl)"), _("Portuguese (pt)"), _("Portuguese/Brazil (pt_BR)"), + _("Romanian (ro)"), _("Russian (ru)"), + _("Sanskrit (sa)"), _("Santali (sat)"), _("Santali in Devanagari script (sat@deva)"), _("Serbian (sr)"), _("Serbian in Latin script (sr@latin)"), + _("Sindhi (sd)"), _("Sindhi in Devanagari script (sd@deva)"), _("Slovak (sk)"), _("Slovenian (sl)"), _("Spanish (es)"), _("Spanish/Mexico (es_MX)"), _("Swedish (sv)"), + _("Tamil (ta)"), _("Telugu (te)"), _("Thai (th)"), _("Turkish (tr)"), + _("Ukrainian (uk)"), _("Urdu (ur)"), + _("Vietnamese (vi)")}; + Glib::ustring langValues[] = {"", + "sq", "ar", "hy", "as", "az", + "eu", "be", "bg", "bn", "bn_BD", "brx", "br", + "ca", "ca@valencia", "zh_CN", "zh_TW", "hr", "cs", + "da", "doi", "nl", "dz", + "de", "el", + "en", "en_AU", "en_CA", "en_GB", "eo", "et", + "fa", "fi", "fr", + "gl", "gu", + "he", "hi", "hu", + "is", "id", "ga", "it", + "ja", + "kn", "ks@aran", "ks@deva", "km", "rw", "kok", "kok@latin", "ko", + "lv", "lt", + "mk", "mai", "ml", "mni", "mni@beng", "mr", "mn", + "ne", "nb", "nn", + "or", + "pa", "pl", "pt", "pt_BR", + "ro", "ru", + "sa", "sat", "sat@deva", "sr", "sr@latin", + "sd", "sd@deva", "sk", "sl", "es", "es_MX", "sv", + "ta", "te", "th", "tr", + "uk", "ur", + "vi" }; + + { + // sorting languages according to translated name + int i = 0; + int j = 0; + int n = sizeof( languages ) / sizeof( Glib::ustring ); + Glib::ustring key_language; + Glib::ustring key_langValue; + for ( j = 1 ; j < n ; j++ ) { + key_language = languages[j]; + key_langValue = langValues[j]; + i = j-1; + while ( i >= 0 + && ( ( languages[i] > key_language + && langValues[i] != "" ) + || key_langValue == "" ) ) + { + languages[i+1] = languages[i]; + langValues[i+1] = langValues[i]; + i--; + } + languages[i+1] = key_language; + langValues[i+1] = key_langValue; + } + } + + _ui_languages.init( "/ui/language", languages, langValues, G_N_ELEMENTS(languages), languages[0]); + _page_ui.add_line( false, _("Language:"), _ui_languages, "", + _("Set the language for menus and number formats"), false, reset_icon()); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + _ui_colorsliders_top.init( _("Work-around color sliders not drawing"), "/options/workarounds/colorsontop", false); + _page_ui.add_line( false, "", _ui_colorsliders_top, "", + _("When on, will attempt to work around bugs in certain GTK themes drawing color sliders"), true); + + + _misc_recent.init("/options/maxrecentdocuments/value", 0.0, 1000.0, 1.0, 1.0, 1.0, true, false); + + Gtk::Button* reset_recent = Gtk::manage(new Gtk::Button(_("Clear list"))); + reset_recent->signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::on_reset_open_recent_clicked)); + + _page_ui.add_line( false, _("Maximum documents in Open _Recent:"), _misc_recent, "", + _("Set the maximum length of the Open Recent list in the File menu, or clear the list"), false, reset_recent); + + _page_ui.add_group_header(_("_Zoom correction factor (in %)")); + _page_ui.add_group_note(_("Adjust the slider until the length of the ruler on your screen matches its real length. This information is used when zooming to 1:1, 1:2, etc., to display objects in their true sizes")); + _ui_zoom_correction.init(300, 30, 0.01, 500.0, 1.0, 10.0, 1.0); + _page_ui.add_line( true, "", _ui_zoom_correction, "", "", true); + + _ui_realworldzoom.init( _("Show zoom percentage corrected by factor"), "/options/zoomcorrection/shown", true); + _page_ui.add_line( false, "", _ui_realworldzoom, "", _("Zoom percentage can be either by the physical units or by pixels.")); + + /* show infobox */ + _show_filters_info_box.init( _("Show filter primitives infobox"), "/options/showfiltersinfobox/value", true); + _page_ui.add_line(false, "", _show_filters_info_box, "", + _("Show icons and descriptions for the filter primitives available at the filter effects dialog"), false, reset_icon()); + + _ui_yaxisdown.init( _("Origin at upper left with y-axis pointing down"), "/options/yaxisdown", true); + _page_ui.add_line( false, "", _ui_yaxisdown, "", + _("When off, origin is at lower left corner and y-axis points up"), false, reset_icon()); + + _ui_rotationlock.init(_("Lock canvas rotation by default"), "/options/rotationlock", false); + _page_ui.add_line(false, "", _ui_rotationlock, "", + _("Prevent accidental canvas rotation by disabling on-canvas keyboard and mouse actions for rotation"), true); + + _page_ui.add_group_header(_("User Interface")); + // _page_ui.add_group_header(_("Handle size")); + _mouse_grabsize.init("/options/grabsize/value", 1, 15, 1, 2, 3, 0); + _page_ui.add_line(true, _("Handle size"), _mouse_grabsize, "", _("Set the relative size of node handles"), true); + _narrow_spinbutton.init(_("Use narrow number entry boxes"), "/theme/narrowSpinButton", false); + _page_ui.add_line(false, "", _narrow_spinbutton, "", _("Make number editing boxes smaller by limiting padding"), false); + _compact_colorselector.init(_("Use compact color selector mode switch"), "/colorselector/switcher", true); + _page_ui.add_line(false, "", _compact_colorselector, "", _("Use compact combo box for selecting color modes"), false); + + _page_ui.add_group_header(_("Status bar")); + auto sb_style = Gtk::make_managed<UI::Widget::PrefCheckButton>(); + sb_style->init(_("Show current style"), "/statusbar/visibility/style", true); + _page_ui.add_line(false, "", *sb_style, "", _("Control visibility of current fill, stroke and opacity in status bar."), true); + auto sb_layer = Gtk::make_managed<UI::Widget::PrefCheckButton>(); + sb_layer->init(_("Show layer selector"), "/statusbar/visibility/layer", true); + _page_ui.add_line(false, "", *sb_layer, "", _("Control visibility of layer selection menu in status bar."), true); + auto sb_coords = Gtk::make_managed<UI::Widget::PrefCheckButton>(); + sb_coords->init(_("Show mouse coordinates"), "/statusbar/visibility/coordinates", true); + _page_ui.add_line(false, "", *sb_coords, "", _("Control visibility of mouse coordinates X & Y in status bar."), true); + auto sb_rotate = Gtk::make_managed<UI::Widget::PrefCheckButton>(); + sb_rotate->init(_("Show canvas rotation"), "/statusbar/visibility/rotation", true); + _page_ui.add_line(false, "", *sb_rotate, "", _("Control visibility of canvas rotation in status bar."), true); + + _page_ui.add_group_header(_("Mouse cursors")); + _ui_cursorscaling.init(_("Enable scaling"), "/options/cursorscaling", true); + _page_ui.add_line(false, "", _ui_cursorscaling, "", _("When off, cursor scaling is disabled. Cursor scaling may be broken when fractional scaling is enabled."), true); + _ui_cursor_shadow.init(_("Show drop shadow"), "/options/cursor-drop-shadow", true); + _page_ui.add_line(false, "", _ui_cursor_shadow, "", _("Control visibility of drop shadow for Inkscape cursors."), true); + + // Theme + _page_theme.add_group_header(_("Theme")); + _dark_theme.init(_("Use dark theme"), "/theme/preferDarkTheme", false); + Glib::ustring current_theme = prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", "")); + Glib::ustring default_theme = prefs->getString("/theme/defaultGtkTheme"); + Glib::ustring theme = ""; + { + dark_themes = INKSCAPE.themecontext->get_available_themes(); + std::vector<Glib::ustring> labels; + std::vector<Glib::ustring> values; + std::map<Glib::ustring, bool>::iterator it = dark_themes.begin(); + // Iterate over the map using Iterator till end. + for (std::pair<std::string, int> element : dark_themes) { + Glib::ustring theme = element.first; + ++it; + if (theme == default_theme) { + continue; + } + values.emplace_back(theme); + labels.emplace_back(theme); + } + std::sort(labels.begin(), labels.end()); + std::sort(values.begin(), values.end()); + labels.erase(unique(labels.begin(), labels.end()), labels.end()); + values.erase(unique(values.begin(), values.end()), values.end()); + values.emplace_back(""); + Glib::ustring default_theme_label = _("Use system theme"); + default_theme_label += " (" + default_theme + ")"; + labels.emplace_back(default_theme_label); + + _gtk_theme.init("/theme/gtkTheme", labels, values, ""); + _page_theme.add_line(false, _("Change GTK theme:"), _gtk_theme, "", "", false); + _gtk_theme.signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::comboThemeChange)); + } + + _sys_user_themes_dir_copy.init(g_build_filename(g_get_user_data_dir(), "themes", nullptr), _("Open themes folder")); + _page_theme.add_line(true, _("User themes:"), _sys_user_themes_dir_copy, "", _("Location of the user’s themes"), true, Gtk::manage(new Gtk::Box())); + _contrast_theme.init("/theme/contrast", 1, 10, 1, 2, 10, 1); + + _page_theme.add_line(true, "", _dark_theme, "", _("Use dark theme"), true); + { + auto font_scale = new Inkscape::UI::Widget::PrefSlider(); + font_scale = Gtk::manage(font_scale); + font_scale->init("/theme/fontscale", 50, 150, 5, 5, 100, 0); // 50% to 150% + font_scale->getSlider()->signal_format_value().connect([=](double val) { + return Glib::ustring::format(std::fixed, std::setprecision(0), val) + "%"; + }); + // Live updates commented out; too disruptive + // font_scale->getSlider()->signal_value_changed().connect([=](){ + // INKSCAPE.themecontext->adjust_global_font_scale(font_scale->getSlider()->get_value() / 100.0); + // }); + auto space = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL); + space->set_valign(Gtk::ALIGN_CENTER); + auto reset = Gtk::make_managed<Gtk::Button>(); + reset->set_tooltip_text(_("Reset font size to 100%")); + reset->set_image_from_icon_name("reset-settings-symbolic"); + reset->set_size_request(30, -1); + auto apply = Gtk::make_managed<Gtk::Button>(_("Apply")); + apply->set_tooltip_text(_("Apply font size changes to the UI")); + apply->set_valign(Gtk::ALIGN_FILL); + apply->set_margin_end(5); + reset->set_valign(Gtk::ALIGN_FILL); + space->add(*apply); + space->add(*reset); + reset->signal_clicked().connect([=](){ + font_scale->getSlider()->set_value(100); + INKSCAPE.themecontext->adjust_global_font_scale(1.0); + }); + apply->signal_clicked().connect([=](){ + INKSCAPE.themecontext->adjust_global_font_scale(font_scale->getSlider()->get_value() / 100.0); + }); + _page_theme.add_line(false, _("_Font scale:"), *font_scale, "", _("Adjust size of UI fonts"), true, space); + } + auto space = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL); + space->set_size_request(_sb_width / 3, -1); + _page_theme.add_line(false, _("_Contrast:"), _contrast_theme, "", + _("Make background brighter or darker to adjust contrast"), true, space); + _contrast_theme.getSlider()->signal_value_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::contrastThemeChange)); + + if (dark_themes[current_theme]) { + _dark_theme.get_parent()->set_no_show_all(false); + _dark_theme.get_parent()->show_all(); + } else { + _dark_theme.get_parent()->set_no_show_all(true); + _dark_theme.get_parent()->hide(); + } + _dark_theme.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::preferDarkThemeChange)); + // Icons + _page_theme.add_group_header(_("Icons")); + { + using namespace Inkscape::IO::Resource; + auto folders = get_foldernames(ICONS, { "application" }); + std::vector<Glib::ustring> labels; + std::vector<Glib::ustring> values; + Glib::ustring default_icon_theme = prefs->getString("/theme/defaultIconTheme"); + for (auto &folder : folders) { + // from https://stackoverflow.com/questions/8520560/get-a-file-name-from-a-path#8520871 + // Maybe we can link boost path utilities + // Remove directory if present. + // Do this before extension removal in case the directory has a period character. + const size_t last_slash_idx = folder.find_last_of("\\/"); + if (std::string::npos != last_slash_idx) { + folder.erase(0, last_slash_idx + 1); + } + // we want use Adwaita instead fallback hicolor theme + if (folder == default_icon_theme) { + continue; + } + labels.emplace_back(folder); + values.emplace_back(folder); + } + std::sort(labels.begin(), labels.end()); + std::sort(values.begin(), values.end()); + labels.erase(unique(labels.begin(), labels.end()), labels.end()); + values.erase(unique(values.begin(), values.end()), values.end()); + values.emplace_back(""); + Glib::ustring default_icon_label = _("Use system icons"); + default_icon_label += " (" + default_icon_theme + ")"; + labels.emplace_back(default_icon_label); + + _icon_theme.init("/theme/iconTheme", labels, values, ""); + _page_theme.add_line(false, _("Change icon theme:"), _icon_theme, "", "", false); + _icon_theme.signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::symbolicThemeCheck)); + _sys_user_icons_dir_copy.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::ICONS, ""), + _("Open icons folder")); + _page_theme.add_line(true, _("User icons: "), _sys_user_icons_dir_copy, "", _("Location of the user’s icons"), true, Gtk::manage(new Gtk::Box())); + } + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", "")); + _symbolic_icons.init(_("Use symbolic icons"), "/theme/symbolicIcons", false); + _symbolic_icons.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::toggleSymbolic)); + _page_theme.add_line(true, "", _symbolic_icons, "", "", true); + _symbolic_base_colors.init(_("Use default base color for icons"), "/theme/symbolicDefaultBaseColors", true); + _symbolic_base_colors.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::resetIconsColorsWrapper)); + _page_theme.add_line(true, "", _symbolic_base_colors, "", "", true); + _symbolic_highlight_colors.init(_("Use default highlight colors for icons"), "/theme/symbolicDefaultHighColors", true); + _symbolic_highlight_colors.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::resetIconsColorsWrapper)); + _page_theme.add_line(true, "", _symbolic_highlight_colors, "", "", true); + _symbolic_base_color.init(_("Color for symbolic icons:"), "/theme/" + themeiconname + "/symbolicBaseColor", + 0x2E3436ff); + _symbolic_success_color.init(_("Color for symbolic success icons:"), + "/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + _symbolic_warning_color.init(_("Color for symbolic warning icons:"), + "/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + _symbolic_error_color.init(_("Color for symbolic error icons:"), "/theme/" + themeiconname + "/symbolicErrorColor", + 0xCC0000ff); + _symbolic_base_color.get_style_context()->add_class("system_base_color"); + _symbolic_success_color.get_style_context()->add_class("system_success_color"); + _symbolic_warning_color.get_style_context()->add_class("system_warning_color"); + _symbolic_error_color.get_style_context()->add_class("system_error_color"); + _symbolic_base_color.get_style_context()->add_class("symboliccolors"); + _symbolic_success_color.get_style_context()->add_class("symboliccolors"); + _symbolic_warning_color.get_style_context()->add_class("symboliccolors"); + _symbolic_error_color.get_style_context()->add_class("symboliccolors"); + _symbolic_base_color.connectChanged(sigc::mem_fun(this, &InkscapePreferences::changeIconsColor)); + _symbolic_warning_color.connectChanged(sigc::mem_fun(this, &InkscapePreferences::changeIconsColor)); + _symbolic_success_color.connectChanged(sigc::mem_fun(this, &InkscapePreferences::changeIconsColor)); + _symbolic_error_color.connectChanged(sigc::mem_fun(this, &InkscapePreferences::changeIconsColor)); + /* _complementary_colors = Gtk::manage(new Gtk::Image()); */ + Gtk::Box *icon_buttons = Gtk::manage(new Gtk::Box()); + icon_buttons->pack_start(_symbolic_base_color, true, true, 4); + _page_theme.add_line(false, "", *icon_buttons, _("Icon color base"), + _("Base color for icons"), false); + Gtk::Box *icon_buttons_hight = Gtk::manage(new Gtk::Box()); + icon_buttons_hight->pack_start(_symbolic_success_color, true, true, 4); + icon_buttons_hight->pack_start(_symbolic_warning_color, true, true, 4); + icon_buttons_hight->pack_start(_symbolic_error_color, true, true, 4); + /* icon_buttons_hight->pack_start(*_complementary_colors, true, true, 4); */ + _page_theme.add_line(false, "", *icon_buttons_hight, _("Icon color highlights"), + _("Highlight colors supported by some symbolic icon themes"), + false); + Gtk::Box *icon_buttons_def = Gtk::manage(new Gtk::Box()); + resetIconsColors(); + changeIconsColor(0xffffffff); + _page_theme.add_line(false, "", *icon_buttons_def, "", + _("Reset theme colors for some symbolic icon themes"), + false); + Glib::ustring menu_icons_labels[] = {_("Yes"), _("No"), _("Theme decides")}; + int menu_icons_values[] = {1, -1, 0}; + _menu_icons.init("/theme/menuIcons", menu_icons_labels, menu_icons_values, G_N_ELEMENTS(menu_icons_labels), 0); + _page_theme.add_line(false, _("Show icons in menus:"), _menu_icons, "", + _("You can either enable or disable all icons in menus. By default, the setting for the 'use-icon' attribute in the 'menus.ui' file determines whether to display icons in menus."), false, reset_icon()); + + + this->AddPage(_page_theme, _("Theming"), iter_ui, PREFS_PAGE_UI_THEME); + symbolicThemeCheck(); + + // Toolbars + _page_toolbars.add_group_header(_("Toolbars")); + try { + auto builder = Gtk::Builder::create_from_file(get_filename(UIS, "toolbar-tool-prefs.ui")); + Gtk::Widget* toolbox = nullptr; + builder->get_widget("tool-toolbar-prefs", toolbox); + + sp_traverse_widget_tree(toolbox, [=](Gtk::Widget* widget){ + if (auto button = dynamic_cast<Gtk::ToggleButton*>(widget)) { + assert(GTK_IS_ACTIONABLE(widget->gobj())); + // do not execute any action: + gtk_actionable_set_action_name(GTK_ACTIONABLE(widget->gobj()), ""); + + button->set_sensitive(); + auto path = ToolboxFactory::get_tool_visible_buttons_path(sp_get_action_target(button)); + auto visible = Inkscape::Preferences::get()->getBool(path, true); + button->set_active(visible); + button->signal_toggled().connect([=](){ + Inkscape::Preferences::get()->setBool(path, button->get_active()); + }); + } + return false; + }); + _page_toolbars.add_line(false, "", *toolbox, "", _("Select visible tool buttons"), true); + + struct tbar_info {const char* label; const char* prefs;} toolbars[] = { + {_("Toolbox icon size:"), ToolboxFactory::tools_icon_size}, + {_("Control bar icon size:"), ToolboxFactory::ctrlbars_icon_size}, + }; + for (auto&& tbox : toolbars) { + auto slider = Gtk::manage(new UI::Widget::PrefSlider(false)); + const int min = ToolboxFactory::min_pixel_size; + const int max = ToolboxFactory::max_pixel_size; + slider->init(tbox.prefs, min, max, 1, 4, min, 0); + slider->getSlider()->signal_format_value().connect([](double val){ + return Glib::ustring::format(std::fixed, std::setprecision(0), val * 100.0 / min) + "%"; + }); + slider->getSlider()->get_style_context()->add_class("small-marks"); + for (int i = min; i <= max; i += 8) { + slider->getSlider()->add_mark(i, Gtk::POS_BOTTOM, i % min ? "" : (std::to_string(100 * i / min) + "%").c_str()); + } + _page_toolbars.add_line(false, tbox.label, *slider, "", _("Adjust toolbar icon size")); + } + + std::vector<PrefItem> snap = { + { _("Simple"), 1, _("Present simplified snapping options that manage all advanced settings"), true }, + { _("Advanced"), 0, _("Expose all snapping options for manual control") } + }; + _page_toolbars.add_line(false, _("Snap controls bar:"), *Gtk::make_managed<PrefRadioButtons>(snap, "/toolbox/simplesnap"), "", ""); + } catch (const Glib::Error &ex) { + g_error("Couldn't load toolbar-tool-prefs user interface file."); + } + + this->AddPage(_page_toolbars, _("Toolbars"), iter_ui, PREFS_PAGE_UI_TOOLBARS); + + // Windows + _win_save_geom.init ( _("Save and restore window geometry for each document"), "/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_FILE, true, nullptr); + _win_save_geom_prefs.init ( _("Remember and use last window's geometry"), "/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_LAST, false, &_win_save_geom); + _win_save_geom_off.init ( _("Don't save window geometry"), "/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_NONE, false, &_win_save_geom); + + _win_native.init ( _("Native open/save dialogs"), "/options/desktopintegration/value", 1, true, nullptr); + _win_gtk.init ( _("GTK open/save dialogs"), "/options/desktopintegration/value", 0, false, &_win_native); + + _win_show_boot.init ( _("Show Welcome dialog"), "/options/boot/enabled", true); + _win_hide_task.init ( _("Dialogs are hidden in taskbar"), "/options/dialogsskiptaskbar/value", true); + _win_save_viewport.init ( _("Save and restore documents viewport"), "/options/savedocviewport/value", true); + _win_zoom_resize.init ( _("Zoom when window is resized"), "/options/stickyzoom/value", false); + _win_ontop_none.init ( C_("Dialog on top", "None"), "/options/transientpolicy/value", PREFS_DIALOGS_WINDOWS_NONE, false, nullptr); + _win_ontop_normal.init ( _("Normal"), "/options/transientpolicy/value", PREFS_DIALOGS_WINDOWS_NORMAL, true, &_win_ontop_none); + _win_ontop_agressive.init ( _("Aggressive"), "/options/transientpolicy/value", PREFS_DIALOGS_WINDOWS_AGGRESSIVE, false, &_win_ontop_none); + + _win_dialogs_labels_auto.init( _("Automatic"), "/options/notebooklabels/value", PREFS_NOTEBOOK_LABELS_AUTO, true, nullptr); + _win_dialogs_labels_active.init( _("Active"), "/options/notebooklabels/value", PREFS_NOTEBOOK_LABELS_ACTIVE, true, nullptr); + _win_dialogs_labels_off.init( _("Off"), "/options/notebooklabels/value", PREFS_NOTEBOOK_LABELS_OFF, false, &_win_dialogs_labels_auto); + + { + Glib::ustring defaultSizeLabels[] = {C_("Window size", "Default"), + C_("Window size", "Small"), + C_("Window size", "Large"), + C_("Window size", "Maximized")}; + int defaultSizeValues[] = {PREFS_WINDOW_SIZE_NATURAL, + PREFS_WINDOW_SIZE_SMALL, + PREFS_WINDOW_SIZE_LARGE, + PREFS_WINDOW_SIZE_MAXIMIZED}; + + _win_default_size.init( "/options/defaultwindowsize/value", defaultSizeLabels, defaultSizeValues, G_N_ELEMENTS(defaultSizeLabels), PREFS_WINDOW_SIZE_NATURAL); + _page_windows.add_line( false, _("Default window size:"), _win_default_size, "", + _("Set the default window size"), false); + } + + _page_windows.add_group_header( _("Saving window size and position"), 4); + _page_windows.add_line( true, "", _win_save_geom_off, "", + _("Let the window manager determine placement of all windows")); + _page_windows.add_line( true, "", _win_save_geom_prefs, "", + _("Remember and use the last window's geometry (saves geometry to user preferences)")); + _page_windows.add_line( true, "", _win_save_geom, "", + _("Save and restore window geometry for each document (saves geometry in the document)")); + +#ifdef _WIN32 + _page_windows.add_group_header( _("Desktop integration")); + _page_windows.add_line( true, "", _win_native, "", + _("Use Windows like open and save dialogs")); + _page_windows.add_line( true, "", _win_gtk, "", + _("Use GTK open and save dialogs ")); +#endif + _page_windows.add_group_header(_("Dialogs settings"), 4); + + std::vector<PrefItem> dock = { + { _("Docked"), PREFS_DIALOGS_BEHAVIOR_DOCKABLE, _("Allow dialog docking"), true }, + { _("Floating"), PREFS_DIALOGS_BEHAVIOR_FLOATING, _("Disable dialog docking") } + }; + _page_windows.add_line(true, _("Dialog behavior"), *Gtk::make_managed<PrefRadioButtons>(dock, "/options/dialogtype/value"), "", "", false, reset_icon()); + +#ifndef _WIN32 // non-Win32 special code to enable transient dialogs + std::vector<PrefItem> on_top = { + { C_("Dialog on top", "None"), PREFS_DIALOGS_WINDOWS_NONE, _("Dialogs are treated as regular windows") }, + { _("Normal"), PREFS_DIALOGS_WINDOWS_NORMAL, _("Dialogs stay on top of document windows"), true }, + { _("Aggressive"), PREFS_DIALOGS_WINDOWS_AGGRESSIVE, _("Same as Normal but may work better with some window managers") } + }; + _page_windows.add_line(true, _("Dialog on top"), *Gtk::make_managed<PrefRadioButtons>(on_top, "/options/transientpolicy/value"), "", ""); +#endif + std::vector<PrefItem> labels = { + { _("Automatic"), PREFS_NOTEBOOK_LABELS_AUTO, _("Dialog names will be displayed when there is enough space"), true }, + { _("Active"), PREFS_NOTEBOOK_LABELS_ACTIVE, _("Only show label on active") }, + { _("Off"), PREFS_NOTEBOOK_LABELS_OFF, _("Only show dialog icons") } + }; + _page_windows.add_line(true, _("Labels behavior"), *Gtk::make_managed<PrefRadioButtons>(labels, "/options/notebooklabels/value"), "", "", false, reset_icon()); + + auto save_dlg = Gtk::make_managed<PrefCheckButton>(); + save_dlg->init(_("Save and restore dialogs' status"), "/options/savedialogposition/value", true); + _page_windows.add_line(true, "", *save_dlg, "", _("Save and restore dialogs' status (the last open windows dialogs are saved when it closes)")); + +#ifndef _WIN32 // FIXME: Temporary Win32 special code to enable transient dialogs + _page_windows.add_line( true, "", _win_hide_task, "", + _("Whether dialog windows are to be hidden in the window manager taskbar")); +#endif + + _page_windows.add_group_header( _("Miscellaneous")); + + _page_windows.add_line( true, "", _win_show_boot, "", + _("Whether the Welcome dialog will be shown when Inkscape starts.")); + _page_windows.add_line( true, "", _win_zoom_resize, "", + _("Zoom drawing when document window is resized, to keep the same area visible (this is the default which can be changed in any window using the button above the right scrollbar)")); + _page_windows.add_line( true, "", _win_save_viewport, "", + _("Save documents viewport (zoom and panning position). Useful to turn off when sharing version controlled files.")); + + this->AddPage(_page_windows, _("Windows"), iter_ui, PREFS_PAGE_UI_WINDOWS); + + // Grids + _page_grids.add_group_header( _("Line color when zooming out")); + + _grids_no_emphasize_on_zoom.init( _("Minor grid line color"), "/options/grids/no_emphasize_when_zoomedout", 1, true, nullptr); + _page_grids.add_line( true, "", _grids_no_emphasize_on_zoom, "", _("The gridlines will be shown in minor grid line color"), false); + _grids_emphasize_on_zoom.init( _("Major grid line color"), "/options/grids/no_emphasize_when_zoomedout", 0, false, &_grids_no_emphasize_on_zoom); + _page_grids.add_line( true, "", _grids_emphasize_on_zoom, "", _("The gridlines will be shown in major grid line color"), false); + + _page_grids.add_group_header( _("Default grid settings")); + + _page_grids.add_line( true, "", _grids_notebook, "", "", false); + _grids_notebook.append_page(_grids_xy, CanvasGrid::getName( GRID_RECTANGULAR )); + _grids_notebook.append_page(_grids_axonom, CanvasGrid::getName( GRID_AXONOMETRIC )); + _grids_xy_units.init("/options/grids/xy/units"); + _grids_xy.add_line( false, _("Grid units:"), _grids_xy_units, "", "", false); + _grids_xy_origin_x.init("/options/grids/xy/origin_x", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); + _grids_xy_origin_y.init("/options/grids/xy/origin_y", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); + _grids_xy_origin_x.set_digits(3); + _grids_xy_origin_y.set_digits(3); + _grids_xy.add_line( false, _("Origin X:"), _grids_xy_origin_x, "", _("X coordinate of grid origin"), false); + _grids_xy.add_line( false, _("Origin Y:"), _grids_xy_origin_y, "", _("Y coordinate of grid origin"), false); + _grids_xy_spacing_x.init("/options/grids/xy/spacing_x", -10000.0, 10000.0, 0.1, 1.0, 1.0, false, false); + _grids_xy_spacing_y.init("/options/grids/xy/spacing_y", -10000.0, 10000.0, 0.1, 1.0, 1.0, false, false); + _grids_xy_spacing_x.set_digits(3); + _grids_xy_spacing_y.set_digits(3); + _grids_xy.add_line( false, _("Spacing X:"), _grids_xy_spacing_x, "", _("Distance between vertical grid lines"), false); + _grids_xy.add_line( false, _("Spacing Y:"), _grids_xy_spacing_y, "", _("Distance between horizontal grid lines"), false); + + _grids_xy_color.init(_("Minor grid line color:"), "/options/grids/xy/color", GRID_DEFAULT_COLOR); + _grids_xy.add_line( false, _("Minor grid line color:"), _grids_xy_color, "", _("Color used for normal grid lines"), false); + _grids_xy_empcolor.init(_("Major grid line color:"), "/options/grids/xy/empcolor", GRID_DEFAULT_EMPCOLOR); + _grids_xy.add_line( false, _("Major grid line color:"), _grids_xy_empcolor, "", _("Color used for major (highlighted) grid lines"), false); + _grids_xy_empspacing.init("/options/grids/xy/empspacing", 1.0, 1000.0, 1.0, 5.0, 5.0, true, false); + _grids_xy.add_line( false, _("Major grid line every:"), _grids_xy_empspacing, "", "", false); + _grids_xy_dotted.init( _("Show dots instead of lines"), "/options/grids/xy/dotted", false); + _grids_xy.add_line( false, "", _grids_xy_dotted, "", _("If set, display dots at gridpoints instead of gridlines"), false); + + // CanvasAxonomGrid properties: + _grids_axonom_units.init("/options/grids/axonom/units"); + _grids_axonom.add_line( false, _("Grid units:"), _grids_axonom_units, "", "", false); + _grids_axonom_origin_x.init("/options/grids/axonom/origin_x", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); + _grids_axonom_origin_y.init("/options/grids/axonom/origin_y", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); + _grids_axonom_origin_x.set_digits(3); + _grids_axonom_origin_y.set_digits(3); + _grids_axonom.add_line( false, _("Origin X:"), _grids_axonom_origin_x, "", _("X coordinate of grid origin"), false); + _grids_axonom.add_line( false, _("Origin Y:"), _grids_axonom_origin_y, "", _("Y coordinate of grid origin"), false); + _grids_axonom_spacing_y.init("/options/grids/axonom/spacing_y", -10000.0, 10000.0, 0.1, 1.0, 1.0, false, false); + _grids_axonom_spacing_y.set_digits(3); + _grids_axonom.add_line( false, _("Spacing Y:"), _grids_axonom_spacing_y, "", _("Base length of z-axis"), false); + _grids_axonom_angle_x.init("/options/grids/axonom/angle_x", -360.0, 360.0, 1.0, 10.0, 30.0, false, false); + _grids_axonom_angle_z.init("/options/grids/axonom/angle_z", -360.0, 360.0, 1.0, 10.0, 30.0, false, false); + _grids_axonom.add_line( false, _("Angle X:"), _grids_axonom_angle_x, "", _("Angle of x-axis"), false); + _grids_axonom.add_line( false, _("Angle Z:"), _grids_axonom_angle_z, "", _("Angle of z-axis"), false); + _grids_axonom_color.init(_("Minor grid line color:"), "/options/grids/axonom/color", GRID_DEFAULT_COLOR); + _grids_axonom.add_line( false, _("Minor grid line color:"), _grids_axonom_color, "", _("Color used for normal grid lines"), false); + _grids_axonom_empcolor.init(_("Major grid line color:"), "/options/grids/axonom/empcolor", GRID_DEFAULT_EMPCOLOR); + _grids_axonom.add_line( false, _("Major grid line color:"), _grids_axonom_empcolor, "", _("Color used for major (highlighted) grid lines"), false); + _grids_axonom_empspacing.init("/options/grids/axonom/empspacing", 1.0, 1000.0, 1.0, 5.0, 5.0, true, false); + _grids_axonom.add_line( false, _("Major grid line every:"), _grids_axonom_empspacing, "", "", false); + + this->AddPage(_page_grids, _("Grids"), iter_ui, PREFS_PAGE_UI_GRIDS); + + // Command Palette + _page_command_palette.add_group_header(_("Display Options")); + + _cp_show_full_action_name.init(_("Show command line argument names"), "/options/commandpalette/showfullactionname/value", false); + _page_command_palette.add_line(true, "", _cp_show_full_action_name, "", _("Show action argument names in the command palette suggestions, most useful for using them on the command line")); + + _cp_show_untranslated_name.init(_("Show untranslated (English) names"), "/options/commandpalette/showuntranslatedname/value", true); + _page_command_palette.add_line(true, "", _cp_show_untranslated_name, "", _("Also show the English names of the command")); + + this->AddPage(_page_command_palette, _("Command Palette"), iter_ui, PREFS_PAGE_COMMAND_PALETTE); + // /Command Palette + + + initKeyboardShortcuts(iter_ui); +} + +static void profileComboChanged( Gtk::ComboBoxText* combo ) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int rowNum = combo->get_active_row_number(); + if ( rowNum < 1 ) { + prefs->setString("/options/displayprofile/uri", ""); + } else { + Glib::ustring active = combo->get_active_text(); + + Glib::ustring path = CMSSystem::getPathForProfile(active); + if ( !path.empty() ) { + prefs->setString("/options/displayprofile/uri", path); + } + } +} + +static void proofComboChanged( Gtk::ComboBoxText* combo ) +{ + Glib::ustring active = combo->get_active_text(); + Glib::ustring path = CMSSystem::getPathForProfile(active); + + if ( !path.empty() ) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/options/softproof/uri", path); + } +} + +static void gamutColorChanged( Gtk::ColorButton* btn ) { + auto rgba = btn->get_rgba(); + auto r = rgba.get_red_u(); + auto g = rgba.get_green_u(); + auto b = rgba.get_blue_u(); + + gchar* tmp = g_strdup_printf("#%02x%02x%02x", (r >> 8), (g >> 8), (b >> 8) ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/options/softproof/gamutcolor", tmp); + g_free(tmp); +} + +void InkscapePreferences::initPageIO() +{ + Gtk::TreeModel::iterator iter_io = this->AddPage(_page_io, _("Input/Output"), PREFS_PAGE_IO); + + _save_use_current_dir.init( _("Use current directory for \"Save As ...\""), "/dialogs/save_as/use_current_dir", true); + _page_io.add_line( false, "", _save_use_current_dir, "", + _("When this option is on, the \"Save as...\" and \"Save a Copy...\" dialogs will always open in the directory where the currently open document is; when it's off, each will open in the directory where you last saved a file using it"), true); + + _misc_default_metadata.init( _("Add default metadata to new documents"), "/metadata/addToNewFile", false); + _page_io.add_line( false, "", _misc_default_metadata, "", + _("Add default metadata to new documents. Default metadata can be set from Document Properties->Metadata."), true); + + _export_all_extensions.init( _("Show all outputs in Export Dialog"), "/dialogs/export/show_all_extensions", false); + _page_io.add_line( false, "", _export_all_extensions, "", + _("Will list all possible output extensions in the Export Dialog selection."), true); + + // Input devices options + _mouse_sens.init ( "/options/cursortolerance/value", 0.0, 30.0, 1.0, 1.0, 8.0, true, false); + _page_mouse.add_line( false, _("_Grab sensitivity:"), _mouse_sens, _("pixels"), + _("How close on the screen you need to be to an object to be able to grab it with mouse (in screen pixels)"), false, reset_icon()); + _mouse_thres.init ( "/options/dragtolerance/value", 0.0, 100.0, 1.0, 1.0, 8.0, true, false); + _page_mouse.add_line( false, _("_Click/drag threshold:"), _mouse_thres, _("pixels"), + _("Maximum mouse drag (in screen pixels) which is considered a click, not a drag"), false); + + _mouse_use_ext_input.init( _("Use pressure-sensitive tablet"), "/options/useextinput/value", true); + _page_mouse.add_line(false, "",_mouse_use_ext_input, "", + _("Use the capabilities of a tablet or other pressure-sensitive device. Disable this only if you have problems with the tablet (you can still use it as a mouse)"), false, reset_icon()); + + _mouse_switch_on_ext_input.init( _("Switch tool based on tablet device"), "/options/switchonextinput/value", false); + _page_mouse.add_line(false, "",_mouse_switch_on_ext_input, "", + _("Change tool as different devices are used on the tablet (pen, eraser, mouse)"), false, reset_icon()); + this->AddPage(_page_mouse, _("Input devices"), iter_io, PREFS_PAGE_IO_MOUSE); + + // SVG output options + _svgoutput_usenamedcolors.init( _("Use named colors"), "/options/svgoutput/usenamedcolors", false); + _page_svgoutput.add_line( false, "", _svgoutput_usenamedcolors, "", _("If set, write the CSS name of the color when available (e.g. 'red' or 'magenta') instead of the numeric value"), false); + + _page_svgoutput.add_group_header( _("XML formatting")); + + _svgoutput_inlineattrs.init( _("Inline attributes"), "/options/svgoutput/inlineattrs", false); + _page_svgoutput.add_line( true, "", _svgoutput_inlineattrs, "", _("Put attributes on the same line as the element tag"), false); + + _svgoutput_indent.init("/options/svgoutput/indent", 0.0, 1000.0, 1.0, 2.0, 2.0, true, false); + _page_svgoutput.add_line( true, _("_Indent, spaces:"), _svgoutput_indent, "", _("The number of spaces to use for indenting nested elements; set to 0 for no indentation"), false); + + _page_svgoutput.add_group_header( _("Path data")); + + int const numPathstringFormat = 3; + Glib::ustring pathstringFormatLabels[numPathstringFormat] = {_("Absolute"), _("Relative"), _("Optimized")}; + int pathstringFormatValues[numPathstringFormat] = {0, 1, 2}; + + _svgoutput_pathformat.init("/options/svgoutput/pathstring_format", pathstringFormatLabels, pathstringFormatValues, numPathstringFormat, 2); + _page_svgoutput.add_line( true, _("Path string format:"), _svgoutput_pathformat, "", _("Path data should be written: only with absolute coordinates, only with relative coordinates, or optimized for string length (mixed absolute and relative coordinates)"), false); + + _svgoutput_forcerepeatcommands.init( _("Force repeat commands"), "/options/svgoutput/forcerepeatcommands", false); + _page_svgoutput.add_line( true, "", _svgoutput_forcerepeatcommands, "", _("Force repeating of the same path command (for example, 'L 1,2 L 3,4' instead of 'L 1,2 3,4')"), false); + + _page_svgoutput.add_group_header( _("Numbers")); + + _svgoutput_numericprecision.init("/options/svgoutput/numericprecision", 1.0, 16.0, 1.0, 2.0, 8.0, true, false); + _page_svgoutput.add_line( true, _("_Numeric precision:"), _svgoutput_numericprecision, "", _("Significant figures of the values written to the SVG file"), false); + + _svgoutput_minimumexponent.init("/options/svgoutput/minimumexponent", -32.0, -1, 1.0, 2.0, -8.0, true, false); + _page_svgoutput.add_line( true, _("Minimum _exponent:"), _svgoutput_minimumexponent, "", _("The smallest number written to SVG is 10 to the power of this exponent; anything smaller is written as zero"), false); + + /* Code to add controls for attribute checking options */ + + /* Add incorrect style properties options */ + _page_svgoutput.add_group_header( _("Improper Attributes Actions")); + + _svgoutput_attrwarn.init( _("Print warnings"), "/options/svgoutput/incorrect_attributes_warn", true); + _page_svgoutput.add_line( true, "", _svgoutput_attrwarn, "", _("Print warning if invalid or non-useful attributes found. Database files located in inkscape_data_dir/attributes."), false); + _svgoutput_attrremove.init( _("Remove attributes"), "/options/svgoutput/incorrect_attributes_remove", false); + _page_svgoutput.add_line( true, "", _svgoutput_attrremove, "", _("Delete invalid or non-useful attributes from element tag"), false); + + /* Add incorrect style properties options */ + _page_svgoutput.add_group_header( _("Inappropriate Style Properties Actions")); + + _svgoutput_stylepropwarn.init( _("Print warnings"), "/options/svgoutput/incorrect_style_properties_warn", true); + _page_svgoutput.add_line( true, "", _svgoutput_stylepropwarn, "", _("Print warning if inappropriate style properties found (i.e. 'font-family' set on a <rect>). Database files located in inkscape_data_dir/attributes."), false); + _svgoutput_stylepropremove.init( _("Remove style properties"), "/options/svgoutput/incorrect_style_properties_remove", false); + _page_svgoutput.add_line( true, "", _svgoutput_stylepropremove, "", _("Delete inappropriate style properties"), false); + + /* Add default or inherited style properties options */ + _page_svgoutput.add_group_header( _("Non-useful Style Properties Actions")); + + _svgoutput_styledefaultswarn.init( _("Print warnings"), "/options/svgoutput/style_defaults_warn", true); + _page_svgoutput.add_line( true, "", _svgoutput_styledefaultswarn, "", _("Print warning if redundant style properties found (i.e. if a property has the default value and a different value is not inherited or if value is the same as would be inherited). Database files located in inkscape_data_dir/attributes."), false); + _svgoutput_styledefaultsremove.init( _("Remove style properties"), "/options/svgoutput/style_defaults_remove", false); + _page_svgoutput.add_line( true, "", _svgoutput_styledefaultsremove, "", _("Delete redundant style properties"), false); + + _page_svgoutput.add_group_header( _("Check Attributes and Style Properties on")); + + _svgoutput_check_reading.init( _("Reading"), "/options/svgoutput/check_on_reading", false); + _page_svgoutput.add_line( true, "", _svgoutput_check_reading, "", _("Check attributes and style properties on reading in SVG files (including those internal to Inkscape which will slow down startup)"), false); + _svgoutput_check_editing.init( _("Editing"), "/options/svgoutput/check_on_editing", false); + _page_svgoutput.add_line( true, "", _svgoutput_check_editing, "", _("Check attributes and style properties while editing SVG files (may slow down Inkscape, mostly useful for debugging)"), false); + _svgoutput_check_writing.init( _("Writing"), "/options/svgoutput/check_on_writing", true); + _page_svgoutput.add_line( true, "", _svgoutput_check_writing, "", _("Check attributes and style properties on writing out SVG files"), false); + + this->AddPage(_page_svgoutput, _("SVG output"), iter_io, PREFS_PAGE_IO_SVGOUTPUT); + + // SVG Export Options ========================================== + + // SVG 2 Fallbacks + _page_svgexport.add_group_header( _("SVG 2")); + _svgexport_insert_text_fallback.init( _("Insert SVG 1.1 fallback in text"), "/options/svgexport/text_insertfallback", true ); + _svgexport_insert_mesh_polyfill.init( _("Insert JavaScript code for mesh gradients"), "/options/svgexport/mesh_insertpolyfill", true ); + _svgexport_insert_hatch_polyfill.init( _("Insert JavaScript code for SVG2 hatches"), "/options/svgexport/hatch_insertpolyfill", true ); + + _page_svgexport.add_line( false, "", _svgexport_insert_text_fallback, "", _("Adds fallback options for non-SVG 2 renderers."), false); + _page_svgexport.add_line( false, "", _svgexport_insert_mesh_polyfill, "", _("Adds a JavaScript polyfill for rendering meshes in web browsers."), false); + _page_svgexport.add_line( false, "", _svgexport_insert_hatch_polyfill, "", _("Adds a JavaScript polyfill for rendering hatches in web browsers."), false); + + // SVG Export Options (SVG 2 -> SVG 1) + _page_svgexport.add_group_header( _("SVG 2 to SVG 1.1")); + + _svgexport_remove_marker_auto_start_reverse.init( _("Use correct marker direction in SVG 1.1 renderers"), "/options/svgexport/marker_autostartreverse", false); + _svgexport_remove_marker_context_paint.init( _("Use correct marker colors in SVG 1.1 renderers"), "/options/svgexport/marker_contextpaint", false); + + _page_svgexport.add_line( false, "", _svgexport_remove_marker_auto_start_reverse, "", _("SVG 2 allows markers to automatically be reversed at the start of a path with 'auto_start_reverse'. This adds a rotated duplicate of the marker's definition."), false); + _page_svgexport.add_line( false, "", _svgexport_remove_marker_context_paint, "", _("SVG 2 allows markers to automatically match the stroke color by using 'context_paint' or 'context_fill'. This adjusts the markers own colors."), false); + + this->AddPage(_page_svgexport, _("SVG export"), iter_io, PREFS_PAGE_IO_SVGEXPORT); + + + // CMS options + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const numIntents = 4; + /* TRANSLATORS: see http://www.newsandtech.com/issues/2004/03-04/pt/03-04_rendering.htm */ + Glib::ustring intentLabels[numIntents] = {_("Perceptual"), _("Relative Colorimetric"), _("Saturation"), _("Absolute Colorimetric")}; + int intentValues[numIntents] = {0, 1, 2, 3}; + + _page_cms.add_group_header( _("Display adjustment")); + + Glib::ustring tmpStr; + for (auto &profile: ColorProfile::getBaseProfileDirs()) { + gchar* part = g_strdup_printf( "\n%s", profile.filename.c_str() ); + tmpStr += part; + g_free(part); + } + + gchar* profileTip = g_strdup_printf(_("The ICC profile to use to calibrate display output.\nSearched directories:%s"), tmpStr.c_str()); + _page_cms.add_line( true, _("Display profile:"), _cms_display_profile, "", + profileTip, false); + g_free(profileTip); + profileTip = nullptr; + + _cms_from_display.init( _("Retrieve profile from display"), "/options/displayprofile/from_display", false); + _page_cms.add_line( true, "", _cms_from_display, "", +#ifdef GDK_WINDOWING_X11 + _("Retrieve profiles from those attached to displays via XICC"), false); +#else + _("Retrieve profiles from those attached to displays"), false); +#endif // GDK_WINDOWING_X11 + + + _cms_intent.init("/options/displayprofile/intent", intentLabels, intentValues, numIntents, 0); + _page_cms.add_line( true, _("Display rendering intent:"), _cms_intent, "", + _("The rendering intent to use to calibrate display output"), false); + + _page_cms.add_group_header( _("Proofing")); + + _cms_softproof.init( _("Simulate output on screen"), "/options/softproof/enable", false); + _page_cms.add_line( true, "", _cms_softproof, "", + _("Simulates output of target device"), false); + + _cms_gamutwarn.init( _("Mark out of gamut colors"), "/options/softproof/gamutwarn", false); + _page_cms.add_line( true, "", _cms_gamutwarn, "", + _("Highlights colors that are out of gamut for the target device"), false); + + Glib::ustring colorStr = prefs->getString("/options/softproof/gamutcolor"); + + Gdk::RGBA tmpColor( colorStr.empty() ? "#00ff00" : colorStr); + _cms_gamutcolor.set_rgba( tmpColor ); + + _page_cms.add_line( true, _("Out of gamut warning color:"), _cms_gamutcolor, "", + _("Selects the color used for out of gamut warning"), false); + + _page_cms.add_line( true, _("Device profile:"), _cms_proof_profile, "", + _("The ICC profile to use to simulate device output"), false); + + _cms_proof_intent.init("/options/softproof/intent", intentLabels, intentValues, numIntents, 0); + _page_cms.add_line( true, _("Device rendering intent:"), _cms_proof_intent, "", + _("The rendering intent to use to calibrate device output"), false); + + _cms_proof_blackpoint.init( _("Black point compensation"), "/options/softproof/bpc", false); + _page_cms.add_line( true, "", _cms_proof_blackpoint, "", + _("Enables black point compensation"), false); + + _cms_proof_preserveblack.init( _("Preserve black"), "/options/softproof/preserveblack", false); + +#if !defined(cmsFLAGS_PRESERVEBLACK) + _cms_proof_preserveblack.set_sensitive( false ); +#endif // !defined(cmsFLAGS_PRESERVEBLACK) + + + { + std::vector<Glib::ustring> names = ::Inkscape::CMSSystem::getDisplayNames(); + Glib::ustring current = prefs->getString( "/options/displayprofile/uri" ); + + gint index = 0; + _cms_display_profile.append(_("<none>")); + index++; + for (auto & name : names) { + _cms_display_profile.append( name ); + Glib::ustring path = CMSSystem::getPathForProfile(name); + if ( !path.empty() && path == current ) { + _cms_display_profile.set_active(index); + } + index++; + } + if ( current.empty() ) { + _cms_display_profile.set_active(0); + } + + names = ::Inkscape::CMSSystem::getSoftproofNames(); + current = prefs->getString("/options/softproof/uri"); + index = 0; + for (auto & name : names) { + _cms_proof_profile.append( name ); + Glib::ustring path = CMSSystem::getPathForProfile(name); + if ( !path.empty() && path == current ) { + _cms_proof_profile.set_active(index); + } + index++; + } + } + + _cms_gamutcolor.signal_color_set().connect( sigc::bind( sigc::ptr_fun(gamutColorChanged), &_cms_gamutcolor) ); + + _cms_display_profile.signal_changed().connect( sigc::bind( sigc::ptr_fun(profileComboChanged), &_cms_display_profile) ); + _cms_proof_profile.signal_changed().connect( sigc::bind( sigc::ptr_fun(proofComboChanged), &_cms_proof_profile) ); + + this->AddPage(_page_cms, _("Color management"), iter_io, PREFS_PAGE_IO_CMS); + + // Autosave options + _save_autosave_enable.init( _("Enable autosave"), "/options/autosave/enable", true); + _page_autosave.add_line(false, "", _save_autosave_enable, "", _("Automatically save the current document(s) at a given interval, thus minimizing loss in case of a crash"), false); + _save_autosave_path.init("/options/autosave/path", true); + if (prefs->getString("/options/autosave/path").empty()) { + // Show the default fallback "tmp dir" if autosave path is not set. + _save_autosave_path.set_text(Glib::build_filename(Glib::get_user_cache_dir(), "inkscape")); + } + _page_autosave.add_line(false, C_("Filesystem", "Autosave _directory:"), _save_autosave_path, "", _("The directory where autosaves will be written. This should be an absolute path (starts with / on UNIX or a drive letter such as C: on Windows)."), false); + _save_autosave_interval.init("/options/autosave/interval", 1.0, 10800.0, 1.0, 10.0, 10.0, true, false); + _page_autosave.add_line(false, _("_Interval (in minutes):"), _save_autosave_interval, "", _("Interval (in minutes) at which document will be autosaved"), false); + _save_autosave_max.init("/options/autosave/max", 1.0, 10000.0, 1.0, 10.0, 10.0, true, false); + _page_autosave.add_line(false, _("_Maximum number of autosaves:"), _save_autosave_max, "", _("Maximum number of autosaved files; use this to limit the storage space used"), false); + + // When changing the interval or enabling/disabling the autosave function, + // update our running configuration + _save_autosave_enable.changed_signal.connect([](bool) { Inkscape::AutoSave::restart(); }); + _save_autosave_interval.changed_signal.connect([](double) { Inkscape::AutoSave::restart(); }); + + this->AddPage(_page_autosave, _("Autosave"), iter_io, PREFS_PAGE_IO_AUTOSAVE); + + // No Result + _page_notfound.add_group_header(_("No matches were found, try another search!")); +} + +void InkscapePreferences::initPageBehavior() +{ + Gtk::TreeModel::iterator iter_behavior = this->AddPage(_page_behavior, _("Behavior"), PREFS_PAGE_BEHAVIOR); + + _misc_simpl.init("/options/simplifythreshold/value", 0.0001, 1.0, 0.0001, 0.0010, 0.0010, false, false); + _page_behavior.add_line( false, _("_Simplification threshold:"), _misc_simpl, "", + _("How strong is the Node tool's Simplify command by default. If you invoke this command several times in quick succession, it will act more and more aggressively; invoking it again after a pause restores the default threshold."), false); + + _markers_color_stock.init ( _("Color stock markers the same color as object"), "/options/markers/colorStockMarkers", true); + _markers_color_custom.init ( _("Color custom markers the same color as object"), "/options/markers/colorCustomMarkers", false); + _markers_color_update.init ( _("Update marker color when object color changes"), "/options/markers/colorUpdateMarkers", true); + + // Selecting options + _sel_all.init ( _("Select in all layers"), "/options/kbselection/inlayer", PREFS_SELECTION_ALL, false, nullptr); + _sel_current.init ( _("Select only within current layer"), "/options/kbselection/inlayer", PREFS_SELECTION_LAYER, true, &_sel_all); + _sel_recursive.init ( _("Select in current layer and sublayers"), "/options/kbselection/inlayer", PREFS_SELECTION_LAYER_RECURSIVE, false, &_sel_all); + _sel_hidden.init ( _("Ignore hidden objects and layers"), "/options/kbselection/onlyvisible", true); + _sel_locked.init ( _("Ignore locked objects and layers"), "/options/kbselection/onlysensitive", true); + _sel_inlayer_same.init ( _("Select same behaves like select all"), "/options/selection/samelikeall", false); + + _sel_layer_deselects.init ( _("Deselect upon layer change"), "/options/selection/layerdeselect", true); + _sel_touch_topmost_only.init ( _("Select the topmost items only when in touch selection mode"), "/options/selection/touchsel_topmost_only", true); + + _page_select.add_line( false, "", _sel_layer_deselects, "", + _("Uncheck this to be able to keep the current objects selected when the current layer changes")); + + _page_select.add_line( false, "", _sel_touch_topmost_only, "", + _("In touch selection mode, if multiple items overlap at a point, select only the topmost item")); + + _page_select.add_group_header( _("Ctrl+A, Tab, Shift+Tab")); + _page_select.add_line( true, "", _sel_all, "", + _("Make keyboard selection commands work on objects in all layers")); + _page_select.add_line( true, "", _sel_current, "", + _("Make keyboard selection commands work on objects in current layer only")); + _page_select.add_line( true, "", _sel_recursive, "", + _("Make keyboard selection commands work on objects in current layer and all its sublayers")); + _page_select.add_line( true, "", _sel_hidden, "", + _("Uncheck this to be able to select objects that are hidden (either by themselves or by being in a hidden layer)")); + _page_select.add_line( true, "", _sel_locked, "", + _("Uncheck this to be able to select objects that are locked (either by themselves or by being in a locked layer)")); + _page_select.add_line( true, "", _sel_inlayer_same, "", + _("Check this to make the 'select same' functions work like the select all functions, restricting to current layer only.")); + + _sel_cycle.init ( _("Wrap when cycling objects in z-order"), "/options/selection/cycleWrap", true); + + _page_select.add_group_header( _("Alt+Scroll Wheel")); + _page_select.add_line( true, "", _sel_cycle, "", + _("Wrap around at start and end when cycling objects in z-order")); + + auto paste_above_selected = Gtk::manage(new UI::Widget::PrefCheckButton()); + paste_above_selected->init(_("Paste above selection instead of layer-top"), "/options/paste/aboveselected", true); + _page_select.add_line(false, "", *paste_above_selected, "", + _("If checked, pasted items and imported documents will be placed immediately above the " + "current selection (z-order). Otherwise, insertion happens on top of all objects in the current layer.")); + + this->AddPage(_page_select, _("Selecting"), iter_behavior, PREFS_PAGE_BEHAVIOR_SELECTING); + + // Transforms options + _trans_scale_stroke.init ( _("Scale stroke width"), "/options/transform/stroke", true); + _trans_scale_corner.init ( _("Scale rounded corners in rectangles"), "/options/transform/rectcorners", false); + _trans_gradient.init ( _("Transform gradients"), "/options/transform/gradient", true); + _trans_pattern.init ( _("Transform patterns"), "/options/transform/pattern", false); + _trans_dash_scale.init(_("Scale dashes with stroke"), "/options/dash/scale", true); + _trans_optimized.init ( _("Optimized"), "/options/preservetransform/value", 0, true, nullptr); + _trans_preserved.init ( _("Preserved"), "/options/preservetransform/value", 1, false, &_trans_optimized); + + _page_transforms.add_line( false, "", _trans_scale_stroke, "", + _("When scaling objects, scale the stroke width by the same proportion")); + _page_transforms.add_line( false, "", _trans_scale_corner, "", + _("When scaling rectangles, scale the radii of rounded corners")); + _page_transforms.add_line( false, "", _trans_gradient, "", + _("Move gradients (in fill or stroke) along with the objects")); + _page_transforms.add_line( false, "", _trans_pattern, "", + _("Move patterns (in fill or stroke) along with the objects")); + _page_transforms.add_line(false, "", _trans_dash_scale, "", _("When changing stroke width, scale dash array")); + _page_transforms.add_group_header( _("Store transformation")); + _page_transforms.add_line( true, "", _trans_optimized, "", + _("If possible, apply transformation to objects without adding a transform= attribute")); + _page_transforms.add_line( true, "", _trans_preserved, "", + _("Always store transformation as a transform= attribute on objects")); + + this->AddPage(_page_transforms, _("Transforms"), iter_behavior, PREFS_PAGE_BEHAVIOR_TRANSFORMS); + + // Scrolling options + _scroll_wheel.init ( "/options/wheelscroll/value", 0.0, 1000.0, 1.0, 1.0, 40.0, true, false); + _page_scrolling.add_line( false, _("Mouse _wheel scrolls by:"), _scroll_wheel, _("pixels"), + _("One mouse wheel notch scrolls by this distance in screen pixels (horizontally with Shift)"), false); + _page_scrolling.add_group_header( _("Ctrl+arrows")); + _scroll_arrow_px.init ( "/options/keyscroll/value", 0.0, 1000.0, 1.0, 1.0, 10.0, true, false); + _page_scrolling.add_line( true, _("Sc_roll by:"), _scroll_arrow_px, _("pixels"), + _("Pressing Ctrl+arrow key scrolls by this distance (in screen pixels)"), false); + _scroll_arrow_acc.init ( "/options/scrollingacceleration/value", 0.0, 5.0, 0.01, 1.0, 0.35, false, false); + _page_scrolling.add_line( true, _("_Acceleration:"), _scroll_arrow_acc, "", + _("Pressing and holding Ctrl+arrow will gradually speed up scrolling (0 for no acceleration)"), false); + _page_scrolling.add_group_header( _("Autoscrolling")); + _scroll_auto_speed.init ( "/options/autoscrollspeed/value", 0.0, 5.0, 0.01, 1.0, 0.7, false, false); + _page_scrolling.add_line( true, _("_Speed:"), _scroll_auto_speed, "", + _("How fast the canvas autoscrolls when you drag beyond canvas edge (0 to turn autoscroll off)"), false); + _scroll_auto_thres.init ( "/options/autoscrolldistance/value", -600.0, 600.0, 1.0, 1.0, -10.0, true, false); + _page_scrolling.add_line( true, _("_Threshold:"), _scroll_auto_thres, _("pixels"), + _("How far (in screen pixels) you need to be from the canvas edge to trigger autoscroll; positive is outside the canvas, negative is within the canvas"), false); + _scroll_space.init ( _("Mouse move pans when Space is pressed"), "/options/spacebarpans/value", true); + _page_scrolling.add_line( true, "", _scroll_space, "", + _("When on, pressing and holding Space and dragging pans canvas")); + this->AddPage(_page_scrolling, _("Scrolling"), iter_behavior, PREFS_PAGE_BEHAVIOR_SCROLLING); + + // Snapping options + _page_snapping.add_group_header( _("Snap indicator")); + + _snap_indicator.init( _("Enable snap indicator"), "/options/snapindicator/value", true); + _page_snapping.add_line( true, "", _snap_indicator, "", + _("After snapping, a symbol is drawn at the point that has snapped")); + + _snap_indicator.changed_signal.connect( sigc::mem_fun(_snap_persistence, &Gtk::Widget::set_sensitive) ); + + _snap_persistence.init("/options/snapindicatorpersistence/value", 0.1, 10, 0.1, 1, 2, 1); + _page_snapping.add_line( true, _("Snap indicator persistence (in seconds):"), _snap_persistence, "", + _("Controls how long the snap indicator message will be shown, before it disappears"), true); + + _snap_indicator_distance.init( _("Show snap distance in case of alignment or distribution snap"), "/options/snapindicatordistance/value", false); + _page_snapping.add_line( true, "", _snap_indicator_distance, "", + _("Show snap distance in case of alignment or distribution snap")); + + _page_snapping.add_group_header( _("What should snap")); + + _snap_closest_only.init( _("Only snap the node closest to the pointer"), "/options/snapclosestonly/value", false); + _page_snapping.add_line( true, "", _snap_closest_only, "", + _("Only try to snap the node that is initially closest to the mouse pointer")); + + _snap_weight.init("/options/snapweight/value", 0, 1, 0.1, 0.2, 0.5, 1); + _page_snapping.add_line( true, _("_Weight factor:"), _snap_weight, "", + _("When multiple snap solutions are found, then Inkscape can either prefer the closest transformation (when set to 0), or prefer the node that was initially the closest to the pointer (when set to 1)"), true); + + _snap_mouse_pointer.init( _("Snap the mouse pointer when dragging a constrained knot"), "/options/snapmousepointer/value", false); + _page_snapping.add_line( true, "", _snap_mouse_pointer, "", + _("When dragging a knot along a constraint line, then snap the position of the mouse pointer instead of snapping the projection of the knot onto the constraint line")); + + _page_snapping.add_group_header( _("Delayed snap")); + + _snap_delay.init("/options/snapdelay/value", 0, 1, 0.1, 0.2, 0, 1); + _page_snapping.add_line( true, _("Delay (in seconds):"), _snap_delay, "", + _("Postpone snapping as long as the mouse is moving, and then wait an additional fraction of a second. This additional delay is specified here. When set to zero or to a very small number, snapping will be immediate."), true); + + + this->AddPage(_page_snapping, _("Snapping"), iter_behavior, PREFS_PAGE_BEHAVIOR_SNAPPING); + + // Steps options + _steps_arrow.init ( "/options/nudgedistance/value", 0.0, 1000.0, 0.01, 2.0, UNIT_TYPE_LINEAR, "px"); + //nudgedistance is limited to 1000 in select-context.cpp: use the same limit here + _page_steps.add_line( false, _("_Arrow keys move by:"), _steps_arrow, "", + _("Pressing an arrow key moves selected object(s) or node(s) by this distance"), false); + _steps_scale.init ( "/options/defaultscale/value", 0.0, 1000.0, 0.01, 2.0, UNIT_TYPE_LINEAR, "px"); + //defaultscale is limited to 1000 in select-context.cpp: use the same limit here + _page_steps.add_line( false, _("> and < _scale by:"), _steps_scale, "", + _("Pressing > or < scales selection up or down by this increment"), false); + _steps_inset.init ( "/options/defaultoffsetwidth/value", 0.0, 3000.0, 0.01, 2.0, UNIT_TYPE_LINEAR, "px"); + _page_steps.add_line( false, _("_Inset/Outset by:"), _steps_inset, "", + _("Inset and Outset commands displace the path by this distance"), false); + _steps_compass.init ( _("Compass-like display of angles"), "/options/compassangledisplay/value", true); + _page_steps.add_line( false, "", _steps_compass, "", + _("When on, angles are displayed with 0 at north, 0 to 360 range, positive clockwise; otherwise with 0 at east, -180 to 180 range, positive counterclockwise")); + int const num_items = 18; + Glib::ustring labels[num_items] = {"90", "60", "45", "36", "30", "22.5", "18", "15", "12", "10", "7.5", "6", "5", "3", "2", "1", "0.5", C_("Rotation angle", "None")}; + int values[num_items] = {2, 3, 4, 5, 6, 8, 10, 12, 15, 18, 24, 30, 36, 60, 90, 180, 360, 0}; + _steps_rot_snap.set_size_request(_sb_width); + _steps_rot_snap.init("/options/rotationsnapsperpi/value", labels, values, num_items, 12); + _page_steps.add_line( false, _("_Rotation snaps every:"), _steps_rot_snap, _("degrees"), + _("Rotating with Ctrl pressed snaps every that much degrees; also, pressing [ or ] rotates by this amount"), false); + _steps_rot_relative.init ( _("Relative snapping of guideline angles"), "/options/relativeguiderotationsnap/value", false); + _page_steps.add_line( false, "", _steps_rot_relative, "", + _("When on, the snap angles when rotating a guideline will be relative to the original angle")); + _steps_zoom.init ( "/options/zoomincrement/value", 101.0, 500.0, 1.0, 1.0, M_SQRT2, true, true); + _page_steps.add_line( false, _("_Zoom in/out by:"), _steps_zoom, _("%"), + _("Zoom tool click, +/- keys, and middle click zoom in and out by this multiplier"), false); + _middle_mouse_zoom.init ( _("Zoom with middle mouse click"), "/options/middlemousezoom/value", true); + _page_steps.add_line( true, "", _middle_mouse_zoom, "", + _("When activated, clicking the middle mouse button (usually the mouse wheel) zooms.")); + _steps_rotate.init ( "/options/rotateincrement/value", 1, 90, 1.0, 5.0, 15, false, false); + _page_steps.add_line( false, _("_Rotate canvas by:"), _steps_rotate, _("degrees"), + _("Rotate canvas clockwise and counter-clockwise by this amount."), false); + this->AddPage(_page_steps, _("Steps"), iter_behavior, PREFS_PAGE_BEHAVIOR_STEPS); + + // Clones options + _clone_option_parallel.init ( _("Move in parallel"), "/options/clonecompensation/value", + SP_CLONE_COMPENSATION_PARALLEL, true, nullptr); + _clone_option_stay.init ( _("Stay unmoved"), "/options/clonecompensation/value", + SP_CLONE_COMPENSATION_UNMOVED, false, &_clone_option_parallel); + _clone_option_transform.init ( _("Move according to transform"), "/options/clonecompensation/value", + SP_CLONE_COMPENSATION_NONE, false, &_clone_option_parallel); + _clone_option_unlink.init ( _("Are unlinked"), "/options/cloneorphans/value", + SP_CLONE_ORPHANS_UNLINK, true, nullptr); + _clone_option_delete.init ( _("Are deleted"), "/options/cloneorphans/value", + SP_CLONE_ORPHANS_DELETE, false, &_clone_option_unlink); + + _page_clones.add_group_header( _("Moving original: clones and linked offsets")); + _page_clones.add_line(true, "", _clone_option_parallel, "", + _("Clones are translated by the same vector as their original")); + _page_clones.add_line(true, "", _clone_option_stay, "", + _("Clones preserve their positions when their original is moved")); + _page_clones.add_line(true, "", _clone_option_transform, "", + _("Each clone moves according to the value of its transform= attribute; for example, a rotated clone will move in a different direction than its original")); + _page_clones.add_group_header( _("Deleting original: clones")); + _page_clones.add_line(true, "", _clone_option_unlink, "", + _("Orphaned clones are converted to regular objects")); + _page_clones.add_line(true, "", _clone_option_delete, "", + _("Orphaned clones are deleted along with their original")); + + _page_clones.add_group_header( _("Duplicating original+clones/linked offset")); + + _clone_relink_on_duplicate.init ( _("Relink duplicated clones"), "/options/relinkclonesonduplicate/value", false); + _page_clones.add_line(true, "", _clone_relink_on_duplicate, "", + _("When duplicating a selection containing both a clone and its original (possibly in groups), relink the duplicated clone to the duplicated original instead of the old original")); + + _page_clones.add_group_header( _("Unlinking clones")); + _clone_to_curves.init ( _("Path operations unlink clones"), "/options/pathoperationsunlink/value", true); + _page_clones.add_line(true, "", _clone_to_curves, "", + _("The following path operations will unlink clones: Stroke to path, Object to path, Boolean operations, Combine, Break apart")); + + //TRANSLATORS: Heading for the Inkscape Preferences "Clones" Page + this->AddPage(_page_clones, _("Clones"), iter_behavior, PREFS_PAGE_BEHAVIOR_CLONES); + + // Clip paths and masks options + _mask_mask_on_top.init ( _("When applying, use the topmost selected object as clippath/mask"), "/options/maskobject/topmost", true); + _page_mask.add_line(false, "", _mask_mask_on_top, "", + _("Uncheck this to use the bottom selected object as the clipping path or mask")); + _mask_mask_on_ungroup.init ( _("When ungroup, clip/mask is preserved in childrens"), "/options/maskobject/maskonungroup", true); + _page_mask.add_line(false, "", _mask_mask_on_ungroup, "", + _("Uncheck this to remove clip/mask on ungroup")); + _mask_mask_remove.init ( _("Remove clippath/mask object after applying"), "/options/maskobject/remove", true); + _page_mask.add_line(false, "", _mask_mask_remove, "", + _("After applying, remove the object used as the clipping path or mask from the drawing")); + + _page_mask.add_group_header( _("Before applying")); + + _mask_grouping_none.init( _("Do not group clipped/masked objects"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_NONE, true, nullptr); + _mask_grouping_separate.init( _("Put every clipped/masked object in its own group"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_SEPARATE, false, &_mask_grouping_none); + _mask_grouping_all.init( _("Put all clipped/masked objects into one group"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_ALL, false, &_mask_grouping_none); + + _page_mask.add_line(true, "", _mask_grouping_none, "", + _("Apply clippath/mask to every object")); + + _page_mask.add_line(true, "", _mask_grouping_separate, "", + _("Apply clippath/mask to groups containing single object")); + + _page_mask.add_line(true, "", _mask_grouping_all, "", + _("Apply clippath/mask to group containing all objects")); + + _page_mask.add_group_header( _("After releasing")); + + _mask_ungrouping.init ( _("Ungroup automatically created groups"), "/options/maskobject/ungrouping", true); + _page_mask.add_line(true, "", _mask_ungrouping, "", + _("Ungroup groups created when setting clip/mask")); + + this->AddPage(_page_mask, _("Clippaths and masks"), iter_behavior, PREFS_PAGE_BEHAVIOR_MASKS); + + + _page_markers.add_group_header( _("Stroke Style Markers")); + _page_markers.add_line( true, "", _markers_color_stock, "", + _("Stroke color same as object, fill color either object fill color or marker fill color")); + _page_markers.add_line( true, "", _markers_color_custom, "", + _("Stroke color same as object, fill color either object fill color or marker fill color")); + _page_markers.add_line( true, "", _markers_color_update, "", + _("Update marker color when object color changes")); + + this->AddPage(_page_markers, _("Markers"), iter_behavior, PREFS_PAGE_BEHAVIOR_MARKERS); + + + _page_cleanup.add_group_header( _("Document cleanup")); + _cleanup_swatches.init ( _("Remove unused swatches when doing a document cleanup"), "/options/cleanupswatches/value", false); // text label + _page_cleanup.add_line( true, "", _cleanup_swatches, "", + _("Remove unused swatches when doing a document cleanup")); // tooltip + this->AddPage(_page_cleanup, _("Cleanup"), iter_behavior, PREFS_PAGE_BEHAVIOR_CLEANUP); + + _page_lpe.add_group_header( _("Tiling")); + _lpe_copy_mirroricons.init ( _("Add advanced tiling options"), "/live_effects/copy/mirroricons", true); // text label + _page_lpe.add_line( true, "", _lpe_copy_mirroricons, "", + _("Enables using 16 advanced mirror options between the copies (so there can be copies that are mirrored differently between the rows and the columns) for Tiling LPE")); // tooltip + this->AddPage(_page_lpe, _("Live Path Effects (LPE)"), iter_behavior, PREFS_PAGE_BEHAVIOR_LPE); +} + +void InkscapePreferences::initPageRendering() +{ + /* threaded blur */ //related comments/widgets/functions should be renamed and option should be moved elsewhere when inkscape is fully multi-threaded + _filter_multi_threaded.init("/options/threading/numthreads", 1.0, 8.0, 1.0, 2.0, 4.0, true, false); + _page_rendering.add_line( false, _("Number of _Threads:"), _filter_multi_threaded, "", _("Configure number of processors/threads to use when rendering filters"), false, reset_icon()); + + // rendering cache + _rendering_cache_size.init("/options/renderingcache/size", 0.0, 4096.0, 1.0, 32.0, 64.0, true, false); + _page_rendering.add_line( false, _("Rendering _cache size:"), _rendering_cache_size, C_("mebibyte (2^20 bytes) abbreviation","MiB"), _("Set the amount of memory per document which can be used to store rendered parts of the drawing for later reuse; set to zero to disable caching"), false); + + // rendering tile multiplier + _rendering_tile_multiplier.init("/options/rendering/tile-multiplier", 1.0, 512.0, 1.0, 16.0, 16.0, true, false); + _page_rendering.add_line( false, _("Rendering tile multiplier:"), _rendering_tile_multiplier, "", _("On modern hardware, increasing this value (default is 16) can help to get a better performance when there are large areas with filtered objects (this includes blur and blend modes) in your drawing. Decrease the value to make zooming and panning in relevant areas faster on low-end hardware in drawings with few or no filters."), false); + + // rendering x-ray radius + _rendering_xray_radius.init("/options/rendering/xray-radius", 1.0, 1500.0, 1.0, 100.0, 100.0, true, false); + _page_rendering.add_line( false, _("X-ray radius:"), _rendering_xray_radius, "", _("Radius of the circular area around the mouse cursor in X-ray mode"), false); + + // rendering outline overlay opacity + _rendering_outline_overlay_opacity.init("/options/rendering/outline-overlay-opacity", 1.0, 100.0, 1.0, 5.0, 50.0, true, false); + _rendering_outline_overlay_opacity.signal_focus_out_event().connect(sigc::mem_fun(*this, &InkscapePreferences::on_outline_overlay_changed)); + _page_rendering.add_line( false, _("Outline overlay opacity:"), _rendering_outline_overlay_opacity, _("%"), _("Opacity of the color in outline overlay view mode"), false); + + // update strategy + int values[] = {1, 2, 3}; + Glib::ustring labels[] = {_("Responsive"), _("Full redraw"), _("Multiscale")}; + _canvas_update_strategy.init("/options/rendering/update_strategy", labels, values, 3, 3); + _page_rendering.add_line(false, _("Update strategy:"), _canvas_update_strategy, "", _("How to update continually changing content when it can't be redrawn fast enough"), false); + + /* blur quality */ + _blur_quality_best.init ( _("Best quality (slowest)"), "/options/blurquality/value", + BLUR_QUALITY_BEST, false, nullptr); + _blur_quality_better.init ( _("Better quality (slower)"), "/options/blurquality/value", + BLUR_QUALITY_BETTER, false, &_blur_quality_best); + _blur_quality_normal.init ( _("Average quality"), "/options/blurquality/value", + BLUR_QUALITY_NORMAL, true, &_blur_quality_best); + _blur_quality_worse.init ( _("Lower quality (faster)"), "/options/blurquality/value", + BLUR_QUALITY_WORSE, false, &_blur_quality_best); + _blur_quality_worst.init ( _("Lowest quality (fastest)"), "/options/blurquality/value", + BLUR_QUALITY_WORST, false, &_blur_quality_best); + + _page_rendering.add_group_header( _("Gaussian blur quality for display")); + _page_rendering.add_line( true, "", _blur_quality_best, "", + _("Best quality, but display may be very slow at high zooms (bitmap export always uses best quality)")); + _page_rendering.add_line( true, "", _blur_quality_better, "", + _("Better quality, but slower display")); + _page_rendering.add_line( true, "", _blur_quality_normal, "", + _("Average quality, acceptable display speed")); + _page_rendering.add_line( true, "", _blur_quality_worse, "", + _("Lower quality (some artifacts), but display is faster")); + _page_rendering.add_line( true, "", _blur_quality_worst, "", + _("Lowest quality (considerable artifacts), but display is fastest")); + + /* filter quality */ + _filter_quality_best.init ( _("Best quality (slowest)"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_BEST, false, nullptr); + _filter_quality_better.init ( _("Better quality (slower)"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_BETTER, false, &_filter_quality_best); + _filter_quality_normal.init ( _("Average quality"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_NORMAL, true, &_filter_quality_best); + _filter_quality_worse.init ( _("Lower quality (faster)"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_WORSE, false, &_filter_quality_best); + _filter_quality_worst.init ( _("Lowest quality (fastest)"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_WORST, false, &_filter_quality_best); + + _page_rendering.add_group_header( _("Filter effects quality for display")); + _page_rendering.add_line( true, "", _filter_quality_best, "", + _("Best quality, but display may be very slow at high zooms (bitmap export always uses best quality)")); + _page_rendering.add_line( true, "", _filter_quality_better, "", + _("Better quality, but slower display")); + _page_rendering.add_line( true, "", _filter_quality_normal, "", + _("Average quality, acceptable display speed")); + _page_rendering.add_line( true, "", _filter_quality_worse, "", + _("Lower quality (some artifacts), but display is faster")); + _page_rendering.add_line( true, "", _filter_quality_worst, "", + _("Lowest quality (considerable artifacts), but display is fastest")); + +#ifdef CAIRO_HAS_DITHER + _cairo_dithering.init(_("Use dithering"), "/options/dithering/render", false); + _page_rendering.add_line(false, "", _cairo_dithering, "", _("Makes gradients smoother. This can significantly impact the size of generated PNG files. To update the display after changing this option, just zoom out/in.")); +#endif + + auto grid = Gtk::make_managed<Gtk::Grid>(); + grid->set_border_width(12); + grid->set_orientation(Gtk::ORIENTATION_VERTICAL); + grid->set_column_spacing(12); + grid->set_row_spacing(6); + auto revealer = Gtk::make_managed<Gtk::Revealer>(); + revealer->add(*grid); + revealer->set_reveal_child(Inkscape::Preferences::get()->getBool("/options/rendering/devmode")); + _canvas_developer_mode_enabled.init(_("Enable developer mode"), "/options/rendering/devmode", false); + _canvas_developer_mode_enabled.signal_toggled().connect([revealer, this] {revealer->set_reveal_child(_canvas_developer_mode_enabled.get_active());}); + _page_rendering.add_group_header(_("Developer mode")); + _page_rendering.add_line(true, "", _canvas_developer_mode_enabled, "", _("Enable additional debugging options"), false); + _page_rendering.add(*revealer); + + auto add_devmode_line = [&] (Glib::ustring const &label, Gtk::Widget &widget, Glib::ustring const &suffix, const Glib::ustring &tip) { + widget.set_tooltip_text(tip); + + auto hb = Gtk::manage(new Gtk::Box()); + hb->set_spacing(12); + hb->set_hexpand(true); + hb->pack_start(widget, false, false); + hb->set_valign(Gtk::ALIGN_CENTER); + + auto label_widget = Gtk::make_managed<Gtk::Label>(label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true); + label_widget->set_mnemonic_widget(widget); + label_widget->set_markup(label_widget->get_text()); + label_widget->set_margin_start(12); + + label_widget->set_valign(Gtk::ALIGN_CENTER); + grid->add(*label_widget); + grid->attach_next_to(*hb, *label_widget, Gtk::POS_RIGHT, 1, 1); + + if (suffix != "") { + auto suffix_widget = Gtk::make_managed<Gtk::Label>(suffix, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true); + suffix_widget->set_markup(suffix_widget->get_text()); + hb->pack_start(*suffix_widget, false, false); + } + }; + + auto add_devmode_group_header = [&] (Glib::ustring name) { + auto label_widget = Gtk::make_managed<Gtk::Label>(Glib::ustring(/*"<span size='large'>*/"<b>") + name + Glib::ustring("</b>"/*</span>"*/) , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true); + label_widget->set_use_markup(true); + label_widget->set_valign(Gtk::ALIGN_CENTER); + grid->add(*label_widget); + }; + + //TRANSLATORS: The following are options for fine-tuning rendering, meant to be used by developers, + //find more explanations at https://gitlab.com/inkscape/inbox/-/issues/6544#note_886540227 + add_devmode_group_header(_("Low-level tuning options")); + _canvas_render_time_limit.init("/options/rendering/render_time_limit", 100.0, 1000000.0, 1.0, 0.0, 1000.0, true, false); + add_devmode_line(_("Render time limit"), _canvas_render_time_limit, C_("microsecond abbreviation", "μs"), _("The maximum time allowed for a rendering time slice")); + _canvas_use_new_bisector.init("", "/options/rendering/use_new_bisector", true); + add_devmode_line(_("Use new bisector algorithm"), _canvas_use_new_bisector, "", _("Use an alternative, more obvious bisection strategy: just chop tile in half along the larger dimension until small enough")); + _canvas_new_bisector_size.init("/options/rendering/new_bisector_size", 1.0, 10000.0, 1.0, 0.0, 500.0, true, false); + add_devmode_line(_("Smallest tile size for new bisector"), _canvas_new_bisector_size, C_("pixel abbreviation", "px"), _("Halve rendering tile rectangles until their largest dimension is this small")); + _rendering_tile_size.init("/options/rendering/tile-size", 1.0, 10000.0, 1.0, 0.0, 16.0, true, false); + add_devmode_line(_("Tile size:"), _rendering_tile_size, "", _("The \"tile size\" parameter previously hard-coded into Inkscape's original tile bisector.")); + _canvas_max_affine_diff.init("/options/rendering/max_affine_diff", 0.0, 100.0, 0.1, 0.0, 1.8, false, false); + add_devmode_line(_("Transformation threshold"), _canvas_max_affine_diff, "", _("How much the viewing transformation can change before throwing away the current redraw and starting again")); + _canvas_pad.init("/options/rendering/pad", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false); + add_devmode_line(_("Buffer padding"), _canvas_pad, C_("pixel abbreviation", "px"), _("Do not throw away rendered content in this area around the window yet")); + _canvas_coarsener_min_size.init("/options/rendering/coarsener_min_size", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false); + add_devmode_line(_("Min size for coarsener algorithm"), _canvas_coarsener_min_size, C_("pixel abbreviation", "px"), _("Coarsener algorithm only processes rectangles smaller/thinner than this.")); + _canvas_coarsener_glue_size.init("/options/rendering/coarsener_glue_size", 0.0, 1000.0, 1.0, 0.0, 80.0, true, false); + add_devmode_line(_("Glue size for coarsener algorithm"), _canvas_coarsener_glue_size, C_("pixel abbreviation", "px"), _("Coarsener algorithm absorbs nearby rectangles within this distance.")); + _canvas_coarsener_min_fullness.init("/options/rendering/coarsener_min_fullness", 0.0, 1.0, 0.0, 0.0, 0.3, false, false); + add_devmode_line(_("Min fullness for coarsener algorithm"), _canvas_coarsener_min_fullness, "", _("Refuse coarsening algorithm's attempt if the result would be more empty than this.")); + + add_devmode_group_header(_("Debugging, profiling and experiments")); + _canvas_debug_framecheck.init("", "/options/rendering/debug_framecheck", false); + add_devmode_line(_("Framecheck"), _canvas_debug_framecheck, "", _("Print profiling data of selected operations to a file")); + _canvas_debug_logging.init("", "/options/rendering/debug_logging", false); + add_devmode_line(_("Logging"), _canvas_debug_logging, "", _("Log certain events to the console")); + _canvas_debug_slow_redraw.init("", "/options/rendering/debug_slow_redraw", false); + add_devmode_line(_("Slow redraw"), _canvas_debug_slow_redraw, "", _("Introduce a fixed delay for each tile")); + _canvas_debug_slow_redraw_time.init("/options/rendering/debug_slow_redraw_time", 0.0, 1000000.0, 1.0, 0.0, 50.0, true, false); + add_devmode_line(_("Slow redraw time"), _canvas_debug_slow_redraw_time, C_("microsecond abbreviation", "μs"), _("The delay to introduce for each tile")); + _canvas_debug_show_redraw.init("", "/options/rendering/debug_show_redraw", false); + add_devmode_line(_("Show redraw"), _canvas_debug_show_redraw, "", _("Paint a translucent random colour over each newly drawn tile")); + _canvas_debug_show_unclean.init("", "/options/rendering/debug_show_unclean", false); + add_devmode_line(_("Show unclean region"), _canvas_debug_show_unclean, "", _("Show the region that needs to be redrawn in red")); + _canvas_debug_show_snapshot.init("", "/options/rendering/debug_show_snapshot", false); + add_devmode_line(_("Show snapshot region"), _canvas_debug_show_snapshot, "", _("Show the region that still contains a saved copy of previously rendered content in blue")); + _canvas_debug_show_clean.init("", "/options/rendering/debug_show_clean", false); + add_devmode_line(_("Show clean region's fragmentation"), _canvas_debug_show_clean, "", _("Show the outlines of the rectangles in the region where rendering is complete in green")); + _canvas_debug_disable_redraw.init("", "/options/rendering/debug_disable_redraw", false); + add_devmode_line(_("Disable redraw"), _canvas_debug_disable_redraw, "", _("Temporarily disable the idle redraw process completely")); + _canvas_debug_sticky_decoupled.init("", "/options/rendering/debug_sticky_decoupled", false); + add_devmode_line(_("Sticky decoupled mode"), _canvas_debug_sticky_decoupled, "", _("Stay in decoupled mode even after rendering is complete")); + + this->AddPage(_page_rendering, _("Rendering"), PREFS_PAGE_RENDERING); +} + +void InkscapePreferences::initPageBitmaps() +{ + /* Note: /options/bitmapoversample removed with Cairo renderer */ + _page_bitmaps.add_group_header( _("Edit")); + _misc_bitmap_autoreload.init(_("Automatically reload images"), "/options/bitmapautoreload/value", true); + _page_bitmaps.add_line( false, "", _misc_bitmap_autoreload, "", + _("Automatically reload linked images when file is changed on disk")); + _misc_bitmap_editor.init("/options/bitmapeditor/value", true); + _page_bitmaps.add_line( false, _("_Bitmap editor:"), _misc_bitmap_editor, "", "", true); + _misc_svg_editor.init("/options/svgeditor/value", true); + _page_bitmaps.add_line( false, _("_SVG editor:"), _misc_svg_editor, "", "", true); + + _page_bitmaps.add_group_header( _("Export")); + _importexport_export_res.init("/dialogs/export/defaultxdpi/value", 0.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false); + _page_bitmaps.add_line( false, _("Default export _resolution:"), _importexport_export_res, _("dpi"), + _("Default image resolution (in dots per inch) in the Export dialog"), false); + _page_bitmaps.add_group_header( _("Create")); + _bitmap_copy_res.init("/options/createbitmap/resolution", 1.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false); + _page_bitmaps.add_line( false, _("Resolution for Create Bitmap _Copy:"), _bitmap_copy_res, _("dpi"), + _("Resolution used by the Create Bitmap Copy command"), false); + + _page_bitmaps.add_group_header( _("Import")); + _bitmap_ask.init(_("Ask about linking and scaling when importing bitmap images"), "/dialogs/import/ask", true); + _page_bitmaps.add_line( true, "", _bitmap_ask, "", + _("Pop-up linking and scaling dialog when importing bitmap image.")); + _svg_ask.init(_("Ask about linking and scaling when importing SVG images"), "/dialogs/import/ask_svg", true); + _page_bitmaps.add_line( true, "", _svg_ask, "", + _("Pop-up linking and scaling dialog when importing SVG image.")); + + _svgoutput_usesodipodiabsref.init(_("Store absolute file path for linked images"), + "/options/svgoutput/usesodipodiabsref", false); + _page_bitmaps.add_line( + true, "", _svgoutput_usesodipodiabsref, "", + _("By default, image links are stored as relative paths whenever possible. If this option is enabled, Inkscape " + "will additionally add an absolute path ('sodipodi:absref' attribute) to the image. This is used as a " + "fall-back for locating the linked image, for example if the SVG document has been moved on disk. Note that this " + "will expose your directory structure in the file's source code, which can include personal information like your username."), + false); + + { + Glib::ustring labels[] = {_("Embed"), _("Link")}; + Glib::ustring values[] = {"embed", "link"}; + _bitmap_link.init("/dialogs/import/link", labels, values, G_N_ELEMENTS(values), "link"); + _page_bitmaps.add_line( false, _("Bitmap import/open mode:"), _bitmap_link, "", "", false); + } + + { + Glib::ustring labels[] = {_("Include"), _("Pages"), _("Embed"), _("Link"), _("New")}; + Glib::ustring values[] = {"include", "pages", "embed", "link", "new"}; + _svg_link.init("/dialogs/import/import_mode_svg", labels, values, G_N_ELEMENTS(values), "include"); + _page_bitmaps.add_line( false, _("SVG import mode:"), _svg_link, "", "", false); + } + + { + Glib::ustring labels[] = {_("None (auto)"), _("Smooth (optimizeQuality)"), _("Blocky (optimizeSpeed)") }; + Glib::ustring values[] = {"auto", "optimizeQuality", "optimizeSpeed"}; + _bitmap_scale.init("/dialogs/import/scale", labels, values, G_N_ELEMENTS(values), "scale"); + _page_bitmaps.add_line( false, _("Image scale (image-rendering):"), _bitmap_scale, "", "", false); + } + + /* Note: /dialogs/import/quality removed use of in r12542 */ + _importexport_import_res.init("/dialogs/import/defaultxdpi/value", 0.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false); + _page_bitmaps.add_line( false, _("Default _import resolution:"), _importexport_import_res, _("dpi"), + _("Default import resolution (in dots per inch) for bitmap and SVG import"), false); + _importexport_import_res_override.init(_("Override file resolution"), "/dialogs/import/forcexdpi", false); + _page_bitmaps.add_line( false, "", _importexport_import_res_override, "", + _("Use default bitmap resolution in favor of information from file")); + + _page_bitmaps.add_group_header( _("Render")); + // rendering outlines for pixmap image tags + _rendering_image_outline.init( _("Images in Outline Mode"), "/options/rendering/imageinoutlinemode", false); + _page_bitmaps.add_line(false, "", _rendering_image_outline, "", _("When active will render images while in outline mode instead of a red box with an x. This is useful for manual tracing.")); + + this->AddPage(_page_bitmaps, _("Imported Images"), PREFS_PAGE_BITMAPS); +} + +void InkscapePreferences::initKeyboardShortcuts(Gtk::TreeModel::iterator iter_ui) +{ + // ------- Shortcut file -------- + auto labels_and_names = Inkscape::Shortcuts::get_file_names(); + _kb_filelist.init( "/options/kbshortcuts/shortcutfile", labels_and_names, labels_and_names[0].second); + + auto tooltip = + Glib::ustring::compose(_("Select a file of predefined shortcuts and modifiers to use. Any customizations you " + "create will be added separately to %1"), + IO::Resource::get_path_string(IO::Resource::USER, IO::Resource::KEYS, "default.xml")); + + _page_keyshortcuts.add_line( false, _("Keyboard file:"), _kb_filelist, "", tooltip.c_str(), false); + + + // ---------- Tree -------- + _kb_store = Gtk::TreeStore::create( _kb_columns ); + _kb_store->set_sort_column ( GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, Gtk::SORT_ASCENDING ); // only sort in onKBListKeyboardShortcuts() + + _kb_filter = Gtk::TreeModelFilter::create(_kb_store); + _kb_filter->set_visible_func (sigc::mem_fun(*this, &InkscapePreferences::onKBSearchFilter)); + + _kb_shortcut_renderer.property_editable() = true; + + _kb_tree.set_model(_kb_filter); + _kb_tree.append_column(_("Name"), _kb_columns.name); + _kb_tree.append_column(_("Shortcut"), _kb_shortcut_renderer); + _kb_tree.append_column(_("Description"), _kb_columns.description); + _kb_tree.append_column(_("ID"), _kb_columns.id); + + _kb_tree.set_expander_column(*_kb_tree.get_column(0)); + + // Name + _kb_tree.get_column(0)->set_resizable(true); + _kb_tree.get_column(0)->set_clickable(true); + _kb_tree.get_column(0)->set_fixed_width (200); + + // Shortcut + _kb_tree.get_column(1)->set_resizable(true); + _kb_tree.get_column(1)->set_clickable(true); + _kb_tree.get_column(1)->set_fixed_width (150); + //_kb_tree.get_column(1)->add_attribute(_kb_shortcut_renderer.property_text(), _kb_columns.shortcut); + _kb_tree.get_column(1)->set_cell_data_func(_kb_shortcut_renderer, sigc::ptr_fun(InkscapePreferences::onKBShortcutRenderer)); + + // Description + auto desc_renderer = dynamic_cast<Gtk::CellRendererText*>(_kb_tree.get_column_cell_renderer(2)); + desc_renderer->property_wrap_mode() = Pango::WRAP_WORD; + desc_renderer->property_wrap_width() = 600; + _kb_tree.get_column(2)->set_resizable(true); + _kb_tree.get_column(2)->set_clickable(true); + _kb_tree.get_column(2)->set_expand(true); + + // ID + _kb_tree.get_column(3)->set_resizable(true); + _kb_tree.get_column(3)->set_clickable(true); + + _kb_shortcut_renderer.signal_accel_edited().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBTreeEdited) ); + _kb_shortcut_renderer.signal_accel_cleared().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBTreeCleared) ); + + _kb_notebook.append_page(_kb_page_shortcuts, _("Shortcuts")); + Gtk::ScrolledWindow* shortcut_scroller = new Gtk::ScrolledWindow(); + shortcut_scroller->add(_kb_tree); + shortcut_scroller->set_hexpand(); + shortcut_scroller->set_vexpand(); + // -------- Search -------- + _kb_search.init("/options/kbshortcuts/value", true); + _kb_page_shortcuts.add_line( false, _("Search:"), _kb_search, "", "", true); + _kb_page_shortcuts.attach(*shortcut_scroller, 0, 3, 2, 1); + + _mod_store = Gtk::TreeStore::create( _mod_columns ); + _mod_tree.set_model(_mod_store); + _mod_tree.append_column(_("Name"), _mod_columns.name); + _mod_tree.append_column("hot", _mod_columns.and_modifiers); + _mod_tree.append_column(_("ID"), _mod_columns.id); + _mod_tree.set_tooltip_column(2); + + // In order to get tooltips on header, we must create our own label. + auto and_keys_header = Gtk::manage(new Gtk::Label(_("Modifier"))); + and_keys_header->set_tooltip_text(_("All keys specified must be held down to activate this functionality.")); + and_keys_header->show(); + auto and_keys_column = _mod_tree.get_column(1); + and_keys_column->set_widget(*and_keys_header); + + auto edit_bar = Gtk::manage(new Gtk::Box()); + _kb_mod_ctrl.set_label("Ctrl"); + _kb_mod_shift.set_label("Shift"); + _kb_mod_alt.set_label("Alt"); + _kb_mod_meta.set_label("Meta"); + _kb_mod_enabled.set_label(_("Enabled")); + edit_bar->add(_kb_mod_ctrl); + edit_bar->add(_kb_mod_shift); + edit_bar->add(_kb_mod_alt); + edit_bar->add(_kb_mod_meta); + edit_bar->add(_kb_mod_enabled); + _kb_mod_ctrl.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_edited)); + _kb_mod_shift.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_edited)); + _kb_mod_alt.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_edited)); + _kb_mod_meta.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_edited)); + _kb_mod_enabled.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_enabled)); + _kb_page_modifiers.add_line(false, _("Change:"), *edit_bar, "", "", true); + + _mod_tree.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_selection_changed)); + on_modifier_selection_changed(); + + _kb_notebook.append_page(_kb_page_modifiers, _("Modifiers")); + Gtk::ScrolledWindow* mod_scroller = new Gtk::ScrolledWindow(); + mod_scroller->add(_mod_tree); + mod_scroller->set_hexpand(); + mod_scroller->set_vexpand(); + //_kb_page_modifiers.add(*mod_scroller); + _kb_page_modifiers.attach(*mod_scroller, 0, 1, 2, 1); + + int row = 2; + _page_keyshortcuts.attach(_kb_notebook, 0, row, 2, 1); + + row++; + + // ------ Reset/Import/Export ------- + auto box_buttons = Gtk::manage(new Gtk::ButtonBox); + + box_buttons->set_layout(Gtk::BUTTONBOX_END); + box_buttons->set_spacing(4); + + box_buttons->set_hexpand(); + _page_keyshortcuts.attach(*box_buttons, 0, row, 3, 1); + + auto kb_reset = Gtk::manage(new Gtk::Button(_("Reset"))); + kb_reset->set_use_underline(); + kb_reset->set_tooltip_text(_("Remove all your customized keyboard shortcuts, and revert to the shortcuts in the shortcut file listed above")); + box_buttons->pack_start(*kb_reset, true, true, 6); + box_buttons->set_child_secondary(*kb_reset); + + auto kb_import = Gtk::manage(new Gtk::Button(_("Import ..."))); + kb_import->set_use_underline(); + kb_import->set_tooltip_text(_("Import custom keyboard shortcuts from a file")); + box_buttons->pack_end(*kb_import, true, true, 6); + + auto kb_export = Gtk::manage(new Gtk::Button(_("Export ..."))); + kb_export->set_use_underline(); + kb_export->set_tooltip_text(_("Export custom keyboard shortcuts to a file")); + box_buttons->pack_end(*kb_export, true, true, 6); + + kb_reset->signal_clicked().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBReset) ); + kb_import->signal_clicked().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBImport) ); + kb_export->signal_clicked().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBExport) ); + _kb_search.signal_key_release_event().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBSearchKeyEvent) ); + _kb_filelist.signal_changed().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBList) ); + _page_keyshortcuts.signal_realize().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBRealize) ); + + this->AddPage(_page_keyshortcuts, _("Keyboard"), iter_ui, PREFS_PAGE_UI_KEYBOARD_SHORTCUTS); + + _kb_shortcuts_loaded = false; + Gtk::TreeStore::iterator iter_group = _kb_store->append(); + (*iter_group)[_kb_columns.name] = _("Loading ..."); + (*iter_group)[_kb_columns.shortcut] = ""; + (*iter_group)[_kb_columns.id] = ""; + (*iter_group)[_kb_columns.description] = ""; + (*iter_group)[_kb_columns.shortcutkey] = Gtk::AccelKey(); + (*iter_group)[_kb_columns.user_set] = 0; + + Gtk::TreeStore::iterator iter_mods = _mod_store->append(); + (*iter_mods)[_mod_columns.name] = _("Loading ..."); + (*iter_group)[_mod_columns.id] = ""; + (*iter_group)[_mod_columns.description] = _("Unable to load keyboard modifier list."); + (*iter_group)[_mod_columns.and_modifiers] = ""; +} + +void InkscapePreferences::onKBList() +{ + // New file path already stored in preferences. + Inkscape::Shortcuts::getInstance().init(); + onKBListKeyboardShortcuts(); +} + +void InkscapePreferences::onKBReset() +{ + Inkscape::Shortcuts::getInstance().clear_user_shortcuts(); + onKBListKeyboardShortcuts(); +} + +void InkscapePreferences::onKBImport() +{ + if (Inkscape::Shortcuts::getInstance().import_shortcuts()) { + onKBListKeyboardShortcuts(); + } +} + +void InkscapePreferences::onKBExport() +{ + Inkscape::Shortcuts::getInstance().export_shortcuts(); +} + +bool InkscapePreferences::onKBSearchKeyEvent(GdkEventKey * /*event*/) +{ + _kb_filter->refilter(); + return FALSE; +} + +void InkscapePreferences::onKBTreeCleared(const Glib::ustring& path) +{ + // Triggered by "Back" key. + Gtk::TreeModel::iterator iter = _kb_filter->get_iter(path); + Glib::ustring id = (*iter)[_kb_columns.id]; + // Gtk::AccelKey current_shortcut_key = (*iter)[_kb_columns.shortcutkey]; + + // Remove current shortcut from user file (won't remove from other files). + Inkscape::Shortcuts::getInstance().remove_user_shortcut(id); + + onKBListKeyboardShortcuts(); +} + +void InkscapePreferences::onKBTreeEdited (const Glib::ustring& path, guint accel_key, Gdk::ModifierType accel_mods, guint hardware_keycode) +{ + Inkscape::Shortcuts& shortcuts = Inkscape::Shortcuts::getInstance(); + + Gtk::TreeModel::iterator iter = _kb_filter->get_iter(path); + + Glib::ustring id = (*iter)[_kb_columns.id]; + Glib::ustring current_shortcut = (*iter)[_kb_columns.shortcut]; + Gtk::AccelKey const current_shortcut_key = (*iter)[_kb_columns.shortcutkey]; + + GdkEventKey event; + event.keyval = accel_key; + event.state = accel_mods; + event.hardware_keycode = hardware_keycode; + Gtk::AccelKey const new_shortcut_key = shortcuts.get_from_event(&event, true); + + if (!new_shortcut_key.is_null() && + (new_shortcut_key.get_key() != current_shortcut_key.get_key() || + new_shortcut_key.get_mod() != current_shortcut_key.get_mod()) + ) { + // Check if there is currently an actions assigned to this shortcut; if yes ask if the shortcut should be reassigned + Glib::ustring action_name; + Glib::ustring accel = Gtk::AccelGroup::name(accel_key, accel_mods); + auto *app = InkscapeApplication::instance()->gtk_app(); + std::vector<Glib::ustring> actions = app->get_actions_for_accel(accel); + if (!actions.empty()) { + action_name = actions[0]; + } + + if (!action_name.empty()) { + // Warn user about duplicated shortcuts. + Glib::ustring message = + Glib::ustring::compose(_("Keyboard shortcut \"%1\"\nis already assigned to \"%2\""), + shortcuts.get_label(new_shortcut_key), action_name); + Gtk::MessageDialog dialog(message, false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO, true); + dialog.set_title(_("Reassign shortcut?")); + dialog.set_secondary_text(_("Are you sure you want to reassign this shortcut?")); + dialog.set_transient_for(*dynamic_cast<Gtk::Window *>(get_toplevel())); + int response = dialog.run(); + if (response != Gtk::RESPONSE_YES) { + return; + } + } + + // Add the new shortcut. + shortcuts.add_user_shortcut(id, new_shortcut_key); + + onKBListKeyboardShortcuts(); + } +} + +bool InkscapePreferences::onKBSearchFilter(const Gtk::TreeModel::const_iterator& iter) +{ + Glib::ustring search = _kb_search.get_text().lowercase(); + if (search.empty()) { + return TRUE; + } + + Glib::ustring name = (*iter)[_kb_columns.name]; + Glib::ustring desc = (*iter)[_kb_columns.description]; + Glib::ustring shortcut = (*iter)[_kb_columns.shortcut]; + Glib::ustring id = (*iter)[_kb_columns.id]; + + if (id.empty()) { + return TRUE; // Keep all group nodes visible + } + + return (name.lowercase().find(search) != name.npos + || shortcut.lowercase().find(search) != name.npos + || desc.lowercase().find(search) != name.npos + || id.lowercase().find(search) != name.npos); +} + +void InkscapePreferences::onKBRealize() +{ + if (!_kb_shortcuts_loaded /*&& _current_page == &_page_keyshortcuts*/) { + _kb_shortcuts_loaded = true; + onKBListKeyboardShortcuts(); + } +} + +InkscapePreferences::ModelColumns &InkscapePreferences::onKBGetCols() +{ + static InkscapePreferences::ModelColumns cols; + return cols; +} + +void InkscapePreferences::onKBShortcutRenderer(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter) { + + Glib::ustring shortcut = (*iter)[onKBGetCols().shortcut]; + unsigned int user_set = (*iter)[onKBGetCols().user_set]; + Gtk::CellRendererAccel *accel = dynamic_cast<Gtk::CellRendererAccel *>(renderer); + if (user_set) { + accel->property_markup() = Glib::ustring("<span font-weight='bold'> " + shortcut + " </span>").c_str(); + } else { + accel->property_markup() = Glib::ustring("<span> " + shortcut + " </span>").c_str(); + } +} + +void InkscapePreferences::on_modifier_selection_changed() +{ + _kb_is_updated = true; + Gtk::TreeStore::iterator iter = _mod_tree.get_selection()->get_selected(); + auto selected = static_cast<bool>(iter); + + _kb_mod_ctrl.set_sensitive(selected); + _kb_mod_shift.set_sensitive(selected); + _kb_mod_alt.set_sensitive(selected); + _kb_mod_meta.set_sensitive(selected); + _kb_mod_enabled.set_sensitive(selected); + + _kb_mod_ctrl.set_active(false); + _kb_mod_shift.set_active(false); + _kb_mod_alt.set_active(false); + _kb_mod_meta.set_active(false); + _kb_mod_enabled.set_active(false); + + if (selected) { + Glib::ustring modifier_id = (*iter)[_mod_columns.id]; + auto modifier = Modifiers::Modifier::get(modifier_id.c_str()); + Inkscape::Modifiers::KeyMask mask = Inkscape::Modifiers::NEVER; + if(modifier != nullptr) { + mask = modifier->get_and_mask(); + } else { + _kb_mod_enabled.set_sensitive(false); + } + if(mask != Inkscape::Modifiers::NEVER) { + _kb_mod_enabled.set_active(true); + _kb_mod_ctrl.set_active(mask & Inkscape::Modifiers::CTRL); + _kb_mod_shift.set_active(mask & Inkscape::Modifiers::SHIFT); + _kb_mod_alt.set_active(mask & Inkscape::Modifiers::ALT); + _kb_mod_meta.set_active(mask & Inkscape::Modifiers::META); + } else { + _kb_mod_ctrl.set_sensitive(false); + _kb_mod_shift.set_sensitive(false); + _kb_mod_alt.set_sensitive(false); + _kb_mod_meta.set_sensitive(false); + } + } + _kb_is_updated = false; +} + +void InkscapePreferences::on_modifier_enabled() +{ + auto active = _kb_mod_enabled.get_active(); + _kb_mod_ctrl.set_sensitive(active); + _kb_mod_shift.set_sensitive(active); + _kb_mod_alt.set_sensitive(active); + _kb_mod_meta.set_sensitive(active); + on_modifier_edited(); +} + +void InkscapePreferences::on_modifier_edited() +{ + Gtk::TreeStore::iterator iter = _mod_tree.get_selection()->get_selected(); + if (!iter || _kb_is_updated) return; + + Glib::ustring modifier_id = (*iter)[_mod_columns.id]; + auto modifier = Modifiers::Modifier::get(modifier_id.c_str()); + if(!_kb_mod_enabled.get_active()) { + modifier->set_user(Inkscape::Modifiers::NEVER, Inkscape::Modifiers::NOT_SET); + } else { + Inkscape::Modifiers::KeyMask mask = 0; + if(_kb_mod_ctrl.get_active()) mask |= Inkscape::Modifiers::CTRL; + if(_kb_mod_shift.get_active()) mask |= Inkscape::Modifiers::SHIFT; + if(_kb_mod_alt.get_active()) mask |= Inkscape::Modifiers::ALT; + if(_kb_mod_meta.get_active()) mask |= Inkscape::Modifiers::META; + modifier->set_user(mask, Inkscape::Modifiers::NOT_SET); + } + Inkscape::Shortcuts& shortcuts = Inkscape::Shortcuts::getInstance(); + shortcuts.write_user(); + (*iter)[_mod_columns.and_modifiers] = modifier->get_label(); +} + +void InkscapePreferences::onKBListKeyboardShortcuts() +{ + Inkscape::Shortcuts& shortcuts = Inkscape::Shortcuts::getInstance(); + + // Save the current selection + Gtk::TreeStore::iterator iter = _kb_tree.get_selection()->get_selected(); + Glib::ustring selected_id = ""; + if (iter) { + selected_id = (*iter)[_kb_columns.id]; + } + + _kb_store->clear(); + _mod_store->clear(); + + // Gio::Actions + + auto iapp = InkscapeApplication::instance(); + auto gapp = iapp->gtk_app(); + + // std::vector<Glib::ustring> actions = shortcuts.list_all_actions(); // All actions (app, win, doc) + + // Simpler and better to get action list from extra data (contains "detailed action names"). + InkActionExtraData& action_data = iapp->get_action_extra_data(); + std::vector<Glib::ustring> actions = action_data.get_actions(); + + // Sort actions by section + auto action_sort = + [&](Glib::ustring &a, Glib::ustring &b) { + return action_data.get_section_for_action(a) < action_data.get_section_for_action(b); + }; + std::sort (actions.begin(), actions.end(), action_sort); + + Glib::ustring old_section; + Gtk::TreeStore::iterator iter_group; + + // Fill sections + for (auto action : actions) { + + Glib::ustring section = action_data.get_section_for_action(action); + if (section.empty()) section = "Misc"; + if (section != old_section) { + iter_group = _kb_store->append(); + (*iter_group)[_kb_columns.name] = section; + (*iter_group)[_kb_columns.shortcut] = ""; + (*iter_group)[_kb_columns.description] = ""; + (*iter_group)[_kb_columns.shortcutkey] = Gtk::AccelKey(); + (*iter_group)[_kb_columns.id] = ""; + (*iter_group)[_kb_columns.user_set] = 0; + old_section = section; + } + + // Find accelerators + std::vector<Glib::ustring> accels = gapp->get_accels_for_action(action); + Glib::ustring shortcut_label; + for (auto accel : accels) { + // Convert to more user friendly notation. + + // ::get_label shows key pad and numeric keys identically. + // TODO: Results in labels like "Numpad Alt+5" + if (accel.find("KP") != Glib::ustring::npos) { + shortcut_label += _("Numpad"); + shortcut_label += " "; + } + unsigned int key = 0; + Gdk::ModifierType mod = Gdk::ModifierType(0); + Gtk::AccelGroup::parse(accel, key, mod); + shortcut_label += Gtk::AccelGroup::get_label(key, mod) + ", "; + } + + if (shortcut_label.size() > 1) { + shortcut_label.erase(shortcut_label.size()-2); + } + + // Find primary (i.e. first) shortcut. + Gtk::AccelKey shortcut_key; + if (accels.size() > 0) { + unsigned int key = 0; + Gdk::ModifierType mod = Gdk::ModifierType(0); + Gtk::AccelGroup::parse(accels[0], key, mod); + shortcut_key = Gtk::AccelKey(key, mod); + } + + // Add the action to the group + Gtk::TreeStore::iterator row = _kb_store->append(iter_group->children()); + (*row)[_kb_columns.name] = action_data.get_label_for_action(action); + (*row)[_kb_columns.shortcut] = shortcut_label; + (*row)[_kb_columns.description] = action_data.get_tooltip_for_action(action); + (*row)[_kb_columns.shortcutkey] = shortcut_key; + (*row)[_kb_columns.id] = action; + (*row)[_kb_columns.user_set] = shortcuts.is_user_set(action); + + if (selected_id == action) { + Gtk::TreeStore::Path sel_path = _kb_filter->convert_child_path_to_path(_kb_store->get_path(row)); + _kb_tree.expand_to_path(sel_path); + _kb_tree.get_selection()->select(sel_path); + } + } + + std::string old_mod_group; + Gtk::TreeStore::iterator iter_mod_group; + + // Modifiers (mouse specific keys) + for(auto modifier: Inkscape::Modifiers::Modifier::getList()) { + auto cat_name = modifier->get_category(); + if (cat_name != old_mod_group) { + iter_mod_group = _mod_store->append(); + (*iter_mod_group)[_mod_columns.name] = cat_name.empty() ? "" : _(cat_name.c_str()); + (*iter_mod_group)[_mod_columns.id] = ""; + (*iter_mod_group)[_mod_columns.description] = ""; + (*iter_mod_group)[_mod_columns.and_modifiers] = ""; + (*iter_mod_group)[_mod_columns.user_set] = 0; + old_mod_group = cat_name; + } + + // Find accelerators + Gtk::TreeStore::iterator iter_modifier = _mod_store->append(iter_mod_group->children()); + (*iter_modifier)[_mod_columns.name] = (modifier->get_name() && strlen(modifier->get_name())) ? _(modifier->get_name()) : ""; + (*iter_modifier)[_mod_columns.id] = modifier->get_id(); + (*iter_modifier)[_mod_columns.description] = (modifier->get_description() && strlen(modifier->get_description())) ? _(modifier->get_description()) : ""; + (*iter_modifier)[_mod_columns.and_modifiers] = modifier->get_label(); + (*iter_modifier)[_mod_columns.user_set] = modifier->is_set_user(); + } + + // re-order once after updating (then disable ordering again to increase performance) + _kb_store->set_sort_column (_kb_columns.id, Gtk::SORT_ASCENDING ); + _kb_store->set_sort_column ( GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, Gtk::SORT_ASCENDING ); + + if (selected_id.empty()) { + _kb_tree.expand_to_path(_kb_store->get_path(_kb_store->get_iter("0:1"))); + } + + // Update all GUI text that includes shortcuts. + for (auto win : gapp->get_windows()) { + shortcuts.update_gui_text_recursive(win); + } + + // Update all GUI text + std::list< SPDesktop* > listbuf; + // Get list of all available desktops + INKSCAPE.get_all_desktops(listbuf); + + // Update text of each desktop to correct Shortcuts + for(SPDesktop *desktop: listbuf) { + // Caution: Checking if pointers are not NULL + if(desktop) { + InkscapeWindow *window = desktop->getInkscapeWindow(); + if(window) { + SPDesktopWidget *dtw = window->get_desktop_widget(); + if(dtw) + build_menu(); + } + } + } +} + +void InkscapePreferences::initPageSpellcheck() +{ +#if WITH_GSPELL + + _spell_ignorenumbers.init( _("Ignore words with digits"), "/dialogs/spellcheck/ignorenumbers", true); + _page_spellcheck.add_line( false, "", _spell_ignorenumbers, "", + _("Ignore words containing digits, such as \"R2D2\""), true); + + _spell_ignoreallcaps.init( _("Ignore words in ALL CAPITALS"), "/dialogs/spellcheck/ignoreallcaps", false); + _page_spellcheck.add_line( false, "", _spell_ignoreallcaps, "", + _("Ignore words in all capitals, such as \"IUPAC\""), true); + + this->AddPage(_page_spellcheck, _("Spellcheck"), PREFS_PAGE_SPELLCHECK); +#endif +} + +template <typename string_type> +static void appendList(Glib::ustring& tmp, const std::vector<string_type> &listing) +{ + for (auto const & str : listing) { + tmp += str; + tmp += "\n"; + } +} + + +void InkscapePreferences::initPageSystem() +{ + _misc_latency_skew.init("/debug/latency/skew", 0.5, 2.0, 0.01, 0.10, 1.0, false, false); + _page_system.add_line( false, _("Latency _skew:"), _misc_latency_skew, "", + _("Factor by which the event clock is skewed from the actual time (0.9766 on some systems)"), false, reset_icon()); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _misc_namedicon_delay.init( _("Pre-render named icons"), "/options/iconrender/named_nodelay", false); + _page_system.add_line( false, "", _misc_namedicon_delay, "", + _("When on, named icons will be rendered before displaying the ui. This is for working around bugs in GTK+ named icon notification"), true); + + _page_system.add_group_header( _("System info")); + + _sys_user_prefs.set_text(prefs->getPrefsFilename()); + _sys_user_prefs.set_editable(false); + Gtk::Button* reset_prefs = Gtk::manage(new Gtk::Button(_("Reset Preferences"))); + reset_prefs->signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::on_reset_prefs_clicked)); + + _page_system.add_line(true, _("User preferences:"), _sys_user_prefs, "", + _("Location of the user’s preferences file"), true, reset_prefs); + + _sys_user_config.init((char const *)Inkscape::IO::Resource::profile_path(""), _("Open preferences folder")); + _page_system.add_line(true, _("User config:"), _sys_user_config, "", _("Location of users configuration"), true); + + auto extensions_folder = IO::Resource::get_path_string(IO::Resource::USER, IO::Resource::EXTENSIONS); + _sys_user_extension_dir.init(extensions_folder, + _("Open extensions folder")); + _page_system.add_line(true, _("User extensions:"), _sys_user_extension_dir, "", + _("Location of the user’s extensions"), true); + + _sys_user_fonts_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::FONTS, ""), + _("Open fonts folder")); + _page_system.add_line(true, _("User fonts:"), _sys_user_fonts_dir, "", _("Location of the user’s fonts"), true); + + _sys_user_themes_dir.init(g_build_filename(g_get_user_data_dir(), "themes", nullptr), _("Open themes folder")); + _page_system.add_line(true, _("User themes:"), _sys_user_themes_dir, "", _("Location of the user’s themes"), true); + + _sys_user_icons_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::ICONS, ""), + _("Open icons folder")); + _page_system.add_line(true, _("User icons:"), _sys_user_icons_dir, "", _("Location of the user’s icons"), true); + + _sys_user_templates_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::TEMPLATES, ""), + _("Open templates folder")); + _page_system.add_line(true, _("User templates:"), _sys_user_templates_dir, "", + _("Location of the user’s templates"), true); + + _sys_user_symbols_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::SYMBOLS, ""), + _("Open symbols folder")); + + _page_system.add_line(true, _("User symbols:"), _sys_user_symbols_dir, "", _("Location of the user’s symbols"), + true); + + _sys_user_paint_servers_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::PAINT, ""), + _("Open paint servers folder")); + + _page_system.add_line(true, _("User paint servers:"), _sys_user_paint_servers_dir, "", + _("Location of the user’s paint servers"), true); + + _sys_user_palettes_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::PALETTES, ""), + _("Open palettes folder")); + _page_system.add_line(true, _("User palettes:"), _sys_user_palettes_dir, "", _("Location of the user’s palettes"), + true); + + _sys_user_keys_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::KEYS, ""), + _("Open keyboard shortcuts folder")); + _page_system.add_line(true, _("User keys:"), _sys_user_keys_dir, "", + _("Location of the user’s keyboard mapping files"), true); + + _sys_user_ui_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::UIS, ""), + _("Open user interface folder")); + _page_system.add_line(true, _("User UI:"), _sys_user_ui_dir, "", + _("Location of the user’s user interface description files"), true); + + _sys_user_cache.set_text(g_get_user_cache_dir()); + _sys_user_cache.set_editable(false); + _page_system.add_line(true, _("User cache:"), _sys_user_cache, "", _("Location of user’s cache"), true); + + Glib::ustring tmp_dir = prefs->getString("/options/autosave/path"); + if (tmp_dir.empty()) { + tmp_dir = Glib::build_filename(Glib::get_user_cache_dir(), "inkscape"); + } + _sys_tmp_files.set_text(tmp_dir); + _sys_tmp_files.set_editable(false); + _page_system.add_line(true, _("Temporary files:"), _sys_tmp_files, "", _("Location of the temporary files used for autosave"), true); + + _sys_data.set_text(get_inkscape_datadir()); + _sys_data.set_editable(false); + _page_system.add_line(true, _("Inkscape data:"), _sys_data, "", _("Location of Inkscape data"), true); + + extensions_folder = IO::Resource::get_path_string(IO::Resource::SYSTEM, IO::Resource::EXTENSIONS); + _sys_extension_dir.set_text(extensions_folder); + _sys_extension_dir.set_editable(false); + _page_system.add_line(true, _("Inkscape extensions:"), _sys_extension_dir, "", _("Location of the Inkscape extensions"), true); + + Glib::ustring tmp; + auto system_data_dirs = Glib::get_system_data_dirs(); + appendList(tmp, system_data_dirs); + _sys_systemdata.get_buffer()->insert(_sys_systemdata.get_buffer()->end(), tmp); + _sys_systemdata.set_editable(false); + _sys_systemdata_scroll.add(_sys_systemdata); + _sys_systemdata_scroll.set_size_request(100, 80); + _sys_systemdata_scroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _sys_systemdata_scroll.set_shadow_type(Gtk::SHADOW_IN); + _page_system.add_line(true, _("System data:"), _sys_systemdata_scroll, "", _("Locations of system data"), true); + + _sys_fontdirs_custom.init("/options/font/custom_fontdirs", 50); + _page_system.add_line(true, _("Custom Font directories"), _sys_fontdirs_custom, "", _("Load additional fonts from custom locations (one path per line)"), true); + + tmp = ""; + auto icon_theme = Gtk::IconTheme::get_default(); + auto paths = icon_theme->get_search_path(); + appendList( tmp, paths ); + _sys_icon.get_buffer()->insert(_sys_icon.get_buffer()->end(), tmp); + _sys_icon.set_editable(false); + _sys_icon_scroll.add(_sys_icon); + _sys_icon_scroll.set_size_request(100, 80); + _sys_icon_scroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _sys_icon_scroll.set_shadow_type(Gtk::SHADOW_IN); + _page_system.add_line(true, _("Icon theme:"), _sys_icon_scroll, "", _("Locations of icon themes"), true); + + this->AddPage(_page_system, _("System"), PREFS_PAGE_SYSTEM); +} + +bool InkscapePreferences::GetSizeRequest(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + DialogPage* page = row[_page_list_columns._col_page]; + _page_frame.add(*page); + this->show_all_children(); + Gtk::Requisition sreq_minimum; + Gtk::Requisition sreq_natural; + get_preferred_size(sreq_minimum, sreq_natural); + _minimum_width = std::max(_minimum_width, sreq_minimum.width); + _minimum_height = std::max(_minimum_height, sreq_minimum.height); + _natural_width = std::max(_natural_width, sreq_natural.width); + _natural_height = std::max(_natural_height, sreq_natural.height); + _page_frame.remove(); + return false; +} + +// Check if iter points to page indicated in preferences. +bool InkscapePreferences::matchPage(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int desired_page = prefs->getInt("/dialogs/preferences/page", 0); + _init = false; + if (desired_page == row[_page_list_columns._col_id]) + { + auto const path = _page_list.get_model()->get_path(*iter); + _page_list.expand_to_path(path); + _page_list.get_selection()->select(iter); + if (desired_page == PREFS_PAGE_UI_THEME) + symbolicThemeCheck(); + return true; + } + return false; +} + +void InkscapePreferences::on_reset_open_recent_clicked() +{ + Glib::RefPtr<Gtk::RecentManager> manager = Gtk::RecentManager::get_default(); + std::vector< Glib::RefPtr< Gtk::RecentInfo > > recent_list = manager->get_items(); + + // Remove only elements that were added by Inkscape + // TODO: This should likely preserve items that were also accessed by other apps. + // However there does not seem to be straightforward way to delete only an application from an item. + for (auto e : recent_list) { + if (e->has_application(g_get_prgname()) + || e->has_application("org.inkscape.Inkscape") + || e->has_application("inkscape") +#ifdef _WIN32 + || e->has_application("inkscape.exe") +#endif + ) { + manager->remove_item(e->get_uri()); + } + } +} + +void InkscapePreferences::on_reset_prefs_clicked() +{ + Inkscape::Preferences::get()->reset(); +} + +void InkscapePreferences::show_not_found() +{ + if (_current_page) + _page_frame.remove(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _current_page = &_page_notfound; + _page_title.set_markup(_("<span size='large'><b>No Results</b></span>")); + _page_frame.add(*_current_page); + _current_page->show(); + this->show_all_children(); + if (prefs->getInt("/dialogs/preferences/page", 0) == PREFS_PAGE_UI_THEME) { + symbolicThemeCheck(); + } +} + +void InkscapePreferences::show_nothing_on_page() +{ + _page_frame.remove(); + _page_title.set_text(""); +} + +void InkscapePreferences::on_pagelist_selection_changed() +{ + // show new selection + Glib::RefPtr<Gtk::TreeSelection> selection = _page_list.get_selection(); + Gtk::TreeModel::iterator iter = selection->get_selected(); + if(iter) + { + if (_current_page) + _page_frame.remove(); + Gtk::TreeModel::Row row = *iter; + _current_page = row[_page_list_columns._col_page]; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!_init) { + prefs->setInt("/dialogs/preferences/page", row[_page_list_columns._col_id]); + } + Glib::ustring col_name_escaped = Glib::Markup::escape_text( row[_page_list_columns._col_name] ); + _page_title.set_markup("<span size='large'><b>" + col_name_escaped + "</b></span>"); + _page_frame.add(*_current_page); + _current_page->show(); + this->show_all_children(); + if (prefs->getInt("/dialogs/preferences/page", 0) == PREFS_PAGE_UI_THEME) { + symbolicThemeCheck(); + } + } +} + +// Show page indicated in preferences file. +void InkscapePreferences::showPage() +{ + _search.set_text(""); + _page_list.get_model()->foreach_iter(sigc::mem_fun(*this, &InkscapePreferences::matchPage)); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h new file mode 100644 index 0000000..87345a4 --- /dev/null +++ b/src/ui/dialog/inkscape-preferences.h @@ -0,0 +1,747 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Inkscape Preferences dialog + */ +/* Authors: + * Carl Hetherington + * Marco Scholten + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Bruno Dilly <bruno.dilly@gmail.com> + * + * Copyright (C) 2004-2013 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_INKSCAPE_PREFERENCES_H +#define INKSCAPE_UI_DIALOG_INKSCAPE_PREFERENCES_H + +// checking if cairo supports dithering +#ifdef WITH_PATCHED_CAIRO +#include "3rdparty/cairo/src/cairo.h" +#else +#include <cairo.h> +#endif + + + +#include <gtkmm/treerowreference.h> +#include <iostream> +#include <iterator> +#include <vector> +#include "ui/widget/preferences-widget.h" +#include <cstddef> +#include <gtkmm/colorbutton.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/treestore.h> +#include <gtkmm/treeview.h> +#include <gtkmm/treemodelfilter.h> +#include <gtkmm/treemodelsort.h> +#include <gtkmm/frame.h> +#include <gtkmm/notebook.h> +#include <gtkmm/textview.h> +#include <gtkmm/searchentry.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/liststore.h> +#include <gtkmm/treemodel.h> +#include <gtkmm/treemodelfilter.h> +#include <glibmm/regex.h> + +#include "ui/dialog/dialog-base.h" + +// UPDATE THIS IF YOU'RE ADDING PREFS PAGES. +// Otherwise the commands that open the dialog with the new page will fail. + +enum { + PREFS_PAGE_TOOLS, + PREFS_PAGE_TOOLS_SELECTOR, + PREFS_PAGE_TOOLS_NODE, + PREFS_PAGE_TOOLS_TWEAK, + PREFS_PAGE_TOOLS_ZOOM, + PREFS_PAGE_TOOLS_MEASURE, + PREFS_PAGE_TOOLS_SHAPES, + PREFS_PAGE_TOOLS_SHAPES_RECT, + PREFS_PAGE_TOOLS_SHAPES_3DBOX, + PREFS_PAGE_TOOLS_SHAPES_ELLIPSE, + PREFS_PAGE_TOOLS_SHAPES_STAR, + PREFS_PAGE_TOOLS_SHAPES_SPIRAL, + PREFS_PAGE_TOOLS_PENCIL, + PREFS_PAGE_TOOLS_PEN, + PREFS_PAGE_TOOLS_CALLIGRAPHY, + PREFS_PAGE_TOOLS_TEXT, + PREFS_PAGE_TOOLS_SPRAY, + PREFS_PAGE_TOOLS_ERASER, + PREFS_PAGE_TOOLS_PAINTBUCKET, + PREFS_PAGE_TOOLS_GRADIENT, + PREFS_PAGE_TOOLS_DROPPER, + PREFS_PAGE_TOOLS_CONNECTOR, + PREFS_PAGE_TOOLS_LPETOOL, + PREFS_PAGE_UI, + PREFS_PAGE_UI_THEME, + PREFS_PAGE_UI_TOOLBARS, + PREFS_PAGE_UI_WINDOWS, + PREFS_PAGE_UI_GRIDS, + PREFS_PAGE_COMMAND_PALETTE, + PREFS_PAGE_UI_KEYBOARD_SHORTCUTS, + PREFS_PAGE_BEHAVIOR, + PREFS_PAGE_BEHAVIOR_SELECTING, + PREFS_PAGE_BEHAVIOR_TRANSFORMS, + PREFS_PAGE_BEHAVIOR_SCROLLING, + PREFS_PAGE_BEHAVIOR_SNAPPING, + PREFS_PAGE_BEHAVIOR_STEPS, + PREFS_PAGE_BEHAVIOR_CLONES, + PREFS_PAGE_BEHAVIOR_MASKS, + PREFS_PAGE_BEHAVIOR_MARKERS, + PREFS_PAGE_BEHAVIOR_CLEANUP, + PREFS_PAGE_BEHAVIOR_LPE, + PREFS_PAGE_IO, + PREFS_PAGE_IO_MOUSE, + PREFS_PAGE_IO_SVGOUTPUT, + PREFS_PAGE_IO_SVGEXPORT, + PREFS_PAGE_IO_CMS, + PREFS_PAGE_IO_AUTOSAVE, + PREFS_PAGE_IO_OPENCLIPART, + PREFS_PAGE_SYSTEM, + PREFS_PAGE_BITMAPS, + PREFS_PAGE_RENDERING, + PREFS_PAGE_SPELLCHECK, + PREFS_PAGE_NOTFOUND +}; + +namespace Gtk { +class Scale; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class InkscapePreferences : public DialogBase +{ +public: + ~InkscapePreferences() override; + + static InkscapePreferences &getInstance() { return *new InkscapePreferences(); } + void showPage(); // Show page indicated by "/dialogs/preferences/page". + +protected: + Gtk::Frame _page_frame; + Gtk::Label _page_title; + Gtk::TreeView _page_list; + Gtk::SearchEntry _search; + Glib::RefPtr<Gtk::TreeStore> _page_list_model; + Gtk::Widget *_highlighted_widget = nullptr; + Glib::RefPtr<Gtk::TreeModelFilter> _page_list_model_filter; + Glib::RefPtr<Gtk::TreeModelSort> _page_list_model_sort; + std::vector<Gtk::Widget *> _search_results; + Glib::RefPtr<Glib::Regex> _rx; + int _num_results = 0; + bool _show_all = false; + + //Pagelist model columns: + class PageListModelColumns : public Gtk::TreeModel::ColumnRecord + { + public: + PageListModelColumns() + { Gtk::TreeModelColumnRecord::add(_col_name); Gtk::TreeModelColumnRecord::add(_col_page); Gtk::TreeModelColumnRecord::add(_col_id); } + Gtk::TreeModelColumn<Glib::ustring> _col_name; + Gtk::TreeModelColumn<int> _col_id; + Gtk::TreeModelColumn<UI::Widget::DialogPage*> _col_page; + }; + PageListModelColumns _page_list_columns; + + UI::Widget::DialogPage _page_tools; + UI::Widget::DialogPage _page_selector; + UI::Widget::DialogPage _page_node; + UI::Widget::DialogPage _page_tweak; + UI::Widget::DialogPage _page_spray; + UI::Widget::DialogPage _page_zoom; + UI::Widget::DialogPage _page_measure; + UI::Widget::DialogPage _page_shapes; + UI::Widget::DialogPage _page_pencil; + UI::Widget::DialogPage _page_pen; + UI::Widget::DialogPage _page_calligraphy; + UI::Widget::DialogPage _page_text; + UI::Widget::DialogPage _page_gradient; + UI::Widget::DialogPage _page_connector; + UI::Widget::DialogPage _page_dropper; + UI::Widget::DialogPage _page_lpetool; + + UI::Widget::DialogPage _page_rectangle; + UI::Widget::DialogPage _page_3dbox; + UI::Widget::DialogPage _page_ellipse; + UI::Widget::DialogPage _page_star; + UI::Widget::DialogPage _page_spiral; + UI::Widget::DialogPage _page_paintbucket; + UI::Widget::DialogPage _page_eraser; + + UI::Widget::DialogPage _page_ui; + UI::Widget::DialogPage _page_notfound; + UI::Widget::DialogPage _page_theme; + UI::Widget::DialogPage _page_toolbars; + UI::Widget::DialogPage _page_windows; + UI::Widget::DialogPage _page_grids; + UI::Widget::DialogPage _page_command_palette; + + UI::Widget::DialogPage _page_behavior; + UI::Widget::DialogPage _page_select; + UI::Widget::DialogPage _page_transforms; + UI::Widget::DialogPage _page_scrolling; + UI::Widget::DialogPage _page_snapping; + UI::Widget::DialogPage _page_steps; + UI::Widget::DialogPage _page_clones; + UI::Widget::DialogPage _page_mask; + UI::Widget::DialogPage _page_markers; + UI::Widget::DialogPage _page_cleanup; + UI::Widget::DialogPage _page_lpe; + + UI::Widget::DialogPage _page_io; + UI::Widget::DialogPage _page_mouse; + UI::Widget::DialogPage _page_svgoutput; + UI::Widget::DialogPage _page_svgexport; + UI::Widget::DialogPage _page_cms; + UI::Widget::DialogPage _page_autosave; + + UI::Widget::DialogPage _page_rendering; + UI::Widget::DialogPage _page_system; + UI::Widget::DialogPage _page_bitmaps; + UI::Widget::DialogPage _page_spellcheck; + + UI::Widget::DialogPage _page_keyshortcuts; + + UI::Widget::PrefSpinButton _mouse_sens; + UI::Widget::PrefSpinButton _mouse_thres; + UI::Widget::PrefSlider _mouse_grabsize; + UI::Widget::PrefCheckButton _mouse_use_ext_input; + UI::Widget::PrefCheckButton _mouse_switch_on_ext_input; + + UI::Widget::PrefSpinButton _scroll_wheel; + UI::Widget::PrefSpinButton _scroll_arrow_px; + UI::Widget::PrefSpinButton _scroll_arrow_acc; + UI::Widget::PrefSpinButton _scroll_auto_speed; + UI::Widget::PrefSpinButton _scroll_auto_thres; + UI::Widget::PrefCheckButton _scroll_space; + + Gtk::Scale *_slider_snapping_delay; + + UI::Widget::PrefCheckButton _snap_default; + UI::Widget::PrefCheckButton _snap_indicator; + UI::Widget::PrefCheckButton _snap_closest_only; + UI::Widget::PrefCheckButton _snap_mouse_pointer; + UI::Widget::PrefCheckButton _snap_indicator_distance; + + UI::Widget::PrefCombo _steps_rot_snap; + UI::Widget::PrefCheckButton _steps_rot_relative; + UI::Widget::PrefCheckButton _steps_compass; + UI::Widget::PrefSpinUnit _steps_arrow; + UI::Widget::PrefSpinUnit _steps_scale; + UI::Widget::PrefSpinUnit _steps_inset; + UI::Widget::PrefSpinButton _steps_zoom; + UI::Widget::PrefCheckButton _middle_mouse_zoom; + UI::Widget::PrefSpinButton _steps_rotate; + + UI::Widget::PrefRadioButton _t_sel_trans_obj; + UI::Widget::PrefRadioButton _t_sel_trans_outl; + UI::Widget::PrefRadioButton _t_sel_cue_none; + UI::Widget::PrefRadioButton _t_sel_cue_mark; + UI::Widget::PrefRadioButton _t_sel_cue_box; + UI::Widget::PrefRadioButton _t_bbox_visual; + UI::Widget::PrefRadioButton _t_bbox_geometric; + + UI::Widget::PrefCheckButton _t_cvg_keep_objects; + UI::Widget::PrefCheckButton _t_cvg_convert_whole_groups; + UI::Widget::PrefCheckButton _t_node_show_outline; + UI::Widget::PrefCheckButton _t_node_live_outline; + UI::Widget::PrefCheckButton _t_node_live_objects; + UI::Widget::PrefCheckButton _t_node_pathflash_enabled; + UI::Widget::PrefCheckButton _t_node_pathflash_selected; + UI::Widget::PrefSpinButton _t_node_pathflash_timeout; + UI::Widget::PrefCheckButton _t_node_show_path_direction; + UI::Widget::PrefCheckButton _t_node_single_node_transform_handles; + UI::Widget::PrefCheckButton _t_node_delete_preserves_shape; + UI::Widget::PrefColorPicker _t_node_pathoutline_color; + + // Command Palette + UI::Widget::PrefCheckButton _cp_show_full_action_name; + UI::Widget::PrefCheckButton _cp_show_untranslated_name; + + UI::Widget::PrefCombo _gtk_theme; + UI::Widget::PrefOpenFolder _sys_user_themes_dir_copy; + UI::Widget::PrefOpenFolder _sys_user_icons_dir_copy; + UI::Widget::PrefCombo _icon_theme; + UI::Widget::PrefCheckButton _dark_theme; + UI::Widget::PrefSlider _contrast_theme; + UI::Widget::PrefCheckButton _narrow_spinbutton; + UI::Widget::PrefCheckButton _compact_colorselector; + UI::Widget::PrefCheckButton _symbolic_icons; + UI::Widget::PrefCheckButton _symbolic_base_colors; + UI::Widget::PrefCheckButton _symbolic_highlight_colors; + UI::Widget::PrefColorPicker _symbolic_base_color; + UI::Widget::PrefColorPicker _symbolic_warning_color; + UI::Widget::PrefColorPicker _symbolic_error_color; + UI::Widget::PrefColorPicker _symbolic_success_color; + /* Gtk::Image *_complementary_colors; */ + UI::Widget::PrefCombo _misc_small_toolbar; + UI::Widget::PrefCombo _misc_small_secondary; + UI::Widget::PrefCombo _misc_small_tools; + UI::Widget::PrefCombo _menu_icons; + + UI::Widget::PrefRadioButton _win_dockable; + UI::Widget::PrefRadioButton _win_floating; + UI::Widget::PrefRadioButton _win_native; + UI::Widget::PrefRadioButton _win_gtk; + UI::Widget::PrefRadioButton _win_save_dialog_pos_on; + UI::Widget::PrefRadioButton _win_save_dialog_pos_off; + UI::Widget::PrefCombo _win_default_size; + UI::Widget::PrefRadioButton _win_ontop_none; + UI::Widget::PrefRadioButton _win_ontop_normal; + UI::Widget::PrefRadioButton _win_ontop_agressive; + UI::Widget::PrefRadioButton _win_dialogs_labels_auto; + UI::Widget::PrefRadioButton _win_dialogs_labels_active; + UI::Widget::PrefRadioButton _win_dialogs_labels_off; + UI::Widget::PrefRadioButton _win_save_geom_off; + UI::Widget::PrefRadioButton _win_save_geom; + UI::Widget::PrefRadioButton _win_save_geom_prefs; + UI::Widget::PrefCheckButton _win_show_boot; + UI::Widget::PrefCheckButton _win_hide_task; + UI::Widget::PrefCheckButton _win_save_viewport; + UI::Widget::PrefCheckButton _win_zoom_resize; + + UI::Widget::PrefCheckButton _pencil_average_all_sketches; + + UI::Widget::PrefCheckButton _calligrapy_keep_selected; + + UI::Widget::PrefCheckButton _connector_ignore_text; + + UI::Widget::PrefRadioButton _clone_option_parallel; + UI::Widget::PrefRadioButton _clone_option_stay; + UI::Widget::PrefRadioButton _clone_option_transform; + UI::Widget::PrefRadioButton _clone_option_unlink; + UI::Widget::PrefRadioButton _clone_option_delete; + UI::Widget::PrefCheckButton _clone_relink_on_duplicate; + UI::Widget::PrefCheckButton _clone_to_curves; + + UI::Widget::PrefCheckButton _mask_mask_on_top; + UI::Widget::PrefCheckButton _mask_mask_remove; + UI::Widget::PrefCheckButton _mask_mask_on_ungroup; + UI::Widget::PrefRadioButton _mask_grouping_none; + UI::Widget::PrefRadioButton _mask_grouping_separate; + UI::Widget::PrefRadioButton _mask_grouping_all; + UI::Widget::PrefCheckButton _mask_ungrouping; + + UI::Widget::PrefSpinButton _filter_multi_threaded; + UI::Widget::PrefSpinButton _rendering_cache_size; + UI::Widget::PrefSpinButton _rendering_tile_multiplier; + UI::Widget::PrefSpinButton _rendering_xray_radius; + UI::Widget::PrefSpinButton _rendering_outline_overlay_opacity; + UI::Widget::PrefCombo _canvas_update_strategy; + UI::Widget::PrefRadioButton _blur_quality_best; + UI::Widget::PrefRadioButton _blur_quality_better; + UI::Widget::PrefRadioButton _blur_quality_normal; + UI::Widget::PrefRadioButton _blur_quality_worse; + UI::Widget::PrefRadioButton _blur_quality_worst; + UI::Widget::PrefRadioButton _filter_quality_best; + UI::Widget::PrefRadioButton _filter_quality_better; + UI::Widget::PrefRadioButton _filter_quality_normal; + UI::Widget::PrefRadioButton _filter_quality_worse; + UI::Widget::PrefRadioButton _filter_quality_worst; +#ifdef CAIRO_HAS_DITHER + UI::Widget::PrefCheckButton _cairo_dithering; +#endif + + UI::Widget::PrefCheckButton _canvas_developer_mode_enabled; + UI::Widget::PrefSpinButton _canvas_render_time_limit; + UI::Widget::PrefCheckButton _canvas_use_new_bisector; + UI::Widget::PrefSpinButton _canvas_new_bisector_size; + UI::Widget::PrefSpinButton _rendering_tile_size; + UI::Widget::PrefSpinButton _canvas_max_affine_diff; + UI::Widget::PrefSpinButton _canvas_pad; + UI::Widget::PrefSpinButton _canvas_coarsener_min_size; + UI::Widget::PrefSpinButton _canvas_coarsener_glue_size; + UI::Widget::PrefSpinButton _canvas_coarsener_min_fullness; + UI::Widget::PrefCheckButton _canvas_debug_framecheck; + UI::Widget::PrefCheckButton _canvas_debug_logging; + UI::Widget::PrefCheckButton _canvas_debug_slow_redraw; + UI::Widget::PrefSpinButton _canvas_debug_slow_redraw_time; + UI::Widget::PrefCheckButton _canvas_debug_show_redraw; + UI::Widget::PrefCheckButton _canvas_debug_show_unclean; + UI::Widget::PrefCheckButton _canvas_debug_show_snapshot; + UI::Widget::PrefCheckButton _canvas_debug_show_clean; + UI::Widget::PrefCheckButton _canvas_debug_disable_redraw; + UI::Widget::PrefCheckButton _canvas_debug_sticky_decoupled; + + UI::Widget::PrefCheckButton _trans_scale_stroke; + UI::Widget::PrefCheckButton _trans_scale_corner; + UI::Widget::PrefCheckButton _trans_gradient; + UI::Widget::PrefCheckButton _trans_pattern; + UI::Widget::PrefCheckButton _trans_dash_scale; + UI::Widget::PrefRadioButton _trans_optimized; + UI::Widget::PrefRadioButton _trans_preserved; + + UI::Widget::PrefRadioButton _sel_all; + UI::Widget::PrefRadioButton _sel_current; + UI::Widget::PrefRadioButton _sel_recursive; + UI::Widget::PrefCheckButton _sel_hidden; + UI::Widget::PrefCheckButton _sel_locked; + UI::Widget::PrefCheckButton _sel_inlayer_same; + UI::Widget::PrefCheckButton _sel_touch_topmost_only; + UI::Widget::PrefCheckButton _sel_layer_deselects; + UI::Widget::PrefCheckButton _sel_cycle; + + UI::Widget::PrefCheckButton _markers_color_stock; + UI::Widget::PrefCheckButton _markers_color_custom; + UI::Widget::PrefCheckButton _markers_color_update; + + UI::Widget::PrefCheckButton _cleanup_swatches; + + UI::Widget::PrefCheckButton _lpe_copy_mirroricons; + + UI::Widget::PrefSpinButton _importexport_export_res; + UI::Widget::PrefSpinButton _importexport_import_res; + UI::Widget::PrefCheckButton _importexport_import_res_override; + UI::Widget::PrefCheckButton _rendering_image_outline; + UI::Widget::PrefSlider _snap_delay; + UI::Widget::PrefSlider _snap_weight; + UI::Widget::PrefSlider _snap_persistence; + UI::Widget::PrefEntry _font_sample; + UI::Widget::PrefCheckButton _font_dialog; + UI::Widget::PrefCombo _font_unit_type; + UI::Widget::PrefCheckButton _font_output_px; + UI::Widget::PrefCheckButton _font_fontsdir_system; + UI::Widget::PrefCheckButton _font_fontsdir_user; + UI::Widget::PrefMultiEntry _font_fontdirs_custom; + + UI::Widget::PrefCheckButton _misc_comment; + UI::Widget::PrefCheckButton _misc_default_metadata; + UI::Widget::PrefCheckButton _export_all_extensions; + UI::Widget::PrefCheckButton _misc_forkvectors; + UI::Widget::PrefSpinButton _misc_gradientangle; + UI::Widget::PrefCheckButton _misc_gradient_collect; + UI::Widget::PrefCheckButton _misc_scripts; + UI::Widget::PrefCheckButton _misc_namedicon_delay; + + // System page + UI::Widget::PrefSpinButton _misc_latency_skew; + UI::Widget::PrefSpinButton _misc_simpl; + Gtk::Entry _sys_user_prefs; + Gtk::Entry _sys_tmp_files; + Gtk::Entry _sys_extension_dir; + UI::Widget::PrefOpenFolder _sys_user_config; + UI::Widget::PrefOpenFolder _sys_user_extension_dir; + UI::Widget::PrefOpenFolder _sys_user_themes_dir; + UI::Widget::PrefOpenFolder _sys_user_ui_dir; + UI::Widget::PrefOpenFolder _sys_user_fonts_dir; + UI::Widget::PrefOpenFolder _sys_user_icons_dir; + UI::Widget::PrefOpenFolder _sys_user_keys_dir; + UI::Widget::PrefOpenFolder _sys_user_palettes_dir; + UI::Widget::PrefOpenFolder _sys_user_templates_dir; + UI::Widget::PrefOpenFolder _sys_user_symbols_dir; + UI::Widget::PrefOpenFolder _sys_user_paint_servers_dir; + UI::Widget::PrefMultiEntry _sys_fontdirs_custom; + Gtk::Entry _sys_user_cache; + Gtk::Entry _sys_data; + Gtk::TextView _sys_icon; + Gtk::ScrolledWindow _sys_icon_scroll; + Gtk::TextView _sys_systemdata; + Gtk::ScrolledWindow _sys_systemdata_scroll; + + // UI page + UI::Widget::PrefCombo _ui_languages; + UI::Widget::PrefCheckButton _ui_colorsliders_top; + UI::Widget::PrefSpinButton _misc_recent; + UI::Widget::PrefCheckButton _ui_realworldzoom; + UI::Widget::PrefCheckButton _ui_partialdynamic; + UI::Widget::ZoomCorrRulerSlider _ui_zoom_correction; + UI::Widget::PrefCheckButton _show_filters_info_box; + UI::Widget::PrefCheckButton _ui_yaxisdown; + UI::Widget::PrefCheckButton _ui_rotationlock; + UI::Widget::PrefCheckButton _ui_cursorscaling; + UI::Widget::PrefCheckButton _ui_cursor_shadow; + + //Spellcheck + UI::Widget::PrefCombo _spell_language; + UI::Widget::PrefCombo _spell_language2; + UI::Widget::PrefCombo _spell_language3; + UI::Widget::PrefCheckButton _spell_ignorenumbers; + UI::Widget::PrefCheckButton _spell_ignoreallcaps; + + // Bitmaps + UI::Widget::PrefCombo _misc_overs_bitmap; + UI::Widget::PrefEntryFileButtonHBox _misc_bitmap_editor; + UI::Widget::PrefEntryFileButtonHBox _misc_svg_editor; + UI::Widget::PrefCheckButton _misc_bitmap_autoreload; + UI::Widget::PrefSpinButton _bitmap_copy_res; + UI::Widget::PrefCheckButton _bitmap_ask; + UI::Widget::PrefCheckButton _svg_ask; + UI::Widget::PrefCombo _bitmap_link; + UI::Widget::PrefCombo _svg_link; + UI::Widget::PrefCombo _bitmap_scale; + UI::Widget::PrefSpinButton _bitmap_import_quality; + + UI::Widget::PrefEntry _kb_search; + UI::Widget::PrefCombo _kb_filelist; + + UI::Widget::PrefCheckButton _save_use_current_dir; + UI::Widget::PrefCheckButton _save_autosave_enable; + UI::Widget::PrefSpinButton _save_autosave_interval; + UI::Widget::PrefEntry _save_autosave_path; + UI::Widget::PrefSpinButton _save_autosave_max; + + Gtk::ComboBoxText _cms_display_profile; + UI::Widget::PrefCheckButton _cms_from_display; + UI::Widget::PrefCombo _cms_intent; + + UI::Widget::PrefCheckButton _cms_softproof; + UI::Widget::PrefCheckButton _cms_gamutwarn; + Gtk::ColorButton _cms_gamutcolor; + Gtk::ComboBoxText _cms_proof_profile; + UI::Widget::PrefCombo _cms_proof_intent; + UI::Widget::PrefCheckButton _cms_proof_blackpoint; + UI::Widget::PrefCheckButton _cms_proof_preserveblack; + + Gtk::Notebook _grids_notebook; + UI::Widget::PrefRadioButton _grids_no_emphasize_on_zoom; + UI::Widget::PrefRadioButton _grids_emphasize_on_zoom; + UI::Widget::DialogPage _grids_xy; + UI::Widget::DialogPage _grids_axonom; + // CanvasXYGrid properties: + UI::Widget::PrefUnit _grids_xy_units; + UI::Widget::PrefSpinButton _grids_xy_origin_x; + UI::Widget::PrefSpinButton _grids_xy_origin_y; + UI::Widget::PrefSpinButton _grids_xy_spacing_x; + UI::Widget::PrefSpinButton _grids_xy_spacing_y; + UI::Widget::PrefColorPicker _grids_xy_color; + UI::Widget::PrefColorPicker _grids_xy_empcolor; + UI::Widget::PrefSpinButton _grids_xy_empspacing; + UI::Widget::PrefCheckButton _grids_xy_dotted; + // CanvasAxonomGrid properties: + UI::Widget::PrefUnit _grids_axonom_units; + UI::Widget::PrefSpinButton _grids_axonom_origin_x; + UI::Widget::PrefSpinButton _grids_axonom_origin_y; + UI::Widget::PrefSpinButton _grids_axonom_spacing_y; + UI::Widget::PrefSpinButton _grids_axonom_angle_x; + UI::Widget::PrefSpinButton _grids_axonom_angle_z; + UI::Widget::PrefColorPicker _grids_axonom_color; + UI::Widget::PrefColorPicker _grids_axonom_empcolor; + UI::Widget::PrefSpinButton _grids_axonom_empspacing; + + // SVG Output page: + UI::Widget::PrefCheckButton _svgoutput_usenamedcolors; + UI::Widget::PrefCheckButton _svgoutput_usesodipodiabsref; + UI::Widget::PrefSpinButton _svgoutput_numericprecision; + UI::Widget::PrefSpinButton _svgoutput_minimumexponent; + UI::Widget::PrefCheckButton _svgoutput_inlineattrs; + UI::Widget::PrefSpinButton _svgoutput_indent; + UI::Widget::PrefCombo _svgoutput_pathformat; + UI::Widget::PrefCheckButton _svgoutput_forcerepeatcommands; + + // Attribute Checking controls for SVG Output page: + UI::Widget::PrefCheckButton _svgoutput_attrwarn; + UI::Widget::PrefCheckButton _svgoutput_attrremove; + UI::Widget::PrefCheckButton _svgoutput_stylepropwarn; + UI::Widget::PrefCheckButton _svgoutput_stylepropremove; + UI::Widget::PrefCheckButton _svgoutput_styledefaultswarn; + UI::Widget::PrefCheckButton _svgoutput_styledefaultsremove; + UI::Widget::PrefCheckButton _svgoutput_check_reading; + UI::Widget::PrefCheckButton _svgoutput_check_editing; + UI::Widget::PrefCheckButton _svgoutput_check_writing; + + // SVG Output export: + UI::Widget::PrefCheckButton _svgexport_insert_text_fallback; + UI::Widget::PrefCheckButton _svgexport_insert_mesh_polyfill; + UI::Widget::PrefCheckButton _svgexport_insert_hatch_polyfill; + UI::Widget::PrefCheckButton _svgexport_remove_marker_auto_start_reverse; + UI::Widget::PrefCheckButton _svgexport_remove_marker_context_paint; + + + Gtk::Notebook _kb_notebook; + UI::Widget::DialogPage _kb_page_shortcuts; + UI::Widget::DialogPage _kb_page_modifiers; + gboolean _kb_shortcuts_loaded; + /* + * Keyboard shortcut members + */ + class ModelColumns: public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns() { + add(name); + add(id); + add(shortcut); + add(description); + add(shortcutkey); + add(user_set); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<Glib::ustring> id; + Gtk::TreeModelColumn<Glib::ustring> shortcut; + Gtk::TreeModelColumn<Glib::ustring> description; + Gtk::TreeModelColumn<Gtk::AccelKey> shortcutkey; + Gtk::TreeModelColumn<unsigned int> user_set; + }; + ModelColumns _kb_columns; + static ModelColumns &onKBGetCols(); + Glib::RefPtr<Gtk::TreeStore> _kb_store; + Gtk::TreeView _kb_tree; + Gtk::CellRendererAccel _kb_shortcut_renderer; + Glib::RefPtr<Gtk::TreeModelFilter> _kb_filter; + + /* + * Keyboard modifiers interface + */ + class ModifierColumns: public Gtk::TreeModel::ColumnRecord { + public: + ModifierColumns() { + add(name); + add(id); + add(description); + add(and_modifiers); + add(user_set); + } + ~ModifierColumns() override = default; + + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<Glib::ustring> id; + Gtk::TreeModelColumn<Glib::ustring> description; + Gtk::TreeModelColumn<Glib::ustring> and_modifiers; + Gtk::TreeModelColumn<unsigned int> user_set; + Gtk::TreeModelColumn<unsigned int> is_enabled; + }; + ModifierColumns _mod_columns; + Glib::RefPtr<Gtk::TreeStore> _mod_store; + Gtk::TreeView _mod_tree; + Gtk::ToggleButton _kb_mod_ctrl; + Gtk::ToggleButton _kb_mod_shift; + Gtk::ToggleButton _kb_mod_alt; + Gtk::ToggleButton _kb_mod_meta; + Gtk::CheckButton _kb_mod_enabled; + bool _kb_is_updated; + + int _minimum_width; + int _minimum_height; + int _natural_width; + int _natural_height; + bool GetSizeRequest(const Gtk::TreeModel::iterator& iter); + void get_preferred_width_vfunc (int& minimum_width, int& natural_width) const override { + minimum_width = _minimum_width; + natural_width = _natural_width; + } + void get_preferred_width_for_height_vfunc (int height, int& minimum_width, int& natural_width) const override { + minimum_width = _minimum_width; + natural_width = _natural_width; + } + void get_preferred_height_vfunc (int& minimum_height, int& natural_height) const override { + minimum_height = _minimum_height; + natural_height = _natural_height; + } + void get_preferred_height_for_width_vfunc (int width, int& minimum_height, int& natural_height) const override { + minimum_height = _minimum_height; + natural_height = _natural_height; + } + int _sb_width; + UI::Widget::DialogPage* _current_page; + + Gtk::TreeModel::iterator AddPage(UI::Widget::DialogPage& p, Glib::ustring title, int id); + Gtk::TreeModel::iterator AddPage(UI::Widget::DialogPage& p, Glib::ustring title, Gtk::TreeModel::iterator parent, int id); + Gtk::TreePath get_next_result(Gtk::TreeIter& iter, bool check_children = true); + Gtk::TreePath get_prev_result(Gtk::TreeIter& iter, bool iterate = true); + bool matchPage(const Gtk::TreeModel::iterator& iter); + + static void AddSelcueCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); + static void AddGradientCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); + static void AddConvertGuidesCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); + static void AddFirstAndLastCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); + static void AddDotSizeSpinbutton(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, double def_value); + static void AddBaseSimplifySpinbutton(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, double def_value); + static void AddNewObjectsStyle(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, const gchar* banner = nullptr); + + void on_pagelist_selection_changed(); + void show_not_found(); + void show_nothing_on_page(); + void show_try_search(); + void on_reset_open_recent_clicked(); + void on_reset_prefs_clicked(); + void on_search_changed(); + void highlight_results(Glib::ustring const &key, Gtk::TreeModel::iterator &iter); + void goto_first_result(); + + void get_widgets_in_grid(Glib::ustring const &key, Gtk::Widget *widget); + int num_widgets_in_grid(Glib::ustring const &key, Gtk::Widget *widget); + void remove_highlight(Gtk::Label *label); + void add_highlight(Gtk::Label *label, Glib::ustring const &key); + + bool recursive_filter(Glib::ustring &key, Gtk::TreeModel::const_iterator const &row); + bool on_navigate_key_press(GdkEventKey *evt); + + void initPageTools(); + void initPageUI(); + void initPageBehavior(); + void initPageIO(); + + void initPageRendering(); + void initPageSpellcheck(); + void initPageBitmaps(); + void initPageSystem(); + void initPageI18n(); // Do we still need it? + void initKeyboardShortcuts(Gtk::TreeModel::iterator iter_ui); + + /* + * Functions for the Keyboard shortcut editor panel + */ + void onKBReset(); + void onKBImport(); + void onKBExport(); + void onKBList(); + void onKBRealize(); + void onKBListKeyboardShortcuts(); + void onKBTreeEdited (const Glib::ustring& path, guint accel_key, Gdk::ModifierType accel_mods, guint hardware_keycode); + void onKBTreeCleared(const Glib::ustring& path_string); + bool onKBSearchKeyEvent(GdkEventKey *event); + bool onKBSearchFilter(const Gtk::TreeModel::const_iterator& iter); + static void onKBShortcutRenderer(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter); + void on_modifier_selection_changed(); + void on_modifier_enabled(); + void on_modifier_edited(); + +private: + Gtk::TreeModel::iterator searchRows(char const* srch, Gtk::TreeModel::iterator& iter, Gtk::TreeModel::Children list_model_childern); + void themeChange(bool contrastslider = false); + void comboThemeChange(); + void contrastThemeChange(); + void preferDarkThemeChange(); + bool contrastChange(GdkEventButton* button_event); + void symbolicThemeCheck(); + void toggleSymbolic(); + void changeIconsColors(); + void resetIconsColors(bool themechange = false); + void resetIconsColorsWrapper(); + void changeIconsColor(guint32 /*color*/); + void get_highlight_colors(guint32 &colorsetbase, guint32 &colorsetsuccess, guint32 &colorsetwarning, + guint32 &colorseterror); + + bool on_outline_overlay_changed(GdkEventFocus * /* focus_event */); + std::map<Glib::ustring, bool> dark_themes; + InkscapePreferences(); + InkscapePreferences(InkscapePreferences const &d); + InkscapePreferences operator=(InkscapePreferences const &d); + bool _init; + Inkscape::PrefObserver _theme_oberver; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_UI_DIALOG_INKSCAPE_PREFERENCES_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/ui/dialog/input.cpp b/src/ui/dialog/input.cpp new file mode 100644 index 0000000..df071c7 --- /dev/null +++ b/src/ui/dialog/input.cpp @@ -0,0 +1,1798 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Input devices dialog (new) - implementation. + */ +/* Author: + * Jon A. Cruz + * + * Copyright (C) 2008 Author + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <map> +#include <set> +#include <list> +#include "ui/widget/frame.h" +#include "ui/widget/scrollprotected.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/buttonbox.h> +#include <gtkmm/cellrenderercombo.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/grid.h> +#include <gtkmm/liststore.h> +#include <gtkmm/menubar.h> +#include <gtkmm/notebook.h> +#include <gtkmm/paned.h> +#include <gtkmm/progressbar.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/treestore.h> +#include <gtkmm/eventbox.h> + +#include "device-manager.h" +#include "preferences.h" + +#include "input.h" + +// clang-format off +/* XPM */ +static char const * core_xpm[] = { +"16 16 4 1", +" c None", +". c #808080", +"+ c #000000", +"@ c #FFFFFF", +" ", +" ", +" ", +" .++++++. ", +" +@+@@+@+ ", +" +@+@@+@+ ", +" +.+..+.+ ", +" +@@@@@@+ ", +" +@@@@@@+ ", +" +@@@@@@+ ", +" +@@@@@@+ ", +" +@@@@@@+ ", +" .++++++. ", +" ", +" ", +" "}; + +/* XPM */ +static char const *eraser[] = { +/* columns rows colors chars-per-pixel */ +"16 16 5 1", +" c black", +". c green", +"X c #808080", +"o c gray100", +"O c None", +/* pixels */ +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOO OO", +"OOOOOOOOOOOO . O", +"OOOOOOOOOOO . OO", +"OOOOOOOOOO . OOO", +"OOOOOOOOO . OOOO", +"OOOOOOOO . OOOOO", +"OOOOOOOXo OOOOOO", +"OOOOOOXoXOOOOOOO", +"OOOOOXoXOOOOOOOO", +"OOOOXoXOOOOOOOOO", +"OOOXoXOOOOOOOOOO", +"OOXoXOOOOOOOOOOO", +"OOXXOOOOOOOOOOOO", +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOOOOO" +}; + +/* XPM */ +static char const *mouse[] = { +/* columns rows colors chars-per-pixel */ +"16 16 3 1", +" c black", +". c gray100", +"X c None", +/* pixels */ +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXX XXXXXXX", +"XXXXX . XXXXXXX", +"XXXX .... XXXXXX", +"XXXX .... XXXXXX", +"XXXXX .... XXXXX", +"XXXXX .... XXXXX", +"XXXXXX .... XXXX", +"XXXXXX .... XXXX", +"XXXXXXX . XXXXX", +"XXXXXXX XXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX" +}; + +/* XPM */ +static char const *pen[] = { +/* columns rows colors chars-per-pixel */ +"16 16 3 1", +" c black", +". c gray100", +"X c None", +/* pixels */ +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXX XX", +"XXXXXXXXXXXX . X", +"XXXXXXXXXXX . XX", +"XXXXXXXXXX . XXX", +"XXXXXXXXX . XXXX", +"XXXXXXXX . XXXXX", +"XXXXXXX . XXXXXX", +"XXXXXX . XXXXXXX", +"XXXXX . XXXXXXXX", +"XXXX . XXXXXXXXX", +"XXX . XXXXXXXXXX", +"XX . XXXXXXXXXXX", +"XX XXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX" +}; + +/* XPM */ +static char const *sidebuttons[] = { +/* columns rows colors chars-per-pixel */ +"16 16 4 1", +" c black", +". c #808080", +"o c green", +"O c None", +/* pixels */ +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOOOOO", +"O..............O", +"O.OOOOOOOOOOOO.O", +"O OOOOOOOO O", +"O o OOOOOOOO o O", +"O o OOOOOOOO o O", +"O OOOOOOOO O", +"O.OOOOOOOOOOOO.O", +"O.OOOOOOOOOOOO.O", +"O.OOOOOOOOOOOO.O", +"O.OOOOOOOOOOOO.O", +"O.OOOOOOOOOOOO.O", +"O..............O", +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOOOOO" +}; + +/* XPM */ +static char const *tablet[] = { +/* columns rows colors chars-per-pixel */ +"16 16 3 1", +" c black", +". c gray100", +"X c None", +/* pixels */ +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"X X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X X", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX" +}; + +/* XPM */ +static char const *tip[] = { +/* columns rows colors chars-per-pixel */ +"16 16 5 1", +" c black", +". c green", +"X c #808080", +"o c gray100", +"O c None", +/* pixels */ +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOOXOO", +"OOOOOOOOOOOOXoXO", +"OOOOOOOOOOOXoXOO", +"OOOOOOOOOOXoXOOO", +"OOOOOOOOOXoXOOOO", +"OOOOOOOOXoXOOOOO", +"OOOOOOO oXOOOOOO", +"OOOOOO . OOOOOOO", +"OOOOO . OOOOOOOO", +"OOOO . OOOOOOOOO", +"OOO . OOOOOOOOOO", +"OO . OOOOOOOOOOO", +"OO OOOOOOOOOOOO", +"OOOOXXXXXOOOOOOO", +"OOOOOOOOOXXXXXOO" +}; + +/* XPM */ +static char const *button_none[] = { +/* columns rows colors chars-per-pixel */ +"8 8 3 1", +" c black", +". c #808080", +"X c None", +/* pixels */ +"XXXXXXXX", +"XX .. XX", +"X .XX. X", +"X.XX X.X", +"X.X XX.X", +"X .XX. X", +"XX .. XX", +"XXXXXXXX" +}; +/* XPM */ +static char const *button_off[] = { +/* columns rows colors chars-per-pixel */ +"8 8 4 1", +" c black", +". c #808080", +"X c gray100", +"o c None", +/* pixels */ +"oooooooo", +"oo. .oo", +"o. XX .o", +"o XXXX o", +"o XXXX o", +"o. XX .o", +"oo. .oo", +"oooooooo" +}; +/* XPM */ +static char const *button_on[] = { +/* columns rows colors chars-per-pixel */ +"8 8 3 1", +" c black", +". c green", +"X c None", +/* pixels */ +"XXXXXXXX", +"XX XX", +"X .. X", +"X .... X", +"X .... X", +"X .. X", +"XX XX", +"XXXXXXXX" +}; + +/* XPM */ +static char const * axis_none_xpm[] = { +"24 8 3 1", +" c None", +". c #000000", +"+ c #808080", +" ", +" .++++++++++++++++++. ", +" .+ . .+. ", +" + . . . + ", +" + . . . + ", +" .+. . +. ", +" .++++++++++++++++++. ", +" "}; +/* XPM */ +static char const * axis_off_xpm[] = { +"24 8 4 1", +" c None", +". c #808080", +"+ c #000000", +"@ c #FFFFFF", +" ", +" .++++++++++++++++++. ", +" .+@@@@@@@@@@@@@@@@@@+. ", +" +@@@@@@@@@@@@@@@@@@@@+ ", +" +@@@@@@@@@@@@@@@@@@@@+ ", +" .+@@@@@@@@@@@@@@@@@@+. ", +" .++++++++++++++++++. ", +" "}; +/* XPM */ +static char const * axis_on_xpm[] = { +"24 8 3 1", +" c None", +". c #000000", +"+ c #00FF00", +" ", +" .................... ", +" ..++++++++++++++++++.. ", +" .++++++++++++++++++++. ", +" .++++++++++++++++++++. ", +" ..++++++++++++++++++.. ", +" .................... ", +" "}; +// clang-format on + +using Inkscape::InputDevice; + +namespace Inkscape { +namespace UI { +namespace Dialog { + + + +class DeviceModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + Gtk::TreeModelColumn<bool> toggler; + Gtk::TreeModelColumn<Glib::ustring> expander; + Gtk::TreeModelColumn<Glib::ustring> description; + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > thumbnail; + Gtk::TreeModelColumn<Glib::RefPtr<InputDevice const> > device; + Gtk::TreeModelColumn<Gdk::InputMode> mode; + + DeviceModelColumns() { add(toggler), add(expander), add(description); add(thumbnail); add(device); add(mode); } +}; + +static std::map<Gdk::InputMode, Glib::ustring> &getModeToString() +{ + static std::map<Gdk::InputMode, Glib::ustring> mapping; + if (mapping.empty()) { + mapping[Gdk::MODE_DISABLED] = _("Disabled"); + mapping[Gdk::MODE_SCREEN] = C_("Input device", "Screen"); + mapping[Gdk::MODE_WINDOW] = _("Window"); + } + + return mapping; +} + +static int getModeId(Gdk::InputMode im) +{ + if (im == Gdk::MODE_DISABLED) return 0; + if (im == Gdk::MODE_SCREEN) return 1; + if (im == Gdk::MODE_WINDOW) return 2; + + return 0; +} + +static std::map<Glib::ustring, Gdk::InputMode> &getStringToMode() +{ + static std::map<Glib::ustring, Gdk::InputMode> mapping; + if (mapping.empty()) { + mapping[_("Disabled")] = Gdk::MODE_DISABLED; + mapping[_("Screen")] = Gdk::MODE_SCREEN; + mapping[_("Window")] = Gdk::MODE_WINDOW; + } + + return mapping; +} + + + +class InputDialogImpl : public InputDialog { +public: + InputDialogImpl(); + ~InputDialogImpl() override = default; + +private: + class ConfPanel : public Gtk::Box + { + public: + ConfPanel(); + ~ConfPanel() override; + + class Blink : public Preferences::Observer + { + public: + Blink(ConfPanel &parent); + ~Blink() override; + void notify(Preferences::Entry const &new_val) override; + + ConfPanel &parent; + }; + + static void commitCellModeChange(Glib::ustring const &path, Glib::ustring const &newText, Glib::RefPtr<Gtk::TreeStore> store); + static void setModeCellString(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter); + + static void commitCellStateChange(Glib::ustring const &path, Glib::RefPtr<Gtk::TreeStore> store); + static void setCellStateToggle(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter); + + void saveSettings(); + void onTreeSelect(); + void useExtToggled(); + + void onModeChange(); + void setKeys(gint count); + void setAxis(gint count); + + Glib::RefPtr<Gtk::TreeStore> confDeviceStore; + Gtk::TreeIter confDeviceIter; + Gtk::TreeView confDeviceTree; + Gtk::ScrolledWindow confDeviceScroller; + Blink watcher; + Gtk::CheckButton useExt; + Gtk::Button save; + Gtk::Paned pane; + Gtk::Box detailsBox; + Gtk::Box titleFrame; + Gtk::Label titleLabel; + Inkscape::UI::Widget::Frame axisFrame; + Inkscape::UI::Widget::Frame keysFrame; + Gtk::Box axisVBox; + Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText> modeCombo; + Gtk::Label modeLabel; + Gtk::Box modeBox; + + class KeysColumns : public Gtk::TreeModel::ColumnRecord + { + public: + KeysColumns() + { + add(name); + add(value); + } + ~KeysColumns() override = default; + + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<Glib::ustring> value; + }; + + KeysColumns keysColumns; + KeysColumns axisColumns; + + Glib::RefPtr<Gtk::ListStore> axisStore; + Gtk::TreeView axisTree; + Gtk::ScrolledWindow axisScroll; + + Glib::RefPtr<Gtk::ListStore> keysStore; + Gtk::TreeView keysTree; + Gtk::ScrolledWindow keysScroll; + Gtk::CellRendererAccel _kb_shortcut_renderer; + + + }; + + static DeviceModelColumns &getCols(); + + enum PixId {PIX_CORE, PIX_PEN, PIX_MOUSE, PIX_TIP, PIX_TABLET, PIX_ERASER, PIX_SIDEBUTTONS, + PIX_BUTTONS_NONE, PIX_BUTTONS_ON, PIX_BUTTONS_OFF, + PIX_AXIS_NONE, PIX_AXIS_ON, PIX_AXIS_OFF}; + + static Glib::RefPtr<Gdk::Pixbuf> getPix(PixId id); + + std::map<Glib::ustring, std::set<guint> > buttonMap; + std::map<Glib::ustring, std::map<guint, std::pair<guint, gdouble> > > axesMap; + + Gdk::InputSource lastSourceSeen; + Glib::ustring lastDevnameSeen; + + Glib::RefPtr<Gtk::TreeStore> deviceStore; + Gtk::TreeIter deviceIter; + Gtk::TreeView deviceTree; + Inkscape::UI::Widget::Frame testFrame; + Inkscape::UI::Widget::Frame axisFrame; + Gtk::ScrolledWindow treeScroller; + Gtk::ScrolledWindow detailScroller; + Gtk::Paned splitter; + Gtk::Paned split2; + Gtk::Label devName; + Gtk::Label devKeyCount; + Gtk::Label devAxesCount; + Gtk::ComboBoxText axesCombo; + Gtk::ProgressBar axesValues[6]; + Gtk::Grid axisTable; + Gtk::ComboBoxText buttonCombo; + Gtk::ComboBoxText linkCombo; + sigc::connection linkConnection; + Gtk::Label keyVal; + Gtk::Entry keyEntry; + Gtk::Notebook topHolder; + Gtk::Image testThumb; + Gtk::Image testButtons[24]; + Gtk::Image testAxes[8]; + Gtk::Grid imageTable; + Gtk::EventBox testDetector; + + ConfPanel cfgPanel; + + + static void setupTree( Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeIter &tablet ); + void setupValueAndCombo( gint reported, gint actual, Gtk::Label& label, Gtk::ComboBoxText& combo ); + void updateTestButtons( Glib::ustring const& key, gint hotButton ); + void updateTestAxes( Glib::ustring const& key, GdkDevice* dev ); + void mapAxesValues( Glib::ustring const& key, gdouble const * axes, GdkDevice* dev); + Glib::ustring getKeyFor( GdkDevice* device ); + bool eventSnoop(GdkEvent* event); + void linkComboChanged(); + void resyncToSelection(); + void handleDeviceChange(Glib::RefPtr<InputDevice const> device); + void updateDeviceAxes(Glib::RefPtr<InputDevice const> device); + void updateDeviceButtons(Glib::RefPtr<InputDevice const> device); + static void updateDeviceLinks(Glib::RefPtr<InputDevice const> device, Gtk::TreeIter tabletIter, Gtk::TreeView *tree); + + static bool findDevice(const Gtk::TreeModel::iterator& iter, + Glib::ustring id, + Gtk::TreeModel::iterator* result); + static bool findDeviceByLink(const Gtk::TreeModel::iterator& iter, + Glib::ustring link, + Gtk::TreeModel::iterator* result); + +}; // class InputDialogImpl + + +DeviceModelColumns &InputDialogImpl::getCols() +{ + static DeviceModelColumns cols; + return cols; +} + +Glib::RefPtr<Gdk::Pixbuf> InputDialogImpl::getPix(PixId id) +{ + static std::map<PixId, Glib::RefPtr<Gdk::Pixbuf> > mappings; + + mappings[PIX_CORE] = Gdk::Pixbuf::create_from_xpm_data(core_xpm); + mappings[PIX_PEN] = Gdk::Pixbuf::create_from_xpm_data(pen); + mappings[PIX_MOUSE] = Gdk::Pixbuf::create_from_xpm_data(mouse); + mappings[PIX_TIP] = Gdk::Pixbuf::create_from_xpm_data(tip); + mappings[PIX_TABLET] = Gdk::Pixbuf::create_from_xpm_data(tablet); + mappings[PIX_ERASER] = Gdk::Pixbuf::create_from_xpm_data(eraser); + mappings[PIX_SIDEBUTTONS] = Gdk::Pixbuf::create_from_xpm_data(sidebuttons); + + mappings[PIX_BUTTONS_NONE] = Gdk::Pixbuf::create_from_xpm_data(button_none); + mappings[PIX_BUTTONS_ON] = Gdk::Pixbuf::create_from_xpm_data(button_on); + mappings[PIX_BUTTONS_OFF] = Gdk::Pixbuf::create_from_xpm_data(button_off); + + mappings[PIX_AXIS_NONE] = Gdk::Pixbuf::create_from_xpm_data(axis_none_xpm); + mappings[PIX_AXIS_ON] = Gdk::Pixbuf::create_from_xpm_data(axis_on_xpm); + mappings[PIX_AXIS_OFF] = Gdk::Pixbuf::create_from_xpm_data(axis_off_xpm); + + Glib::RefPtr<Gdk::Pixbuf> pix; + if (mappings.find(id) != mappings.end()) { + pix = mappings[id]; + } + + return pix; +} + + +// Now that we've defined the *Impl class, we can do the method to acquire one. +InputDialog &InputDialog::getInstance() +{ + InputDialog *dialog = new InputDialogImpl(); + return *dialog; +} + + +InputDialogImpl::InputDialogImpl() : + InputDialog(), + lastSourceSeen(static_cast<Gdk::InputSource>(-1)), + lastDevnameSeen(""), + deviceStore(Gtk::TreeStore::create(getCols())), + deviceIter(), + deviceTree(deviceStore), + testFrame(_("Test Area")), + axisFrame(_("Axis")), + treeScroller(), + detailScroller(), + splitter(), + split2(Gtk::ORIENTATION_VERTICAL), + axisTable(), + linkCombo(), + topHolder(), + imageTable(), + testDetector(), + cfgPanel() +{ + treeScroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + treeScroller.set_shadow_type(Gtk::SHADOW_IN); + treeScroller.add(deviceTree); + treeScroller.set_size_request(50, 0); + + split2.pack1(axisFrame, false, false); + split2.pack2(testFrame, true, true); + + splitter.pack1(treeScroller); + splitter.pack2(split2); + + testDetector.add(imageTable); + testFrame.add(testDetector); + testThumb.set(getPix(PIX_TABLET)); + testThumb.set_margin_top(24); + testThumb.set_margin_bottom(24); + testThumb.set_margin_start(24); + testThumb.set_margin_end(24); + testThumb.set_hexpand(); + testThumb.set_vexpand(); + imageTable.attach(testThumb, 0, 0, 8, 1); + + { + guint col = 0; + guint row = 1; + for (auto & testButton : testButtons) { + testButton.set(getPix(PIX_BUTTONS_NONE)); + imageTable.attach(testButton, col, row, 1, 1); + col++; + if (col > 7) { + col = 0; + row++; + } + } + + col = 0; + for (auto & testAxe : testAxes) { + testAxe.set(getPix(PIX_AXIS_NONE)); + imageTable.attach(testAxe, col * 2, row, 2, 1); + col++; + if (col > 3) { + col = 0; + row++; + } + } + } + + + // This is a hidden preference to enable the "hardware" details in a separate tab + // By default this is not available to users + if (Preferences::get()->getBool("/dialogs/inputdevices/test")) { + topHolder.append_page(cfgPanel, _("Configuration")); + topHolder.append_page(splitter, _("Hardware")); + topHolder.show_all(); + topHolder.set_current_page(0); + pack_start(topHolder); + } else { + pack_start(cfgPanel); + } + + + int rowNum = 0; + + axisFrame.add(axisTable); + + Gtk::Label *lbl = Gtk::manage(new Gtk::Label(_("Link:"))); + axisTable.attach(*lbl, 0, rowNum, 1, 1); + linkCombo.append(_("None")); + linkCombo.set_active_text(_("None")); + linkCombo.set_sensitive(false); + linkConnection = linkCombo.signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::linkComboChanged)); + axisTable.attach(linkCombo, 1, rowNum, 1, 1); + rowNum++; + + lbl = Gtk::manage(new Gtk::Label(_("Axes count:"))); + axisTable.attach(*lbl, 0, rowNum, 1, 1); + axisTable.attach(devAxesCount, 1, rowNum, 1, 1); + rowNum++; + + for (auto & axesValue : axesValues) { + lbl = Gtk::manage(new Gtk::Label(_("axis:"))); + lbl->set_hexpand(); + axisTable.attach(*lbl, 0, rowNum, 1, 1); + + axesValue.set_hexpand(); + axisTable.attach(axesValue, 1, rowNum, 1, 1); + axesValue.set_sensitive(false); + + rowNum++; + + + } + + lbl = Gtk::manage(new Gtk::Label(_("Button count:"))); + + axisTable.attach(*lbl, 0, rowNum, 1, 1); + axisTable.attach(devKeyCount, 1, rowNum, 1, 1); + + rowNum++; + + axisTable.attach(keyVal, 0, rowNum, 2, 1); + + rowNum++; + + testDetector.signal_event().connect(sigc::mem_fun(*this, &InputDialogImpl::eventSnoop)); + + // TODO: Extension event stuff has been removed from public API in GTK+ 3 + // Need to check that this hasn't broken anything + testDetector.add_events(Gdk::POINTER_MOTION_MASK|Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK |Gdk::PROXIMITY_IN_MASK|Gdk::PROXIMITY_OUT_MASK|Gdk::SCROLL_MASK); + + axisTable.attach(keyEntry, 0, rowNum, 2, 1); + + rowNum++; + + + axisTable.set_sensitive(false); + +//- 16x16/devices +// gnome-dev-mouse-optical +// input-mouse +// input-tablet +// mouse + + //Add the TreeView's view columns: + deviceTree.append_column("I", getCols().thumbnail); + deviceTree.append_column("Bar", getCols().description); + + deviceTree.set_enable_tree_lines(); + deviceTree.set_headers_visible(false); + deviceTree.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::resyncToSelection)); + + + setupTree( deviceStore, deviceIter ); + + Inkscape::DeviceManager::getManager().signalDeviceChanged().connect(sigc::mem_fun(*this, &InputDialogImpl::handleDeviceChange)); + Inkscape::DeviceManager::getManager().signalAxesChanged().connect(sigc::mem_fun(*this, &InputDialogImpl::updateDeviceAxes)); + Inkscape::DeviceManager::getManager().signalButtonsChanged().connect(sigc::mem_fun(*this, &InputDialogImpl::updateDeviceButtons)); + Inkscape::DeviceManager::getManager().signalLinkChanged().connect(sigc::bind(sigc::ptr_fun(&InputDialogImpl::updateDeviceLinks), deviceIter, &deviceTree)); + + deviceTree.expand_all(); + show_all_children(); +} + +class TabletTmp { +public: + TabletTmp() = default; + + Glib::ustring name; + std::list<Glib::RefPtr<InputDevice const> > devices; +}; + +static Glib::ustring getCommon( std::list<Glib::ustring> const &names ) +{ + Glib::ustring result; + + if ( !names.empty() ) { + size_t pos = 0; + bool match = true; + while ( match ) { + if ( names.begin()->length() > pos ) { + gunichar ch = (*names.begin())[pos]; + for (const auto & name : names) { + if ( (pos >= name.length()) + || (name[pos] != ch) ) { + match = false; + break; + } + } + if (match) { + result += ch; + pos++; + } + } else { + match = false; + } + } + } + + return result; +} + + +void InputDialogImpl::ConfPanel::onModeChange() +{ + Glib::ustring newText = modeCombo.get_active_text(); + + Glib::RefPtr<Gtk::TreeSelection> sel = confDeviceTree.get_selection(); + Gtk::TreeModel::iterator iter = sel->get_selected(); + if (iter) { + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + if (dev && (getStringToMode().find(newText) != getStringToMode().end())) { + Gdk::InputMode mode = getStringToMode()[newText]; + Inkscape::DeviceManager::getManager().setMode( dev->getId(), mode ); + } + } + +} + + +void InputDialogImpl::setupTree( Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeIter &tablet ) +{ + std::list<Glib::RefPtr<InputDevice const> > devList = Inkscape::DeviceManager::getManager().getDevices(); + if ( !devList.empty() ) { + //Gtk::TreeModel::Row row = *(store->append()); + //row[getCols().description] = _("Hardware"); + + // Let's make some tablets!!! + std::list<TabletTmp> tablets; + std::set<Glib::ustring> consumed; + + // Phase 1 - figure out which tablets are present + for (auto dev : devList) { + if ( dev ) { + if ( dev->getSource() != Gdk::SOURCE_MOUSE ) { + consumed.insert( dev->getId() ); + if ( tablets.empty() ) { + TabletTmp tmp; + tablets.push_back(tmp); + } + tablets.back().devices.push_back(dev); + } + } else { + g_warning("Null device in list"); + } + } + + // Phase 2 - build a UI for the present devices + for (auto & it : tablets) { + tablet = store->prepend(/*row.children()*/); + Gtk::TreeModel::Row childrow = *tablet; + if ( it.name.empty() ) { + // Check to see if we can derive one + std::list<Glib::ustring> names; + for (auto & device : it.devices) { + names.push_back( device->getName() ); + } + Glib::ustring common = getCommon(names); + if ( !common.empty() ) { + it.name = common; + } + } + childrow[getCols().description] = it.name.empty() ? _("Tablet") : it.name ; + childrow[getCols().thumbnail] = getPix(PIX_TABLET); + + // Check if there is an eraser we can link to a pen + for ( std::list<Glib::RefPtr<InputDevice const> >::iterator it2 = it.devices.begin(); it2 != it.devices.end(); ++it2 ) { + Glib::RefPtr<InputDevice const> dev = *it2; + if ( dev->getSource() == Gdk::SOURCE_PEN ) { + for (auto dev2 : it.devices) { + if ( dev2->getSource() == Gdk::SOURCE_ERASER ) { + DeviceManager::getManager().setLinkedTo(dev->getId(), dev2->getId()); + break; // only check the first eraser... for now + } + break; // only check the first pen... for now + } + } + } + + for (auto dev : it.devices) { + Gtk::TreeModel::Row deviceRow = *(store->append(childrow.children())); + deviceRow[getCols().description] = dev->getName(); + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + switch ( dev->getSource() ) { + case Gdk::SOURCE_MOUSE: + deviceRow[getCols().thumbnail] = getPix(PIX_CORE); + break; + case Gdk::SOURCE_PEN: + if (deviceRow[getCols().description] == _("pad")) { + deviceRow[getCols().thumbnail] = getPix(PIX_SIDEBUTTONS); + } else { + deviceRow[getCols().thumbnail] = getPix(PIX_TIP); + } + break; + case Gdk::SOURCE_CURSOR: + deviceRow[getCols().thumbnail] = getPix(PIX_MOUSE); + break; + case Gdk::SOURCE_ERASER: + deviceRow[getCols().thumbnail] = getPix(PIX_ERASER); + break; + default: + ; // nothing + } + } + } + + for (auto dev : devList) { + if ( dev && (consumed.find( dev->getId() ) == consumed.end()) ) { + Gtk::TreeModel::Row deviceRow = *(store->prepend(/*row.children()*/)); + deviceRow[getCols().description] = dev->getName(); + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + deviceRow[getCols().thumbnail] = getPix(PIX_CORE); + } + } + + } else { + g_warning("No devices found"); + } +} + + +InputDialogImpl::ConfPanel::ConfPanel() : + Gtk::Box(Gtk::ORIENTATION_VERTICAL), + confDeviceStore(Gtk::TreeStore::create(getCols())), + confDeviceIter(), + confDeviceTree(confDeviceStore), + confDeviceScroller(), + watcher(*this), + useExt(_("_Use pressure-sensitive tablet (requires restart)"), true), + save(_("_Save"), true), + detailsBox(Gtk::ORIENTATION_VERTICAL, 4), + titleFrame(Gtk::ORIENTATION_HORIZONTAL, 4), + titleLabel(""), + axisFrame(_("Axes")), + keysFrame(_("Keys")), + modeLabel(_("Mode:")), + modeBox(Gtk::ORIENTATION_HORIZONTAL, 4), + axisVBox(Gtk::ORIENTATION_VERTICAL) + +{ + + + confDeviceScroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + confDeviceScroller.set_shadow_type(Gtk::SHADOW_IN); + confDeviceScroller.add(confDeviceTree); + confDeviceScroller.set_size_request(120, 0); + + /* class Foo : public Gtk::TreeModel::ColumnRecord { + public : + Gtk::TreeModelColumn<Glib::ustring> one; + Foo() {add(one);} + }; + static Foo foo; + + //Add the TreeView's view columns: + { + Gtk::CellRendererToggle *rendr = new Gtk::CellRendererToggle(); + Gtk::TreeViewColumn *col = new Gtk::TreeViewColumn("xx", *rendr); + if (col) { + confDeviceTree.append_column(*col); + col->set_cell_data_func(*rendr, sigc::ptr_fun(setCellStateToggle)); + rendr->signal_toggled().connect(sigc::bind(sigc::ptr_fun(commitCellStateChange), confDeviceStore)); + } + }*/ + + //int expPos = confDeviceTree.append_column("", getCols().expander); + + confDeviceTree.append_column("I", getCols().thumbnail); + confDeviceTree.append_column("Bar", getCols().description); + + //confDeviceTree.get_column(0)->set_fixed_width(100); + //confDeviceTree.get_column(1)->set_expand(); + +/* { + Gtk::TreeViewColumn *col = new Gtk::TreeViewColumn("X", *rendr); + if (col) { + confDeviceTree.append_column(*col); + col->set_cell_data_func(*rendr, sigc::ptr_fun(setModeCellString)); + rendr->signal_edited().connect(sigc::bind(sigc::ptr_fun(commitCellModeChange), confDeviceStore)); + rendr->property_editable() = true; + } + }*/ + + //confDeviceTree.set_enable_tree_lines(); + confDeviceTree.property_enable_tree_lines() = false; + confDeviceTree.property_enable_grid_lines() = false; + confDeviceTree.set_headers_visible(false); + //confDeviceTree.set_expander_column( *confDeviceTree.get_column(expPos - 1) ); + + confDeviceTree.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::onTreeSelect)); + + setupTree( confDeviceStore, confDeviceIter ); + + Inkscape::DeviceManager::getManager().signalLinkChanged().connect(sigc::bind(sigc::ptr_fun(&InputDialogImpl::updateDeviceLinks), confDeviceIter, &confDeviceTree)); + + confDeviceTree.expand_all(); + + useExt.set_active(Preferences::get()->getBool("/options/useextinput/value")); + useExt.signal_toggled().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::useExtToggled)); + + auto buttonBox = Gtk::manage(new Gtk::ButtonBox); + buttonBox->set_layout (Gtk::BUTTONBOX_END); + //Gtk::Alignment *align = new Gtk::Alignment(Gtk::ALIGN_END, Gtk::ALIGN_START, 0, 0); + buttonBox->add(save); + save.signal_clicked().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::saveSettings)); + + titleFrame.pack_start(titleLabel, true, true); + //titleFrame.set_shadow_type(Gtk::SHADOW_IN); + + modeCombo.append(getModeToString()[Gdk::MODE_DISABLED]); + modeCombo.append(getModeToString()[Gdk::MODE_SCREEN]); + modeCombo.append(getModeToString()[Gdk::MODE_WINDOW]); + modeCombo.set_tooltip_text(_("A device can be 'Disabled', its coordinates mapped to the whole 'Screen', or to a single (usually focused) 'Window'")); + modeCombo.signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::onModeChange)); + + modeBox.pack_start(modeLabel, false, false); + modeBox.pack_start(modeCombo, true, true); + + axisVBox.add(axisScroll); + axisFrame.add(axisVBox); + + keysFrame.add(keysScroll); + + /** + * Scrolled Window + */ + keysScroll.add(keysTree); + keysScroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + keysScroll.set_shadow_type(Gtk::SHADOW_IN); + keysScroll.set_size_request(120, 80); + + keysStore = Gtk::ListStore::create(keysColumns); + + _kb_shortcut_renderer.property_editable() = true; + + keysTree.set_model(keysStore); + keysTree.set_headers_visible(false); + keysTree.append_column("Name", keysColumns.name); + keysTree.append_column("Value", keysColumns.value); + + //keysTree.append_column("Value", _kb_shortcut_renderer); + //keysTree.get_column(1)->add_attribute(_kb_shortcut_renderer.property_text(), keysColumns.value); + //_kb_shortcut_renderer.signal_accel_edited().connect( sigc::mem_fun(*this, &InputDialogImpl::onKBTreeEdited) ); + //_kb_shortcut_renderer.signal_accel_cleared().connect( sigc::mem_fun(*this, &InputDialogImpl::onKBTreeCleared) ); + + axisStore = Gtk::ListStore::create(axisColumns); + + axisTree.set_model(axisStore); + axisTree.set_headers_visible(false); + axisTree.append_column("Name", axisColumns.name); + axisTree.append_column("Value", axisColumns.value); + + /** + * Scrolled Window + */ + axisScroll.add(axisTree); + axisScroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + axisScroll.set_shadow_type(Gtk::SHADOW_IN); + axisScroll.set_size_request(0, 150); + + pane.pack1(confDeviceScroller); + pane.pack2(detailsBox); + + detailsBox.pack_start(titleFrame, false, false, 6); + detailsBox.pack_start(modeBox, false, false, 6); + detailsBox.pack_start(axisFrame, false, false); + detailsBox.pack_start(keysFrame, false, false); + detailsBox.set_border_width(4); + + pack_start(pane, true, true); + pack_start(useExt, Gtk::PACK_SHRINK); + pack_start(*buttonBox, false, false); + + // Select the first device + confDeviceTree.get_selection()->select(confDeviceStore->get_iter("0")); + +} + +InputDialogImpl::ConfPanel::~ConfPanel() += default; + +void InputDialogImpl::ConfPanel::setModeCellString(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter) +{ + if (iter) { + Gtk::CellRendererCombo *combo = dynamic_cast<Gtk::CellRendererCombo *>(rndr); + if (combo) { + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + Gdk::InputMode mode = (*iter)[getCols().mode]; + if (dev && (getModeToString().find(mode) != getModeToString().end())) { + combo->property_text() = getModeToString()[mode]; + } else { + combo->property_text() = ""; + } + } + } +} + +void InputDialogImpl::ConfPanel::commitCellModeChange(Glib::ustring const &path, Glib::ustring const &newText, Glib::RefPtr<Gtk::TreeStore> store) +{ + Gtk::TreeIter iter = store->get_iter(path); + if (iter) { + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + if (dev && (getStringToMode().find(newText) != getStringToMode().end())) { + Gdk::InputMode mode = getStringToMode()[newText]; + Inkscape::DeviceManager::getManager().setMode( dev->getId(), mode ); + } + } + + +} + +void InputDialogImpl::ConfPanel::setCellStateToggle(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter) +{ + if (iter) { + Gtk::CellRendererToggle *toggle = dynamic_cast<Gtk::CellRendererToggle *>(rndr); + if (toggle) { + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + if (dev) { + Gdk::InputMode mode = (*iter)[getCols().mode]; + toggle->set_active(mode != Gdk::MODE_DISABLED); + } else { + toggle->set_active(false); + } + } + } +} + +void InputDialogImpl::ConfPanel::commitCellStateChange(Glib::ustring const &path, Glib::RefPtr<Gtk::TreeStore> store) +{ + Gtk::TreeIter iter = store->get_iter(path); + if (iter) { + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + if (dev) { + Gdk::InputMode mode = (*iter)[getCols().mode]; + if (mode == Gdk::MODE_DISABLED) { + Inkscape::DeviceManager::getManager().setMode( dev->getId(), Gdk::MODE_SCREEN ); + } else { + Inkscape::DeviceManager::getManager().setMode( dev->getId(), Gdk::MODE_DISABLED ); + } + } + } +} + +void InputDialogImpl::ConfPanel::onTreeSelect() +{ + Glib::RefPtr<Gtk::TreeSelection> treeSel = confDeviceTree.get_selection(); + Gtk::TreeModel::iterator iter = treeSel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring val = row[getCols().description]; + Glib::RefPtr<InputDevice const> dev = row[getCols().device]; + Gdk::InputMode mode = (*iter)[getCols().mode]; + modeCombo.set_active(getModeId(mode)); + + titleLabel.set_markup("<b>" + row[getCols().description] + "</b>"); + + if (dev) { + setKeys(dev->getNumKeys()); + setAxis(dev->getNumAxes()); + } + } +} +void InputDialogImpl::ConfPanel::saveSettings() +{ + Inkscape::DeviceManager::getManager().saveConfig(); +} + +void InputDialogImpl::ConfPanel::useExtToggled() +{ + bool active = useExt.get_active(); + if (active != Preferences::get()->getBool("/options/useextinput/value")) { + Preferences::get()->setBool("/options/useextinput/value", active); + if (active) { + // As a work-around for a common problem, enable tablet toggles on the calligraphic tool. + // Covered in Launchpad bug #196195. + Preferences::get()->setBool("/tools/tweak/usepressure", true); + Preferences::get()->setBool("/tools/calligraphic/usepressure", true); + Preferences::get()->setBool("/tools/calligraphic/usetilt", true); + } + } +} + +InputDialogImpl::ConfPanel::Blink::Blink(ConfPanel &parent) : + Preferences::Observer("/options/useextinput/value"), + parent(parent) +{ + Preferences::get()->addObserver(*this); +} + +InputDialogImpl::ConfPanel::Blink::~Blink() +{ + Preferences::get()->removeObserver(*this); +} + +void InputDialogImpl::ConfPanel::Blink::notify(Preferences::Entry const &new_val) +{ + parent.useExt.set_active(new_val.getBool()); +} + +void InputDialogImpl::handleDeviceChange(Glib::RefPtr<InputDevice const> device) +{ +// g_message("OUCH!!!! for %p hits %s", &device, device->getId().c_str()); + std::vector<Glib::RefPtr<Gtk::TreeStore> > stores; + stores.push_back(deviceStore); + stores.push_back(cfgPanel.confDeviceStore); + + for (auto & store : stores) { + Gtk::TreeModel::iterator deviceIter; + store->foreach_iter( sigc::bind<Glib::ustring, Gtk::TreeModel::iterator*>( + sigc::ptr_fun(&InputDialogImpl::findDevice), + device->getId(), + &deviceIter) ); + if ( deviceIter ) { + Gdk::InputMode mode = device->getMode(); + Gtk::TreeModel::Row row = *deviceIter; + if (row[getCols().mode] != mode) { + row[getCols().mode] = mode; + } + } + } +} + +void InputDialogImpl::updateDeviceAxes(Glib::RefPtr<InputDevice const> device) +{ + gint live = device->getLiveAxes(); + + std::map<guint, std::pair<guint, gdouble> > existing = axesMap[device->getId()]; + gint mask = 0x1; + for ( gint num = 0; num < 32; num++, mask <<= 1) { + if ( (mask & live) != 0 ) { + if ( (existing.find(num) == existing.end()) || (existing[num].first < 2) ) { + axesMap[device->getId()][num].first = 2; + axesMap[device->getId()][num].second = 0.0; + } + } + } + updateTestAxes( device->getId(), nullptr ); +} + +void InputDialogImpl::updateDeviceButtons(Glib::RefPtr<InputDevice const> device) +{ + gint live = device->getLiveButtons(); + std::set<guint> existing = buttonMap[device->getId()]; + gint mask = 0x1; + for ( gint num = 0; num < 32; num++, mask <<= 1) { + if ( (mask & live) != 0 ) { + if ( existing.find(num) == existing.end() ) { + buttonMap[device->getId()].insert(num); + } + } + } + updateTestButtons(device->getId(), -1); +} + + +bool InputDialogImpl::findDevice(const Gtk::TreeModel::iterator& iter, + Glib::ustring id, + Gtk::TreeModel::iterator* result) +{ + bool stop = false; + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + if ( dev && (dev->getId() == id) ) { + if ( result ) { + *result = iter; + } + stop = true; + } + return stop; +} + +bool InputDialogImpl::findDeviceByLink(const Gtk::TreeModel::iterator& iter, + Glib::ustring link, + Gtk::TreeModel::iterator* result) +{ + bool stop = false; + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + if ( dev && (dev->getLink() == link) ) { + if ( result ) { + *result = iter; + } + stop = true; + } + return stop; +} + +void InputDialogImpl::updateDeviceLinks(Glib::RefPtr<InputDevice const> device, Gtk::TreeIter tabletIter, Gtk::TreeView *tree) +{ + Glib::RefPtr<Gtk::TreeStore> deviceStore = Glib::RefPtr<Gtk::TreeStore>::cast_dynamic(tree->get_model()); + +// g_message("Links!!!! for %p hits [%s] with link of [%s]", &device, device->getId().c_str(), device->getLink().c_str()); + Gtk::TreeModel::iterator deviceIter; + deviceStore->foreach_iter( sigc::bind<Glib::ustring, Gtk::TreeModel::iterator*>( + sigc::ptr_fun(&InputDialogImpl::findDevice), + device->getId(), + &deviceIter) ); + + if ( deviceIter ) { + // Found the device concerned. Can proceed. + + if ( device->getLink().empty() ) { + // is now unlinked +// g_message("Item %s is unlinked", device->getId().c_str()); + if ( deviceIter->parent() != tabletIter ) { + // Not the child of the tablet. move on up + + Glib::RefPtr<InputDevice const> dev = (*deviceIter)[getCols().device]; + Glib::ustring descr = (*deviceIter)[getCols().description]; + Glib::RefPtr<Gdk::Pixbuf> thumb = (*deviceIter)[getCols().thumbnail]; + + Gtk::TreeModel::Row deviceRow = *deviceStore->append(tabletIter->children()); + deviceRow[getCols().description] = descr; + deviceRow[getCols().thumbnail] = thumb; + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + + Gtk::TreeModel::iterator oldParent = deviceIter->parent(); + deviceStore->erase(deviceIter); + if ( oldParent->children().empty() ) { + deviceStore->erase(oldParent); + } + } + } else { + // is linking + if ( deviceIter->parent() == tabletIter ) { + // Simple case. Not already linked + + Gtk::TreeIter newGroup = deviceStore->append(tabletIter->children()); + (*newGroup)[getCols().description] = _("Pen"); + (*newGroup)[getCols().thumbnail] = getPix(PIX_PEN); + + Glib::RefPtr<InputDevice const> dev = (*deviceIter)[getCols().device]; + Glib::ustring descr = (*deviceIter)[getCols().description]; + Glib::RefPtr<Gdk::Pixbuf> thumb = (*deviceIter)[getCols().thumbnail]; + + Gtk::TreeModel::Row deviceRow = *deviceStore->append(newGroup->children()); + deviceRow[getCols().description] = descr; + deviceRow[getCols().thumbnail] = thumb; + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + + + Gtk::TreeModel::iterator linkIter; + deviceStore->foreach_iter( sigc::bind<Glib::ustring, Gtk::TreeModel::iterator*>( + sigc::ptr_fun(&InputDialogImpl::findDeviceByLink), + device->getId(), + &linkIter) ); + if ( linkIter ) { + dev = (*linkIter)[getCols().device]; + descr = (*linkIter)[getCols().description]; + thumb = (*linkIter)[getCols().thumbnail]; + + deviceRow = *deviceStore->append(newGroup->children()); + deviceRow[getCols().description] = descr; + deviceRow[getCols().thumbnail] = thumb; + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + Gtk::TreeModel::iterator oldParent = linkIter->parent(); + deviceStore->erase(linkIter); + if ( oldParent->children().empty() ) { + deviceStore->erase(oldParent); + } + } + + Gtk::TreeModel::iterator oldParent = deviceIter->parent(); + deviceStore->erase(deviceIter); + if ( oldParent->children().empty() ) { + deviceStore->erase(oldParent); + } + tree->expand_row(Gtk::TreePath(newGroup), true); + } + } + } +} + +void InputDialogImpl::linkComboChanged() { + Glib::RefPtr<Gtk::TreeSelection> treeSel = deviceTree.get_selection(); + Gtk::TreeModel::iterator iter = treeSel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring val = row[getCols().description]; + Glib::RefPtr<InputDevice const> dev = row[getCols().device]; + if ( dev ) { + if ( linkCombo.get_active_row_number() == 0 ) { + // It is the "None" entry + DeviceManager::getManager().setLinkedTo(dev->getId(), ""); + } else { + Glib::ustring linkName = linkCombo.get_active_text(); + std::list<Glib::RefPtr<InputDevice const> > devList = Inkscape::DeviceManager::getManager().getDevices(); + for ( std::list<Glib::RefPtr<InputDevice const> >::const_iterator it = devList.begin(); it != devList.end(); ++it ) { + if ( linkName == (*it)->getName() ) { + DeviceManager::getManager().setLinkedTo(dev->getId(), (*it)->getId()); + break; + } + } + } + } + } +} + +void InputDialogImpl::resyncToSelection() { + bool clear = true; + Glib::RefPtr<Gtk::TreeSelection> treeSel = deviceTree.get_selection(); + Gtk::TreeModel::iterator iter = treeSel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring val = row[getCols().description]; + Glib::RefPtr<InputDevice const> dev = row[getCols().device]; + + if ( dev ) { + axisTable.set_sensitive(true); + + linkConnection.block(); + linkCombo.remove_all(); + linkCombo.append(_("None")); + linkCombo.set_active(0); + if ( dev->getSource() != Gdk::SOURCE_MOUSE ) { + Glib::ustring linked = dev->getLink(); + std::list<Glib::RefPtr<InputDevice const> > devList = Inkscape::DeviceManager::getManager().getDevices(); + for ( std::list<Glib::RefPtr<InputDevice const> >::const_iterator it = devList.begin(); it != devList.end(); ++it ) { + if ( ((*it)->getSource() != Gdk::SOURCE_MOUSE) && ((*it) != dev) ) { + linkCombo.append((*it)->getName().c_str()); + if ( (linked.length() > 0) && (linked == (*it)->getId()) ) { + linkCombo.set_active_text((*it)->getName().c_str()); + } + } + } + linkCombo.set_sensitive(true); + } else { + linkCombo.set_sensitive(false); + } + linkConnection.unblock(); + + clear = false; + devName.set_label(row[getCols().description]); + axisFrame.set_label(row[getCols().description]); + setupValueAndCombo( dev->getNumAxes(), dev->getNumAxes(), devAxesCount, axesCombo); + setupValueAndCombo( dev->getNumKeys(), dev->getNumKeys(), devKeyCount, buttonCombo); + + + } + } + + axisTable.set_sensitive(!clear); + if (clear) { + axisFrame.set_label(""); + devName.set_label(""); + devAxesCount.set_label(""); + devKeyCount.set_label(""); + } +} + +void InputDialogImpl::ConfPanel::setAxis(gint count) +{ + /* + * TODO - Make each axis editable + */ + axisStore->clear(); + + static Glib::ustring axesLabels[6] = {_("X"), _("Y"), _("Pressure"), _("X tilt"), _("Y tilt"), _("Wheel")}; + + for ( gint barNum = 0; barNum < static_cast<gint>(G_N_ELEMENTS(axesLabels)); barNum++ ) { + + Gtk::TreeModel::Row row = *(axisStore->append()); + row[axisColumns.name] = axesLabels[barNum]; + if (barNum < count) { + row[axisColumns.value] = Glib::ustring::format(barNum+1); + } else { + row[axisColumns.value] = C_("Input device axe", "None"); + } + } + +} +void InputDialogImpl::ConfPanel::setKeys(gint count) +{ + /* + * TODO - Make each key assignable + */ + + keysStore->clear(); + + for (gint i = 0; i < count; i++) { + Gtk::TreeModel::Row row = *(keysStore->append()); + row[keysColumns.name] = Glib::ustring::format(i+1); + row[keysColumns.value] = _("Disabled"); + } + + +} +void InputDialogImpl::setupValueAndCombo( gint reported, gint actual, Gtk::Label& label, Gtk::ComboBoxText& combo ) +{ + gchar *tmp = g_strdup_printf("%d", reported); + label.set_label(tmp); + g_free(tmp); + + combo.remove_all(); + for ( gint i = 1; i <= reported; ++i ) { + tmp = g_strdup_printf("%d", i); + combo.append(tmp); + g_free(tmp); + } + + if ( (1 <= actual) && (actual <= reported) ) { + combo.set_active(actual - 1); + } +} + +void InputDialogImpl::updateTestButtons( Glib::ustring const& key, gint hotButton ) +{ + for ( gint i = 0; i < static_cast<gint>(G_N_ELEMENTS(testButtons)); i++ ) { + if ( buttonMap[key].find(i) != buttonMap[key].end() ) { + if ( i == hotButton ) { + testButtons[i].set(getPix(PIX_BUTTONS_ON)); + } else { + testButtons[i].set(getPix(PIX_BUTTONS_OFF)); + } + } else { + testButtons[i].set(getPix(PIX_BUTTONS_NONE)); + } + } +} + +void InputDialogImpl::updateTestAxes( Glib::ustring const& key, GdkDevice* dev ) +{ + //static gdouble epsilon = 0.0001; + { + Glib::RefPtr<Gtk::TreeSelection> treeSel = deviceTree.get_selection(); + Gtk::TreeModel::iterator iter = treeSel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring val = row[getCols().description]; + Glib::RefPtr<InputDevice const> idev = row[getCols().device]; + if ( !idev || (idev->getId() != key) ) { + dev = nullptr; + } + } + } + + for ( gint i = 0; i < static_cast<gint>(G_N_ELEMENTS(testAxes)); i++ ) { + if ( axesMap[key].find(i) != axesMap[key].end() ) { + switch ( axesMap[key][i].first ) { + case 0: + case 1: + testAxes[i].set(getPix(PIX_AXIS_NONE)); + if ( dev && (i < static_cast<gint>(G_N_ELEMENTS(axesValues)) ) ) { + axesValues[i].set_sensitive(false); + } + break; + case 2: + testAxes[i].set(getPix(PIX_AXIS_OFF)); + axesValues[i].set_sensitive(true); + if ( dev && (i < static_cast<gint>(G_N_ELEMENTS(axesValues)) ) ) { + // FIXME: Device axis ranges are inaccessible in GTK+ 3 and + // are deprecated in GTK+ 2. Progress-bar ranges are disabled + // until we find an alternative solution + + // if ( (dev->axes[i].max - dev->axes[i].min) > epsilon ) { + axesValues[i].set_sensitive(true); + // axesValues[i].set_fraction( (axesMap[key][i].second- dev->axes[i].min) / (dev->axes[i].max - dev->axes[i].min) ); + // } + + gchar* str = g_strdup_printf("%f", axesMap[key][i].second); + axesValues[i].set_text(str); + g_free(str); + } + break; + case 3: + testAxes[i].set(getPix(PIX_AXIS_ON)); + axesValues[i].set_sensitive(true); + if ( dev && (i < static_cast<gint>(G_N_ELEMENTS(axesValues)) ) ) { + + // FIXME: Device axis ranges are inaccessible in GTK+ 3 and + // are deprecated in GTK+ 2. Progress-bar ranges are disabled + // until we find an alternative solution + + // if ( (dev->axes[i].max - dev->axes[i].min) > epsilon ) { + axesValues[i].set_sensitive(true); + // axesValues[i].set_fraction( (axesMap[key][i].second- dev->axes[i].min) / (dev->axes[i].max - dev->axes[i].min) ); + // } + + gchar* str = g_strdup_printf("%f", axesMap[key][i].second); + axesValues[i].set_text(str); + g_free(str); + } + } + + } else { + testAxes[i].set(getPix(PIX_AXIS_NONE)); + } + } + if ( !dev ) { + for (auto & axesValue : axesValues) { + axesValue.set_fraction(0.0); + axesValue.set_text(""); + axesValue.set_sensitive(false); + } + } +} + +void InputDialogImpl::mapAxesValues( Glib::ustring const& key, gdouble const * axes, GdkDevice* dev ) +{ + auto device = Glib::wrap(dev); + auto numAxes = device->get_n_axes(); + + static gdouble epsilon = 0.0001; + if ( (numAxes > 0) && axes) { + for ( guint axisNum = 0; axisNum < numAxes; axisNum++ ) { + // 0 == new, 1 == set value, 2 == changed value, 3 == active + gdouble diff = axesMap[key][axisNum].second - axes[axisNum]; + switch(axesMap[key][axisNum].first) { + case 0: + { + axesMap[key][axisNum].first = 1; + axesMap[key][axisNum].second = axes[axisNum]; + } + break; + case 1: + { + if ( (diff > epsilon) || (diff < -epsilon) ) { +// g_message("Axis %d changed on %s]", axisNum, key.c_str()); + axesMap[key][axisNum].first = 3; + axesMap[key][axisNum].second = axes[axisNum]; + updateTestAxes(key, dev); + DeviceManager::getManager().addAxis(key, axisNum); + } + } + break; + case 2: + { + if ( (diff > epsilon) || (diff < -epsilon) ) { + axesMap[key][axisNum].first = 3; + axesMap[key][axisNum].second = axes[axisNum]; + updateTestAxes(key, dev); + } + } + break; + case 3: + { + if ( (diff > epsilon) || (diff < -epsilon) ) { + axesMap[key][axisNum].second = axes[axisNum]; + } else { + axesMap[key][axisNum].first = 2; + updateTestAxes(key, dev); + } + } + } + } + } + // std::map<Glib::ustring, std::map<guint, std::pair<guint, gdouble> > > axesMap; +} + +Glib::ustring InputDialogImpl::getKeyFor( GdkDevice* device ) +{ + Glib::ustring key; + auto devicemm = Glib::wrap(device); + + auto source = devicemm->get_source(); + const auto name = devicemm->get_name(); + + switch ( source ) { + case Gdk::SOURCE_MOUSE: + key = "M:"; + break; + case Gdk::SOURCE_CURSOR: + key = "C:"; + break; + case Gdk::SOURCE_PEN: + key = "P:"; + break; + case Gdk::SOURCE_ERASER: + key = "E:"; + break; + default: + key = "?:"; + } + key += name; + + return key; +} + +bool InputDialogImpl::eventSnoop(GdkEvent* event) +{ + int modmod = 0; + + auto source = lastSourceSeen; + Glib::ustring devName = lastDevnameSeen; + Glib::ustring key; + gint hotButton = -1; + + /* Code for determining which input device caused the event fails with GTK3 + * because event->device gives a "generic" input device, not the one that + * actually caused the event. See snoop_extended in desktop-events.cpp + */ + + switch ( event->type ) { + case GDK_KEY_PRESS: + case GDK_KEY_RELEASE: + { + auto keyEvt = reinterpret_cast<GdkEventKey*>(event); + auto name = Gtk::AccelGroup::name(keyEvt->keyval, static_cast<Gdk::ModifierType>(keyEvt->state)); + keyVal.set_label(name); +// g_message("%d KEY state:0x%08x 0x%04x [%s]", keyEvt->type, keyEvt->state, keyEvt->keyval, name); + } + break; + case GDK_BUTTON_PRESS: + modmod = 1; + // fallthrough + case GDK_BUTTON_RELEASE: + { + auto btnEvt = reinterpret_cast<GdkEventButton*>(event); + auto device = Glib::wrap(btnEvt->device); + if (device) { + key = getKeyFor(btnEvt->device); + source = device->get_source(); + devName = device->get_name(); + mapAxesValues(key, btnEvt->axes, btnEvt->device); + + if ( buttonMap[key].find(btnEvt->button) == buttonMap[key].end() ) { +// g_message("New button found for %s = %d", key.c_str(), btnEvt->button); + buttonMap[key].insert(btnEvt->button); + DeviceManager::getManager().addButton(key, btnEvt->button); + } + hotButton = modmod ? btnEvt->button : -1; + updateTestButtons(key, hotButton); + } + auto name = Gtk::AccelGroup::name(0, static_cast<Gdk::ModifierType>(btnEvt->state)); + keyVal.set_label(name); +// g_message("%d BTN state:0x%08x %c %4d [%s] dev:%p [%s] ", +// btnEvt->type, btnEvt->state, +// (modmod ? '+':'-'), +// btnEvt->button, name, btnEvt->device, +// (btnEvt->device ? btnEvt->device->name : "null") + +// ); + } + break; + case GDK_MOTION_NOTIFY: + { + GdkEventMotion* btnMtn = reinterpret_cast<GdkEventMotion*>(event); + auto device = Glib::wrap(btnMtn->device); + if (device) { + key = getKeyFor(btnMtn->device); + source = device->get_source(); + devName = device->get_name(); + mapAxesValues(key, btnMtn->axes, btnMtn->device); + } + auto name = Gtk::AccelGroup::name(0, static_cast<Gdk::ModifierType>(btnMtn->state)); + keyVal.set_label(name); +// g_message("%d MOV state:0x%08x [%s] dev:%p [%s] %3.2f %3.2f %3.2f %3.2f %3.2f %3.2f", btnMtn->type, btnMtn->state, +// name, btnMtn->device, +// (btnMtn->device ? btnMtn->device->name : "null"), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 0)) ? btnMtn->axes[0]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 1)) ? btnMtn->axes[1]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 2)) ? btnMtn->axes[2]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 3)) ? btnMtn->axes[3]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 4)) ? btnMtn->axes[4]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 5)) ? btnMtn->axes[5]:0) +// ); + } + break; + default: + ;// nothing + } + + + if ( (lastSourceSeen != source) || (lastDevnameSeen != devName) ) { + switch (source) { + case Gdk::SOURCE_MOUSE: { + testThumb.set(getPix(PIX_CORE)); + break; + } + case Gdk::SOURCE_CURSOR: { +// g_message("flip to cursor"); + testThumb.set(getPix(PIX_MOUSE)); + break; + } + case Gdk::SOURCE_PEN: { + if (devName == _("pad")) { +// g_message("flip to pad"); + testThumb.set(getPix(PIX_SIDEBUTTONS)); + } else { +// g_message("flip to pen"); + testThumb.set(getPix(PIX_TIP)); + } + break; + } + case Gdk::SOURCE_ERASER: { +// g_message("flip to eraser"); + testThumb.set(getPix(PIX_ERASER)); + break; + } + /// \fixme GTK3 added new GDK_SOURCEs that should be handled here! + case Gdk::SOURCE_KEYBOARD: + case Gdk::SOURCE_TOUCHSCREEN: + case Gdk::SOURCE_TOUCHPAD: + case Gdk::SOURCE_TRACKPOINT: + case Gdk::SOURCE_TABLET_PAD: + g_warning("InputDialogImpl::eventSnoop : unhandled GDK_SOURCE type!"); + break; + } + + updateTestButtons(key, hotButton); + lastSourceSeen = source; + lastDevnameSeen = devName; + } + + return false; +} + + +} // end namespace Inkscape +} // end namespace UI +} // end namespace Dialog + + +/* + 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/ui/dialog/input.h b/src/ui/dialog/input.h new file mode 100644 index 0000000..90fb8a5 --- /dev/null +++ b/src/ui/dialog/input.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Input devices dialog (new) + */ +/* Author: + * Jon A. Cruz + * + * Copyright (C) 2008 Author + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_INPUT_H +#define INKSCAPE_UI_DIALOG_INPUT_H + +#include "ui/dialog/dialog-base.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class InputDialog : public DialogBase +{ +public: + static InputDialog &getInstance(); + + InputDialog() : DialogBase("/dialogs/inputdevices", "Input") {} + ~InputDialog() override = default; +}; + +} // namespace Dialog +} // namesapce UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_INPUT_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/ui/dialog/knot-properties.cpp b/src/ui/dialog/knot-properties.cpp new file mode 100644 index 0000000..048cd33 --- /dev/null +++ b/src/ui/dialog/knot-properties.cpp @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for moving knots. Only used by Measure Tool. + */ + +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.com> + * Andrius R. <knutux@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2004 Bryce Harrington + * Copyright (C) 2006 Andrius R. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "knot-properties.h" + +#include <boost/lexical_cast.hpp> + +#include <glibmm/i18n.h> +#include <glibmm/main.h> + +#include "desktop.h" +#include "ui/knot/knot.h" +#include "util/units.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +KnotPropertiesDialog::KnotPropertiesDialog() + : _knotpoint(nullptr), + _position_visible(false), + _close_button(_("_Close"), true) +{ + Gtk::Box *mainVBox = get_content_area(); + + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + _unit_name = ""; + + // Layer name widgets + _knot_x_entry.set_activates_default(true); + _knot_x_entry.set_digits(4); + _knot_x_entry.set_increments(1,1); + _knot_x_entry.set_range(-G_MAXDOUBLE, G_MAXDOUBLE); + _knot_x_entry.set_hexpand(); + _knot_x_label.set_label(_("Position X:")); + _knot_x_label.set_halign(Gtk::ALIGN_END); + _knot_x_label.set_valign(Gtk::ALIGN_CENTER); + + _knot_y_entry.set_activates_default(true); + _knot_y_entry.set_digits(4); + _knot_y_entry.set_increments(1,1); + _knot_y_entry.set_range(-G_MAXDOUBLE, G_MAXDOUBLE); + _knot_y_entry.set_hexpand(); + _knot_y_label.set_label(_("Position Y:")); + _knot_y_label.set_halign(Gtk::ALIGN_END); + _knot_y_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_knot_x_label, 0, 0, 1, 1); + _layout_table.attach(_knot_x_entry, 1, 0, 1, 1); + + _layout_table.attach(_knot_y_label, 0, 1, 1, 1); + _layout_table.attach(_knot_y_entry, 1, 1, 1, 1); + + mainVBox->pack_start(_layout_table, true, true, 4); + + // Buttons + _close_button.set_can_default(); + + _apply_button.set_use_underline(true); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &KnotPropertiesDialog::_close)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &KnotPropertiesDialog::_apply)); + + signal_delete_event().connect( + sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &KnotPropertiesDialog::_close)), + true + ) + ); + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); + + set_focus(_knot_y_entry); +} + +KnotPropertiesDialog::~KnotPropertiesDialog() { +} + +void KnotPropertiesDialog::showDialog(SPDesktop *desktop, const SPKnot *pt, Glib::ustring const unit_name) +{ + KnotPropertiesDialog *dialog = new KnotPropertiesDialog(); + dialog->_setKnotPoint(pt->position(), unit_name); + dialog->_setPt(pt); + + dialog->set_title(_("Modify Knot Position")); + dialog->_apply_button.set_label(_("_Move")); + + dialog->set_modal(true); + desktop->setWindowTransient (dialog->gobj()); + dialog->property_destroy_with_parent() = true; + + dialog->show(); + dialog->present(); +} + +void +KnotPropertiesDialog::_apply() +{ + double d_x = Inkscape::Util::Quantity::convert(_knot_x_entry.get_value(), _unit_name, "px"); + double d_y = Inkscape::Util::Quantity::convert(_knot_y_entry.get_value(), _unit_name, "px"); + _knotpoint->moveto(Geom::Point(d_x, d_y)); + _knotpoint->moved_signal.emit(_knotpoint, _knotpoint->position(), 0); + _close(); +} + +void +KnotPropertiesDialog::_close() +{ + destroy_(); + Glib::signal_idle().connect( + sigc::bind_return( + sigc::bind(sigc::ptr_fun<void*, void>(&::operator delete), this), + false + ) + ); +} + +bool KnotPropertiesDialog::_handleKeyEvent(GdkEventKey * /*event*/) +{ + + /*switch (get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + _apply(); + return true; + } + break; + }*/ + return false; +} + +void KnotPropertiesDialog::_handleButtonEvent(GdkEventButton* event) +{ + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + _apply(); + } +} + +void KnotPropertiesDialog::_setKnotPoint(Geom::Point knotpoint, Glib::ustring const unit_name) +{ + _unit_name = unit_name; + _knot_x_entry.set_value( Inkscape::Util::Quantity::convert(knotpoint.x(), "px", _unit_name)); + _knot_y_entry.set_value( Inkscape::Util::Quantity::convert(knotpoint.y(), "px", _unit_name)); + _knot_x_label.set_label(g_strdup_printf(_("Position X (%s):"), _unit_name.c_str())); + _knot_y_label.set_label(g_strdup_printf(_("Position Y (%s):"), _unit_name.c_str())); +} + +void KnotPropertiesDialog::_setPt(const SPKnot *pt) +{ + _knotpoint = const_cast<SPKnot *>(pt); +} + +} // namespace +} // namespace +} // namespace + + +/* + 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/ui/dialog/knot-properties.h b/src/ui/dialog/knot-properties.h new file mode 100644 index 0000000..18f6745 --- /dev/null +++ b/src/ui/dialog/knot-properties.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.com> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_KNOT_PROPERTIES_H +#define INKSCAPE_DIALOG_KNOT_PROPERTIES_H + +#include <gtkmm/dialog.h> +#include <gtkmm/grid.h> +#include <gtkmm/label.h> +#include <gtkmm/spinbutton.h> +#include <2geom/point.h> + +#include "ui/tools/measure-tool.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +// Used in Measure tool to set ends of "ruler" (via Shift-click)." + +class KnotPropertiesDialog : public Gtk::Dialog { + public: + KnotPropertiesDialog(); + ~KnotPropertiesDialog() override; + + Glib::ustring getName() const { return "LayerPropertiesDialog"; } + + static void showDialog(SPDesktop *desktop, const SPKnot *pt, Glib::ustring const unit_name); + +protected: + + SPKnot *_knotpoint; + + Gtk::Label _knot_x_label; + Gtk::SpinButton _knot_x_entry; + Gtk::Label _knot_y_label; + Gtk::SpinButton _knot_y_entry; + Gtk::Grid _layout_table; + bool _position_visible; + + Gtk::Button _close_button; + Gtk::Button _apply_button; + Glib::ustring _unit_name; + + sigc::connection _destroy_connection; + + static KnotPropertiesDialog &_instance() { + static KnotPropertiesDialog instance; + return instance; + } + + void _setPt(const SPKnot *pt); + + void _apply(); + void _close(); + + void _setKnotPoint(Geom::Point knotpoint, Glib::ustring const unit_name); + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); + + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton* event); + friend class Inkscape::UI::Tools::MeasureTool; + +private: + KnotPropertiesDialog(KnotPropertiesDialog const &); // no copy + KnotPropertiesDialog &operator=(KnotPropertiesDialog const &); // no assign +}; + +} // namespace +} // namespace +} // namespace + + +#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_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/ui/dialog/layer-properties.cpp b/src/ui/dialog/layer-properties.cpp new file mode 100644 index 0000000..76a2778 --- /dev/null +++ b/src/ui/dialog/layer-properties.cpp @@ -0,0 +1,435 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for renaming layers. + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Andrius R. <knutux@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2004 Bryce Harrington + * Copyright (C) 2006 Andrius R. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "layer-properties.h" + +#include <glibmm/i18n.h> +#include <glibmm/main.h> + +#include "inkscape.h" +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "layer-manager.h" +#include "message-stack.h" +#include "preferences.h" +#include "selection-chemistry.h" + +#include "ui/icon-names.h" +#include "ui/widget/imagetoggler.h" +#include "ui/tools/tool-base.h" +#include "object/sp-root.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +LayerPropertiesDialog::LayerPropertiesDialog(LayerPropertiesDialogType type) + : _type{type} + , _close_button(_("_Cancel"), true) +{ + auto mainVBox = get_content_area(); + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + + // Layer name widgets + _layer_name_entry.set_activates_default(true); + _layer_name_label.set_label(_("Layer name:")); + _layer_name_label.set_halign(Gtk::ALIGN_START); + _layer_name_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_layer_name_label, 0, 0, 1, 1); + + _layer_name_entry.set_halign(Gtk::ALIGN_FILL); + _layer_name_entry.set_valign(Gtk::ALIGN_FILL); + _layer_name_entry.set_hexpand(); + _layout_table.attach(_layer_name_entry, 1, 0, 1, 1); + + mainVBox->pack_start(_layout_table, true, true, 4); + + // Buttons + _close_button.set_can_default(); + + _apply_button.set_use_underline(true); + _apply_button.set_can_default(); + + _close_button.signal_clicked().connect([=]() {_close();}); + _apply_button.signal_clicked().connect([=]() {_apply();}); + + signal_delete_event().connect([=](GdkEventAny*) -> bool { + _close(); + return true; + }); + + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); +} + +LayerPropertiesDialog::~LayerPropertiesDialog() = default; + +/** Static member function which displays a modal dialog of the given type */ +void LayerPropertiesDialog::_showDialog(LayerPropertiesDialogType type, SPDesktop *desktop, SPObject *layer) +{ + auto dialog = new LayerPropertiesDialog(type); // Will be destroyed on idle - see _close() + + dialog->_setDesktop(desktop); + dialog->_setLayer(layer); + + dialog->_setup(); + + dialog->set_modal(true); + desktop->setWindowTransient(dialog->gobj()); + dialog->property_destroy_with_parent() = true; + + dialog->show(); + dialog->present(); +} + +/** Performs an action depending on the type of the dialog */ +void LayerPropertiesDialog::_apply() +{ + switch (_type) { + case LayerPropertiesDialogType::CREATE: + _doCreate(); + break; + + case LayerPropertiesDialogType::MOVE: + _doMove(); + break; + + case LayerPropertiesDialogType::RENAME: + _doRename(); + break; + + case LayerPropertiesDialogType::NONE: + default: + break; + } + _close(); +} + +/** Closes the dialog and asks the idle thread to destroy it */ +void LayerPropertiesDialog::_close() +{ + _setLayer(nullptr); + _setDesktop(nullptr); + destroy_(); + Glib::signal_idle().connect_once([=]() {delete this;}); +} + +/** Creates a new layer based on the input entered in the dialog window */ +void LayerPropertiesDialog::_doCreate() +{ + LayerRelativePosition position = LPOS_ABOVE; + if (_position_visible) { + Gtk::ListStore::iterator activeRow(_layer_position_combo.get_active()); + position = activeRow->get_value(_dropdown_columns.position); + int index = _layer_position_combo.get_active_row_number(); + Preferences::get()->setInt("/dialogs/layerProp/addLayerPosition", index); + } + Glib::ustring name(_layer_name_entry.get_text()); + if (name.empty()) { + return; + } + + auto root = _desktop->getDocument()->getRoot(); + SPObject *new_layer = Inkscape::create_layer(root, _layer, position); + + if (!name.empty()) { + _desktop->layerManager().renameLayer(new_layer, name.c_str(), true); + } + _desktop->getSelection()->clear(); + _desktop->layerManager().setCurrentLayer(new_layer); + DocumentUndo::done(_desktop->getDocument(), _("Add layer"), INKSCAPE_ICON("layer-new")); + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("New layer created.")); +} + +/** Moves selection to the chosen layer */ +void LayerPropertiesDialog::_doMove() +{ + if (auto moveto = _selectedLayer()) { + _desktop->selection->toLayer(moveto); + } +} + +/** Renames a layer based on the user input in the dialog window */ +void LayerPropertiesDialog::_doRename() +{ + Glib::ustring name(_layer_name_entry.get_text()); + if (name.empty()) { + return; + } + LayerManager &layman = _desktop->layerManager(); + layman.renameLayer(layman.currentLayer(), name.c_str(), false); + + DocumentUndo::done(_desktop->getDocument(), _("Rename layer"), INKSCAPE_ICON("layer-rename")); + // TRANSLATORS: This means "The layer has been renamed" + _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Renamed layer")); +} + +/** Sets up the dialog depending on its type */ +void LayerPropertiesDialog::_setup() +{ + g_assert(_desktop != nullptr); + LayerManager &layman = _desktop->layerManager(); + + switch (_type) { + case LayerPropertiesDialogType::CREATE: { + set_title(_("Add Layer")); + Glib::ustring new_name = layman.getNextLayerName(nullptr, layman.currentLayer()->label()); + _layer_name_entry.set_text(new_name); + _apply_button.set_label(_("_Add")); + _setup_position_controls(); + break; + } + + case LayerPropertiesDialogType::MOVE: { + set_title(_("Move to Layer")); + _layer_name_entry.set_text(_("Layer")); + _apply_button.set_label(_("_Move")); + _apply_button.set_sensitive(layman.getLayerCount()); + _setup_layers_controls(); + break; + } + + case LayerPropertiesDialogType::RENAME: { + set_title(_("Rename Layer")); + gchar const *name = layman.currentLayer()->label(); + _layer_name_entry.set_text(name ? name : _("Layer")); + _apply_button.set_label(_("_Rename")); + break; + } + + case LayerPropertiesDialogType::NONE: + default: + break; + } +} + +/** Sets up the combo box for choosing the relative position of the new layer */ +void LayerPropertiesDialog::_setup_position_controls() +{ + if (!_layer || _desktop->getDocument()->getRoot() == _layer) { + // no layers yet, so option above/below/sublayer is useless + return; + } + + _position_visible = true; + _dropdown_list = Gtk::ListStore::create(_dropdown_columns); + _layer_position_combo.set_model(_dropdown_list); + _layer_position_combo.pack_start(_label_renderer); + _layer_position_combo.set_cell_data_func(_label_renderer, + [=](Gtk::TreeModel::const_iterator const &row) { + _prepareLabelRenderer(row); + }); + + Gtk::ListStore::iterator row; + row = _dropdown_list->append(); + row->set_value(_dropdown_columns.position, LPOS_ABOVE); + row->set_value(_dropdown_columns.name, Glib::ustring(_("Above current"))); + _layer_position_combo.set_active(row); + row = _dropdown_list->append(); + row->set_value(_dropdown_columns.position, LPOS_BELOW); + row->set_value(_dropdown_columns.name, Glib::ustring(_("Below current"))); + row = _dropdown_list->append(); + row->set_value(_dropdown_columns.position, LPOS_CHILD); + row->set_value(_dropdown_columns.name, Glib::ustring(_("As sublayer of current"))); + + int position = Preferences::get()->getIntLimited("/dialogs/layerProp/addLayerPosition", 0, 0, 2); + _layer_position_combo.set_active(position); + + _layer_position_label.set_label(_("Position:")); + _layer_position_label.set_halign(Gtk::ALIGN_START); + _layer_position_label.set_valign(Gtk::ALIGN_CENTER); + + _layer_position_combo.set_halign(Gtk::ALIGN_FILL); + _layer_position_combo.set_valign(Gtk::ALIGN_FILL); + _layer_position_combo.set_hexpand(); + _layout_table.attach(_layer_position_combo, 1, 1, 1, 1); + + _layout_table.attach(_layer_position_label, 0, 1, 1, 1); + + show_all_children(); +} + +/** Sets up the tree view of current layers */ +void LayerPropertiesDialog::_setup_layers_controls() +{ + ModelColumns *zoop = new ModelColumns(); + _model = zoop; + _store = Gtk::TreeStore::create( *zoop ); + _tree.set_model( _store ); + _tree.set_headers_visible(false); + + auto *eyeRenderer = Gtk::manage(new UI::Widget::ImageToggler(INKSCAPE_ICON("object-visible"), + INKSCAPE_ICON("object-hidden"))); + int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1; + Gtk::TreeViewColumn *col = _tree.get_column(visibleColNum); + if (col) { + col->add_attribute(eyeRenderer->property_active(), _model->_colVisible); + } + + auto *renderer = Gtk::manage(new UI::Widget::ImageToggler(INKSCAPE_ICON("object-locked"), + INKSCAPE_ICON("object-unlocked"))); + int lockedColNum = _tree.append_column("lock", *renderer) - 1; + col = _tree.get_column(lockedColNum); + if (col) { + col->add_attribute(renderer->property_active(), _model->_colLocked); + } + + Gtk::CellRendererText *_text_renderer = Gtk::manage(new Gtk::CellRendererText()); + int nameColNum = _tree.append_column("Name", *_text_renderer) - 1; + Gtk::TreeView::Column *_name_column = _tree.get_column(nameColNum); + _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); + + _tree.set_expander_column(*_tree.get_column(nameColNum)); + _tree.signal_key_press_event().connect([=](GdkEventKey *ev) {return _handleKeyEvent(ev);}, false); + _tree.signal_button_press_event().connect_notify([=](GdkEventButton *b) {_handleButtonEvent(b);}); + + _scroller.add(_tree); + _scroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _scroller.set_shadow_type(Gtk::SHADOW_IN); + _scroller.set_size_request(220, 180); + + SPDocument* document = _desktop->doc(); + SPRoot* root = document->getRoot(); + if (root) { + SPObject* target = _desktop->layerManager().currentLayer(); + _store->clear(); + _addLayer(root, nullptr, target, 0); + } + + _layout_table.remove(_layer_name_entry); + _layout_table.remove(_layer_name_label); + + _scroller.set_halign(Gtk::ALIGN_FILL); + _scroller.set_valign(Gtk::ALIGN_FILL); + _scroller.set_hexpand(); + _scroller.set_vexpand(); + _scroller.set_propagate_natural_width(true); + _scroller.set_propagate_natural_height(true); + _layout_table.attach(_scroller, 0, 1, 2, 1); + + show_all_children(); +} + +/** Inserts the new layer into the document */ +void LayerPropertiesDialog::_addLayer(SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, + int level) +{ + int const max_nest_depth = 20; + if (!_desktop || !layer || level >= max_nest_depth) { + g_warn_message("Inkscape", __FILE__, __LINE__, __func__, "Maximum layer nesting reached."); + return; + } + LayerManager &layman = _desktop->layerManager(); + unsigned int counter = layman.childCount(layer); + for (unsigned int i = 0; i < counter; i++) { + SPObject *child = _desktop->layerManager().nthChildOf(layer, i); + if (!child) { + continue; + } +#if DUMP_LAYERS + g_message(" %3d layer:%p {%s} [%s]", level, child, child->id, child->label() ); +#endif // DUMP_LAYERS + + Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend(); + Gtk::TreeModel::Row row = *iter; + row[_model->_colObject] = child; + row[_model->_colLabel] = child->label() ? child->label() : child->getId(); + row[_model->_colVisible] = SP_IS_ITEM(child) ? !SP_ITEM(child)->isHidden() : false; + row[_model->_colLocked] = SP_IS_ITEM(child) ? SP_ITEM(child)->isLocked() : false; + + if (target && child == target) { + _tree.expand_to_path(_store->get_path(iter)); + Glib::RefPtr<Gtk::TreeSelection> select = _tree.get_selection(); + select->select(iter); + } + + _addLayer(child, &row, target, level + 1); + } +} + +SPObject* LayerPropertiesDialog::_selectedLayer() +{ + SPObject* obj = nullptr; + + Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + obj = row[_model->_colObject]; + } + + return obj; +} + +bool LayerPropertiesDialog::_handleKeyEvent(GdkEventKey *event) +{ + switch (Inkscape::UI::Tools::get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + _apply(); + _close(); + return true; + } + } + return false; +} + +void LayerPropertiesDialog::_handleButtonEvent(GdkEventButton* event) +{ + if ((event->type == GDK_2BUTTON_PRESS) && (event->button == 1)) { + _apply(); + } +} + +/** Formats the label for a given layer row + */ +void LayerPropertiesDialog::_prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row) +{ + Glib::ustring name = (*row)[_dropdown_columns.name]; + _label_renderer.property_markup() = name; +} + + +void LayerPropertiesDialog::_setLayer(SPObject *layer) { + if (layer) { + sp_object_ref(layer, nullptr); + } + if (_layer) { + sp_object_unref(_layer, nullptr); + } + _layer = layer; +} + +} // namespace Dialogs +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/layer-properties.h b/src/ui/dialog/layer-properties.h new file mode 100644 index 0000000..6ebbc07 --- /dev/null +++ b/src/ui/dialog/layer-properties.h @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for renaming layers + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_LAYER_PROPERTIES_H +#define INKSCAPE_DIALOG_LAYER_PROPERTIES_H + +#include <gtkmm/dialog.h> +#include <gtkmm/entry.h> +#include <gtkmm/label.h> +#include <gtkmm/grid.h> + +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> +#include <gtkmm/treeview.h> +#include <gtkmm/treestore.h> +#include <gtkmm/scrolledwindow.h> + +#include "layer-manager.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +/* FIXME: split the LayerPropertiesDialog class into three separate dialogs */ +enum class LayerPropertiesDialogType +{ + NONE, + CREATE, + MOVE, + RENAME +}; + +class LayerPropertiesDialog : public Gtk::Dialog { +public: + LayerPropertiesDialog(LayerPropertiesDialogType type); + ~LayerPropertiesDialog() override; + LayerPropertiesDialog(LayerPropertiesDialog const &) = delete; // no copy + LayerPropertiesDialog &operator=(LayerPropertiesDialog const &) = delete; // no assign + + Glib::ustring getName() const { return "LayerPropertiesDialog"; } + + static void showRename(SPDesktop *desktop, SPObject *layer) { + _showDialog(LayerPropertiesDialogType::RENAME, desktop, layer); + } + static void showCreate(SPDesktop *desktop, SPObject *layer) { + _showDialog(LayerPropertiesDialogType::CREATE, desktop, layer); + } + static void showMove(SPDesktop *desktop, SPObject *layer) { + _showDialog(LayerPropertiesDialogType::MOVE, desktop, layer); + } + +private: + LayerPropertiesDialogType _type = LayerPropertiesDialogType::NONE; + SPDesktop *_desktop = nullptr; + SPObject *_layer = nullptr; + + class PositionDropdownColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn<LayerRelativePosition> position; + Gtk::TreeModelColumn<Glib::ustring> name; + + PositionDropdownColumns() { + add(position); add(name); + } + }; + + Gtk::Label _layer_name_label; + Gtk::Entry _layer_name_entry; + Gtk::Label _layer_position_label; + Gtk::ComboBox _layer_position_combo; + Gtk::Grid _layout_table; + + bool _position_visible = false; + + class ModelColumns : public Gtk::TreeModel::ColumnRecord + { + public: + + ModelColumns() + { + add(_colObject); + add(_colVisible); + add(_colLocked); + add(_colLabel); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<SPObject*> _colObject; + Gtk::TreeModelColumn<Glib::ustring> _colLabel; + Gtk::TreeModelColumn<bool> _colVisible; + Gtk::TreeModelColumn<bool> _colLocked; + }; + + Gtk::TreeView _tree; + ModelColumns* _model; + Glib::RefPtr<Gtk::TreeStore> _store; + Gtk::ScrolledWindow _scroller; + + + PositionDropdownColumns _dropdown_columns; + Gtk::CellRendererText _label_renderer; + Glib::RefPtr<Gtk::ListStore> _dropdown_list; + + Gtk::Button _close_button; + Gtk::Button _apply_button; + + sigc::connection _destroy_connection; + + void _setDesktop(SPDesktop *desktop) { _desktop = desktop; }; + void _setLayer(SPObject *layer); + + static void _showDialog(LayerPropertiesDialogType type, SPDesktop *desktop, SPObject *layer); + void _apply(); + void _close(); + + void _setup_position_controls(); + void _setup_layers_controls(); + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); + + void _addLayer(SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level); + SPObject* _selectedLayer(); + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton* event); + + void _doCreate(); + void _doMove(); + void _doRename(); + void _setup(); +}; + +} // namespace +} // namespace +} // namespace + + +#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_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/ui/dialog/livepatheffect-add.cpp b/src/ui/dialog/livepatheffect-add.cpp new file mode 100644 index 0000000..b7a48e7 --- /dev/null +++ b/src/ui/dialog/livepatheffect-add.cpp @@ -0,0 +1,981 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for adding a live path effect. + * + * Author: + * + * Copyright (C) 2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "livepatheffect-add.h" + +#include <cmath> +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "io/resource.h" +#include "live_effects/effect.h" +#include "object/sp-clippath.h" +#include "object/sp-item-group.h" +#include "object/sp-mask.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "preferences.h" +#include "ui/widget/canvas.h" +#include "ui/themes.h" +#include "ui/util.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +bool sp_has_fav(Glib::ustring effect) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs"); + size_t pos = favlist.find(effect); + if (pos != std::string::npos) { + return true; + } + return false; +} + +void sp_add_fav(Glib::ustring effect) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs"); + if (!sp_has_fav(effect)) { + prefs->setString("/dialogs/livepatheffect/favs", favlist + effect + ";"); + } +} + +void sp_remove_fav(Glib::ustring effect) +{ + if (sp_has_fav(effect)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs"); + effect += ";"; + size_t pos = favlist.find(effect); + if (pos != std::string::npos) { + favlist.erase(pos, effect.length()); + prefs->setString("/dialogs/livepatheffect/favs", favlist); + } + } +} + +void sp_add_top_window_classes_callback(Gtk::Widget *widg) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + Gtk::Widget *canvas = desktop->canvas; + Gtk::Window *toplevel_window = dynamic_cast<Gtk::Window *>(canvas->get_toplevel()); + if (toplevel_window) { + Gtk::Window *current_window = dynamic_cast<Gtk::Window *>(widg); + if (!current_window) { + current_window = dynamic_cast<Gtk::Window *>(widg->get_toplevel()); + } + if (current_window) { + if (toplevel_window->get_style_context()->has_class("dark")) { + current_window->get_style_context()->add_class("dark"); + current_window->get_style_context()->remove_class("bright"); + } else { + current_window->get_style_context()->add_class("bright"); + current_window->get_style_context()->remove_class("dark"); + } + if (toplevel_window->get_style_context()->has_class("symbolic")) { + current_window->get_style_context()->add_class("symbolic"); + current_window->get_style_context()->remove_class("regular"); + } else { + current_window->get_style_context()->remove_class("symbolic"); + current_window->get_style_context()->add_class("regular"); + } + } + } + } +} + +void sp_add_top_window_classes(Gtk::Widget *widg) +{ + if (!widg) { + return; + } + if (!widg->get_realized()) { + widg->signal_realize().connect(sigc::bind(sigc::ptr_fun(&sp_add_top_window_classes_callback), widg)); + } else { + sp_add_top_window_classes_callback(widg); + } +} + +bool LivePathEffectAdd::mouseover(GdkEventCrossing *evt, GtkWidget *wdg) +{ + GdkDisplay *display = gdk_display_get_default(); + GdkCursor *cursor = gdk_cursor_new_for_display(display, GDK_HAND2); + GdkWindow *window = gtk_widget_get_window(wdg); + gdk_window_set_cursor(window, cursor); + g_object_unref(cursor); + return true; +} + +bool LivePathEffectAdd::mouseout(GdkEventCrossing *evt, GtkWidget *wdg) +{ + GdkWindow *window = gtk_widget_get_window(wdg); + gdk_window_set_cursor(window, nullptr); + hide_pop_description(evt); + return true; +} + +LivePathEffectAdd::LivePathEffectAdd() + : converter(Inkscape::LivePathEffect::LPETypeConverter) + , _applied(false) + , _showfavs(false) +{ + auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-livepatheffect-add.glade"); + try { + _builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for path effect dialog"); + return; + } + _builder->get_widget("LPEDialogSelector", _LPEDialogSelector); + _builder->get_widget("LPESelectorFlowBox", _LPESelectorFlowBox); + _builder->get_widget("LPESelectorEffectInfoPop", _LPESelectorEffectInfoPop); + _builder->get_widget("LPEFilter", _LPEFilter); + _builder->get_widget("LPEInfo", _LPEInfo); + _builder->get_widget("LPEExperimental", _LPEExperimental); + _builder->get_widget("LPEScrolled", _LPEScrolled); + _builder->get_widget("LPESelectorEffectEventFavShow", _LPESelectorEffectEventFavShow); + _builder->get_widget("LPESelectorEffectInfoEventBox", _LPESelectorEffectInfoEventBox); + _builder->get_widget("LPESelectorEffectRadioPackLess", _LPESelectorEffectRadioPackLess); + _builder->get_widget("LPESelectorEffectRadioPackMore", _LPESelectorEffectRadioPackMore); + _builder->get_widget("LPESelectorEffectRadioList", _LPESelectorEffectRadioList); + + _LPEFilter->signal_search_changed().connect(sigc::mem_fun(*this, &LivePathEffectAdd::on_search)); + _LPEDialogSelector->add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | + Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK | Gdk::KEY_PRESS_MASK); + _LPESelectorFlowBox->signal_set_focus_child().connect(sigc::mem_fun(*this, &LivePathEffectAdd::on_focus)); + + gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-livepatheffect-effect.glade"); + for (int i = 0; i < static_cast<int>(converter._length); ++i) { + Glib::RefPtr<Gtk::Builder> builder_effect; + try { + builder_effect = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data = &converter.data(i); + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + LPESelectorEffect->signal_button_press_event().connect( + sigc::bind<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>( + sigc::mem_fun(*this, &LivePathEffectAdd::apply), builder_effect, &converter.data(i))); + Gtk::EventBox *LPESelectorEffectEventExpander; + builder_effect->get_widget("LPESelectorEffectEventExpander", LPESelectorEffectEventExpander); + LPESelectorEffectEventExpander->signal_button_press_event().connect( + sigc::bind<Glib::RefPtr<Gtk::Builder>>(sigc::mem_fun(*this, &LivePathEffectAdd::expand), builder_effect)); + LPESelectorEffectEventExpander->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj()))); + LPESelectorEffectEventExpander->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj()))); + Gtk::Label *LPEName; + builder_effect->get_widget("LPEName", LPEName); + const Glib::ustring label = g_dpgettext2(nullptr, "path effect", converter.get_label(data->id).c_str()); + const Glib::ustring untranslated_label = converter.get_label(data->id); + if (untranslated_label == label) { + LPEName->set_text(label); + } else { + LPEName->set_markup((label + "\n<span size='x-small'>" + untranslated_label + "</span>").c_str()); + } + Gtk::Label *LPEDescription; + builder_effect->get_widget("LPEDescription", LPEDescription); + const Glib::ustring description = _(converter.get_description(data->id).c_str()); + LPEDescription->set_text(description); + Gtk::ToggleButton *LPEExperimentalToggle; + builder_effect->get_widget("LPEExperimentalToggle", LPEExperimentalToggle); + bool active = converter.get_experimental(data->id) ? true : false; + LPEExperimentalToggle->set_active(active); + Gtk::Image *LPEIcon; + builder_effect->get_widget("LPEIcon", LPEIcon); + LPEIcon->set_from_icon_name(converter.get_icon(data->id), Gtk::IconSize(Gtk::ICON_SIZE_DIALOG)); + Gtk::EventBox *LPESelectorEffectEventInfo; + builder_effect->get_widget("LPESelectorEffectEventInfo", LPESelectorEffectEventInfo); + LPESelectorEffectEventInfo->signal_enter_notify_event().connect(sigc::bind<Glib::RefPtr<Gtk::Builder>>( + sigc::mem_fun(*this, &LivePathEffectAdd::pop_description), builder_effect)); + Gtk::EventBox *LPESelectorEffectEventFav; + builder_effect->get_widget("LPESelectorEffectEventFav", LPESelectorEffectEventFav); + Gtk::Image *fav = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFav->get_child()); + if (sp_has_fav(LPEName->get_text())) { + fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } else { + fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } + Gtk::EventBox *LPESelectorEffectEventFavTop; + builder_effect->get_widget("LPESelectorEffectEventFavTop", LPESelectorEffectEventFavTop); + LPESelectorEffectEventFav->signal_button_press_event().connect(sigc::bind<Glib::RefPtr<Gtk::Builder>>( + sigc::mem_fun(*this, &LivePathEffectAdd::fav_toggler), builder_effect)); + LPESelectorEffectEventFavTop->signal_button_press_event().connect(sigc::bind<Glib::RefPtr<Gtk::Builder>>( + sigc::mem_fun(*this, &LivePathEffectAdd::fav_toggler), builder_effect)); + Gtk::Image *favtop = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFavTop->get_child()); + if (sp_has_fav(LPEName->get_text())) { + favtop->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } else { + favtop->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } + Gtk::EventBox *LPESelectorEffectEventApply; + builder_effect->get_widget("LPESelectorEffectEventApply", LPESelectorEffectEventApply); + LPESelectorEffectEventApply->signal_button_press_event().connect( + sigc::bind<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>( + sigc::mem_fun(*this, &LivePathEffectAdd::apply), builder_effect, &converter.data(i))); + LPESelectorEffectEventApply->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffectEventApply->gobj()))); + LPESelectorEffectEventApply->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffectEventApply->gobj()))); + Gtk::ButtonBox *LPESelectorButtonBox; + builder_effect->get_widget("LPESelectorButtonBox", LPESelectorButtonBox); + LPESelectorButtonBox->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj()))); + LPESelectorButtonBox->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj()))); + LPESelectorEffect->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj()))); + LPESelectorEffect->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj()))); + _LPESelectorFlowBox->insert(*LPESelectorEffect, i); + LPESelectorEffect->get_parent()->signal_key_press_event().connect( + sigc::bind<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>( + sigc::mem_fun(*this, &LivePathEffectAdd::on_press_enter), builder_effect, &converter.data(i))); + LPESelectorEffect->get_parent()->get_style_context()->add_class( + ("LPEIndex" + Glib::ustring::format(i)).c_str()); + } + _LPESelectorFlowBox->set_activate_on_single_click(false); + _visiblelpe = _LPESelectorFlowBox->get_children().size(); + _LPEInfo->set_visible(false); + _LPESelectorEffectRadioPackLess->signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 0)); + _LPESelectorEffectRadioPackMore->signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 1)); + _LPESelectorEffectRadioList->signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 2)); + _LPESelectorEffectEventFavShow->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(_LPESelectorEffectEventFavShow->gobj()))); + _LPESelectorEffectEventFavShow->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(_LPESelectorEffectEventFavShow->gobj()))); + _LPESelectorEffectEventFavShow->signal_button_press_event().connect( + sigc::mem_fun(*this, &LivePathEffectAdd::show_fav_toggler)); + _LPESelectorEffectInfoEventBox->signal_leave_notify_event().connect( + sigc::mem_fun(*this, &LivePathEffectAdd::hide_pop_description)); + _LPESelectorEffectInfoEventBox->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(_LPESelectorEffectInfoEventBox->gobj()))); + _LPESelectorEffectInfoEventBox->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(_LPESelectorEffectInfoEventBox->gobj()))); + _LPEExperimental->property_active().signal_changed().connect( + sigc::mem_fun(*this, &LivePathEffectAdd::reload_effect_list)); + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + int width; + int height; + window->get_size(width, height); + _LPEDialogSelector->resize(std::min(width - 300, 1440), std::min(height - 300, 900)); + _LPESelectorFlowBox->set_focus_vadjustment(_LPEScrolled->get_vadjustment()); + _LPEDialogSelector->show_all_children(); + _lasteffect = nullptr; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0); + switch (mode) { + case 0: + _LPESelectorEffectRadioPackLess->set_active(); + viewChanged(0); + break; + case 1: + _LPESelectorEffectRadioPackMore->set_active(); + viewChanged(1); + break; + default: + _LPESelectorEffectRadioList->set_active(); + viewChanged(2); + } + Gtk::Widget *widg = dynamic_cast<Gtk::Widget *>(_LPEDialogSelector); + INKSCAPE.themecontext->getChangeThemeSignal().connect(sigc::bind(sigc::ptr_fun(sp_add_top_window_classes), widg)); + sp_add_top_window_classes(widg); +} +const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *LivePathEffectAdd::getActiveData() +{ + return instance()._to_add; +} + +void LivePathEffectAdd::viewChanged(gint mode) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool changed = false; + if (mode == 2 && !_LPEDialogSelector->get_style_context()->has_class("LPEList")) { + _LPEDialogSelector->get_style_context()->add_class("LPEList"); + _LPEDialogSelector->get_style_context()->remove_class("LPEPackLess"); + _LPEDialogSelector->get_style_context()->remove_class("LPEPackMore"); + _LPESelectorFlowBox->set_max_children_per_line(1); + changed = true; + } else if (mode == 1 && !_LPEDialogSelector->get_style_context()->has_class("LPEPackMore")) { + _LPEDialogSelector->get_style_context()->remove_class("LPEList"); + _LPEDialogSelector->get_style_context()->remove_class("LPEPackLess"); + _LPEDialogSelector->get_style_context()->add_class("LPEPackMore"); + _LPESelectorFlowBox->set_max_children_per_line(30); + changed = true; + } else if (mode == 0 && !_LPEDialogSelector->get_style_context()->has_class("LPEPackLess")) { + _LPEDialogSelector->get_style_context()->remove_class("LPEList"); + _LPEDialogSelector->get_style_context()->add_class("LPEPackLess"); + _LPEDialogSelector->get_style_context()->remove_class("LPEPackMore"); + _LPESelectorFlowBox->set_max_children_per_line(30); + changed = true; + } + prefs->setInt("/dialogs/livepatheffect/dialogmode", mode); + if (changed) { + _LPESelectorFlowBox->unset_sort_func(); + _LPESelectorFlowBox->set_sort_func(sigc::mem_fun(this, &LivePathEffectAdd::on_sort)); + std::vector<Gtk::FlowBoxChild *> selected = _LPESelectorFlowBox->get_selected_children(); + if (selected.size() == 1) { + _LPESelectorFlowBox->get_selected_children()[0]->grab_focus(); + } + } +} + +void LivePathEffectAdd::on_focus(Gtk::Widget *widget) +{ + Gtk::FlowBoxChild *child = dynamic_cast<Gtk::FlowBoxChild *>(widget); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0); + if (child && mode != 2) { + for (auto i : _LPESelectorFlowBox->get_children()) { + Gtk::FlowBoxChild *leitem = dynamic_cast<Gtk::FlowBoxChild *>(i); + Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(leitem->get_child()); + if (eventbox) { + Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child()); + if (box) { + std::vector<Gtk::Widget *> contents = box->get_children(); + Gtk::Box *actions = dynamic_cast<Gtk::Box *>(contents[5]); + if (actions) { + actions->set_visible(false); + } + Gtk::EventBox *expander = dynamic_cast<Gtk::EventBox *>(contents[4]); + if (expander) { + expander->set_visible(true); + } + } + } + } + Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(child->get_child()); + if (eventbox) { + Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child()); + if (box) { + std::vector<Gtk::Widget *> contents = box->get_children(); + Gtk::EventBox *expander = dynamic_cast<Gtk::EventBox *>(contents[4]); + if (expander) { + expander->set_visible(false); + } + } + } + + child->show_all_children(); + _LPESelectorFlowBox->select_child(*child); + } +} + +bool LivePathEffectAdd::pop_description(GdkEventCrossing *evt, Glib::RefPtr<Gtk::Builder> builder_effect) +{ + Gtk::Image *LPESelectorEffectInfo; + builder_effect->get_widget("LPESelectorEffectInfo", LPESelectorEffectInfo); + _LPESelectorEffectInfoPop->set_relative_to(*LPESelectorEffectInfo); + + Gtk::Label *LPEName; + builder_effect->get_widget("LPEName", LPEName); + Gtk::Label *LPEDescription; + builder_effect->get_widget("LPEDescription", LPEDescription); + Gtk::Image *LPEIcon; + builder_effect->get_widget("LPEIcon", LPEIcon); + + Gtk::Image *LPESelectorEffectInfoIcon; + _builder->get_widget("LPESelectorEffectInfoIcon", LPESelectorEffectInfoIcon); + LPESelectorEffectInfoIcon->set_from_icon_name(LPEIcon->get_icon_name(), Gtk::IconSize(Gtk::ICON_SIZE_DIALOG)); + + Gtk::Label *LPESelectorEffectInfoName; + _builder->get_widget("LPESelectorEffectInfoName", LPESelectorEffectInfoName); + LPESelectorEffectInfoName->set_text(LPEName->get_text()); + + Gtk::Label *LPESelectorEffectInfoDescription; + _builder->get_widget("LPESelectorEffectInfoDescription", LPESelectorEffectInfoDescription); + LPESelectorEffectInfoDescription->set_text(LPEDescription->get_text()); + + _LPESelectorEffectInfoPop->show(); + + return true; +} + +bool LivePathEffectAdd::hide_pop_description(GdkEventCrossing *evt) +{ + _LPESelectorEffectInfoPop->hide(); + return true; +} + +bool LivePathEffectAdd::fav_toggler(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect) +{ + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + Gtk::Label *LPEName; + builder_effect->get_widget("LPEName", LPEName); + Gtk::Image *LPESelectorEffectFav; + builder_effect->get_widget("LPESelectorEffectFav", LPESelectorEffectFav); + Gtk::Image *LPESelectorEffectFavTop; + builder_effect->get_widget("LPESelectorEffectFavTop", LPESelectorEffectFavTop); + Gtk::EventBox *LPESelectorEffectEventFavTop; + builder_effect->get_widget("LPESelectorEffectEventFavTop", LPESelectorEffectEventFavTop); + if (LPESelectorEffectFav && LPESelectorEffectEventFavTop) { + if (sp_has_fav(LPEName->get_text())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0); + if (mode == 2) { + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + } else { + LPESelectorEffectEventFavTop->set_visible(false); + LPESelectorEffectEventFavTop->hide(); + } + LPESelectorEffectFavTop->set_from_icon_name("draw-star-outline", + Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectFav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + sp_remove_fav(LPEName->get_text()); + LPESelectorEffect->get_parent()->get_style_context()->remove_class("lpefav"); + LPESelectorEffect->get_parent()->get_style_context()->add_class("lpenormal"); + LPESelectorEffect->get_parent()->get_style_context()->add_class("lpe"); + if (_showfavs) { + reload_effect_list(); + } + } else { + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + LPESelectorEffectFavTop->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectFav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + sp_add_fav(LPEName->get_text()); + LPESelectorEffect->get_parent()->get_style_context()->add_class("lpefav"); + LPESelectorEffect->get_parent()->get_style_context()->remove_class("lpenormal"); + LPESelectorEffect->get_parent()->get_style_context()->add_class("lpe"); + } + } + return true; +} + +bool LivePathEffectAdd::show_fav_toggler(GdkEventButton *evt) +{ + _showfavs = !_showfavs; + Gtk::Image *favimage = dynamic_cast<Gtk::Image *>(_LPESelectorEffectEventFavShow->get_child()); + if (favimage) { + if (_showfavs) { + favimage->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } else { + favimage->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } + } + reload_effect_list(); + return true; +} + +bool LivePathEffectAdd::apply(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect, + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add) +{ + _to_add = to_add; + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + Gtk::FlowBoxChild *flowboxchild = dynamic_cast<Gtk::FlowBoxChild *>(LPESelectorEffect->get_parent()); + _LPESelectorFlowBox->select_child(*flowboxchild); + if (flowboxchild && flowboxchild->get_style_context()->has_class("lpedisabled")) { + return true; + } + _applied = true; + _lasteffect = flowboxchild; + _LPEDialogSelector->response(Gtk::RESPONSE_APPLY); + _LPEDialogSelector->hide(); + return true; +} + +bool LivePathEffectAdd::on_press_enter(GdkEventKey *key, Glib::RefPtr<Gtk::Builder> builder_effect, + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add) +{ + if (key->keyval == 65293 || key->keyval == 65421) { + _to_add = to_add; + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + Gtk::FlowBoxChild *flowboxchild = dynamic_cast<Gtk::FlowBoxChild *>(LPESelectorEffect->get_parent()); + if (flowboxchild && flowboxchild->get_style_context()->has_class("lpedisabled")) { + return true; + } + _applied = true; + _lasteffect = flowboxchild; + _LPEDialogSelector->response(Gtk::RESPONSE_APPLY); + _LPEDialogSelector->hide(); + return true; + } + return false; +} + +bool LivePathEffectAdd::expand(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect) +{ + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + Gtk::FlowBoxChild *child = dynamic_cast<Gtk::FlowBoxChild *>(LPESelectorEffect->get_parent()); + if (child) { + child->grab_focus(); + } + return true; +} + + + +bool LivePathEffectAdd::on_filter(Gtk::FlowBoxChild *child) +{ + std::vector<Glib::ustring> classes = child->get_style_context()->list_classes(); + int pos = 0; + for (auto childclass : classes) { + size_t s = childclass.find("LPEIndex", 0); + if (s != -1) { + childclass = childclass.erase(0, 8); + pos = std::stoi(childclass.raw()); + } + } + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data = &converter.data(pos); + bool disable = false; + if (_item_type == "group" && !converter.get_on_group(data->id)) { + disable = true; + } else if (_item_type == "shape" && !converter.get_on_shape(data->id)) { + disable = true; + } else if (_item_type == "path" && !converter.get_on_path(data->id)) { + disable = true; + } + + if (!_has_clip && data->id == Inkscape::LivePathEffect::POWERCLIP) { + disable = true; + } + if (!_has_mask && data->id == Inkscape::LivePathEffect::POWERMASK) { + disable = true; + } + + if (disable) { + child->get_style_context()->add_class("lpedisabled"); + } else { + child->get_style_context()->remove_class("lpedisabled"); + } + child->set_valign(Gtk::ALIGN_START); + Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(child->get_child()); + if (eventbox) { + Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child()); + if (box) { + std::vector<Gtk::Widget *> contents = box->get_children(); + Gtk::Overlay *overlay = dynamic_cast<Gtk::Overlay *>(contents[0]); + std::vector<Gtk::Widget *> content_overlay = overlay->get_children(); + + Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(contents[1]); + if (!sp_has_fav(lpename->get_text()) && _showfavs) { + return false; + } + Gtk::ToggleButton *experimental = dynamic_cast<Gtk::ToggleButton *>(contents[3]); + if (experimental) { + if (experimental->get_active() && !_LPEExperimental->get_active()) { + return false; + } + } + Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(contents[2]); + if (lpedesc) { + size_t s = lpedesc->get_text().uppercase().find(_LPEFilter->get_text().uppercase(), 0); + if (s != -1) { + _visiblelpe++; + return true; + } + } + if (_LPEFilter->get_text().length() < 1) { + _visiblelpe++; + return true; + } + if (lpename) { + size_t s = lpename->get_text().uppercase().find(_LPEFilter->get_text().uppercase(), 0); + if (s != -1) { + _visiblelpe++; + return true; + } + } + } + } + return false; +} + +void LivePathEffectAdd::reload_effect_list() +{ + /* if(_LPEExperimental->get_active()) { + _LPEExperimental->get_style_context()->add_class("active"); + } else { + _LPEExperimental->get_style_context()->remove_class("active"); + } */ + _visiblelpe = 0; + _LPESelectorFlowBox->invalidate_filter(); + if (_showfavs) { + if (_visiblelpe == 0) { + _LPEInfo->set_text(_("You don't have any favorites yet. Click on the favorites star again to see all LPEs.")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } else { + _LPEInfo->set_text(_("These are your favorite effects")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } + } else { + _LPEInfo->set_text(_("Nothing found! Please try again with different search terms.")); + _LPEInfo->set_visible(false); + _LPEInfo->get_style_context()->remove_class("lpeinfowarn"); + } +} + +void LivePathEffectAdd::on_search() +{ + _visiblelpe = 0; + _LPESelectorFlowBox->invalidate_filter(); + if (_showfavs) { + if (_visiblelpe == 0) { + _LPEInfo->set_text(_("Nothing found! Please try again with different search terms.")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } else { + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } + } else { + if (_visiblelpe == 0) { + _LPEInfo->set_text(_("Nothing found! Please try again with different search terms.")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } else { + _LPEInfo->set_visible(false); + _LPEInfo->get_style_context()->remove_class("lpeinfowarn"); + } + } +} + +int LivePathEffectAdd::on_sort(Gtk::FlowBoxChild *child1, Gtk::FlowBoxChild *child2) +{ + Glib::ustring name1 = ""; + Glib::ustring name2 = ""; + Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(child1->get_child()); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0); + if (mode == 2) { + eventbox->set_halign(Gtk::ALIGN_START); + } else { + eventbox->set_halign(Gtk::ALIGN_CENTER); + } + if (eventbox) { + Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child()); + if (mode == 2) { + box->set_orientation(Gtk::ORIENTATION_HORIZONTAL); + } else { + box->set_orientation(Gtk::ORIENTATION_VERTICAL); + } + if (box) { + std::vector<Gtk::Widget *> contents = box->get_children(); + Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(contents[1]); + name1 = lpename->get_text(); + if (lpename) { + if (mode == 2) { + lpename->set_justify(Gtk::JUSTIFY_LEFT); + lpename->set_halign(Gtk::ALIGN_START); + lpename->set_valign(Gtk::ALIGN_CENTER); + lpename->set_width_chars(-1); + lpename->set_max_width_chars(-1); + } else { + lpename->set_justify(Gtk::JUSTIFY_CENTER); + lpename->set_halign(Gtk::ALIGN_CENTER); + lpename->set_valign(Gtk::ALIGN_CENTER); + lpename->set_width_chars(14); + lpename->set_max_width_chars(23); + } + } + Gtk::EventBox *lpemore = dynamic_cast<Gtk::EventBox *>(contents[4]); + if (lpemore) { + if (mode == 2) { + lpemore->hide(); + } else { + if (child1->is_selected()) { + lpemore->hide(); + } else { + lpemore->show(); + } + } + } + Gtk::ButtonBox *lpebuttonbox = dynamic_cast<Gtk::ButtonBox *>(contents[5]); + if (lpebuttonbox) { + if (mode == 2) { + lpebuttonbox->hide(); + } else { + if (child1->is_selected()) { + lpebuttonbox->show(); + } else { + lpebuttonbox->hide(); + } + } + } + Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(contents[2]); + if (lpedesc) { + if (mode == 2) { + lpedesc->show(); + lpedesc->set_justify(Gtk::JUSTIFY_LEFT); + lpedesc->set_halign(Gtk::ALIGN_START); + lpedesc->set_valign(Gtk::ALIGN_CENTER); + lpedesc->set_ellipsize(Pango::ELLIPSIZE_END); + } else { + lpedesc->hide(); + lpedesc->set_justify(Gtk::JUSTIFY_CENTER); + lpedesc->set_halign(Gtk::ALIGN_CENTER); + lpedesc->set_valign(Gtk::ALIGN_CENTER); + lpedesc->set_ellipsize(Pango::ELLIPSIZE_NONE); + } + } + Gtk::Overlay *overlay = dynamic_cast<Gtk::Overlay *>(contents[0]); + if (overlay) { + std::vector<Gtk::Widget *> contents_overlay = overlay->get_children(); + Gtk::Image *icon = dynamic_cast<Gtk::Image *>(contents_overlay[0]); + if (icon) { + if (mode == 2) { + icon->set_pixel_size(40); + icon->set_margin_end(25); + overlay->set_margin_end(5); + } else { + icon->set_pixel_size(60); + icon->set_margin_end(0); + overlay->set_margin_end(0); + } + } + Gtk::EventBox *LPESelectorEffectEventFavTop = dynamic_cast<Gtk::EventBox *>(contents_overlay[1]); + if (LPESelectorEffectEventFavTop) { + Gtk::Image *fav = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFavTop->get_child()); + if (sp_has_fav(name1)) { + fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + child1->get_style_context()->add_class("lpefav"); + child1->get_style_context()->remove_class("lpenormal"); + } else if (!sp_has_fav(name1)) { + fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectEventFavTop->set_visible(false); + LPESelectorEffectEventFavTop->hide(); + child1->get_style_context()->remove_class("lpefav"); + child1->get_style_context()->add_class("lpenormal"); + } + if (mode == 2) { + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END); + LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_CENTER); + } else { + LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END); + LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_START); + } + child1->get_style_context()->add_class("lpe"); + } + } + } + } + eventbox = dynamic_cast<Gtk::EventBox *>(child2->get_child()); + if (mode == 2) { + eventbox->set_halign(Gtk::ALIGN_START); + } else { + eventbox->set_halign(Gtk::ALIGN_CENTER); + } + if (eventbox) { + Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child()); + if (mode == 2) { + box->set_orientation(Gtk::ORIENTATION_HORIZONTAL); + } else { + box->set_orientation(Gtk::ORIENTATION_VERTICAL); + } + if (box) { + std::vector<Gtk::Widget *> contents = box->get_children(); + Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(contents[1]); + name2 = lpename->get_text(); + if (lpename) { + if (mode == 2) { + lpename->set_justify(Gtk::JUSTIFY_LEFT); + lpename->set_halign(Gtk::ALIGN_START); + lpename->set_valign(Gtk::ALIGN_CENTER); + lpename->set_width_chars(-1); + lpename->set_max_width_chars(-1); + } else { + lpename->set_justify(Gtk::JUSTIFY_CENTER); + lpename->set_halign(Gtk::ALIGN_CENTER); + lpename->set_valign(Gtk::ALIGN_CENTER); + lpename->set_width_chars(14); + lpename->set_max_width_chars(23); + } + } + Gtk::EventBox *lpemore = dynamic_cast<Gtk::EventBox *>(contents[4]); + if (lpemore) { + if (mode == 2) { + lpemore->hide(); + } else { + if (child2->is_selected()) { + lpemore->hide(); + } else { + lpemore->show(); + } + } + } + Gtk::ButtonBox *lpebuttonbox = dynamic_cast<Gtk::ButtonBox *>(contents[5]); + if (lpebuttonbox) { + if (mode == 2) { + lpebuttonbox->hide(); + } else { + if (child2->is_selected()) { + lpebuttonbox->show(); + } else { + lpebuttonbox->hide(); + } + } + } + Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(contents[2]); + if (lpedesc) { + if (mode == 2) { + lpedesc->show(); + lpedesc->set_justify(Gtk::JUSTIFY_LEFT); + lpedesc->set_halign(Gtk::ALIGN_START); + lpedesc->set_valign(Gtk::ALIGN_CENTER); + lpedesc->set_ellipsize(Pango::ELLIPSIZE_END); + } else { + lpedesc->hide(); + lpedesc->set_justify(Gtk::JUSTIFY_CENTER); + lpedesc->set_halign(Gtk::ALIGN_CENTER); + lpedesc->set_valign(Gtk::ALIGN_CENTER); + lpedesc->set_ellipsize(Pango::ELLIPSIZE_NONE); + } + } + Gtk::Overlay *overlay = dynamic_cast<Gtk::Overlay *>(contents[0]); + if (overlay) { + std::vector<Gtk::Widget *> contents_overlay = overlay->get_children(); + Gtk::Image *icon = dynamic_cast<Gtk::Image *>(contents_overlay[0]); + if (icon) { + if (mode == 2) { + icon->set_pixel_size(33); + icon->set_margin_end(40); + } else { + icon->set_pixel_size(60); + icon->set_margin_end(0); + } + } + Gtk::EventBox *LPESelectorEffectEventFavTop = dynamic_cast<Gtk::EventBox *>(contents_overlay[1]); + Gtk::Image *fav = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFavTop->get_child()); + if (LPESelectorEffectEventFavTop) { + if (sp_has_fav(name2)) { + fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + child2->get_style_context()->add_class("lpefav"); + child2->get_style_context()->remove_class("lpenormal"); + } else if (!sp_has_fav(name2)) { + fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectEventFavTop->set_visible(false); + LPESelectorEffectEventFavTop->hide(); + child2->get_style_context()->remove_class("lpefav"); + child2->get_style_context()->add_class("lpenormal"); + } + if (mode == 2) { + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END); + LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_CENTER); + } else { + LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END); + LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_START); + } + child2->get_style_context()->add_class("lpe"); + } + } + } + } + std::vector<Glib::ustring> effect; + effect.push_back(name1); + effect.push_back(name2); + sort(effect.begin(), effect.end()); + /* if (sp_has_fav(name1) && sp_has_fav(name2)) { + return effect[0] == name1?-1:1; + } + if (sp_has_fav(name1)) { + return -1; + } */ + if (effect[0] == name1) { //&& !sp_has_fav(name2)) { + return -1; + } + return 1; +} + + +void LivePathEffectAdd::onClose() { _LPEDialogSelector->hide(); } + +void LivePathEffectAdd::onKeyEvent(GdkEventKey *evt) +{ + if (evt->keyval == GDK_KEY_Escape) { + onClose(); + } +} + +void LivePathEffectAdd::show(SPDesktop *desktop) +{ + LivePathEffectAdd &dial = instance(); + Inkscape::Selection *sel = desktop->getSelection(); + if (sel && !sel->isEmpty()) { + SPItem *item = sel->singleItem(); + if (item) { + SPShape *shape = dynamic_cast<SPShape *>(item); + SPPath *path = dynamic_cast<SPPath *>(item); + SPGroup *group = dynamic_cast<SPGroup *>(item); + dial._has_clip = (item->getClipObject() != nullptr); + dial._has_mask = (item->getMaskObject() != nullptr); + dial._item_type = ""; + if (group) { + dial._item_type = "group"; + } else if (path) { + dial._item_type = "path"; + } else if (shape) { + dial._item_type = "shape"; + } else { + dial._LPEDialogSelector->hide(); + return; + } + } + } + dial._applied = false; + dial._LPESelectorFlowBox->unset_sort_func(); + dial._LPESelectorFlowBox->unset_filter_func(); + dial._LPESelectorFlowBox->set_filter_func(sigc::mem_fun(dial, &LivePathEffectAdd::on_filter)); + dial._LPESelectorFlowBox->set_sort_func(sigc::mem_fun(dial, &LivePathEffectAdd::on_sort)); + Glib::RefPtr<Gtk::Adjustment> vadjust = dial._LPEScrolled->get_vadjustment(); + vadjust->set_value(vadjust->get_lower()); + Gtk::Window *window = desktop->getToplevel(); + dial._LPEDialogSelector->set_transient_for(*window); + dial._LPEDialogSelector->show(); + int searchlen = dial._LPEFilter->get_text().length(); + if (searchlen > 0) { + dial._LPEFilter->select_region (0, searchlen); + dial._LPESelectorFlowBox->unselect_all(); + } else if (dial._lasteffect) { + dial._lasteffect->grab_focus(); + } + dial._LPEDialogSelector->run(); + dial._LPEDialogSelector->hide(); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/livepatheffect-add.h b/src/ui/dialog/livepatheffect-add.h new file mode 100644 index 0000000..bd8dca4 --- /dev/null +++ b/src/ui/dialog/livepatheffect-add.h @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for adding a live path effect. + * + * Author: + * + * Copyright (C) 2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_LIVEPATHEFFECT_ADD_H +#define INKSCAPE_DIALOG_LIVEPATHEFFECT_ADD_H + +#include "live_effects/effect-enum.h" +#include <gtkmm/adjustment.h> +#include <gtkmm/box.h> +#include <gtkmm/builder.h> +#include <gtkmm/dialog.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/flowbox.h> +#include <gtkmm/flowboxchild.h> +#include <gtkmm/image.h> +#include <gtkmm/label.h> +#include <gtkmm/overlay.h> +#include <gtkmm/popover.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/searchentry.h> +#include <gtkmm/stylecontext.h> +#include <gtkmm/switch.h> +#include <gtkmm/viewport.h> + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A dialog widget to list the live path effects that can be added + * + */ +class LivePathEffectAdd { + public: + LivePathEffectAdd(); + ~LivePathEffectAdd() = default; + ; + + /** + * Show the dialog + */ + static void show(SPDesktop *desktop); + /** + * Returns true is the "Add" button was pressed + */ + static bool isApplied() { return instance()._applied; } + + static const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *getActiveData(); + + protected: + /** + * Close button was clicked + */ + void onClose(); + bool on_filter(Gtk::FlowBoxChild *child); + int on_sort(Gtk::FlowBoxChild *child1, Gtk::FlowBoxChild *child2); + void on_search(); + void on_focus(Gtk::Widget *widg); + bool pop_description(GdkEventCrossing *evt, Glib::RefPtr<Gtk::Builder> builder_effect); + bool hide_pop_description(GdkEventCrossing *evt); + bool fav_toggler(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect); + bool apply(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect, + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add); + bool on_press_enter(GdkEventKey *key, Glib::RefPtr<Gtk::Builder> builder_effect, + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add); + bool expand(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect); + bool show_fav_toggler(GdkEventButton *evt); + void viewChanged(gint mode); + bool mouseover(GdkEventCrossing *evt, GtkWidget *wdg); + bool mouseout(GdkEventCrossing *evt, GtkWidget *wdg); + void reload_effect_list(); + /** + * Add button was clicked + */ + void onAdd(); + /** + * Tree was clicked + */ + void onButtonEvent(GdkEventButton* evt); + + /** + * Key event + */ + void onKeyEvent(GdkEventKey* evt); +private: + Gtk::Button _add_button; + Gtk::Button _close_button; + Gtk::Dialog *_LPEDialogSelector; + Glib::RefPtr<Gtk::Builder> _builder; + Gtk::FlowBox *_LPESelectorFlowBox; + Gtk::Popover *_LPESelectorEffectInfoPop; + Gtk::EventBox *_LPESelectorEffectEventFavShow; + Gtk::EventBox *_LPESelectorEffectInfoEventBox; + Gtk::RadioButton *_LPESelectorEffectRadioList; + Gtk::RadioButton *_LPESelectorEffectRadioPackLess; + Gtk::RadioButton *_LPESelectorEffectRadioPackMore; + Gtk::Switch *_LPEExperimental; + Gtk::SearchEntry *_LPEFilter; + Gtk::ScrolledWindow *_LPEScrolled; + Gtk::Label *_LPEInfo; + Gtk::Box *_LPESelector; + guint _visiblelpe; + Glib::ustring _item_type; + bool _has_clip; + bool _has_mask; + Gtk::FlowBoxChild *_lasteffect; + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *_to_add; + bool _showfavs; + bool _applied; + class Effect; + const LivePathEffect::EnumEffectDataConverter<LivePathEffect::EffectType> &converter; + static LivePathEffectAdd &instance() + { + static LivePathEffectAdd instance_; + return instance_; + } + LivePathEffectAdd(LivePathEffectAdd const &) = delete; // no copy + LivePathEffectAdd &operator=(LivePathEffectAdd const &) = delete; // no assign +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_DIALOG_LIVEPATHEFFECT_ADD_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/ui/dialog/livepatheffect-editor.cpp b/src/ui/dialog/livepatheffect-editor.cpp new file mode 100644 index 0000000..9705dc8 --- /dev/null +++ b/src/ui/dialog/livepatheffect-editor.cpp @@ -0,0 +1,639 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Live Path Effect editing dialog - implementation. + */ +/* Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * Steren Giannini <steren.giannini@gmail.com> + * Bastien Bouclet <bgkweb@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2007 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "livepatheffect-editor.h" + +#include <gtkmm/expander.h> + +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "livepatheffect-add.h" +#include "path-chemistry.h" +#include "selection-chemistry.h" +#include "svg/svg.h" +#include "ui/icon-loader.h" + +#include "live_effects/effect.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpeobject.h" + +#include "object/sp-item-group.h" +#include "object/sp-path.h" +#include "object/sp-use.h" +#include "object/sp-symbol.h" +#include "object/sp-text.h" + +#include "ui/icon-names.h" +#include "ui/tools/node-tool.h" +#include "ui/widget/imagetoggler.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/*#################### + * Callback functions + */ + + +void LivePathEffectEditor::selectionChanged(Inkscape::Selection * selection) +{ + selection_changed_lock = true; + lpe_list_locked = false; + onSelectionChanged(selection); + _on_button_release(nullptr); //to force update widgets + selection_changed_lock = false; +} + +void LivePathEffectEditor::selectionModified(Inkscape::Selection * selection, guint flags) +{ + lpe_list_locked = false; + onSelectionChanged(selection); +} + +static void lpe_style_button(Gtk::Button& btn, char const* iconName) +{ + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show( child ); + btn.add(*Gtk::manage(Glib::wrap(child))); + btn.set_relief(Gtk::RELIEF_NONE); +} + + +/* + * LivePathEffectEditor + * + * TRANSLATORS: this dialog is accessible via menu Path - Path Effect Editor... + * + */ + +LivePathEffectEditor::LivePathEffectEditor() + : DialogBase("/dialogs/livepatheffect", "LivePathEffect") + , lpe_list_locked(false) + , effectwidget(nullptr) + , status_label("", Gtk::ALIGN_CENTER) + , effectcontrol_frame("") + , button_add() + , button_remove() + , button_original() + , button_up() + , button_down() + , current_lpeitem(nullptr) + , current_lperef(nullptr) + , effectcontrol_vbox(Gtk::ORIENTATION_VERTICAL) + , effectlist_vbox(Gtk::ORIENTATION_VERTICAL) + , effectapplication_hbox(Gtk::ORIENTATION_HORIZONTAL, 4) +{ + set_spacing(4); + + //Add the TreeView, inside a ScrolledWindow, with the button underneath: + scrolled_window.add(effectlist_view); + scrolled_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + scrolled_window.set_shadow_type(Gtk::SHADOW_IN); + scrolled_window.set_size_request(210, 70); + fix_inner_scroll(&scrolled_window); + + effectcontrol_vbox.set_spacing(4); + + effectlist_vbox.pack_start(scrolled_window, Gtk::PACK_EXPAND_WIDGET); + effectlist_vbox.pack_end(toolbar_hbox, Gtk::PACK_SHRINK); + effectcontrol_eventbox.add_events(Gdk::BUTTON_RELEASE_MASK); + effectcontrol_eventbox.signal_button_release_event().connect(sigc::mem_fun(*this, &LivePathEffectEditor::_on_button_release) ); + effectcontrol_eventbox.add(effectcontrol_vbox); + effectcontrol_frame.add(effectcontrol_eventbox); + + button_add.set_tooltip_text(_("Add path effect")); + lpe_style_button(button_add, INKSCAPE_ICON("list-add")); + button_add.set_relief(Gtk::RELIEF_NONE); + + button_remove.set_tooltip_text(_("Delete current path effect")); + lpe_style_button(button_remove, INKSCAPE_ICON("list-remove")); + button_remove.set_relief(Gtk::RELIEF_NONE); + + button_original.set_tooltip_text(_("Select origin item")); + lpe_style_button(button_original, INKSCAPE_ICON("clone-original")); + button_original.set_relief(Gtk::RELIEF_NONE); + + button_up.set_tooltip_text(_("Raise the current path effect")); + lpe_style_button(button_up, INKSCAPE_ICON("go-up")); + button_up.set_relief(Gtk::RELIEF_NONE); + + button_down.set_tooltip_text(_("Lower the current path effect")); + lpe_style_button(button_down, INKSCAPE_ICON("go-down")); + button_down.set_relief(Gtk::RELIEF_NONE); + + // Add toolbar items to toolbar + toolbar_hbox.set_layout (Gtk::BUTTONBOX_END); + toolbar_hbox.add( button_add ); + toolbar_hbox.set_child_secondary( button_add , true); + toolbar_hbox.add( button_remove ); + toolbar_hbox.set_child_secondary( button_remove , true); + toolbar_hbox.add(button_original); + toolbar_hbox.add( button_up ); + toolbar_hbox.add( button_down ); + toolbar_hbox.set_child_non_homogeneous (button_add,true); + toolbar_hbox.set_child_non_homogeneous (button_remove,true); + toolbar_hbox.set_child_non_homogeneous (button_up,true); + toolbar_hbox.set_child_non_homogeneous (button_down,true); + toolbar_hbox.set_child_non_homogeneous(button_original, true); + + //Create the Tree model: + effectlist_store = Gtk::ListStore::create(columns); + effectlist_view.set_model(effectlist_store); + effectlist_view.set_headers_visible(false); + + // Handle tree selections + effectlist_selection = effectlist_view.get_selection(); + effectlist_selection->signal_changed().connect( sigc::mem_fun(*this, &LivePathEffectEditor::on_effect_selection_changed) ); + + //Add the visibility icon column: + Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) ); + int visibleColNum = effectlist_view.append_column("is_visible", *eyeRenderer) - 1; + eyeRenderer->signal_toggled().connect( sigc::mem_fun(*this, &LivePathEffectEditor::on_visibility_toggled) ); + eyeRenderer->property_activatable() = true; + Gtk::TreeViewColumn* col = effectlist_view.get_column(visibleColNum); + if ( col ) { + col->add_attribute( eyeRenderer->property_active(), columns.col_visible ); + } + + //Add the effect name column: + effectlist_view.append_column("Effect", columns.col_name); + + pack_start(effectlist_vbox, true, true); + pack_start(status_label, false, false); + pack_start(effectcontrol_frame, false, false); + + effectcontrol_frame.hide(); + selection_changed_lock = false; + // connect callback functions to buttons + button_add.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onAdd)); + button_remove.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onRemove)); + button_original.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onOriginal)); + button_up.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onUp)); + button_down.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onDown)); + + show_all_children(); +} + +LivePathEffectEditor::~LivePathEffectEditor() +{ + if (effectwidget) { + effectcontrol_vbox.remove(*effectwidget); + delete effectwidget; + effectwidget = nullptr; + } +} + +bool LivePathEffectEditor::_on_button_release(GdkEventButton* button_event) { + Glib::RefPtr<Gtk::TreeSelection> sel = effectlist_view.get_selection(); + if (sel->count_selected_rows () == 0) { + return true; + } + Gtk::TreeModel::iterator it = sel->get_selected(); + std::shared_ptr<LivePathEffect::LPEObjectReference> lperef = (*it)[columns.lperef]; + if (lperef && current_lpeitem && current_lperef != lperef) { + if (lperef->getObject()) { + LivePathEffect::Effect * effect = lperef->lpeobject->get_lpe(); + if (effect) { + effect->refresh_widgets = true; + showParams(*effect); + } + } + } + return true; +} + +void +LivePathEffectEditor::showParams(LivePathEffect::Effect& effect) +{ + if (effectwidget && !effect.refresh_widgets) { + return; + } + if (effectwidget) { + effectcontrol_vbox.remove(*effectwidget); + delete effectwidget; + effectwidget = nullptr; + } + effectwidget = effect.newWidget(); + effectcontrol_frame.set_label(effect.getName()); + effectcontrol_vbox.pack_start(*effectwidget, true, true); + + button_remove.show(); + status_label.hide(); + effectcontrol_frame.show(); + effectcontrol_vbox.show_all_children(); + // fixme: add resizing of dialog + effect.refresh_widgets = false; +} + +void +LivePathEffectEditor::selectInList(LivePathEffect::Effect* effect) +{ + Gtk::TreeNodeChildren chi = effectlist_view.get_model()->children(); + for (Gtk::TreeIter ci = chi.begin() ; ci != chi.end(); ci++) { + if (ci->get_value(columns.lperef)->lpeobject->get_lpe() == effect && effectlist_view.get_selection()) { + effectlist_view.get_selection()->select(ci); + break; + } + } +} + + +void +LivePathEffectEditor::showText(Glib::ustring const &str) +{ + if (effectwidget) { + effectcontrol_vbox.remove(*effectwidget); + delete effectwidget; + effectwidget = nullptr; + } + + status_label.show(); + status_label.set_label(str); + + effectcontrol_frame.hide(); + + // fixme: do resizing of dialog ? +} + +void +LivePathEffectEditor::set_sensitize_all(bool sensitive) +{ + //combo_effecttype.set_sensitive(sensitive); + button_add.set_sensitive(sensitive); + button_remove.set_sensitive(sensitive); + effectlist_view.set_sensitive(sensitive); + button_up.set_sensitive(sensitive); + button_down.set_sensitive(sensitive); +} + +void +LivePathEffectEditor::onSelectionChanged(Inkscape::Selection *sel) +{ + if (lpe_list_locked) { + // this was triggered by selecting a row in the list, so skip reloading + lpe_list_locked = false; + return; + } + current_lpeitem = nullptr; + effectlist_store->clear(); + button_original.set_sensitive(false); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + if ( item ) { + button_original.set_sensitive(true); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if ( lpeitem ) { + effect_list_reload(lpeitem); + current_lpeitem = lpeitem; + set_sensitize_all(true); + if ( lpeitem->hasPathEffect() ) { + Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE(); + if (lpe) { + showParams(*lpe); + lpe_list_locked = true; + selectInList(lpe); + } else { + showText(_("Unknown effect is applied")); + } + } else { + showText(_("Click button to add an effect")); + button_remove.set_sensitive(false); + button_up.set_sensitive(false); + button_down.set_sensitive(false); + } + } else { + SPUse *use = dynamic_cast<SPUse *>(item); + if (use) { + // test whether linked object is supported by the CLONE_ORIGINAL LPE + SPItem *root = use->root(); + SPItem *orig = use->get_original(); + if (dynamic_cast<SPSymbol *>(root)) { + showText(_("Path effect cannot be applied to symbols")); + set_sensitize_all(false); + } else if (dynamic_cast<SPShape *>(orig) || dynamic_cast<SPGroup *>(orig) || + dynamic_cast<SPText *>(orig)) { + // Note that an SP_USE cannot have an LPE applied, so we only need to worry about the "add + // effect" case. + set_sensitize_all(true); + showText(_("Click add button to convert clone")); + button_remove.set_sensitive(false); + button_up.set_sensitive(false); + button_down.set_sensitive(false); + } else { + showText(_("Select a path or shape")); + set_sensitize_all(false); + } + } else { + showText(_("Select a path or shape")); + set_sensitize_all(false); + } + } + } else { + showText(_("Only one item can be selected")); + set_sensitize_all(false); + } + } else { + showText(_("Select a path or shape")); + set_sensitize_all(false); + } +} + +/* + * First clears the effectlist_store, then appends all effects from the effectlist. + */ +void +LivePathEffectEditor::effect_list_reload(SPLPEItem *lpeitem) +{ + effectlist_store->clear(); + + PathEffectList effectlist = lpeitem->getEffectList(); + PathEffectList::iterator it; + for( it = effectlist.begin() ; it!=effectlist.end(); ++it) + { + if ( !(*it)->lpeobject ) { + continue; + } + + if ((*it)->lpeobject->get_lpe()) { + Gtk::TreeModel::Row row = *(effectlist_store->append()); + row[columns.col_name] = (*it)->lpeobject->get_lpe()->getName(); + row[columns.lperef] = *it; + row[columns.col_visible] = (*it)->lpeobject->get_lpe()->isVisible(); + } else { + Gtk::TreeModel::Row row = *(effectlist_store->append()); + row[columns.col_name] = _("Unknown effect"); + row[columns.lperef] = *it; + row[columns.col_visible] = false; + } + } +} + +/*######################################################################## +# BUTTON CLICK HANDLERS (callbacks) +########################################################################*/ + +// TODO: factor out the effect applying code which can be called from anywhere. (selection-chemistry.cpp also needs it) +void LivePathEffectEditor::onAdd() +{ + auto selection = getSelection(); + if (selection && !selection->isEmpty() ) { + SPItem *item = selection->singleItem(); + if (item) { + if ( dynamic_cast<SPLPEItem *>(item) ) { + // show effectlist dialog + using Inkscape::UI::Dialog::LivePathEffectAdd; + LivePathEffectAdd::show(getDesktop()); + if ( !LivePathEffectAdd::isApplied()) { + return; + } + + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data = + LivePathEffectAdd::getActiveData(); + if (!data) { + return; + } + item = selection->singleItem(); // get new item + + LivePathEffect::Effect::createAndApply(data->key.c_str(), getDocument(), item); + DocumentUndo::done(getDocument(), _("Create and apply path effect"), INKSCAPE_ICON("dialog-path-effects")); + + lpe_list_locked = false; + onSelectionChanged(selection); + } else { + SPUse *use = dynamic_cast<SPUse *>(item); + if ( use ) { + // item is a clone. do not show effectlist dialog. + // convert to path, apply CLONE_ORIGINAL LPE, link it to the cloned path + + // test whether linked object is supported by the CLONE_ORIGINAL LPE + SPItem *orig = use->get_original(); + if ( dynamic_cast<SPShape *>(orig) || + dynamic_cast<SPGroup *>(orig) || + dynamic_cast<SPText *>(orig) ) + { + // select original + selection->set(orig); + + // delete clone but remember its id and transform + gchar *id = g_strdup(item->getAttribute("id")); + gchar *transform = g_strdup(item->getAttribute("transform")); + item->deleteObject(false); + item = nullptr; + + // run sp_selection_clone_original_path_lpe + selection->cloneOriginalPathLPE(true); + + SPItem *new_item = selection->singleItem(); + // Check that the cloning was successful. We don't want to change the ID of the original referenced path! + if (new_item && (new_item != orig)) { + new_item->setAttribute("id", id); + if (transform) { + Geom::Affine item_t(Geom::identity()); + sp_svg_transform_read(transform, &item_t); + new_item->transform *= item_t; + new_item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + new_item->setAttribute("class", "fromclone"); + } + g_free(id); + g_free(transform); + + /// \todo Add the LPE stack of the original path? + + DocumentUndo::done(getDocument(), _("Create and apply Clone original path effect"), INKSCAPE_ICON("dialog-path-effects")); + + lpe_list_locked = false; + onSelectionChanged(selection); + } + } + } + } + } +} + +void +LivePathEffectEditor::onRemove() +{ + auto selection = getSelection(); + if (selection && !selection->isEmpty() ) { + SPItem *item = selection->singleItem(); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem) { + sp_lpe_item_update_patheffect(lpeitem, false, false); + lpeitem->removeCurrentPathEffect(false); + current_lperef = nullptr; + DocumentUndo::done(getDocument(), _("Remove path effect"), INKSCAPE_ICON("dialog-path-effects")); + lpe_list_locked = false; + onSelectionChanged(selection); + } + } + +} + +gboolean removeselectclass(gpointer data) +{ + SPItem *item = reinterpret_cast<SPItem *>(data); + const gchar *classitem = item->getAttribute("class"); + if (classitem) { + Glib::ustring classtoparent = classitem; + classtoparent.erase(classtoparent.find("lpeselectparent "), 16); + if (classtoparent.empty()) { + item->setAttribute("class", nullptr); + } else { + item->setAttribute("class", classtoparent.c_str()); + } + } + return FALSE; +} + +void LivePathEffectEditor::onOriginal() +{ + auto selection = getSelection(); + if (selection && !selection->isEmpty()) { + if (SPItem *item = selection->singleItem()) { + const gchar *classtoparentchar = item->getAttribute("class"); + Glib::ustring classtoparent = "lpeselectparent "; + if (classtoparentchar) { + classtoparent += classtoparentchar; + } + // here we fire a update and the lpe original check for this class and select + item->setAttribute("class", classtoparent.c_str()); + selection->set(item); + g_timeout_add(100, &removeselectclass, item); + } + } +} + +void LivePathEffectEditor::onUp() +{ + auto selection = getSelection(); + if (selection && !selection->isEmpty() ) { + SPItem *item = selection->singleItem(); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem) { + Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE(); + lpeitem->upCurrentPathEffect(); + DocumentUndo::done(getDocument(), _("Move path effect up"), INKSCAPE_ICON("dialog-path-effects")); + effect_list_reload(lpeitem); + if (lpe) { + showParams(*lpe); + lpe_list_locked = true; + selectInList(lpe); + } + } + } +} + +void LivePathEffectEditor::onDown() +{ + auto selection = getSelection(); + if (selection && !selection->isEmpty() ) { + SPItem *item = selection->singleItem(); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if ( lpeitem ) { + Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE(); + lpeitem->downCurrentPathEffect(); + DocumentUndo::done(getDocument(), _("Move path effect down"), INKSCAPE_ICON("dialog-path-effects")); + effect_list_reload(lpeitem); + if (lpe) { + showParams(*lpe); + lpe_list_locked = true; + selectInList(lpe); + } + } + } +} + +void LivePathEffectEditor::on_effect_selection_changed() +{ + Glib::RefPtr<Gtk::TreeSelection> sel = effectlist_view.get_selection(); + if (sel->count_selected_rows () == 0) { + button_remove.set_sensitive(false); + return; + } + button_remove.set_sensitive(true); + Gtk::TreeModel::iterator it = sel->get_selected(); + std::shared_ptr<LivePathEffect::LPEObjectReference> lperef = (*it)[columns.lperef]; + + if (lperef && current_lpeitem && current_lperef != lperef) { + // The last condition ignore Gtk::TreeModel may occasionally be changed emitted when nothing has happened + if (current_lpeitem->pathEffectsEnabled() && lperef->getObject()) { + lpe_list_locked = true; // prevent reload of the list which would lose selection + current_lpeitem->setCurrentPathEffect(lperef); + current_lperef = lperef; + LivePathEffect::Effect * effect = lperef->lpeobject->get_lpe(); + if (effect) { + effect->refresh_widgets = true; + showParams(*effect); + // To reload knots and helper paths + auto selection = getSelection(); + if (selection && !selection->isEmpty() && !selection_changed_lock) { + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(selection->singleItem()); + if (lpeitem) { + // this is need because set dont update selected LPE knots + selection->clear(); + selection->add(lpeitem); + Inkscape::UI::Tools::sp_update_helperpath(getDesktop()); + } + } + } + } + } +} + +void LivePathEffectEditor::on_visibility_toggled( Glib::ustring const& str ) +{ + + Gtk::TreeModel::Children::iterator iter = effectlist_view.get_model()->get_iter(str); + Gtk::TreeModel::Row row = *iter; + + std::shared_ptr<LivePathEffect::LPEObjectReference> lpeobjref = row[columns.lperef]; + + if ( lpeobjref && lpeobjref->lpeobject->get_lpe() ) { + bool newValue = !row[columns.col_visible]; + row[columns.col_visible] = newValue; + /* FIXME: this explicit writing to SVG is wrong. The lpe_item should have a method to disable/enable an effect within its stack. + * So one can call: lpe_item->setActive(lpeobjref->lpeobject); */ + lpeobjref->lpeobject->get_lpe()->getRepr()->setAttribute("is_visible", newValue ? "true" : "false"); + auto selection = getSelection(); + if (selection && !selection->isEmpty() ) { + SPItem *item = selection->singleItem(); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if ( lpeitem ) { + lpeobjref->lpeobject->get_lpe()->doOnVisibilityToggled(lpeitem); + } + } + DocumentUndo::done(getDocument(), newValue ? _("Activate path effect") : _("Deactivate path effect"), INKSCAPE_ICON("dialog-path-effects")); + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/livepatheffect-editor.h b/src/ui/dialog/livepatheffect-editor.h new file mode 100644 index 0000000..b9cf35f --- /dev/null +++ b/src/ui/dialog/livepatheffect-editor.h @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Live Path Effect editing dialog + */ +/* Author: + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * + * Copyright (C) 2007 Author + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_LIVE_PATH_EFFECT_H +#define INKSCAPE_UI_DIALOG_LIVE_PATH_EFFECT_H + +#include <gtkmm/buttonbox.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/frame.h> +#include <gtkmm/label.h> +#include <gtkmm/liststore.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/toolbar.h> +#include <gtkmm/treeview.h> + +#include "live_effects/effect-enum.h" +#include "object/sp-item.h" +#include "selection.h" +#include "ui/dialog/dialog-base.h" +#include "ui/widget/combo-enums.h" +#include "ui/widget/frame.h" + +class SPLPEItem; + +namespace Inkscape { + +namespace LivePathEffect { + class Effect; + class LPEObjectReference; +} + +namespace UI { +namespace Dialog { + +class LivePathEffectEditor : public DialogBase +{ +public: + LivePathEffectEditor(); + ~LivePathEffectEditor() override; + + static LivePathEffectEditor &getInstance() { return *new LivePathEffectEditor(); } + + void selectionChanged(Inkscape::Selection *selection) override; + void selectionModified(Inkscape::Selection *selection, guint flags) override; + + void onSelectionChanged(Inkscape::Selection *selection); + + virtual void on_effect_selection_changed(); + +private: + void effect_list_reload(SPLPEItem *lpeitem); + void set_sensitize_all(bool sensitive); + void showParams(LivePathEffect::Effect& effect); + void showText(Glib::ustring const &str); + void selectInList(LivePathEffect::Effect* effect); + + // callback methods for buttons on grids page. + void onAdd(); + void onRemove(); + void onUp(); + void onDown(); + void onOriginal(); + + class ModelColumns : public Gtk::TreeModel::ColumnRecord + { + public: + ModelColumns() + { + add(col_name); + add(lperef); + add(col_visible); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<Glib::ustring> col_name; + Gtk::TreeModelColumn<std::shared_ptr<LivePathEffect::LPEObjectReference>> lperef; + Gtk::TreeModelColumn<bool> col_visible; + }; + + bool lpe_list_locked; + bool selection_changed_lock; + //Inkscape::UI::Widget::ComboBoxEnum<LivePathEffect::EffectType> combo_effecttype; + + Gtk::Widget * effectwidget; + Gtk::Label status_label; + UI::Widget::Frame effectcontrol_frame; + Gtk::Box effectapplication_hbox; + Gtk::Box effectcontrol_vbox; + Gtk::EventBox effectcontrol_eventbox; + Gtk::Box effectlist_vbox; + ModelColumns columns; + Gtk::ScrolledWindow scrolled_window; + Gtk::TreeView effectlist_view; + Glib::RefPtr<Gtk::ListStore> effectlist_store; + Glib::RefPtr<Gtk::TreeSelection> effectlist_selection; + + void on_visibility_toggled( Glib::ustring const& str); + bool _on_button_release(GdkEventButton* button_event); + Gtk::ButtonBox toolbar_hbox; + Gtk::Button button_add; + Gtk::Button button_remove; + Gtk::Button button_original; + Gtk::Button button_up; + Gtk::Button button_down; + + SPLPEItem * current_lpeitem; + + std::shared_ptr<LivePathEffect::LPEObjectReference> current_lperef; + + friend void lpeeditor_selection_changed (Inkscape::Selection * selection, gpointer data); + friend void lpeeditor_selection_modified (Inkscape::Selection * selection, guint /*flags*/, gpointer data); + + LivePathEffectEditor(LivePathEffectEditor const &d) = delete; + LivePathEffectEditor& operator=(LivePathEffectEditor const &d) = delete; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_LIVE_PATH_EFFECT_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/ui/dialog/lpe-fillet-chamfer-properties.cpp b/src/ui/dialog/lpe-fillet-chamfer-properties.cpp new file mode 100644 index 0000000..f2a2655 --- /dev/null +++ b/src/ui/dialog/lpe-fillet-chamfer-properties.cpp @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * From the code of Liam P.White from his Power Stroke Knot dialog + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> +#include "lpe-fillet-chamfer-properties.h" +#include <boost/lexical_cast.hpp> +#include <glibmm/i18n.h> +#include "inkscape.h" +#include "desktop.h" +#include "document-undo.h" +#include "layer-manager.h" +#include "message-stack.h" + +#include "selection-chemistry.h" + +//#include "event-context.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +FilletChamferPropertiesDialog::FilletChamferPropertiesDialog() + : _knotpoint(nullptr), + _position_visible(false), + _close_button(_("_Cancel"), true) +{ + Gtk::Box *mainVBox = get_content_area(); + mainVBox->set_homogeneous(false); + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + + // Layer name widgets + _fillet_chamfer_position_numeric.set_digits(4); + _fillet_chamfer_position_numeric.set_increments(1,1); + //todo: get the max allowable infinity freeze the widget + _fillet_chamfer_position_numeric.set_range(0., SCALARPARAM_G_MAXDOUBLE); + _fillet_chamfer_position_numeric.set_hexpand(); + _fillet_chamfer_position_label.set_label(_("Radius (pixels):")); + _fillet_chamfer_position_label.set_halign(Gtk::ALIGN_END); + _fillet_chamfer_position_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_fillet_chamfer_position_label, 0, 0, 1, 1); + _layout_table.attach(_fillet_chamfer_position_numeric, 1, 0, 1, 1); + _fillet_chamfer_chamfer_subdivisions.set_digits(0); + _fillet_chamfer_chamfer_subdivisions.set_increments(1,1); + //todo: get the max allowable infinity freeze the widget + _fillet_chamfer_chamfer_subdivisions.set_range(0, SCALARPARAM_G_MAXDOUBLE); + _fillet_chamfer_chamfer_subdivisions.set_hexpand(); + _fillet_chamfer_chamfer_subdivisions_label.set_label(_("Chamfer subdivisions:")); + _fillet_chamfer_chamfer_subdivisions_label.set_halign(Gtk::ALIGN_END); + _fillet_chamfer_chamfer_subdivisions_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_fillet_chamfer_chamfer_subdivisions_label, 0, 1, 1, 1); + _layout_table.attach(_fillet_chamfer_chamfer_subdivisions, 1, 1, 1, 1); + _fillet_chamfer_type_fillet.set_label(_("Fillet")); + _fillet_chamfer_type_fillet.set_group(_fillet_chamfer_type_group); + _fillet_chamfer_type_inverse_fillet.set_label(_("Inverse fillet")); + _fillet_chamfer_type_inverse_fillet.set_group(_fillet_chamfer_type_group); + _fillet_chamfer_type_chamfer.set_label(_("Chamfer")); + _fillet_chamfer_type_chamfer.set_group(_fillet_chamfer_type_group); + _fillet_chamfer_type_inverse_chamfer.set_label(_("Inverse chamfer")); + _fillet_chamfer_type_inverse_chamfer.set_group(_fillet_chamfer_type_group); + + + mainVBox->pack_start(_layout_table, true, true, 4); + mainVBox->pack_start(_fillet_chamfer_type_fillet, true, true, 4); + mainVBox->pack_start(_fillet_chamfer_type_inverse_fillet, true, true, 4); + mainVBox->pack_start(_fillet_chamfer_type_chamfer, true, true, 4); + mainVBox->pack_start(_fillet_chamfer_type_inverse_chamfer, true, true, 4); + + // Buttons + _close_button.set_can_default(); + + _apply_button.set_use_underline(true); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &FilletChamferPropertiesDialog::_close)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &FilletChamferPropertiesDialog::_apply)); + + signal_delete_event().connect(sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &FilletChamferPropertiesDialog::_close)), + true)); + + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); + + set_focus(_fillet_chamfer_position_numeric); +} + +FilletChamferPropertiesDialog::~FilletChamferPropertiesDialog() +{ +} + +void FilletChamferPropertiesDialog::showDialog(SPDesktop *desktop, double _amount, + const Inkscape::LivePathEffect::FilletChamferKnotHolderEntity *pt, + bool _use_distance, bool _aprox_radius, NodeSatellite _nodesatellite) +{ + FilletChamferPropertiesDialog *dialog = new FilletChamferPropertiesDialog(); + + dialog->_setUseDistance(_use_distance); + dialog->_setAprox(_aprox_radius); + dialog->_setAmount(_amount); + dialog->_setNodeSatellite(_nodesatellite); + dialog->_setPt(pt); + + dialog->set_title(_("Modify Fillet-Chamfer")); + dialog->_apply_button.set_label(_("_Modify")); + + dialog->set_modal(true); + desktop->setWindowTransient(dialog->gobj()); + dialog->property_destroy_with_parent() = true; + + dialog->show(); + dialog->present(); +} + +void FilletChamferPropertiesDialog::_apply() +{ + + double d_pos = _fillet_chamfer_position_numeric.get_value(); + if (d_pos >= 0) { + if (_fillet_chamfer_type_fillet.get_active() == true) { + _nodesatellite.nodesatellite_type = FILLET; + } else if (_fillet_chamfer_type_inverse_fillet.get_active() == true) { + _nodesatellite.nodesatellite_type = INVERSE_FILLET; + } else if (_fillet_chamfer_type_inverse_chamfer.get_active() == true) { + _nodesatellite.nodesatellite_type = INVERSE_CHAMFER; + } else { + _nodesatellite.nodesatellite_type = CHAMFER; + } + if (_flexible) { + if (d_pos > 99.99999 || d_pos < 0) { + d_pos = 0; + } + d_pos = d_pos / 100; + } + _nodesatellite.amount = d_pos; + size_t steps = (size_t)_fillet_chamfer_chamfer_subdivisions.get_value(); + if (steps < 1) { + steps = 1; + } + _nodesatellite.steps = steps; + _knotpoint->knot_set_offset(_nodesatellite); + } + _close(); +} + +void FilletChamferPropertiesDialog::_close() +{ + destroy_(); + Glib::signal_idle().connect( + sigc::bind_return( + sigc::bind(sigc::ptr_fun<void*, void>(&::operator delete), this), + false + ) + ); +} + +bool FilletChamferPropertiesDialog::_handleKeyEvent(GdkEventKey * /*event*/) +{ + return false; +} + +void FilletChamferPropertiesDialog::_handleButtonEvent(GdkEventButton *event) +{ + if ((event->type == GDK_2BUTTON_PRESS) && (event->button == 1)) { + _apply(); + } +} + +void FilletChamferPropertiesDialog::_setNodeSatellite(NodeSatellite nodesatellite) +{ + double position; + std::string distance_or_radius = std::string(_("Radius")); + if (_aprox) { + distance_or_radius = std::string(_("Radius approximated")); + } + if (_use_distance) { + distance_or_radius = std::string(_("Knot distance")); + } + if (nodesatellite.is_time) { + position = _amount * 100; + _flexible = true; + _fillet_chamfer_position_label.set_label(_("Position (%):")); + } else { + _flexible = false; + auto posConcat = Glib::ustring::compose (_("%1:"), distance_or_radius); + _fillet_chamfer_position_label.set_label(_(posConcat.c_str())); + position = _amount; + } + _fillet_chamfer_position_numeric.set_value(position); + _fillet_chamfer_chamfer_subdivisions.set_value(nodesatellite.steps); + if (nodesatellite.nodesatellite_type == FILLET) { + _fillet_chamfer_type_fillet.set_active(true); + } else if (nodesatellite.nodesatellite_type == INVERSE_FILLET) { + _fillet_chamfer_type_inverse_fillet.set_active(true); + } else if (nodesatellite.nodesatellite_type == CHAMFER) { + _fillet_chamfer_type_chamfer.set_active(true); + } else if (nodesatellite.nodesatellite_type == INVERSE_CHAMFER) { + _fillet_chamfer_type_inverse_chamfer.set_active(true); + } + _nodesatellite = nodesatellite; +} + +void FilletChamferPropertiesDialog::_setPt( + const Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity *pt) +{ + _knotpoint = const_cast< + Inkscape::LivePathEffect::FilletChamferKnotHolderEntity *>( + pt); +} + + +void FilletChamferPropertiesDialog::_setAmount(double amount) +{ + _amount = amount; +} + + + +void FilletChamferPropertiesDialog::_setUseDistance(bool use_knot_distance) +{ + _use_distance = use_knot_distance; +} + +void FilletChamferPropertiesDialog::_setAprox(bool _aprox_radius) +{ + _aprox = _aprox_radius; +} + +} // namespace +} // namespace +} // namespace + +/* + 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/ui/dialog/lpe-fillet-chamfer-properties.h b/src/ui/dialog/lpe-fillet-chamfer-properties.h new file mode 100644 index 0000000..99e446f --- /dev/null +++ b/src/ui/dialog/lpe-fillet-chamfer-properties.h @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * + * From the code of Liam P.White from his Power Stroke Knot dialog + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_FILLET_CHAMFER_PROPERTIES_H +#define INKSCAPE_DIALOG_FILLET_CHAMFER_PROPERTIES_H + +#include <2geom/point.h> +#include <gtkmm.h> + +#include "live_effects/parameter/nodesatellitesarray.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +class FilletChamferPropertiesDialog : public Gtk::Dialog { +public: + FilletChamferPropertiesDialog(); + ~FilletChamferPropertiesDialog() override; + + Glib::ustring getName() const + { + return "LayerPropertiesDialog"; + } + + static void showDialog(SPDesktop *desktop, double _amount, + const Inkscape::LivePathEffect::FilletChamferKnotHolderEntity *pt, bool _use_distance, + bool _aprox_radius, NodeSatellite _nodesatellite); + +protected: + + Inkscape::LivePathEffect::FilletChamferKnotHolderEntity * + _knotpoint; + + Gtk::Label _fillet_chamfer_position_label; + Gtk::SpinButton _fillet_chamfer_position_numeric; + Gtk::RadioButton::Group _fillet_chamfer_type_group; + Gtk::RadioButton _fillet_chamfer_type_fillet; + Gtk::RadioButton _fillet_chamfer_type_inverse_fillet; + Gtk::RadioButton _fillet_chamfer_type_chamfer; + Gtk::RadioButton _fillet_chamfer_type_inverse_chamfer; + Gtk::Label _fillet_chamfer_chamfer_subdivisions_label; + Gtk::SpinButton _fillet_chamfer_chamfer_subdivisions; + + Gtk::Grid _layout_table; + bool _position_visible; + + Gtk::Button _close_button; + Gtk::Button _apply_button; + + sigc::connection _destroy_connection; + + static FilletChamferPropertiesDialog &_instance() + { + static FilletChamferPropertiesDialog instance; + return instance; + } + + void _setPt(const Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity *pt); + void _setUseDistance(bool use_knot_distance); + void _setAprox(bool aprox_radius); + void _setAmount(double amount); + void _setNodeSatellite(NodeSatellite nodesatellite); + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); + + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton *event); + + void _apply(); + void _close(); + bool _flexible; + NodeSatellite _nodesatellite; + bool _use_distance; + double _amount; + bool _aprox; + + friend class Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity; + +private: + FilletChamferPropertiesDialog( + FilletChamferPropertiesDialog const &); // no copy + FilletChamferPropertiesDialog &operator=( + FilletChamferPropertiesDialog const &); // no assign +}; + +} // namespace +} // namespace +} // namespace + +#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_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/ui/dialog/lpe-powerstroke-properties.cpp b/src/ui/dialog/lpe-powerstroke-properties.cpp new file mode 100644 index 0000000..cd5fa28 --- /dev/null +++ b/src/ui/dialog/lpe-powerstroke-properties.cpp @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for renaming layers. + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.com> + * Andrius R. <knutux@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2004 Bryce Harrington + * Copyright (C) 2006 Andrius R. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lpe-powerstroke-properties.h" +#include <boost/lexical_cast.hpp> +#include <glibmm/i18n.h> +#include "inkscape.h" +#include "desktop.h" +#include "document-undo.h" +#include "layer-manager.h" + +#include "selection-chemistry.h" +//#include "event-context.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +PowerstrokePropertiesDialog::PowerstrokePropertiesDialog() + : _knotpoint(nullptr), + _position_visible(false), + _close_button(_("_Cancel"), true) +{ + Gtk::Box *mainVBox = get_content_area(); + + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + + // Layer name widgets + _powerstroke_position_entry.set_activates_default(true); + _powerstroke_position_entry.set_digits(4); + _powerstroke_position_entry.set_increments(1,1); + _powerstroke_position_entry.set_range(-SCALARPARAM_G_MAXDOUBLE, SCALARPARAM_G_MAXDOUBLE); + _powerstroke_position_entry.set_hexpand(); + _powerstroke_position_label.set_label(_("Position:")); + _powerstroke_position_label.set_halign(Gtk::ALIGN_END); + _powerstroke_position_label.set_valign(Gtk::ALIGN_CENTER); + + _powerstroke_width_entry.set_activates_default(true); + _powerstroke_width_entry.set_digits(4); + _powerstroke_width_entry.set_increments(1,1); + _powerstroke_width_entry.set_range(-SCALARPARAM_G_MAXDOUBLE, SCALARPARAM_G_MAXDOUBLE); + _powerstroke_width_entry.set_hexpand(); + _powerstroke_width_label.set_label(_("Width:")); + _powerstroke_width_label.set_halign(Gtk::ALIGN_END); + _powerstroke_width_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_powerstroke_position_label,0,0,1,1); + _layout_table.attach(_powerstroke_position_entry,1,0,1,1); + _layout_table.attach(_powerstroke_width_label, 0,1,1,1); + _layout_table.attach(_powerstroke_width_entry, 1,1,1,1); + + mainVBox->pack_start(_layout_table, true, true, 4); + + // Buttons + _close_button.set_can_default(); + + _apply_button.set_use_underline(true); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &PowerstrokePropertiesDialog::_close)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &PowerstrokePropertiesDialog::_apply)); + + signal_delete_event().connect( + sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &PowerstrokePropertiesDialog::_close)), + true + ) + ); + + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); + + set_focus(_powerstroke_width_entry); +} + +PowerstrokePropertiesDialog::~PowerstrokePropertiesDialog() { +} + +void PowerstrokePropertiesDialog::showDialog(SPDesktop *desktop, Geom::Point knotpoint, const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt) +{ + PowerstrokePropertiesDialog *dialog = new PowerstrokePropertiesDialog(); + + dialog->_setKnotPoint(knotpoint); + dialog->_setPt(pt); + + dialog->set_title(_("Modify Node Position")); + dialog->_apply_button.set_label(_("_Move")); + + dialog->set_modal(true); + desktop->setWindowTransient (dialog->gobj()); + dialog->property_destroy_with_parent() = true; + + dialog->show(); + dialog->present(); +} + +void +PowerstrokePropertiesDialog::_apply() +{ + double d_pos = _powerstroke_position_entry.get_value(); + double d_width = _powerstroke_width_entry.get_value(); + _knotpoint->knot_set_offset(Geom::Point(d_pos, d_width)); + _close(); +} + +void +PowerstrokePropertiesDialog::_close() +{ + destroy_(); + Glib::signal_idle().connect( + sigc::bind_return( + sigc::bind(sigc::ptr_fun<void*, void>(&::operator delete), this), + false + ) + ); +} + +bool PowerstrokePropertiesDialog::_handleKeyEvent(GdkEventKey * /*event*/) +{ + + /*switch (get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + _apply(); + return true; + } + break; + }*/ + return false; +} + +void PowerstrokePropertiesDialog::_handleButtonEvent(GdkEventButton* event) +{ + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + _apply(); + } +} + +void PowerstrokePropertiesDialog::_setKnotPoint(Geom::Point knotpoint) +{ + _powerstroke_position_entry.set_value(knotpoint.x()); + _powerstroke_width_entry.set_value(knotpoint.y()); +} + +void PowerstrokePropertiesDialog::_setPt(const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt) +{ + _knotpoint = const_cast<Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *>(pt); +} + +} // namespace +} // namespace +} // namespace + + +/* + 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/ui/dialog/lpe-powerstroke-properties.h b/src/ui/dialog/lpe-powerstroke-properties.h new file mode 100644 index 0000000..161d1fc --- /dev/null +++ b/src/ui/dialog/lpe-powerstroke-properties.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for renaming layers + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.com> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_POWERSTROKE_PROPERTIES_H +#define INKSCAPE_DIALOG_POWERSTROKE_PROPERTIES_H + +#include <2geom/point.h> +#include <gtkmm.h> +#include "live_effects/parameter/powerstrokepointarray.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +class PowerstrokePropertiesDialog : public Gtk::Dialog { + public: + PowerstrokePropertiesDialog(); + ~PowerstrokePropertiesDialog() override; + + Glib::ustring getName() const { return "LayerPropertiesDialog"; } + + static void showDialog(SPDesktop *desktop, Geom::Point knotpoint, const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt); + +protected: + + Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *_knotpoint; + + Gtk::Label _powerstroke_position_label; + Gtk::SpinButton _powerstroke_position_entry; + Gtk::Label _powerstroke_width_label; + Gtk::SpinButton _powerstroke_width_entry; + Gtk::Grid _layout_table; + bool _position_visible; + + Gtk::Button _close_button; + Gtk::Button _apply_button; + + sigc::connection _destroy_connection; + + static PowerstrokePropertiesDialog &_instance() { + static PowerstrokePropertiesDialog instance; + return instance; + } + + void _setPt(const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt); + + void _apply(); + void _close(); + + void _setKnotPoint(Geom::Point knotpoint); + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); + + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton* event); + + friend class Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity; + +private: + PowerstrokePropertiesDialog(PowerstrokePropertiesDialog const &); // no copy + PowerstrokePropertiesDialog &operator=(PowerstrokePropertiesDialog const &); // no assign +}; + +} // namespace +} // namespace +} // namespace + + +#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_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/ui/dialog/memory.cpp b/src/ui/dialog/memory.cpp new file mode 100644 index 0000000..10829e6 --- /dev/null +++ b/src/ui/dialog/memory.cpp @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Memory statistics dialog. + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2005 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/dialog/memory.h" +#include <glibmm/i18n.h> +#include <glibmm/main.h> + +#include <gtkmm/liststore.h> +#include <gtkmm/treeview.h> +#include <gtkmm/dialog.h> + +#include "inkgc/gc-core.h" +#include "debug/heap.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +namespace { + +Glib::ustring format_size(std::size_t value) { + if (!value) { + return Glib::ustring("0"); + } + + typedef std::vector<char> Digits; + typedef std::vector<Digits *> Groups; + + Groups groups; + + Digits *digits; + + while (value) { + unsigned places=3; + digits = new Digits(); + digits->reserve(places); + + while ( value && places ) { + digits->push_back('0' + (char)( value % 10 )); + value /= 10; + --places; + } + + groups.push_back(digits); + } + + Glib::ustring temp; + + while (true) { + digits = groups.back(); + while (!digits->empty()) { + temp.append(1, digits->back()); + digits->pop_back(); + } + delete digits; + + groups.pop_back(); + if (groups.empty()) { + break; + } + + temp.append(","); + } + + return temp; +} + +} + +struct Memory::Private { + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<Glib::ustring> used; + Gtk::TreeModelColumn<Glib::ustring> slack; + Gtk::TreeModelColumn<Glib::ustring> total; + + ModelColumns() { add(name); add(used); add(slack); add(total); } + }; + + Private() { + model = Gtk::ListStore::create(columns); + view.set_model(model); + view.append_column(_("Heap"), columns.name); + view.append_column(_("In Use"), columns.used); + // TRANSLATORS: "Slack" refers to memory which is in the heap but currently unused. + // More typical usage is to call this memory "free" rather than "slack". + view.append_column(_("Slack"), columns.slack); + view.append_column(_("Total"), columns.total); + } + + void update(); + + void start_update_task(); + void stop_update_task(); + + ModelColumns columns; + Glib::RefPtr<Gtk::ListStore> model; + Gtk::TreeView view; + + sigc::connection update_task; +}; + +void Memory::Private::update() { + Debug::Heap::Stats total = { 0, 0 }; + + int aggregate_features = Debug::Heap::SIZE_AVAILABLE | Debug::Heap::USED_AVAILABLE; + Gtk::ListStore::iterator row; + + row = model->children().begin(); + + for ( unsigned i = 0 ; i < Debug::heap_count() ; i++ ) { + Debug::Heap *heap=Debug::get_heap(i); + if (heap) { + Debug::Heap::Stats stats=heap->stats(); + int features=heap->features(); + + aggregate_features &= features; + + if ( row == model->children().end() ) { + row = model->append(); + } + + row->set_value(columns.name, Glib::ustring(heap->name())); + if ( features & Debug::Heap::SIZE_AVAILABLE ) { + row->set_value(columns.total, format_size(stats.size)); + total.size += stats.size; + } else { + row->set_value(columns.total, Glib::ustring(_("Unknown"))); + } + if ( features & Debug::Heap::USED_AVAILABLE ) { + row->set_value(columns.used, format_size(stats.bytes_used)); + total.bytes_used += stats.bytes_used; + } else { + row->set_value(columns.used, Glib::ustring(_("Unknown"))); + } + if ( features & Debug::Heap::SIZE_AVAILABLE && + features & Debug::Heap::USED_AVAILABLE ) + { + row->set_value(columns.slack, format_size(stats.size - stats.bytes_used)); + } else { + row->set_value(columns.slack, Glib::ustring(_("Unknown"))); + } + + ++row; + } + } + + if ( row == model->children().end() ) { + row = model->append(); + } + + Glib::ustring value; + + row->set_value(columns.name, Glib::ustring(_("Combined"))); + + if ( aggregate_features & Debug::Heap::SIZE_AVAILABLE ) { + row->set_value(columns.total, format_size(total.size)); + } else { + row->set_value(columns.total, Glib::ustring("> ") + format_size(total.size)); + } + + if ( aggregate_features & Debug::Heap::USED_AVAILABLE ) { + row->set_value(columns.used, format_size(total.bytes_used)); + } else { + row->set_value(columns.used, Glib::ustring("> ") + format_size(total.bytes_used)); + } + + if ( aggregate_features & Debug::Heap::SIZE_AVAILABLE && + aggregate_features & Debug::Heap::USED_AVAILABLE ) + { + row->set_value(columns.slack, format_size(total.size - total.bytes_used)); + } else { + row->set_value(columns.slack, Glib::ustring(_("Unknown"))); + } + + ++row; + + while ( row != model->children().end() ) { + row = model->erase(row); + } +} + +void Memory::Private::start_update_task() { + update_task.disconnect(); + update_task = Glib::signal_timeout().connect( + sigc::bind_return(sigc::mem_fun(*this, &Private::update), true), + 500 + ); +} + +void Memory::Private::stop_update_task() { + update_task.disconnect(); +} + +Memory::Memory() + : DialogBase("/dialogs/memory", "Memory") + , _private(*(new Memory::Private())) +{ + // Private conf + pack_start(_private.view); + + _private.update(); + + signal_show().connect(sigc::mem_fun(_private, &Private::start_update_task)); + signal_hide().connect(sigc::mem_fun(_private, &Private::stop_update_task)); + + // Add button + Gtk::Button *button = Gtk::manage(new Gtk::Button(_("Recalculate"))); + button->signal_button_press_event().connect(sigc::mem_fun(*this, &Memory::_apply)); + + Gtk::ButtonBox *button_box = Gtk::manage(new Gtk::ButtonBox()); + button_box->set_layout(Gtk::BUTTONBOX_END); + button_box->set_spacing(6); + button_box->set_border_width(4); + button_box->pack_end(*button); + pack_end(*button_box, Gtk::PACK_SHRINK, 0); + + // Start + _private.start_update_task(); + + show_all_children(); +} + +Memory::~Memory() { + _private.stop_update_task(); + delete &_private; +} + +bool Memory::_apply(GdkEventButton * /* button */) +{ + GC::Core::gcollect(); + _private.update(); + + return false; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/memory.h b/src/ui/dialog/memory.h new file mode 100644 index 0000000..b528cf0 --- /dev/null +++ b/src/ui/dialog/memory.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Memory statistics dialog + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UI_DIALOG_MEMORY_H +#define SEEN_INKSCAPE_UI_DIALOG_MEMORY_H + +#include "ui/dialog/dialog-base.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class Memory : public DialogBase +{ +public: + Memory(); + ~Memory() override; + + static Memory &getInstance() { return *new Memory(); } + +protected: + bool _apply(GdkEventButton *); + +private: + Memory(Memory const &d) = delete; // no copy + void operator=(Memory const &d) = delete; // no assign + + struct Private; + Private &_private; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#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/ui/dialog/messages.cpp b/src/ui/dialog/messages.cpp new file mode 100644 index 0000000..3e29501 --- /dev/null +++ b/src/ui/dialog/messages.cpp @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Messages dialog - implementation. + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "messages.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +//######################################################################### +//## E V E N T S +//######################################################################### + +/** + * Also a public method. Remove all text from the dialog + */ +void Messages::clear() +{ + Glib::RefPtr<Gtk::TextBuffer> buffer = messageText.get_buffer(); + buffer->erase(buffer->begin(), buffer->end()); +} + + +//######################################################################### +//## C O N S T R U C T O R / D E S T R U C T O R +//######################################################################### +/** + * Constructor + */ +Messages::Messages() + : DialogBase("/dialogs/messages", "Messages") + , buttonClear(_("_Clear"), _("Clear log messages")) + , checkCapture(_("Capture log messages"), _("Capture log messages")) + , buttonBox(Gtk::ORIENTATION_HORIZONTAL) +{ + /* + * Menu replaced with buttons + * + menuBar.items().push_back( Gtk::Menu_Helpers::MenuElem(_("_File"), fileMenu) ); + fileMenu.items().push_back( Gtk::Menu_Helpers::MenuElem(_("_Clear"), + sigc::mem_fun(*this, &Messages::clear) ) ); + fileMenu.items().push_back( Gtk::Menu_Helpers::MenuElem(_("Capture log messages"), + sigc::mem_fun(*this, &Messages::captureLogMessages) ) ); + fileMenu.items().push_back( Gtk::Menu_Helpers::MenuElem(_("Release log messages"), + sigc::mem_fun(*this, &Messages::releaseLogMessages) ) ); + contents->pack_start(menuBar, Gtk::PACK_SHRINK); + */ + + //### Set up the text widget + messageText.set_editable(false); + textScroll.add(messageText); + textScroll.set_policy(Gtk::POLICY_ALWAYS, Gtk::POLICY_ALWAYS); + pack_start(textScroll); + + buttonBox.set_spacing(6); + buttonBox.pack_start(checkCapture, true, true, 6); + buttonBox.pack_end(buttonClear, false, false, 10); + pack_start(buttonBox, Gtk::PACK_SHRINK); + + // sick of this thing shrinking too much + set_size_request(400, -1); + + show_all_children(); + + message(_("Ready.")); + + buttonClear.signal_clicked().connect(sigc::mem_fun(*this, &Messages::clear)); + checkCapture.signal_clicked().connect(sigc::mem_fun(*this, &Messages::toggleCapture)); + + /* + * TODO - Setting this preference doesn't capture messages that the user can see. + * Inkscape creates an instance of a dialog on startup and sends messages there, but when the user + * opens the dialog View > Messages the DialogManager creates a new instance of this class that is not capturing messages. + * + * message(_("Enable log display by setting dialogs.debug 'redirect' attribute to 1 in preferences.xml")); + */ + + handlerDefault = 0; + handlerGlibmm = 0; + handlerAtkmm = 0; + handlerPangomm = 0; + handlerGdkmm = 0; + handlerGtkmm = 0; + +} + +Messages::~Messages() += default; + + +//######################################################################### +//## M E T H O D S +//######################################################################### + +void Messages::message(char *msg) +{ + Glib::RefPtr<Gtk::TextBuffer> buffer = messageText.get_buffer(); + Glib::ustring uMsg = msg; + if (uMsg[uMsg.length()-1] != '\n') + uMsg += '\n'; + buffer->insert (buffer->end(), uMsg); +} + +// dialogLoggingCallback is already used in debug.cpp +static void dialogLoggingCallback(const gchar */*log_domain*/, + GLogLevelFlags /*log_level*/, + const gchar *messageText, + gpointer user_data) +{ + Messages *dlg = static_cast<Messages *>(user_data); + dlg->message(const_cast<char*>(messageText)); + +} + +void Messages::toggleCapture() +{ + if (checkCapture.get_active()) { + captureLogMessages(); + } else { + releaseLogMessages(); + } +} + +void Messages::captureLogMessages() +{ + /* + This might likely need more code, to capture Gtkmm + and Glibmm warnings, or maybe just simply grab stdout/stderr + */ + GLogLevelFlags flags = (GLogLevelFlags) (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | + G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE | + G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG); + if ( !handlerDefault ) { + handlerDefault = g_log_set_handler(nullptr, flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerGlibmm ) { + handlerGlibmm = g_log_set_handler("glibmm", flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerAtkmm ) { + handlerAtkmm = g_log_set_handler("atkmm", flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerPangomm ) { + handlerPangomm = g_log_set_handler("pangomm", flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerGdkmm ) { + handlerGdkmm = g_log_set_handler("gdkmm", flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerGtkmm ) { + handlerGtkmm = g_log_set_handler("gtkmm", flags, + dialogLoggingCallback, (gpointer)this); + } + message(_("Log capture started.")); +} + +void Messages::releaseLogMessages() +{ + if ( handlerDefault ) { + g_log_remove_handler(nullptr, handlerDefault); + handlerDefault = 0; + } + if ( handlerGlibmm ) { + g_log_remove_handler("glibmm", handlerGlibmm); + handlerGlibmm = 0; + } + if ( handlerAtkmm ) { + g_log_remove_handler("atkmm", handlerAtkmm); + handlerAtkmm = 0; + } + if ( handlerPangomm ) { + g_log_remove_handler("pangomm", handlerPangomm); + handlerPangomm = 0; + } + if ( handlerGdkmm ) { + g_log_remove_handler("gdkmm", handlerGdkmm); + handlerGdkmm = 0; + } + if ( handlerGtkmm ) { + g_log_remove_handler("gtkmm", handlerGtkmm); + handlerGtkmm = 0; + } + message(_("Log capture stopped.")); +} + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/messages.h b/src/ui/dialog/messages.h new file mode 100644 index 0000000..62ed7c3 --- /dev/null +++ b/src/ui/dialog/messages.h @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Messages dialog + * + * A very simple dialog for displaying Inkscape messages. Messages + * sent to g_log(), g_warning(), g_message(), ets, are routed here, + * in order to avoid messing with the startup console. + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004, 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_MESSAGES_H +#define INKSCAPE_UI_DIALOG_MESSAGES_H + +#include <glibmm/i18n.h> +#include <gtkmm/box.h> +#include <gtkmm/button.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/menu.h> +#include <gtkmm/menubar.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/textview.h> + +#include "ui/dialog/dialog-base.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class Messages : public DialogBase +{ +public: + Messages(); + ~Messages() override; + + static Messages &getInstance() { return *new Messages(); } + + /** + * Clear all information from the dialog + */ + void clear(); + + /** + * Display a message + */ + void message(char *msg); + + /** + * Redirect g_log() messages to this widget + */ + void captureLogMessages(); + + /** + * Return g_log() messages to normal handling + */ + void releaseLogMessages(); + + void toggleCapture(); + +protected: + //Gtk::MenuBar menuBar; + //Gtk::Menu fileMenu; + Gtk::ScrolledWindow textScroll; + Gtk::TextView messageText; + Gtk::Box buttonBox; + Gtk::Button buttonClear; + Gtk::CheckButton checkCapture; + + //Handler ID's + guint handlerDefault; + guint handlerGlibmm; + guint handlerAtkmm; + guint handlerPangomm; + guint handlerGdkmm; + guint handlerGtkmm; + +private: + Messages(Messages const &d) = delete; + Messages operator=(Messages const &d) = delete; +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_MESSAGES_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/ui/dialog/new-from-template.cpp b/src/ui/dialog/new-from-template.cpp new file mode 100644 index 0000000..bfccdb6 --- /dev/null +++ b/src/ui/dialog/new-from-template.cpp @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template main dialog - implementation + */ +/* Authors: + * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "new-from-template.h" +#include "file.h" + +#include "include/gtkmm_version.h" + +namespace Inkscape { +namespace UI { + + +NewFromTemplate::NewFromTemplate() + : _create_template_button(_("Create from template")) +{ + set_title(_("New From Template")); + resize(400, 400); + + _main_widget = new TemplateLoadTab(this); + + get_content_area()->pack_start(*_main_widget); + + _create_template_button.set_halign(Gtk::ALIGN_END); + _create_template_button.set_valign(Gtk::ALIGN_END); + _create_template_button.set_margin_end(15); + + get_content_area()->pack_end(_create_template_button, Gtk::PACK_SHRINK); + + _create_template_button.signal_clicked().connect( + sigc::mem_fun(*this, &NewFromTemplate::_createFromTemplate)); + _create_template_button.set_sensitive(false); + + show_all(); +} + +NewFromTemplate::~NewFromTemplate() +{ + delete _main_widget; +} + +void NewFromTemplate::setCreateButtonSensitive(bool value) +{ + _create_template_button.set_sensitive(value); +} + +void NewFromTemplate::_createFromTemplate() +{ + _main_widget->createTemplate(); + _onClose(); +} + +void NewFromTemplate::_onClose() +{ + response(0); +} + +void NewFromTemplate::load_new_from_template() +{ + NewFromTemplate dl; + dl.run(); +} + +} +} diff --git a/src/ui/dialog/new-from-template.h b/src/ui/dialog/new-from-template.h new file mode 100644 index 0000000..a308fe4 --- /dev/null +++ b/src/ui/dialog/new-from-template.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template main dialog + */ +/* Authors: + * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_SEEN_UI_DIALOG_NEW_FROM_TEMPLATE_H +#define INKSCAPE_SEEN_UI_DIALOG_NEW_FROM_TEMPLATE_H + +#include <gtkmm/dialog.h> +#include <gtkmm/button.h> + +#include "template-load-tab.h" + + +namespace Inkscape { +namespace UI { + + +class NewFromTemplate : public Gtk::Dialog +{ + +friend class TemplateLoadTab; +public: + static void load_new_from_template(); + void setCreateButtonSensitive(bool value); + ~NewFromTemplate() override; + +private: + NewFromTemplate(); + Gtk::Button _create_template_button; + TemplateLoadTab* _main_widget; + + void _createFromTemplate(); + void _onClose(); +}; + +} +} +#endif diff --git a/src/ui/dialog/object-attributes.cpp b/src/ui/dialog/object-attributes.cpp new file mode 100644 index 0000000..9588211 --- /dev/null +++ b/src/ui/dialog/object-attributes.cpp @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Generic object attribute editor + *//* + * Authors: + * see git history + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "inkscape.h" + +#include "object/sp-anchor.h" +#include "object/sp-image.h" + +#include "ui/dialog/object-attributes.h" + +#include "widgets/sp-attribute-widget.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +struct SPAttrDesc { + gchar const *label; + gchar const *attribute; +}; + +static const SPAttrDesc anchor_desc[] = { + { N_("Href:"), "xlink:href"}, + { N_("Target:"), "target"}, + { N_("Type:"), "xlink:type"}, + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkRoleAttribute + // Identifies the type of the related resource with an absolute URI + { N_("Role:"), "xlink:role"}, + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkArcRoleAttribute + // For situations where the nature/role alone isn't enough, this offers an additional URI defining the purpose of the link. + { N_("Arcrole:"), "xlink:arcrole"}, + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkTitleAttribute + { N_("Title:"), "xlink:title"}, + { N_("Show:"), "xlink:show"}, + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkActuateAttribute + { N_("Actuate:"), "xlink:actuate"}, + { nullptr, nullptr} +}; + +static const SPAttrDesc image_desc[] = { + { N_("URL:"), "xlink:href"}, + { N_("X:"), "x"}, + { N_("Y:"), "y"}, + { N_("Width:"), "width"}, + { N_("Height:"), "height"}, + { nullptr, nullptr} +}; + +static const SPAttrDesc image_nohref_desc[] = { + { N_("X:"), "x"}, + { N_("Y:"), "y"}, + { N_("Width:"), "width"}, + { N_("Height:"), "height"}, + { nullptr, nullptr} +}; + +ObjectAttributes::ObjectAttributes() + : DialogBase("/dialogs/objectattr/", "ObjectAttributes") + , blocked(false) + , CurrentItem(nullptr) + , attrTable(Gtk::manage(new SPAttributeTable())) +{ + attrTable->show(); +} + +void ObjectAttributes::widget_setup () +{ + if (blocked || !getDesktop()) { + return; + } + + Inkscape::Selection *selection = getDesktop()->getSelection(); + SPItem *item = selection->singleItem(); + if (!item) + { + set_sensitive (false); + CurrentItem = nullptr; + //no selection anymore or multiple objects selected, means that we need + //to close the connections to the previously selected object + return; + } + + blocked = true; + + // CPPIFY + SPObject *obj = item; //to get the selected item +// GObjectClass *klass = G_OBJECT_GET_CLASS(obj); //to deduce the object's type +// GType type = G_TYPE_FROM_CLASS(klass); + const SPAttrDesc *desc; + +// if (type == SP_TYPE_ANCHOR) + if (SP_IS_ANCHOR(item)) + { + desc = anchor_desc; + } +// else if (type == SP_TYPE_IMAGE) + else if (SP_IS_IMAGE(item)) + { + Inkscape::XML::Node *ir = obj->getRepr(); + const gchar *href = ir->attribute("xlink:href"); + if ( (!href) || ((strncmp(href, "data:", 5) == 0)) ) + { + desc = image_nohref_desc; + } + else + { + desc = image_desc; + } + } + else + { + blocked = false; + set_sensitive (false); + return; + } + + std::vector<Glib::ustring> labels; + std::vector<Glib::ustring> attrs; + if (CurrentItem != item) + { + int len = 0; + while (desc[len].label) + { + labels.emplace_back(desc[len].label); + attrs.emplace_back(desc[len].attribute); + len += 1; + } + attrTable->set_object(obj, labels, attrs, (GtkWidget*)gobj()); + CurrentItem = item; + } + else + { + attrTable->change_object(obj); + } + + set_sensitive (true); + show_all(); + blocked = false; +} + +void ObjectAttributes::selectionChanged(Selection *selection) +{ + widget_setup(); +} + +void ObjectAttributes::selectionModified(Selection *selection, guint flags) +{ + if (flags & ( SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_PARENT_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG) ) { + attrTable->reread_properties(); + } +} + + +} +} +} + +/* + 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/ui/dialog/object-attributes.h b/src/ui/dialog/object-attributes.h new file mode 100644 index 0000000..21c106a --- /dev/null +++ b/src/ui/dialog/object-attributes.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Generic object attribute editor + *//* + * Authors: + * see git history + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_OBJECT_ATTRIBUTES_H +#define SEEN_DIALOGS_OBJECT_ATTRIBUTES_H + +#include "ui/dialog/dialog-base.h" + +class SPAttributeTable; +class SPItem; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A dialog widget to show object attributes (currently for images and links). + */ +class ObjectAttributes : public DialogBase +{ +public: + ObjectAttributes (); + ~ObjectAttributes () override = default; + + void selectionChanged(Selection *selection) override; + void selectionModified(Selection *selection, guint flags) override; + + /** + * Returns a new instance of the object attributes dialog. + * + * Auxiliary function needed by the DialogManager. + */ + static ObjectAttributes &getInstance() { return *new ObjectAttributes(); } + + /** + * Updates entries and other child widgets on selection change, object modification, etc. + */ + void widget_setup(); + +private: + /** + * Is UI update bloched? + */ + bool blocked; + + /** + * Contains a pointer to the currently selected item (NULL in case nothing is or multiple objects are selected). + */ + SPItem *CurrentItem; + + /** + * Child widget to show the object attributes. + * + * attrTable makes the labels and edit boxes for the attributes defined + * in the SPAttrDesc arrays at the top of the cpp-file. This widgets also + * ensures object attribute modifications by the user are set. + */ + SPAttributeTable *attrTable; +}; + +} +} +} + +#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/ui/dialog/object-properties.cpp b/src/ui/dialog/object-properties.cpp new file mode 100644 index 0000000..6acce53 --- /dev/null +++ b/src/ui/dialog/object-properties.cpp @@ -0,0 +1,581 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file Object properties dialog. + */ +/* + * Inkscape, an Open Source vector graphics editor + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright (C) 2012 Kris De Gussem <Kris.DeGussem@gmail.com> + * c++ version based on former C-version (GPL v2+) with authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <goejendaagh@zonnet.nl> + * Abhishek Sharma + */ + +#include "object-properties.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/grid.h> + +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "style.h" +#include "style-enums.h" + +#include "object/sp-image.h" +#include "ui/icon-names.h" +#include "widgets/sp-attribute-widget.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +ObjectProperties::ObjectProperties() + : DialogBase("/dialogs/object/", "ObjectProperties") + , _blocked(false) + , _current_item(nullptr) + , _label_id(_("_ID:"), true) + , _label_label(_("_Label:"), true) + , _label_title(_("_Title:"), true) + , _label_dpi(_("_DPI SVG:"), true) + , _label_image_rendering(_("_Image Rendering:"), true) + , _label_color(_("Highlight Color:"), true) + , _highlight_color(_("Highlight Color"), "", 0xff0000ff, true) + , _cb_hide(_("_Hide"), true) + , _cb_lock(_("L_ock"), true) + , _cb_aspect_ratio(_("Preserve Ratio"), true) + , _exp_interactivity(_("_Interactivity"), true) + , _attr_table(Gtk::manage(new SPAttributeTable())) +{ + //initialize labels for the table at the bottom of the dialog + _int_attrs.emplace_back("onclick"); + _int_attrs.emplace_back("onmouseover"); + _int_attrs.emplace_back("onmouseout"); + _int_attrs.emplace_back("onmousedown"); + _int_attrs.emplace_back("onmouseup"); + _int_attrs.emplace_back("onmousemove"); + _int_attrs.emplace_back("onfocusin"); + _int_attrs.emplace_back("onfocusout"); + _int_attrs.emplace_back("onload"); + + _int_labels.emplace_back("onclick:"); + _int_labels.emplace_back("onmouseover:"); + _int_labels.emplace_back("onmouseout:"); + _int_labels.emplace_back("onmousedown:"); + _int_labels.emplace_back("onmouseup:"); + _int_labels.emplace_back("onmousemove:"); + _int_labels.emplace_back("onfocusin:"); + _int_labels.emplace_back("onfocusout:"); + _int_labels.emplace_back("onload:"); + + _init(); +} + +void ObjectProperties::_init() +{ + set_spacing(0); + + auto grid_top = Gtk::manage(new Gtk::Grid()); + grid_top->set_row_spacing(4); + grid_top->set_column_spacing(0); + grid_top->set_border_width(4); + + pack_start(*grid_top, false, false, 0); + + + /* Create the label for the object id */ + _label_id.set_label(_label_id.get_label() + " "); + _label_id.set_halign(Gtk::ALIGN_START); + _label_id.set_valign(Gtk::ALIGN_CENTER); + + /* Create the entry box for the object id */ + _entry_id.set_tooltip_text(_("The id= attribute (only letters, digits, and the characters .-_: allowed)")); + _entry_id.set_max_length(64); + _entry_id.set_hexpand(); + _entry_id.set_valign(Gtk::ALIGN_CENTER); + + _label_id.set_mnemonic_widget(_entry_id); + + // pressing enter in the id field is the same as clicking Set: + _entry_id.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + // focus is in the id field initially: + _entry_id.grab_focus(); + + + /* Create the label for the object label */ + _label_label.set_label(_label_label.get_label() + " "); + _label_label.set_halign(Gtk::ALIGN_START); + _label_label.set_valign(Gtk::ALIGN_CENTER); + + /* Create the entry box for the object label */ + _entry_label.set_tooltip_text(_("A freeform label for the object")); + _entry_label.set_max_length(256); + + _entry_label.set_hexpand(); + _entry_label.set_valign(Gtk::ALIGN_CENTER); + + _label_label.set_mnemonic_widget(_entry_label); + + // pressing enter in the label field is the same as clicking Set: + _entry_label.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + + + /* Create the label for the object title */ + _label_title.set_label(_label_title.get_label() + " "); + _label_title.set_halign(Gtk::ALIGN_START); + _label_title.set_valign(Gtk::ALIGN_CENTER); + + /* Create the entry box for the object title */ + _entry_title.set_sensitive (FALSE); + _entry_title.set_max_length (256); + + _entry_title.set_hexpand(); + _entry_title.set_valign(Gtk::ALIGN_CENTER); + + _label_title.set_mnemonic_widget(_entry_title); + // pressing enter in the label field is the same as clicking Set: + _entry_title.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + + _label_color.set_mnemonic_widget(_highlight_color); + _label_color.set_halign(Gtk::ALIGN_START); + _highlight_color.connectChanged(sigc::mem_fun(*this, &ObjectProperties::_highlightChanged)); + + /* Create the frame for the object description */ + Gtk::Label *label_desc = Gtk::manage(new Gtk::Label(_("_Description:"), true)); + UI::Widget::Frame *frame_desc = Gtk::manage(new UI::Widget::Frame("", FALSE)); + frame_desc->set_label_widget(*label_desc); + frame_desc->set_padding (0,0,0,0); + pack_start(*frame_desc, true, true, 0); + + /* Create the text view box for the object description */ + _ft_description.set_border_width(4); + _ft_description.set_sensitive(FALSE); + frame_desc->add(_ft_description); + _ft_description.set_shadow_type(Gtk::SHADOW_IN); + + _tv_description.set_wrap_mode(Gtk::WRAP_WORD); + _tv_description.get_buffer()->set_text(""); + _ft_description.add(_tv_description); + _tv_description.add_mnemonic_label(*label_desc); + + /* Create the label for the object title */ + _label_dpi.set_label(_label_dpi.get_label() + " "); + _label_dpi.set_halign(Gtk::ALIGN_START); + _label_dpi.set_valign(Gtk::ALIGN_CENTER); + + /* Create the entry box for the SVG DPI */ + _spin_dpi.set_digits(2); + _spin_dpi.set_range(1, 1200); + + _label_dpi.set_mnemonic_widget(_spin_dpi); + // pressing enter in the label field is the same as clicking Set: + _spin_dpi.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + + /* Image rendering */ + /* Create the label for the object ImageRendering */ + _label_image_rendering.set_label(_label_image_rendering.get_label() + " "); + _label_image_rendering.set_halign(Gtk::ALIGN_START); + _label_image_rendering.set_valign(Gtk::ALIGN_CENTER); + + /* Create the combo box text for the 'image-rendering' property */ + for (unsigned i = 0; enum_image_rendering[i].key; ++i) { + _combo_image_rendering.append(enum_image_rendering[i].key); + } + _combo_image_rendering.set_tooltip_text(_("The 'image-rendering' property can influence how a bitmap is re-scaled:\n" + "\t• 'auto': no preference (scaled image is usually smooth but blurred)\n" + "\t• 'optimizeQuality': prefer rendering quality (usually smooth but blurred)\n" + "\t• 'optimizeSpeed': prefer rendering speed (usually blocky)\n" + "\t• 'crisp-edges': rescale without blurring edges (often blocky)\n" + "\t• 'pixelated': render blocky\n" + "Note that the specification of this property is not finalized. " + "Support and interpretation of these values varies between renderers.")); + + _combo_image_rendering.set_valign(Gtk::ALIGN_CENTER); + + _label_image_rendering.set_mnemonic_widget(_combo_image_rendering); + + _combo_image_rendering.signal_changed().connect( + sigc::mem_fun(this, &ObjectProperties::_imageRenderingChanged) + ); + + + grid_top->attach(_label_id, 0, 0, 1, 1); + grid_top->attach(_entry_id, 1, 0, 1, 1); + grid_top->attach(_label_label, 0, 1, 1, 1); + grid_top->attach(_entry_label, 1, 1, 1, 1); + grid_top->attach(_label_title, 0, 2, 1, 1); + grid_top->attach(_entry_title, 1, 2, 1, 1); + grid_top->attach(_label_color, 0, 3, 1, 1); + grid_top->attach(_highlight_color, 1, 3, 1, 1); + grid_top->attach(_label_dpi, 0, 4, 1, 1); + grid_top->attach(_spin_dpi, 1, 4, 1, 1); + grid_top->attach(_label_image_rendering, 0, 5, 1, 1); + grid_top->attach(_combo_image_rendering, 1, 5, 1, 1); + + /* Check boxes */ + Gtk::Box *hb_checkboxes = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + pack_start(*hb_checkboxes, Gtk::PACK_SHRINK, 0); + + auto grid_cb = Gtk::manage(new Gtk::Grid()); + grid_cb->set_row_homogeneous(); + grid_cb->set_column_homogeneous(true); + + grid_cb->set_border_width(4); + hb_checkboxes->pack_start(*grid_cb, true, true, 0); + + /* Hide */ + _cb_hide.set_tooltip_text (_("Check to make the object invisible")); + _cb_hide.set_hexpand(); + _cb_hide.set_valign(Gtk::ALIGN_CENTER); + grid_cb->attach(_cb_hide, 0, 0, 1, 1); + + _cb_hide.signal_toggled().connect(sigc::mem_fun(this, &ObjectProperties::_hiddenToggled)); + + /* Lock */ + // TRANSLATORS: "Lock" is a verb here + _cb_lock.set_tooltip_text(_("Check to make the object insensitive (not selectable by mouse)")); + _cb_lock.set_hexpand(); + _cb_lock.set_valign(Gtk::ALIGN_CENTER); + grid_cb->attach(_cb_lock, 1, 0, 1, 1); + + _cb_lock.signal_toggled().connect(sigc::mem_fun(this, &ObjectProperties::_sensitivityToggled)); + + /* Preserve aspect ratio */ + _cb_aspect_ratio.set_tooltip_text(_("Check to preserve aspect ratio on images")); + _cb_aspect_ratio.set_hexpand(); + _cb_aspect_ratio.set_valign(Gtk::ALIGN_CENTER); + grid_cb->attach(_cb_aspect_ratio, 0, 1, 1, 1); + + _cb_aspect_ratio.signal_toggled().connect(sigc::mem_fun(this, &ObjectProperties::_aspectRatioToggled)); + + + /* Button for setting the object's id, label, title and description. */ + Gtk::Button *btn_set = Gtk::manage(new Gtk::Button(_("_Set"), true)); + btn_set->set_hexpand(); + btn_set->set_valign(Gtk::ALIGN_CENTER); + grid_cb->attach(*btn_set, 1, 1, 1, 1); + + btn_set->signal_clicked().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + + /* Interactivity options */ + _exp_interactivity.set_vexpand(false); + pack_start(_exp_interactivity, Gtk::PACK_SHRINK); + show_all(); +} + +void ObjectProperties::update_entries() +{ + if (_blocked || !getDesktop()) { + return; + } + + auto selection = getSelection(); + if (!selection) return; + + if (!selection->singleItem()) { + set_sensitive (false); + _current_item = nullptr; + //no selection anymore or multiple objects selected, means that we need + //to close the connections to the previously selected object + _attr_table->clear(); + _highlight_color.setRgba32(0x0); + return; + } else { + set_sensitive (true); + } + + SPItem *item = selection->singleItem(); + if (_current_item == item) + { + //otherwise we would end up wasting resources through the modify selection + //callback when moving an object (endlessly setting the labels and recreating _attr_table) + return; + } + _blocked = true; + _cb_aspect_ratio.set_active(g_strcmp0(item->getAttribute("preserveAspectRatio"), "none") != 0); + _cb_lock.set_active(item->isLocked()); /* Sensitive */ + _cb_hide.set_active(item->isExplicitlyHidden()); /* Hidden */ + _highlight_color.setRgba32(item->highlight_color()); + _highlight_color.closeWindow(); + + if (item->cloned) { + /* ID */ + _entry_id.set_text(""); + _entry_id.set_sensitive(FALSE); + _label_id.set_text(_("Ref")); + + /* Label */ + _entry_label.set_text(""); + _entry_label.set_sensitive(FALSE); + _label_label.set_text(_("Ref")); + + } else { + SPObject *obj = static_cast<SPObject*>(item); + + /* ID */ + _entry_id.set_text(obj->getId() ? obj->getId() : ""); + _entry_id.set_sensitive(TRUE); + _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" ")); + + /* Label */ + char const *currentlabel = obj->label(); + char const *placeholder = ""; + if (!currentlabel) { + currentlabel = ""; + placeholder = obj->defaultLabel(); + } + _entry_label.set_text(currentlabel); + _entry_label.set_placeholder_text(placeholder); + _entry_label.set_sensitive(TRUE); + + /* Title */ + gchar *title = obj->title(); + if (title) { + _entry_title.set_text(title); + g_free(title); + } + else { + _entry_title.set_text(""); + } + _entry_title.set_sensitive(TRUE); + + /* Image Rendering */ + if (SP_IS_IMAGE(item)) { + _combo_image_rendering.show(); + _label_image_rendering.show(); + _combo_image_rendering.set_active(obj->style->image_rendering.value); + if (obj->getAttribute("inkscape:svg-dpi")) { + _spin_dpi.set_value(std::stod(obj->getAttribute("inkscape:svg-dpi"))); + _spin_dpi.show(); + _label_dpi.show(); + } else { + _spin_dpi.hide(); + _label_dpi.hide(); + } + } else { + _combo_image_rendering.hide(); + _combo_image_rendering.unset_active(); + _label_image_rendering.hide(); + _spin_dpi.hide(); + _label_dpi.hide(); + } + + /* Description */ + gchar *desc = obj->desc(); + if (desc) { + _tv_description.get_buffer()->set_text(desc); + g_free(desc); + } else { + _tv_description.get_buffer()->set_text(""); + } + _ft_description.set_sensitive(TRUE); + + if (_current_item == nullptr) { + _attr_table->set_object(obj, _int_labels, _int_attrs, (GtkWidget*) _exp_interactivity.gobj()); + } else { + _attr_table->change_object(obj); + } + _attr_table->show_all(); + } + _current_item = item; + _blocked = false; +} + +void ObjectProperties::_labelChanged() +{ + if (_blocked) { + return; + } + + SPItem *item = getSelection()->singleItem(); + g_return_if_fail (item != nullptr); + + _blocked = true; + + /* Retrieve the label widget for the object's id */ + gchar *id = g_strdup(_entry_id.get_text().c_str()); + g_strcanon(id, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.:", '_'); + if (g_strcmp0(id, item->getId()) == 0) { + _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" ")); + } else if (!*id || !isalnum (*id)) { + _label_id.set_text(_("Id invalid! ")); + } else if (getDocument()->getObjectById(id) != nullptr) { + _label_id.set_text(_("Id exists! ")); + } else { + _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" ")); + item->setAttribute("id", id); + DocumentUndo::done(getDocument(), _("Set object ID"), INKSCAPE_ICON("dialog-object-properties")); + } + g_free(id); + + /* Retrieve the label widget for the object's label */ + Glib::ustring label = _entry_label.get_text(); + + /* Give feedback on success of setting the drawing object's label + * using the widget's label text + */ + SPObject *obj = static_cast<SPObject*>(item); + char const *currentlabel = obj->label(); + if (label.compare(currentlabel ? currentlabel : "")) { + obj->setLabel(label.c_str()); + DocumentUndo::done(getDocument(), _("Set object label"), INKSCAPE_ICON("dialog-object-properties")); + } + + /* Retrieve the title */ + if (obj->setTitle(_entry_title.get_text().c_str())) { + DocumentUndo::done(getDocument(), _("Set object title"), INKSCAPE_ICON("dialog-object-properties")); + } + + /* Retrieve the DPI */ + if (SP_IS_IMAGE(obj)) { + Glib::ustring dpi_value = Glib::ustring::format(_spin_dpi.get_value()); + obj->setAttribute("inkscape:svg-dpi", dpi_value); + DocumentUndo::done(getDocument(), _("Set image DPI"), INKSCAPE_ICON("dialog-object-properties")); + } + + /* Retrieve the description */ + Gtk::TextBuffer::iterator start, end; + _tv_description.get_buffer()->get_bounds(start, end); + Glib::ustring desc = _tv_description.get_buffer()->get_text(start, end, TRUE); + if (obj->setDesc(desc.c_str())) { + DocumentUndo::done(getDocument(), _("Set object description"), INKSCAPE_ICON("dialog-object-properties")); + } + + _blocked = false; +} + +void ObjectProperties::_highlightChanged(guint rgba) +{ + if (_blocked) + return; + + if (auto item = getSelection()->singleItem()) { + item->setHighlight(rgba); + DocumentUndo::done(getDocument(), _("Set item highlight color"), INKSCAPE_ICON("dialog-object-properties")); + } +} + +void ObjectProperties::_imageRenderingChanged() +{ + if (_blocked) { + return; + } + + SPItem *item = getSelection()->singleItem(); + g_return_if_fail (item != nullptr); + + _blocked = true; + + Glib::ustring scale = _combo_image_rendering.get_active_text(); + + // We should unset if the parent computed value is auto and the desired value is auto. + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "image-rendering", scale.c_str()); + Inkscape::XML::Node *image_node = item->getRepr(); + if (image_node) { + sp_repr_css_change(image_node, css, "style"); + DocumentUndo::done(getDocument(), _("Set image rendering option"), INKSCAPE_ICON("dialog-object-properties")); + } + sp_repr_css_attr_unref(css); + + _blocked = false; +} + +void ObjectProperties::_sensitivityToggled() +{ + if (_blocked) { + return; + } + + SPItem *item = getSelection()->singleItem(); + g_return_if_fail(item != nullptr); + + _blocked = true; + item->setLocked(_cb_lock.get_active()); + DocumentUndo::done(getDocument(), _cb_lock.get_active() ? _("Lock object") : _("Unlock object"), INKSCAPE_ICON("dialog-object-properties")); + _blocked = false; +} + +void ObjectProperties::_aspectRatioToggled() +{ + if (_blocked) { + return; + } + + SPItem *item = getSelection()->singleItem(); + g_return_if_fail(item != nullptr); + + _blocked = true; + + const char *active; + if (_cb_aspect_ratio.get_active()) { + active = "xMidYMid"; + } + else { + active = "none"; + } + /* Retrieve the DPI */ + if (SP_IS_IMAGE(item)) { + Glib::ustring dpi_value = Glib::ustring::format(_spin_dpi.get_value()); + item->setAttribute("preserveAspectRatio", active); + DocumentUndo::done(getDocument(), _("Set preserve ratio"), INKSCAPE_ICON("dialog-object-properties")); + } + _blocked = false; +} + +void ObjectProperties::_hiddenToggled() +{ + if (_blocked) { + return; + } + + SPItem *item = getSelection()->singleItem(); + g_return_if_fail(item != nullptr); + + _blocked = true; + item->setExplicitlyHidden(_cb_hide.get_active()); + DocumentUndo::done(getDocument(), _cb_hide.get_active() ? _("Hide object") : _("Unhide object"), INKSCAPE_ICON("dialog-object-properties")); + _blocked = false; +} + +void ObjectProperties::selectionChanged(Selection *selection) +{ + update_entries(); +} + +void ObjectProperties::desktopReplaced() +{ + update_entries(); +} + +} +} +} + +/* + 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/ui/dialog/object-properties.h b/src/ui/dialog/object-properties.h new file mode 100644 index 0000000..86f2fd1 --- /dev/null +++ b/src/ui/dialog/object-properties.h @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file Object properties dialog. + */ +/* + * Inkscape, an Open Source vector graphics editor + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright (C) 2012 Kris De Gussem <Kris.DeGussem@gmail.com> + * c++version based on former C-version (GPL v2+) with authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <goejendaagh@zonnet.nl> + * Abhishek Sharma + */ + +#ifndef SEEN_DIALOGS_ITEM_PROPERTIES_H +#define SEEN_DIALOGS_ITEM_PROPERTIES_H + + +#include <gtkmm/checkbutton.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/entry.h> +#include <gtkmm/expander.h> +#include <gtkmm/frame.h> +#include <gtkmm/spinbutton.h> +#include <gtkmm/textview.h> + +#include "ui/dialog/dialog-base.h" +#include "ui/widget/scrollprotected.h" +#include "ui/widget/color-picker.h" +#include "ui/widget/frame.h" + +class SPAttributeTable; +class SPItem; + +namespace Gtk { +class Grid; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A dialog widget to show object properties. + * + * A widget to enter an ID, label, title and description for an object. + * In addition it allows to edit the properties of an object. + */ +class ObjectProperties : public DialogBase +{ +public: + ObjectProperties(); + ~ObjectProperties() override {}; + + static ObjectProperties &getInstance() { return *new ObjectProperties(); } + + /// Updates entries and other child widgets on selection change, object modification, etc. + void update_entries(); + void selectionChanged(Selection *selection) override; + +private: + bool _blocked; + SPItem *_current_item; //to store the current item, for not wasting resources + std::vector<Glib::ustring> _int_attrs; + std::vector<Glib::ustring> _int_labels; + + Gtk::Label _label_id; //the label for the object ID + Gtk::Entry _entry_id; //the entry for the object ID + Gtk::Label _label_label; //the label for the object label + Gtk::Entry _entry_label; //the entry for the object label + Gtk::Label _label_title; //the label for the object title + Gtk::Entry _entry_title; //the entry for the object title + + Gtk::Label _label_color; //the label for the object highlight + Inkscape::UI::Widget::ColorPicker _highlight_color; // color picker for the object highlight + + Gtk::Label _label_image_rendering; // the label for 'image-rendering' + Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText> _combo_image_rendering; // the combo box text for 'image-rendering' + + Gtk::Frame _ft_description; //the frame for the text of the object description + Gtk::TextView _tv_description; //the text view object showing the object description + + Gtk::CheckButton _cb_hide; //the check button hide + Gtk::CheckButton _cb_lock; //the check button lock + Gtk::CheckButton _cb_aspect_ratio; //the preserve aspect ratio of images + + Gtk::Label _label_dpi; //the entry for the dpi value + Gtk::SpinButton _spin_dpi; //the expander for interactivity + Gtk::Expander _exp_interactivity; //the expander for interactivity + SPAttributeTable *_attr_table; //the widget for showing the on... names at the bottom + + /// Constructor auxiliary function creating the child widgets. + void _init(); + + /// Sets object properties (ID, label, title, description) on user input. + void _labelChanged(); + + // Callback for highlight color + void _highlightChanged(guint rgba); + + /// Callback for 'image-rendering'. + void _imageRenderingChanged(); + + /// Callback for checkbox Lock. + void _sensitivityToggled(); + + /// Callback for checkbox Hide. + void _hiddenToggled(); + + /// Callback for checkbox Preserve Aspect Ratio. + void _aspectRatioToggled(); + + void desktopReplaced() override; +}; +} +} +} + +#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/ui/dialog/objects.cpp b/src/ui/dialog/objects.cpp new file mode 100644 index 0000000..fb8e98f --- /dev/null +++ b/src/ui/dialog/objects.cpp @@ -0,0 +1,1468 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple panel for objects (originally developed for Ponyscape, an Inkscape derivative) + * + * Authors: + * Martin Owens, completely rewritten + * Theodore Janeczko + * Tweaked by Liam P White for use in Inkscape + * Tavmjong Bah + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * Tavmjong Bah 2017 + * Martin Owens 2020-2021 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "objects.h" + +#include <gtkmm/cellrenderer.h> +#include <gtkmm/icontheme.h> +#include <gtkmm/imagemenuitem.h> +#include <gtkmm/separatormenuitem.h> +#include <glibmm/main.h> +#include <glibmm/i18n.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "layer-manager.h" +#include "message-stack.h" + +#include "actions/actions-tools.h" + +#include "include/gtkmm_version.h" + +#include "object/filters/blend.h" +#include "object/filters/gaussian-blur.h" +#include "object/sp-clippath.h" +#include "object/sp-mask.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "style.h" + +#include "ui/dialog-events.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/selected-color.h" +#include "ui/shortcuts.h" +#include "ui/tools/node-tool.h" + +#include "ui/contextmenu.h" +#include "ui/util.h" +#include "ui/widget/canvas.h" +#include "ui/widget/imagetoggler.h" +#include "ui/widget/shapeicon.h" + +// alpha (transparency) multipliers corresponding to item selection state combinations (SelectionState) +// when 0 - do not color item's background +static double const SELECTED_ALPHA[8] = { + 0.00, //0 not selected + 1.00, //1 selected + 0.50, //2 layer focused + 1.00, //3 layer focused & selected + 0.00, //4 child of focused layer + 1.00, //5 selected child of focused layer + 0.50, //6 2 and 4 + 1.00 //7 1, 2 and 4 +}; + +//#define DUMP_LAYERS 1 + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ObjectWatcher : public Inkscape::XML::NodeObserver +{ +public: + ObjectWatcher() = delete; + ObjectWatcher(ObjectsPanel *panel, SPItem *, Gtk::TreeRow *row, bool layers_only); + ~ObjectWatcher() override; + + void updateRowInfo(); + void updateRowHighlight(); + void updateRowAncestorState(bool invisible, bool locked); + void updateRowBg(guint32 rgba = 0.0); + + ObjectWatcher *findChild(Node *node); + void addDummyChild(); + bool addChild(SPItem *, bool dummy = true); + void addChildren(SPItem *, bool dummy = false); + void setSelectedBit(SelectionState mask, bool enabled); + void setSelectedBitRecursive(SelectionState mask, bool enabled); + void setSelectedBitChildren(SelectionState mask, bool enabled); + void moveChild(Node &child, Node *sibling); + + Gtk::TreeNodeChildren getChildren() const; + Gtk::TreeIter getChildIter(Node *) const; + + void notifyChildAdded(Node &, Node &, Node *) override; + void notifyChildRemoved(Node &, Node &, Node *) override; + void notifyChildOrderChanged(Node &, Node &child, Node *, Node *) override; + void notifyAttributeChanged(Node &, GQuark, Util::ptr_shared, Util::ptr_shared) override; + + /// Associate this watcher with a tree row + void setRow(const Gtk::TreeModel::Path &path) + { + assert(path); + row_ref = Gtk::TreeModel::RowReference(panel->_store, path); + } + void setRow(const Gtk::TreeModel::Row &row) + { + setRow(panel->_store->get_path(row)); + } + + // Get the path out of this watcher + Gtk::TreeModel::Path getTreePath() const { + return row_ref.get_path(); + } + + /// True if this watchr has a valid row reference. + bool hasRow() const { return bool(row_ref); } + + /// Transfer a child watcher to its new parent + void transferChild(Node *childnode) + { + auto *target = panel->getWatcher(childnode->parent()); + assert(target != this); + auto nh = child_watchers.extract(childnode); + assert(nh); + bool inserted = target->child_watchers.insert(std::move(nh)).inserted; + assert(inserted); + } + + /// The XML node associated with this watcher. + Node *getRepr() const { return node; } + std::optional<Gtk::TreeRow> getRow() const { + if (auto path = row_ref.get_path()) { + if(auto iter = panel->_store->get_iter(path)) { + return *iter; + } + } + return std::nullopt; + } + + std::unordered_map<Node const *, std::unique_ptr<ObjectWatcher>> child_watchers; + +private: + Node *node; + Gtk::TreeModel::RowReference row_ref; + ObjectsPanel *panel; + SelectionState selection_state; + bool layers_only; +}; + +class ObjectsPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + ModelColumns() + { + add(_colNode); + add(_colLabel); + add(_colType); + add(_colIconColor); + add(_colClipMask); + add(_colBgColor); + add(_colInvisible); + add(_colLocked); + add(_colAncestorInvisible); + add(_colAncestorLocked); + add(_colHover); + } + ~ModelColumns() override = default; + Gtk::TreeModelColumn<Node*> _colNode; + Gtk::TreeModelColumn<Glib::ustring> _colLabel; + Gtk::TreeModelColumn<Glib::ustring> _colType; + Gtk::TreeModelColumn<unsigned int> _colIconColor; + Gtk::TreeModelColumn<unsigned int> _colClipMask; + Gtk::TreeModelColumn<Gdk::RGBA> _colBgColor; + Gtk::TreeModelColumn<bool> _colInvisible; + Gtk::TreeModelColumn<bool> _colLocked; + Gtk::TreeModelColumn<bool> _colAncestorInvisible; + Gtk::TreeModelColumn<bool> _colAncestorLocked; + Gtk::TreeModelColumn<bool> _colHover; +}; + +/** + * Gets an instance of the Objects panel + */ +ObjectsPanel& ObjectsPanel::getInstance() +{ + return *new ObjectsPanel(); +} + +/** + * Creates a new ObjectWatcher, a gtk TreeView iterated watching device. + * + * @param panel The panel to which the object watcher belongs + * @param obj The object to watch + * @param iter The optional list store iter for the item, if not provided, + * assumes this is the root 'document' object. + * @param layers If true, only show and watch layers, not groups or other objects. + */ +ObjectWatcher::ObjectWatcher(ObjectsPanel* panel, SPItem* obj, Gtk::TreeRow *row, bool layers) + : panel(panel) + , layers_only(layers) + , row_ref() + , selection_state(0) + , node(obj->getRepr()) +{ + if(row != nullptr) { + assert(row->children().empty()); + setRow(*row); + updateRowInfo(); + } + node->addObserver(*this); + + // Only show children for groups (and their subclasses like SPAnchor or SPRoot) + if (!dynamic_cast<SPGroup const*>(obj)) { + return; + } + // Add children as a dummy row to avoid excensive execution when + // the tree is really large, but not in layers mode. + addChildren(obj, (bool)row && !layers); +} +ObjectWatcher::~ObjectWatcher() +{ + node->removeObserver(*this); + Gtk::TreeModel::Path path; + if (bool(row_ref) && (path = row_ref.get_path())) { + auto iter = panel->_store->get_iter(path); + if(iter) { + panel->_store->erase(iter); + } + } + child_watchers.clear(); +} + +/** + * Update the information in the row from the stored node + */ +void ObjectWatcher::updateRowInfo() { + if (auto item = dynamic_cast<SPItem *>(panel->getObject(node))) { + assert(row_ref); + assert(row_ref.get_path()); + + auto _model = panel->_model; + auto row = *panel->_store->get_iter(row_ref.get_path()); + row[_model->_colNode] = node; + + // show ids without "#" + char const *id = item->getId(); + row[_model->_colLabel] = (id && !item->label()) ? id : item->defaultLabel(); + + row[_model->_colType] = item->typeName(); + row[_model->_colClipMask] = + (item->getClipObject() ? Inkscape::UI::Widget::OVERLAY_CLIP : 0) | + (item->getMaskObject() ? Inkscape::UI::Widget::OVERLAY_MASK : 0); + row[_model->_colInvisible] = item->isHidden(); + row[_model->_colLocked] = !item->isSensitive(); + + updateRowHighlight(); + updateRowAncestorState(row[_model->_colAncestorInvisible], row[_model->_colAncestorLocked]); + } +} + +/** + * Propegate changes to the highlight color to all children. + */ +void ObjectWatcher::updateRowHighlight() { + if (auto item = dynamic_cast<SPItem *>(panel->getObject(node))) { + auto row = *panel->_store->get_iter(row_ref.get_path()); + auto new_color = item->highlight_color(); + if (new_color != row[panel->_model->_colIconColor]) { + row[panel->_model->_colIconColor] = new_color; + updateRowBg(new_color); + for (auto &watcher : child_watchers) { + watcher.second->updateRowHighlight(); + } + } + } +} + +/** + * Propegate a change in visibility or locked state to all children + */ +void ObjectWatcher::updateRowAncestorState(bool invisible, bool locked) { + auto _model = panel->_model; + auto row = *panel->_store->get_iter(row_ref.get_path()); + row[_model->_colAncestorInvisible] = invisible; + row[_model->_colAncestorLocked] = locked; + for (auto &watcher : child_watchers) { + watcher.second->updateRowAncestorState( + invisible || row[_model->_colInvisible], + locked || row[_model->_colLocked]); + } +} + +Gdk::RGBA selection_color; + +/** + * Updates the row's background colour as indicated by it's selection. + */ +void ObjectWatcher::updateRowBg(guint32 rgba) +{ + assert(row_ref); + if (auto row = *panel->_store->get_iter(row_ref.get_path())) { + auto alpha = SELECTED_ALPHA[selection_state]; + if (alpha == 0.0) { + row[panel->_model->_colBgColor] = Gdk::RGBA(); + return; + } + + const auto& sel = selection_color; + auto gdk_color = Gdk::RGBA(); + gdk_color.set_red(sel.get_red()); + gdk_color.set_green(sel.get_green()); + gdk_color.set_blue(sel.get_blue()); + gdk_color.set_alpha(sel.get_alpha() * alpha); + row[panel->_model->_colBgColor] = gdk_color; + } +} + +/** + * Flip the selected state bit on or off as needed, calls updateRowBg if changed. + * + * @param mask - The selection bit to set or unset + * @param enabled - If the bit should be set or unset + */ +void ObjectWatcher::setSelectedBit(SelectionState mask, bool enabled) { + if (!row_ref) return; + SelectionState value = selection_state; + SelectionState original = value; + if (enabled) { + value |= mask; + } else { + value &= ~mask; + } + if (value != original) { + selection_state = value; + updateRowBg(); + } +} + +/** + * Flip the selected state bit on or off as needed, on this watcher and all + * its direct and indirect children. + */ +void ObjectWatcher::setSelectedBitRecursive(SelectionState mask, bool enabled) +{ + setSelectedBit(mask, enabled); + setSelectedBitChildren(mask, enabled); +} +void ObjectWatcher::setSelectedBitChildren(SelectionState mask, bool enabled) +{ + for (auto &pair : child_watchers) { + pair.second->setSelectedBitRecursive(mask, enabled); + } +} + +/** + * Find the child watcher for the given node. + */ +ObjectWatcher *ObjectWatcher::findChild(Node *node) +{ + auto it = child_watchers.find(node); + if (it != child_watchers.end()) { + return it->second.get(); + } + return nullptr; +} + +/** + * Add the child object to this node. + * + * @param child - SPObject to be added + * @param dummy - Add a dummy objects (hidden) instead + * + * @returns true if child added was a dummy objects + */ +bool ObjectWatcher::addChild(SPItem *child, bool dummy) +{ + auto group = dynamic_cast<SPGroup *>(child); + if (layers_only && (!group || group->layerMode() != SPGroup::LAYER)) { + return false; + } + + auto const children = getChildren(); + if (dummy && row_ref) { + if (children.empty()) { + auto const iter = panel->_store->append(children); + assert(panel->isDummy(*iter)); + return true; + } else if (panel->isDummy(children[0])) { + return false; + } + } + + auto *node = child->getRepr(); + assert(node); + Gtk::TreeModel::Row row = *(panel->_store->prepend(children)); + + // Ancestor states are handled inside the list store (so we don't have to re-ask every update) + auto _model = panel->_model; + if (row_ref) { + auto parent_row = *panel->_store->get_iter(row_ref.get_path()); + row[_model->_colAncestorInvisible] = parent_row[_model->_colAncestorInvisible] || parent_row[_model->_colInvisible]; + row[_model->_colAncestorLocked] = parent_row[_model->_colAncestorLocked] || parent_row[_model->_colLocked]; + } else { + row[_model->_colAncestorInvisible] = false; + row[_model->_colAncestorLocked] = false; + } + + auto &watcher = child_watchers[node]; + assert(!watcher); + watcher.reset(new ObjectWatcher(panel, child, &row, layers_only)); + + // Make sure new children have the right focus set. + if ((selection_state & LAYER_FOCUSED) != 0) { + watcher->setSelectedBit(LAYER_FOCUS_CHILD, true); + } + return false; +} + +/** + * Add all SPItem children as child rows. + */ +void ObjectWatcher::addChildren(SPItem *obj, bool dummy) +{ + assert(child_watchers.empty()); + + for (auto &child : obj->children) { + if (auto item = dynamic_cast<SPItem *>(&child)) { + if (addChild(item, dummy) && dummy) { + // one dummy child is enough to make the group expandable + break; + } + } + } +} + +/** + * Move the child to just after the given sibling + * + * @param child - SPObject to be moved + * @param sibling - Optional sibling Object to add next to, if nullptr the + * object is moved to BEFORE the first item. + */ +void ObjectWatcher::moveChild(Node &child, Node *sibling) +{ + auto child_iter = getChildIter(&child); + if (!child_iter) + return; // This means the child was never added, probably not an SPItem. + + // sibling might not be an SPItem and thus not be represented in the + // TreeView. Find the closest SPItem and use that for the reordering. + while (sibling && !dynamic_cast<SPItem const *>(panel->getObject(sibling))) { + sibling = sibling->prev(); + } + + auto sibling_iter = getChildIter(sibling); + panel->_store->move(child_iter, sibling_iter); +} + +/** + * Get the TreeRow's children iterator + * + * @returns Gtk Tree Node Children iterator + */ +Gtk::TreeNodeChildren ObjectWatcher::getChildren() const +{ + Gtk::TreeModel::Path path; + if (row_ref && (path = row_ref.get_path())) { + return panel->_store->get_iter(path)->children(); + } + assert(!row_ref); + return panel->_store->children(); +} + +/** + * Convert SPObject to TreeView Row, assuming the object is a child. + * + * @param child - The child object to find in this branch + * @returns Gtk TreeRow for the child, or end() if not found + */ +Gtk::TreeIter ObjectWatcher::getChildIter(Node *node) const +{ + auto childrows = getChildren(); + + if (!node) { + return childrows.end(); + } + + // Note: TreeRow inherits from TreeIter, so this `row` variable is + // also an iterator and a valid return value. + for (auto &row : childrows) { + if (panel->getRepr(row) == node) { + return row; + } + } + // In layer mode, we will come here for all non-layers + return childrows.begin(); +} + +void ObjectWatcher::notifyChildAdded( Node &node, Node &child, Node *prev ) +{ + assert(this->node == &node); + // Ignore XML nodes which are not displayable items + if (auto item = dynamic_cast<SPItem *>(panel->getObject(&child))) { + addChild(item); + moveChild(child, prev); + } +} +void ObjectWatcher::notifyChildRemoved( Node &node, Node &child, Node* /*prev*/ ) +{ + assert(this->node == &node); + + if (child_watchers.erase(&child) > 0) { + return; + } + + if (node.firstChild() == nullptr) { + assert(row_ref); + auto iter = panel->_store->get_iter(row_ref.get_path()); + panel->removeDummyChildren(*iter); + } +} +void ObjectWatcher::notifyChildOrderChanged( Node &parent, Node &child, Node */*old_prev*/, Node *new_prev ) +{ + assert(this->node == &parent); + + moveChild(child, new_prev); +} +void ObjectWatcher::notifyAttributeChanged( Node &node, GQuark name, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ ) +{ + assert(this->node == &node); + + // The root <svg> node doesn't have a row + if (this == panel->getRootWatcher()) { + return; + } + + // Almost anything could change the icon, so update upon any change, defer for lots of updates. + + // examples of not-so-obvious cases: + // - width/height: Can change type "circle" to an "ellipse" + + static std::set<GQuark> const excluded{ + g_quark_from_static_string("transform"), + g_quark_from_static_string("x"), + g_quark_from_static_string("y"), + g_quark_from_static_string("d"), + g_quark_from_static_string("sodipodi:nodetypes"), + }; + + if (excluded.count(name)) { + return; + } + + updateRowInfo(); +} + + +/** + * Get the object from the node. + * + * @param node - XML Node involved in the signal. + * @returns SPObject matching the node, returns nullptr if not found. + */ +SPObject *ObjectsPanel::getObject(Node *node) { + if (node != nullptr && getDocument()) + return getDocument()->getObjectByRepr(node); + return nullptr; +} + +/** + * Get the object watcher from the xml node (reverse lookup), it uses a ancesstor + * recursive pattern to match up with the root_watcher. + * + * @param node - The node to look up. + * @return the ObjectWatcher object if it's possible to find. + */ +ObjectWatcher* ObjectsPanel::getWatcher(Node *node) +{ + assert(node); + if (root_watcher->getRepr() == node) { + return root_watcher; + } else if (node->parent()) { + if (auto parent_watcher = getWatcher(node->parent())) { + return parent_watcher->findChild(node); + } + } + return nullptr; +} + +class ColorTagRenderer : public Gtk::CellRenderer { +public: + ColorTagRenderer() : + Glib::ObjectBase(typeid(CellRenderer)), + Gtk::CellRenderer(), + _property_color(*this, "tagcolor", 0) { + property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; + + int dummy_width; + // height size is not critical + Gtk::IconSize::lookup(Gtk::ICON_SIZE_MENU, dummy_width, _height); + } + + ~ColorTagRenderer() override = default; + + Glib::PropertyProxy<unsigned int> property_color() { + return _property_color.get_proxy(); + } + + int get_width() const { + return _width; + } + + sigc::signal<void, const Glib::ustring&> signal_clicked() { + return _signal_clicked; + } + +private: + void render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags) override { + cr->rectangle(cell_area.get_x(), cell_area.get_y(), cell_area.get_width(), cell_area.get_height()); + ColorRGBA color(_property_color.get_value()); + cr->set_source_rgb(color[0], color[1], color[2]); + cr->fill(); + } + + void get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const override { + min_w = nat_w = _width; + } + + void get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const override { + min_h = 1; + nat_h = _height; + } + + bool activate_vfunc(GdkEvent* event, Gtk::Widget& /*widget*/, const Glib::ustring& path, + const Gdk::Rectangle& /*background_area*/, const Gdk::Rectangle& /*cell_area*/, + Gtk::CellRendererState /*flags*/) override { + _signal_clicked.emit(path); + return false; + } + + int _width = 8; + int _height; + Glib::Property<unsigned int> _property_color; + sigc::signal<void, const Glib::ustring&> _signal_clicked; +}; + +/** + * Constructor + */ +ObjectsPanel::ObjectsPanel() : + DialogBase("/dialogs/objects", "Objects"), + root_watcher(nullptr), + _model(nullptr), + _layer(nullptr), + _is_editing(false), + _page(Gtk::ORIENTATION_VERTICAL), + _color_picker(_("Highlight color"), "", 0, true) +{ + //Create the tree model and store + ModelColumns *zoop = new ModelColumns(); + _model = zoop; + + _store = Gtk::TreeStore::create( *zoop ); + + _color_picker.hide(); + + //Set up the tree + _tree.set_model( _store ); + _tree.set_headers_visible(false); + // Reorderable means that we allow drag-and-drop, but we only allow that + // when at least one row is selected + _tree.set_reorderable(true); + _tree.enable_model_drag_dest (Gdk::ACTION_MOVE); + + //Label + _name_column = Gtk::manage(new Gtk::TreeViewColumn()); + _text_renderer = Gtk::manage(new Gtk::CellRendererText()); + _text_renderer->property_editable() = true; + _text_renderer->property_ellipsize().set_value(Pango::ELLIPSIZE_END); + _text_renderer->signal_editing_started().connect([=](Gtk::CellEditable*,const Glib::ustring&){ + _is_editing = true; + }); + _text_renderer->signal_editing_canceled().connect([=](){ + _is_editing = false; + }); + _text_renderer->signal_edited().connect([=](const Glib::ustring&,const Glib::ustring&){ + _is_editing = false; + }); + + const int icon_col_width = 24; + auto icon_renderer = Gtk::manage(new Inkscape::UI::Widget::CellRendererItemIcon()); + icon_renderer->property_xpad() = 2; + icon_renderer->property_width() = icon_col_width; + _tree.append_column(*_name_column); + _name_column->set_expand(true); + _name_column->pack_start(*icon_renderer, false); + _name_column->pack_start(*_text_renderer, true); + _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); + _name_column->add_attribute(_text_renderer->property_cell_background_rgba(), _model->_colBgColor); + _name_column->add_attribute(icon_renderer->property_shape_type(), _model->_colType); + _name_column->add_attribute(icon_renderer->property_color(), _model->_colIconColor); + _name_column->add_attribute(icon_renderer->property_clipmask(), _model->_colClipMask); + _name_column->add_attribute(icon_renderer->property_cell_background_rgba(), _model->_colBgColor); + + // Visible icon + auto *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-hidden"), INKSCAPE_ICON("object-visible"))); + int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1; + if (auto eye = _tree.get_column(visibleColNum)) { + eye->add_attribute(eyeRenderer->property_active(), _model->_colInvisible); + eye->add_attribute(eyeRenderer->property_cell_background_rgba(), _model->_colBgColor); + eye->add_attribute(eyeRenderer->property_activatable(), _model->_colHover); + eye->add_attribute(eyeRenderer->property_gossamer(), _model->_colAncestorInvisible); + eye->set_fixed_width(icon_col_width); + _eye_column = eye; + } + + // Unlocked icon + Inkscape::UI::Widget::ImageToggler * lockRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked"))); + int lockedColNum = _tree.append_column("lock", *lockRenderer) - 1; + if (auto lock = _tree.get_column(lockedColNum)) { + lock->add_attribute(lockRenderer->property_active(), _model->_colLocked); + lock->add_attribute(lockRenderer->property_cell_background_rgba(), _model->_colBgColor); + lock->add_attribute(lockRenderer->property_activatable(), _model->_colHover); + lock->add_attribute(lockRenderer->property_gossamer(), _model->_colAncestorLocked); + lock->set_fixed_width(icon_col_width); + _lock_column = lock; + } + + // hierarchy indicator - using item's layer highlight color + auto tag_renderer = Gtk::manage(new ColorTagRenderer()); + int tag_column = _tree.append_column("tag", *tag_renderer) - 1; + if (auto tag = _tree.get_column(tag_column)) { + tag->add_attribute(tag_renderer->property_color(), _model->_colIconColor); + tag->set_fixed_width(tag_renderer->get_width()); + } + tag_renderer->signal_clicked().connect([=](const Glib::ustring& path) { + // object's color indicator clicked - open color picker + _clicked_item_row = *_store->get_iter(path); + if (auto item = getItem(_clicked_item_row)) { + // find object's color + _color_picker.setRgba32(item->highlight_color()); + _color_picker.open(); + } + }); + + _color_picker.connectChanged([=](guint rgba) { + if (auto item = getItem(_clicked_item_row)) { + item->setHighlight(rgba); + DocumentUndo::maybeDone(getDocument(), "highligh-color", _("Set item highlight color"), INKSCAPE_ICON("dialog-object-properties")); + } + }); + + //Set the expander and search columns + _tree.set_expander_column(*_name_column); + // Disable search (it doesn't make much sense) + _tree.set_search_column(-1); + _tree.set_enable_search(false); + _tree.get_selection()->set_mode(Gtk::SELECTION_NONE); + + //Set up tree signals + _tree.signal_button_press_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false); + _tree.signal_button_release_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false); + _tree.signal_key_press_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleKeyEvent), false); + _tree.signal_key_release_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleKeyEvent), false); + _tree.signal_motion_notify_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleMotionEvent), false); + + // Set a status bar text when entering the widget + _tree.signal_enter_notify_event().connect([=](GdkEventCrossing*){ + getDesktop()->messageStack()->flash(Inkscape::NORMAL_MESSAGE, + _("<b>Hold ALT</b> while hovering over item to highlight, <b>hold SHIFT</b> and click to hide/lock all.")); + return false; + }, false); + // watch mouse leave too to clear any state. + _tree.signal_leave_notify_event().connect([=](GdkEventCrossing*){ return _handleMotionEvent(nullptr); }, false); + + // Before expanding a row, replace the dummy child with the actual children + _tree.signal_test_expand_row().connect([this](const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &) { + if (cleanDummyChildren(*iter)) { + if (auto selection = getSelection()) { + selectionChanged(selection); + } + } + return false; + }); + + _tree.signal_drag_motion().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_motion), false); + _tree.signal_drag_drop().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_drop), false); + _tree.signal_drag_begin().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_start), false); + _tree.signal_drag_end().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_end), false); + + //Set up the label editing signals + _text_renderer->signal_edited().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleEdited)); + + //Set up the scroller window and pack the page + // turn off overlay scrollbars - they block access to the 'lock' icon + _scroller.set_overlay_scrolling(false); + _scroller.add(_tree); + _scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + _scroller.set_shadow_type(Gtk::SHADOW_IN); + Gtk::Requisition sreq; + Gtk::Requisition sreq_natural; + _scroller.get_preferred_size(sreq_natural, sreq); + int minHeight = 70; + if (sreq.height < minHeight) { + // Set a min height to see the layers when used with Ubuntu liboverlay-scrollbar + _scroller.set_size_request(sreq.width, minHeight); + } + + _page.pack_start(_buttonsRow, Gtk::PACK_SHRINK); + _page.pack_end(_scroller, Gtk::PACK_EXPAND_WIDGET); + pack_start(_page, Gtk::PACK_EXPAND_WIDGET); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + { + auto child = Glib::wrap(sp_get_icon_image("layer-duplicate", GTK_ICON_SIZE_SMALL_TOOLBAR)); + child->show(); + _object_mode.add(*child); + _object_mode.set_relief(Gtk::RELIEF_NONE); + } + _object_mode.set_tooltip_text(_("Switch to layers only view.")); + _object_mode.property_active() = prefs->getBool("/dialogs/objects/layers_only", false); + _object_mode.property_active().signal_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_objects_toggle)); + + _buttonsPrimary.pack_start(_object_mode, Gtk::PACK_SHRINK); + _buttonsPrimary.pack_start(*_addBarButton(INKSCAPE_ICON("layer-new"), _("Add layer..."), "win.layer-new"), Gtk::PACK_SHRINK); + + _buttonsSecondary.pack_end(*_addBarButton(INKSCAPE_ICON("edit-delete"), _("Remove object"), "app.delete-selection"), Gtk::PACK_SHRINK); + _buttonsSecondary.pack_end(*_addBarButton(INKSCAPE_ICON("go-down"), _("Move Down"), "app.selection-stack-down"), Gtk::PACK_SHRINK); + _buttonsSecondary.pack_end(*_addBarButton(INKSCAPE_ICON("go-up"), _("Move Up"), "app.selection-stack-up"), Gtk::PACK_SHRINK); + + _buttonsRow.pack_start(_buttonsPrimary, Gtk::PACK_SHRINK); + _buttonsRow.pack_end(_buttonsSecondary, Gtk::PACK_SHRINK); + + selection_color = get_background_color(_tree.get_style_context(), Gtk::STATE_FLAG_SELECTED); + _tree_style = _tree.signal_style_updated().connect([=](){ + selection_color = get_background_color(_tree.get_style_context(), Gtk::STATE_FLAG_SELECTED); + + if (!root_watcher) return; + for (auto&& kv : root_watcher->child_watchers) { + if (kv.second) { + kv.second->updateRowHighlight(); + } + } + }); + // Clear and update entire tree (do not use this in changed/modified signals) + _watch_object_mode = prefs->createObserver("/dialogs/objects/layers_only", [=]() { setRootWatcher(); }); + + update(); + show_all_children(); +} + +/** + * Destructor + */ +ObjectsPanel::~ObjectsPanel() +{ + if (root_watcher) { + delete root_watcher; + } + root_watcher = nullptr; + + if (_model) { + delete _model; + _model = nullptr; + } +} + +void ObjectsPanel::_objects_toggle() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/objects/layers_only", _object_mode.get_active()); +} + +void ObjectsPanel::desktopReplaced() +{ + layer_changed.disconnect(); + + if (auto desktop = getDesktop()) { + layer_changed = desktop->layerManager().connectCurrentLayerChanged(sigc::mem_fun(*this, &ObjectsPanel::layerChanged)); + } +} + +void ObjectsPanel::documentReplaced() +{ + setRootWatcher(); +} + +void ObjectsPanel::setRootWatcher() +{ + if (root_watcher) { + delete root_watcher; + } + root_watcher = nullptr; + + if (auto document = getDocument()) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool layers_only = prefs->getBool("/dialogs/objects/layers_only", false); + root_watcher = new ObjectWatcher(this, document->getRoot(), nullptr, layers_only); + layerChanged(getDesktop()->layerManager().currentLayer()); + selectionChanged(getSelection()); + } +} + +void ObjectsPanel::selectionChanged(Selection *selected) +{ + root_watcher->setSelectedBitRecursive(SELECTED_OBJECT, false); + + for (auto item : selected->items()) { + ObjectWatcher *watcher = nullptr; + // This both unpacks the tree, and populates lazy loading + for (auto &parent : item->ancestorList(true)) { + if (parent->getRepr() == root_watcher->getRepr()) { + watcher = root_watcher; + } else if (watcher) { + if ((watcher = watcher->findChild(parent->getRepr()))) { + if (auto row = watcher->getRow()) { + cleanDummyChildren(*row); + } + } + } + } + if (watcher) { + if (auto final_watcher = watcher->findChild(item->getRepr())) { + final_watcher->setSelectedBit(SELECTED_OBJECT, true); + _tree.expand_to_path(final_watcher->getTreePath()); + } else { + g_warning("Can't find final step in tree selection!"); + } + } else { + g_warning("Can't find a mid step in tree selection!"); + } + } +} + +/** + * Happens when the layer selected is changed. + * + * @param layer - The layer now selected + */ +void ObjectsPanel::layerChanged(SPObject *layer) +{ + root_watcher->setSelectedBitRecursive(LAYER_FOCUS_CHILD | LAYER_FOCUSED, false); + + if (!layer) return; + auto watcher = getWatcher(layer->getRepr()); + if (watcher && watcher != root_watcher) { + watcher->setSelectedBitChildren(LAYER_FOCUS_CHILD, true); + watcher->setSelectedBit(LAYER_FOCUSED, true); + } + _layer = layer; +} + + +/** + * Stylizes a button using the given icon name and tooltip + */ +Gtk::Button* ObjectsPanel::_addBarButton(char const* iconName, char const* tooltip, char const *action_name) +{ + Gtk::Button* btn = Gtk::manage(new Gtk::Button()); + auto child = Glib::wrap(sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR)); + child->show(); + btn->add(*child); + btn->set_relief(Gtk::RELIEF_NONE); + btn->set_tooltip_text(tooltip); + // Must use C API until GTK4. + gtk_actionable_set_action_name(GTK_ACTIONABLE(btn->gobj()), action_name); + return btn; +} + +/** + * Sets visibility of items in the tree + * @param iter Current item in the tree + */ +bool ObjectsPanel::toggleVisible(GdkEventButton* event, Gtk::TreeModel::Row row) +{ + if (SPItem* item = getItem(row)) { + if (event->state & GDK_SHIFT_MASK) { + // Toggle Visible for layers (hide all other layers) + if (auto desktop = getDesktop()) { + if (desktop->layerManager().isLayer(item)) { + desktop->layerManager().toggleLayerSolo(item); + DocumentUndo::done(getDocument(), _("Hide other layers"), ""); + } + } + } else { + item->setHidden(!row[_model->_colInvisible]); + // Use maybeDone so user can flip back and forth without making loads of undo items + DocumentUndo::maybeDone(getDocument(), "toggle-vis", _("Toggle item visibility"), ""); + } + } + return true; +} + +/** + * Sets sensitivity of items in the tree + * @param iter Current item in the tree + * @param locked Whether the item should be locked + */ +bool ObjectsPanel::toggleLocked(GdkEventButton* event, Gtk::TreeModel::Row row) +{ + if (SPItem* item = getItem(row)) { + if (event->state & GDK_SHIFT_MASK) { + // Toggle lock for layers (lock all other layers) + if (auto desktop = getDesktop()) { + if (desktop->layerManager().isLayer(item)) { + desktop->layerManager().toggleLockOtherLayers(item); + DocumentUndo::done(getDocument(), _("Lock other layers"), ""); + } + } + } else { + item->setLocked(!row[_model->_colLocked]); + // Use maybeDone so user can flip back and forth without making loads of undo items + DocumentUndo::maybeDone(getDocument(), "toggle-lock", _("Toggle item locking"), ""); + } + } + return true; +} + +/** + * Handles keyboard events + * @param event Keyboard event passed in from GDK + * @return Whether the event should be eaten (om nom nom) + */ +bool ObjectsPanel::_handleKeyEvent(GdkEventKey *event) +{ + auto desktop = getDesktop(); + if (!desktop) + return false; + + bool press = event->type == GDK_KEY_PRESS; + Gtk::AccelKey shortcut = Inkscape::Shortcuts::get_from_event(event); + switch (shortcut.get_key()) { + case GDK_KEY_Escape: + if (desktop->canvas) { + desktop->canvas->grab_focus(); + return true; + } + break; + + // space and return enter label editing mode; leave them for the tree to handle + case GDK_KEY_Return: + case GDK_KEY_space: + return false; + + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + _handleTransparentHover(press); + return false; + } + + return false; +} + +/** + * Handles mouse movements + * @param event Motion event passed in from GDK + * @returns Whether the event should be eaten. + */ +bool ObjectsPanel::_handleMotionEvent(GdkEventMotion* motion_event) +{ + if (_is_editing) return false; + + // Unhover any existing hovered row. + if (_hovered_row_ref) { + if (auto row = *_store->get_iter(_hovered_row_ref.get_path())) + row[_model->_colHover] = false; + } + // Allow this function to be called by LEAVE motion + if (!motion_event) { + _hovered_row_ref = Gtk::TreeModel::RowReference(); + _handleTransparentHover(false); + return false; + } + + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x, y; + if (_tree.get_path_at_pos((int)motion_event->x, (int)motion_event->y, path, col, x, y)) { + if (auto row = *_store->get_iter(path)) { + row[_model->_colHover] = true; + _hovered_row_ref = Gtk::TreeModel::RowReference(_store, path); + } + } + + _handleTransparentHover(motion_event->state & GDK_MOD1_MASK); + return false; +} + +void ObjectsPanel::_handleTransparentHover(bool enabled) +{ + SPItem *item = nullptr; + if (enabled && _hovered_row_ref) { + if (auto row = *_store->get_iter(_hovered_row_ref.get_path())) { + item = getItem(row); + } + } + + if (item == _solid_item) + return; + + // Set the target item, this prevents rerunning too. + _solid_item = item; + auto desktop = getDesktop(); + + // Reset all the items in the list. + for (auto &item : _translucent_items) { + Inkscape::DrawingItem *arenaitem = item->get_arenaitem(desktop->dkey); + arenaitem->setOpacity(SP_SCALE24_TO_FLOAT(item->style->opacity.value)); + } + _translucent_items.clear(); + + if (item) { + _generateTranslucentItems(getDocument()->getRoot()); + + for (auto &item : _translucent_items) { + Inkscape::DrawingItem *arenaitem = item->get_arenaitem(desktop->dkey); + arenaitem->setOpacity(0.2); + } + } +} + +/** + * Generate a new list of sibling items (recursive) + */ +void ObjectsPanel::_generateTranslucentItems(SPItem *parent) +{ + if (parent == _solid_item) + return; + if (parent->isAncestorOf(_solid_item)) { + for (auto &child: parent->children) { + if (auto item = dynamic_cast<SPItem *>(&child)) { + _generateTranslucentItems(item); + } + } + } else { + _translucent_items.push_back(parent); + } +} + +/** + * Handles mouse up events + * @param event Mouse event from GDK + * @return whether to eat the event (om nom nom) + */ +bool ObjectsPanel::_handleButtonEvent(GdkEventButton* event) +{ + auto selection = getSelection(); + if (!selection) + return false; + + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x, y; + if (_tree.get_path_at_pos((int)event->x, (int)event->y, path, col, x, y)) { + if (auto row = *_store->get_iter(path)) { + if (event->type == GDK_BUTTON_RELEASE) { + if (col == _eye_column) { + return toggleVisible(event, row); + } else if (col == _lock_column) { + return toggleLocked(event, row); + } + } + } + // Only the label reacts to clicks, nothing else, only need to test horz + Gdk::Rectangle r; + _tree.get_cell_area(path, *_name_column, r); + if (x < r.get_x() || x > (r.get_x() + r.get_width())) + return false; + + // This doesn't work, it might be being eaten. + if (event->type == GDK_2BUTTON_PRESS) { + _tree.set_cursor(path, *col, true); + _is_editing = true; + return true; + } + _is_editing = _is_editing && event->type == GDK_BUTTON_RELEASE; + auto row = *_store->get_iter(path); + if (!row) return false; + SPItem *item = getItem(row); + + if (!item) return false; + SPGroup *group = SP_GROUP(item); + + // Load the right click menu + const bool context_menu = event->type == GDK_BUTTON_PRESS && event->button == 3; + + // Select items on button release to not confuse drag (unless it's a right-click) + // Right-click selects too to set up the stage for context menu which frequently relies on current selection! + if (!_is_editing && (event->type == GDK_BUTTON_RELEASE || context_menu)) { + if (event->state & GDK_SHIFT_MASK && !selection->isEmpty()) { + // Select everything between this row and the last selected item + selection->setBetween(item); + } else if (event->state & GDK_CONTROL_MASK) { + selection->toggle(item); + } else if (group && group->layerMode() == SPGroup::LAYER) { + // if right-clicking on a layer, make it current for context menu actions to work correctly + if (context_menu) { + if (getDesktop()->layerManager().currentLayer() != item) { + getDesktop()->layerManager().setCurrentLayer(item, true); + } + } else { + selection->set(item); + } + } else if (!context_menu) { + selection->set(item); + } + + if (context_menu) { + ContextMenu *menu = new ContextMenu(getDesktop(), item, true); // true == hide menu item for opening this dialog! + menu->attach_to_widget(*this); // So actions work! + menu->show(); + menu->popup_at_pointer(nullptr); + } + return true; + } else { + // Remember the item for we are about to drag it! + current_item = item; + } + } + return false; +} + +/** + * Handle a successful item label edit + * @param path Tree path of the item currently being edited + * @param new_text New label text + */ +void ObjectsPanel::_handleEdited(const Glib::ustring& path, const Glib::ustring& new_text) +{ + _is_editing = false; + if (auto row = *_store->get_iter(path)) { + if (auto item = getItem(row)) { + if (!new_text.empty() && (!item->label() || new_text != item->label())) { + item->setLabel(new_text.c_str()); + DocumentUndo::done(getDocument(), _("Rename object"), ""); + } + } + } +} + +/** + * Take over the select row functionality from the TreeView, this is because + * we have two selections (layer and object selection) and require a custom + * method of rendering the result to the treeview. + */ +bool ObjectsPanel::select_row( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const &path, bool /*sel*/ ) +{ + return true; +} + +/** + * Get the XML node which is associated with a row. Can be NULL for dummy children. + */ +Node *ObjectsPanel::getRepr(Gtk::TreeModel::Row const &row) const +{ + return row[_model->_colNode]; +} + +/** + * Get the item which is associated with a row. If getRepr(row) is not NULL, + * then this call is expected to also not be NULL. + */ +SPItem *ObjectsPanel::getItem(Gtk::TreeModel::Row const &row) const +{ + auto const this_const = const_cast<ObjectsPanel *>(this); + return dynamic_cast<SPItem *>(this_const->getObject(getRepr(row))); +} + +/** + * Return true if this row has dummy children. + */ +bool ObjectsPanel::hasDummyChildren(Gtk::TreeModel::Row const &row) const +{ + for (auto &c : row.children()) { + if (isDummy(c)) { + return true; + } + } + return false; +} + +/** + * If the given row has dummy children, remove them. + * @pre Eiter all, or no children are dummies + * @post If the function returns true, the row has no children + * @return False if there are children and they are not dummies + */ +bool ObjectsPanel::removeDummyChildren(Gtk::TreeModel::Row const &row) +{ + auto &children = row.children(); + if (!children.empty()) { + Gtk::TreeStore::iterator child = children[0]; + if (!isDummy(*child)) { + assert(!hasDummyChildren(row)); + return false; + } + + do { + assert(child->parent() == row); + assert(isDummy(*child)); + child = _store->erase(child); + } while (child && child->parent() == row); + } + return true; +} + +bool ObjectsPanel::cleanDummyChildren(Gtk::TreeModel::Row const &row) +{ + if (removeDummyChildren(row)) { + assert(row); + getWatcher(getRepr(row))->addChildren(getItem(row)); + return true; + } + return false; +} + +/** + * Signal handler for "drag-motion" + * + * Refuses drops into non-group items. + */ +bool ObjectsPanel::on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &context, int x, int y, guint time) +{ + Gtk::TreeModel::Path path; + Gtk::TreeViewDropPosition pos; + + auto selection = getSelection(); + auto document = getDocument(); + + if (!selection || !document) + goto finally; + + _tree.get_dest_row_at_pos(x, y, path, pos); + + if (path) { + auto iter = _store->get_iter(path); + auto repr = getRepr(*iter); + auto obj = document->getObjectByRepr(repr); + + bool const drop_into = pos != Gtk::TREE_VIEW_DROP_BEFORE && // + pos != Gtk::TREE_VIEW_DROP_AFTER; + + // don't drop on self + if (selection->includes(obj)) { + goto finally; + } + + auto item = getItem(*iter); + + // only groups can have children + if (drop_into && !dynamic_cast<SPGroup const *>(item)) { + goto finally; + } + + context->drag_status(Gdk::ACTION_MOVE, time); + return false; + } + +finally: + // remove drop highlight + _tree.unset_drag_dest_row(); + context->drag_refuse(time); + return true; +} + +/** + * Signal handler for "drag-drop". + * + * Do the actual work of drag-and-drop. + */ +bool ObjectsPanel::on_drag_drop(const Glib::RefPtr<Gdk::DragContext> &context, int x, int y, guint time) +{ + Gtk::TreeModel::Path path; + Gtk::TreeViewDropPosition pos; + _tree.get_dest_row_at_pos(x, y, path, pos); + + if (!path) { + return true; + } + + auto drop_repr = getRepr(*_store->get_iter(path)); + bool const drop_into = pos != Gtk::TREE_VIEW_DROP_BEFORE && // + pos != Gtk::TREE_VIEW_DROP_AFTER; + + auto selection = getSelection(); + auto document = getDocument(); + if (selection && document) { + if (drop_into) { + selection->toLayer(document->getObjectByRepr(drop_repr)); + } else { + Node *after = (pos == Gtk::TREE_VIEW_DROP_BEFORE) ? drop_repr : drop_repr->prev(); + selection->toLayer(document->getObjectByRepr(drop_repr->parent()), false, after); + } + } + + on_drag_end(context); + return true; +} + +void ObjectsPanel::on_drag_start(const Glib::RefPtr<Gdk::DragContext> &context) +{ + auto selection = _tree.get_selection(); + selection->set_mode(Gtk::SELECTION_MULTIPLE); + selection->unselect_all(); + + auto obj_selection = getSelection(); + if (!obj_selection) + return; + + if (current_item && !obj_selection->includes(current_item)) { + // This means the item the user started to drag is not one that is selected + // So we'll deselect everything and start dragging this item instead. + auto watcher = getWatcher(current_item->getRepr()); + if (watcher) { + auto path = watcher->getTreePath(); + selection->select(path); + obj_selection->set(current_item); + } + } else { + // Drag all the items currently selected (multi-row) + for (auto item : obj_selection->items()) { + auto watcher = getWatcher(item->getRepr()); + if (watcher) { + auto path = watcher->getTreePath(); + selection->select(path); + } + } + } +} + +void ObjectsPanel::on_drag_end(const Glib::RefPtr<Gdk::DragContext> &context) +{ + auto selection = _tree.get_selection(); + selection->unselect_all(); + selection->set_mode(Gtk::SELECTION_NONE); + current_item = nullptr; +} + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/objects.h b/src/ui/dialog/objects.h new file mode 100644 index 0000000..a0e1764 --- /dev/null +++ b/src/ui/dialog/objects.h @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple dialog for objects UI. + * + * Authors: + * Theodore Janeczko + * Tavmjong Bah + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * Tavmjong Bah 2017 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_OBJECTS_PANEL_H +#define SEEN_OBJECTS_PANEL_H + +#include <gtkmm/box.h> +#include <gtkmm/dialog.h> + +#include "helper/auto-connection.h" +#include "xml/node-observer.h" + +#include "ui/dialog/dialog-base.h" +#include "ui/widget/color-picker.h" + +#include "selection.h" +#include "color-rgba.h" +#include "helper/auto-connection.h" + +using Inkscape::XML::Node; + +class SPObject; +class SPGroup; +// struct SPColorSelector; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ObjectsPanel; +class ObjectWatcher; + +enum {COL_LABEL, COL_VISIBLE, COL_LOCKED}; + +using SelectionState = int; +enum SelectionStates : SelectionState { + SELECTED_NOT = 0, // Object is NOT in desktop's selection + SELECTED_OBJECT = 1, // Object is in the desktop's selection + LAYER_FOCUSED = 2, // This layer is the desktop's focused layer + LAYER_FOCUS_CHILD = 4 // This object is a child of the focused layer +}; + +/** + * A panel that displays objects. + */ +class ObjectsPanel : public DialogBase +{ +public: + ObjectsPanel(); + ~ObjectsPanel() override; + + class ModelColumns; + static ObjectsPanel& getInstance(); + +protected: + + void desktopReplaced() override; + void documentReplaced() override; + void layerChanged(SPObject *obj); + void selectionChanged(Selection *selected) override; + + // Accessed by ObjectWatcher directly (friend class) + SPObject* getObject(Node *node); + ObjectWatcher* getWatcher(Node *node); + ObjectWatcher *getRootWatcher() const { return root_watcher; }; + + Node *getRepr(Gtk::TreeModel::Row const &row) const; + SPItem *getItem(Gtk::TreeModel::Row const &row) const; + std::optional<Gtk::TreeRow> getRow(SPItem *item) const; + + bool isDummy(Gtk::TreeModel::Row const &row) const { return getRepr(row) == nullptr; } + bool hasDummyChildren(Gtk::TreeModel::Row const &row) const; + bool removeDummyChildren(Gtk::TreeModel::Row const &row); + bool cleanDummyChildren(Gtk::TreeModel::Row const &row); + + Glib::RefPtr<Gtk::TreeStore> _store; + ModelColumns* _model; + + void setRootWatcher(); +private: + + Inkscape::PrefObserver _watch_object_mode; + ObjectWatcher* root_watcher; + SPItem *current_item = nullptr; + + Inkscape::auto_connection layer_changed; + SPObject *_layer; + Gtk::TreeModel::RowReference _hovered_row_ref; + + //Show icons in the context menu + bool _show_contextmenu_icons; + bool _is_editing; + + std::vector<Gtk::Widget*> _watching; + std::vector<Gtk::Widget*> _watchingNonTop; + std::vector<Gtk::Widget*> _watchingNonBottom; + + Gtk::TreeView _tree; + Gtk::CellRendererText *_text_renderer; + Gtk::TreeView::Column *_name_column; + Gtk::TreeView::Column *_eye_column = nullptr; + Gtk::TreeView::Column *_lock_column = nullptr; + Gtk::Box _buttonsRow; + Gtk::Box _buttonsPrimary; + Gtk::Box _buttonsSecondary; + Gtk::ScrolledWindow _scroller; + Gtk::Menu _popupMenu; + Gtk::Box _page; + Gtk::ToggleButton _object_mode; + Inkscape::auto_connection _tree_style; + Inkscape::UI::Widget::ColorPicker _color_picker; + Gtk::TreeRow _clicked_item_row; + + ObjectsPanel(ObjectsPanel const &) = delete; // no copy + ObjectsPanel &operator=(ObjectsPanel const &) = delete; // no assign + + Gtk::Button *_addBarButton(char const* iconName, char const* tooltip, char const *action_name); + void _objects_toggle(); + + bool toggleVisible(GdkEventButton* event, Gtk::TreeModel::Row row); + bool toggleLocked(GdkEventButton* event, Gtk::TreeModel::Row row); + + bool _handleButtonEvent(GdkEventButton *event); + bool _handleKeyEvent(GdkEventKey *event); + bool _handleMotionEvent(GdkEventMotion* motion_event); + + void _handleEdited(const Glib::ustring& path, const Glib::ustring& new_text); + void _handleTransparentHover(bool enabled); + void _generateTranslucentItems(SPItem *parent); + + bool select_row( Glib::RefPtr<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b ); + + bool on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &, int, int, guint) override; + bool on_drag_drop(const Glib::RefPtr<Gdk::DragContext> &, int, int, guint) override; + void on_drag_start(const Glib::RefPtr<Gdk::DragContext> &); + void on_drag_end(const Glib::RefPtr<Gdk::DragContext> &) override; + + friend class ObjectWatcher; + + SPItem *_solid_item; + std::list<SPItem *> _translucent_items; +}; + + + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + + +#endif // SEEN_OBJECTS_PANEL_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/ui/dialog/paint-servers.cpp b/src/ui/dialog/paint-servers.cpp new file mode 100644 index 0000000..5fc4895 --- /dev/null +++ b/src/ui/dialog/paint-servers.cpp @@ -0,0 +1,567 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Paint Servers dialog + */ +/* Authors: + * Valentin Ionita + * Rafael Siejakowski <rs@rs-math.net> + * + * Copyright (C) 2019 Valentin Ionita + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include <iostream> +#include <map> + +#include <giomm/listmodel.h> +#include <glibmm/regex.h> +#include <gtkmm/drawingarea.h> +#include <gtkmm/iconview.h> +#include <gtkmm/liststore.h> +#include <gtkmm/stockid.h> +#include <gtkmm/switch.h> + +#include "document.h" +#include "inkscape.h" +#include "paint-servers.h" +#include "path-prefix.h" +#include "style.h" + +#include "io/resource.h" +#include "object/sp-defs.h" +#include "object/sp-hatch.h" +#include "object/sp-pattern.h" +#include "object/sp-root.h" +#include "ui/cache/svg_preview_cache.h" +#include "ui/widget/scrollprotected.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static Glib::ustring const wrapper = R"=====( +<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"> + <defs id="Defs"/> + <rect id="Back" x="0" y="0" width="100px" height="100px" fill="lightgray"/> + <rect id="Rect" x="0" y="0" width="100px" height="100px" stroke="black"/> +</svg> +)====="; + +// Constructor +PaintServersDialog::PaintServersDialog() + : DialogBase("/dialogs/paint", "PaintServers") + , target_selected(true) + , ALLDOCS(_("All paint servers")) + , CURRENTDOC(_("Current document")) + , columns() +{ + current_store = ALLDOCS; + + store[ALLDOCS] = Gtk::ListStore::create(columns); + store[CURRENTDOC] = Gtk::ListStore::create(columns); + + // Grid holding the contents + Gtk::Grid *grid = Gtk::manage(new Gtk::Grid()); + grid->set_margin_start(3); + grid->set_margin_end(3); + grid->set_margin_top(3); + grid->set_row_spacing(3); + pack_start(*grid, Gtk::PACK_EXPAND_WIDGET); + + // Grid row 0 + Gtk::Label *file_label = Gtk::manage(new Gtk::Label(Glib::ustring(_("Server")) + ": ")); + grid->attach(*file_label, 0, 0, 1, 1); + + dropdown = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>()); + dropdown->append(ALLDOCS); + dropdown->append(CURRENTDOC); + dropdown->set_active_text(ALLDOCS); + dropdown->set_hexpand(); + grid->attach(*dropdown, 1, 0, 1, 1); + + // Grid row 1 + Gtk::Label *fill_label = Gtk::manage(new Gtk::Label(Glib::ustring(_("Change")) + ": ")); + grid->attach(*fill_label, 0, 1, 1, 1); + + target_dropdown = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>()); + target_dropdown->append(_("Fill")); + target_dropdown->append(_("Stroke")); + target_dropdown->set_active_text(_("Fill")); + target_dropdown->set_hexpand(); + grid->attach(*target_dropdown, 1, 1, 1, 1); + + // Grid row 2 + icon_view = Gtk::manage(new Gtk::IconView( + static_cast<Glib::RefPtr<Gtk::TreeModel>>(store[current_store]) + )); + icon_view->set_tooltip_column(0); + icon_view->set_pixbuf_column(2); + icon_view->set_size_request(200, -1); + icon_view->show_all_children(); + icon_view->set_selection_mode(Gtk::SELECTION_SINGLE); + icon_view->set_activate_on_single_click(true); + + Gtk::ScrolledWindow *scroller = Gtk::manage(new Gtk::ScrolledWindow()); + scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS); + scroller->set_hexpand(); + scroller->set_vexpand(); + scroller->add(*icon_view); + scroller->set_overlay_scrolling(false); + grid->attach(*scroller, 0, 2, 2, 1); + fix_inner_scroll(scroller); + + // Events + target_dropdown->signal_changed().connect( + sigc::mem_fun(*this, &PaintServersDialog::on_target_changed) + ); + + dropdown->signal_changed().connect([=]() {onPaintSourceDocumentChanged();}); + icon_view->signal_item_activated().connect([=](Gtk::TreeModel::Path const &p) {onPaintClicked(p);}); + + // Get wrapper document (rectangle to fill with paint server). + preview_document = SPDocument::createNewDocFromMem(wrapper.c_str(), wrapper.length(), true); + + SPObject *rect = preview_document->getObjectById("Rect"); + SPObject *defs = preview_document->getObjectById("Defs"); + if (!rect || !defs) { + std::cerr << "PaintServersDialog::PaintServersDialog: Failed to get wrapper defs or rectangle!!" << std::endl; + } + + // Set up preview document. + unsigned key = SPItem::display_key_new(1); + preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + preview_document->ensureUpToDate(); + renderDrawing.setRoot(preview_document->getRoot()->invoke_show(renderDrawing, key, SP_ITEM_SHOW_DISPLAY)); + + _loadStockPaints(); +} + +void PaintServersDialog::documentReplaced() +{ + auto document = getDocument(); + if (!document) { + return; + } + document_map[CURRENTDOC] = document; + _loadFromCurrentDocument(); + _regenerateAll(); +} + +PaintServersDialog::~PaintServersDialog() = default; + +// Get url or color value. +Glib::ustring get_url(Glib::ustring paint) +{ + + Glib::MatchInfo matchInfo; + + // Paint server + static Glib::RefPtr<Glib::Regex> regex1 = Glib::Regex::create(":(url\\(#([A-z0-9\\-_\\.#])*\\))"); + regex1->match(paint, matchInfo); + + if (matchInfo.matches()) { + return matchInfo.fetch(1); + } + + // Color + static Glib::RefPtr<Glib::Regex> regex2 = Glib::Regex::create(":(([A-z0-9#])*)"); + regex2->match(paint, matchInfo); + + if (matchInfo.matches()) { + return matchInfo.fetch(1); + } + + return Glib::ustring(); +} + +// This is too complicated to use selectors! +void recurse_find_paint(SPObject* in, std::vector<Glib::ustring>& list) +{ + + g_return_if_fail(in != nullptr); + + // Add paint servers in <defs> section. + if (dynamic_cast<SPPaintServer *>(in)) { + if (in->getId()) { + // Need to check as one can't construct Glib::ustring with nullptr. + list.push_back (Glib::ustring("url(#") + in->getId() + ")"); + } + // Don't recurse into paint servers. + return; + } + + // Add paint servers referenced by shapes. + if (dynamic_cast<SPShape *>(in)) { + list.push_back (get_url(in->style->fill.write())); + list.push_back (get_url(in->style->stroke.write())); + } + + for (auto child: in->childList(false)) { + recurse_find_paint(child, list); + } +} + +/** Load stock paints from files in share/paint */ +void PaintServersDialog::_loadStockPaints() +{ + std::vector<PaintDescription> paints; + + // Extract out paints from files in share/paint. + for (auto const &path : get_filenames(Inkscape::IO::Resource::PAINT, {".svg"})) { + SPDocument *doc = SPDocument::createNewDoc(path.c_str(), false); + _loadPaintsFromDocument(doc, paints); + } + + _createPaints(paints); +} + +/** Load paint servers from the <defs> of the current document */ +void PaintServersDialog::_loadFromCurrentDocument() +{ + auto document = getDocument(); + if (!document) { + return; + } + + std::vector<PaintDescription> paints; + _loadPaintsFromDocument(document, paints); + + // There can only be one current document, so we clear the corresponding store + store[CURRENTDOC]->clear(); + _createPaints(paints); +} + +/** Creates a collection of paints from the given vector of descriptions */ +void PaintServersDialog::_createPaints(std::vector<PaintDescription> &collection) +{ + // Sort and remove duplicates. + auto paints_cmp = [](PaintDescription const &a, PaintDescription const &b) -> bool { + return a.url < b.url; + }; + std::sort(collection.begin(), collection.end(), paints_cmp); + collection.erase(std::unique(collection.begin(), collection.end()), collection.end()); + + for (auto &paint : collection) { + _instantiatePaint(paint); + } +} + +/** Create a paint from a description and generate its bitmap preview */ +void PaintServersDialog::_instantiatePaint(PaintDescription &paint) +{ + if (store.find(paint.doc_title) == store.end()) { + store[paint.doc_title] = Gtk::ListStore::create(columns); + } + + Glib::ustring id; + paint.bitmap = get_pixbuf(paint.source_document, paint.url, id); + if (!paint.bitmap) { + return; + } + + Gtk::ListStore::iterator iter = store[paint.doc_title]->append(); + (*iter)[columns.id] = id; + (*iter)[columns.paint] = paint.url; + (*iter)[columns.pixbuf] = paint.bitmap; + (*iter)[columns.document] = paint.doc_title; + + if (document_map.find(paint.doc_title) == document_map.end()) { + document_map[paint.doc_title] = paint.source_document; + dropdown->append(paint.doc_title); + } +} + +/** Returns a PaintDescription for a paint already present in the store */ +PaintDescription PaintServersDialog::_descriptionFromIterator(Gtk::ListStore::iterator const &iter) const +{ + Glib::ustring doc_title = (*iter)[columns.document]; + SPDocument *doc_ptr; + try { + doc_ptr = document_map.at(doc_title); + } catch (std::out_of_range &exception) { + doc_ptr = nullptr; + } + Glib::ustring paint_url = (*iter)[columns.paint]; + PaintDescription result(doc_ptr, doc_title, std::move(paint_url)); + + // Fill in fields that are set only on instantiation + result.id = (*iter)[columns.id]; + result.bitmap = (*iter)[columns.pixbuf]; + return result; +} + +/** Regenerates the list of all paint servers from the already loaded paints */ +void PaintServersDialog::_regenerateAll() +{ + // Save active item + bool showing_all = (current_store == ALLDOCS); + Gtk::TreePath active; + if (showing_all) { + std::vector<Gtk::TreePath> selected = icon_view->get_selected_items(); + if (selected.empty()) { + showing_all = false; + } else { + active = selected[0]; + } + } + + std::vector<PaintDescription> all_paints; + + for (auto const &[doc, paint_list] : store) { + if (doc == ALLDOCS) { + continue; // ignore the target store + } + paint_list->foreach_iter([&](Gtk::ListStore::iterator const &paint) -> bool + { + all_paints.push_back(_descriptionFromIterator(paint)); + return false; + }); + } + + // Sort and remove duplicates. When the duplicate entry is from the current document, + // we remove it preferentially, keeping the stock paint if available. + std::sort(all_paints.begin(), all_paints.end(), + [=](PaintDescription const &a, PaintDescription const &b) -> bool + { + return (a.url < b.url) || ((a.url == b.url) && a.doc_title != CURRENTDOC); + }); + all_paints.erase(std::unique(all_paints.begin(), all_paints.end()), all_paints.end()); + + store[ALLDOCS]->clear(); + + // Add paints from the cleaned up list to the store + for (auto &&paint : all_paints) { + Gtk::ListStore::iterator iter = store[ALLDOCS]->append(); + (*iter)[columns.id] = std::move(paint.id); + (*iter)[columns.paint] = std::move(paint.url); + (*iter)[columns.pixbuf] = std::move(paint.bitmap); + (*iter)[columns.document] = std::move(paint.doc_title); + } + + // Restore active item + if (showing_all) { + icon_view->select_path(active); + } +} + +Glib::RefPtr<Gdk::Pixbuf> PaintServersDialog::get_pixbuf(SPDocument *document, Glib::ustring const &paint, + Glib::ustring &id) +{ + + SPObject *rect = preview_document->getObjectById("Rect"); + SPObject *defs = preview_document->getObjectById("Defs"); + + Glib::RefPtr<Gdk::Pixbuf> pixbuf(nullptr); + if (paint.empty()) { + return pixbuf; + } + + // Set style on wrapper + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill", paint.c_str()); + rect->changeCSS(css, "style"); + sp_repr_css_attr_unref(css); + + // Insert paint into defs if required + Glib::MatchInfo matchInfo; + static Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create("url\\(#([A-Za-z0-9#._-]*)\\)"); + regex->match(paint, matchInfo); + if (matchInfo.matches()) { + id = matchInfo.fetch(1); + + // Delete old paint if necessary + std::vector<SPObject *> old_paints = preview_document->getObjectsBySelector("defs > *"); + for (auto paint : old_paints) { + paint->deleteObject(false); + } + + // Add new paint + SPObject *new_paint = document->getObjectById(id); + if (!new_paint) { + std::cerr << "PaintServersDialog::get_pixbuf: cannot find paint server: " << id << std::endl; + return pixbuf; + } + + // Create a copy repr of the paint + Inkscape::XML::Document *xml_doc = preview_document->getReprDoc(); + Inkscape::XML::Node *repr = new_paint->getRepr()->duplicate(xml_doc); + defs->appendChild(repr); + } else { + // Temporary block solid color fills. + return pixbuf; + } + + preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + preview_document->ensureUpToDate(); + + Geom::OptRect dbox = static_cast<SPItem *>(rect)->visualBounds(); + + if (!dbox) { + return pixbuf; + } + + double size = std::max(dbox->width(), dbox->height()); + + pixbuf = Glib::wrap(render_pixbuf(renderDrawing, 1, *dbox, size)); + + return pixbuf; +} + +/** @brief Load paint servers from the given source document + * @param document - the source document + * @param output - the paint descriptions will be added to this vector */ +void PaintServersDialog::_loadPaintsFromDocument(SPDocument *document, std::vector<PaintDescription> &output) +{ + Glib::ustring document_title; + if (!document->getRoot()->title()) { + document_title = CURRENTDOC; + } else { + document_title = Glib::ustring(document->getRoot()->title()); + } + + // Find all paints + std::vector<Glib::ustring> urls; + recurse_find_paint(document->getRoot(), urls); + + for (auto const &url : urls) { + output.emplace_back(document, document_title, std::move(url)); + } +} + +void PaintServersDialog::on_target_changed() +{ + target_selected = !target_selected; +} + +/** Handles the change of the dropdown for selecting paint sources */ +void PaintServersDialog::onPaintSourceDocumentChanged() +{ + current_store = dropdown->get_active_text(); + icon_view->set_model(store[current_store]); +} + +/** Event handler for when a paint entry in the dialog has been activated */ +void PaintServersDialog::onPaintClicked(Gtk::TreeModel::Path const &path) +{ + // Get the current selected elements + Selection *selection = getSelection(); + std::vector<SPObject*> const selected_items(selection->items().begin(), selection->items().end()); + + if (selected_items.empty()) { + return; + } + + Gtk::ListStore::iterator iter = store[current_store]->get_iter(path); + Glib::ustring id = (*iter)[columns.id]; + Glib::ustring paint = (*iter)[columns.paint]; + Glib::RefPtr<Gdk::Pixbuf> pixbuf = (*iter)[columns.pixbuf]; + Glib::ustring hatches_document_title = (*iter)[columns.document]; + SPDocument *hatches_document = document_map[hatches_document_title]; + SPObject *paint_server = hatches_document->getObjectById(id); + + bool paint_server_exists = false; + for (auto const &server : store[CURRENTDOC]->children()) { + if (server[columns.id] == id) { + paint_server_exists = true; + break; + } + } + + SPDocument *document = getDocument(); + if (!paint_server_exists) { + // Add the paint server to the current document definition + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *repr = paint_server->getRepr()->duplicate(xml_doc); + document->getDefs()->appendChild(repr); + Inkscape::GC::release(repr); + + // Add the pixbuf to the current document store + iter = store[CURRENTDOC]->append(); + (*iter)[columns.id] = id; + (*iter)[columns.paint] = paint; + (*iter)[columns.pixbuf] = pixbuf; + (*iter)[columns.document] = CURRENTDOC; + } + + // Recursively find elements in groups, if any + std::vector<SPObject*> items; + for (auto item : selected_items) { + std::vector<SPObject*> current_items = extract_elements(item); + items.insert(std::end(items), std::begin(current_items), std::end(current_items)); + } + + for (auto item : items) { + item->style->getFillOrStroke(target_selected)->read(paint.c_str()); + item->updateRepr(); + } + + _cleanupUnused(); +} + +/** Cleans up paints that aren't used in the document anymore and updates our store accordingly */ +void PaintServersDialog::_cleanupUnused() +{ + auto doc = getDocument(); + if (!doc) { + return; + } + doc->collectOrphans(); + + // We check if the removal of orphans deleted some paints for which we're still + // holding representations in the dialog. If that happened, we must remove these + // entries from our list store. + std::vector<Gtk::ListStore::Path> removed; + + store[CURRENTDOC]->foreach( + [=, &removed](const Gtk::ListStore::Path &path, const Gtk::ListStore::iterator &it) -> bool + { + if (!doc->getObjectById((*it)[columns.id])) { + removed.push_back(path); + } + return false; + } + ); + + for (auto const &path : removed) { + store[CURRENTDOC]->erase(store[CURRENTDOC]->get_iter(path)); + } + + if (!removed.empty()) { + _regenerateAll(); + } +} + +/** Recursively extracts elements from groups, if any */ +std::vector<SPObject*> PaintServersDialog::extract_elements(SPObject* item) +{ + std::vector<SPObject*> elements; + std::vector<SPObject*> children = item->childList(false); + if (!children.size()) { + elements.push_back(item); + } else { + for (auto e : children) { + std::vector<SPObject*> current_items = extract_elements(e); + elements.insert(std::end(elements), std::begin(current_items), std::end(current_items)); + } + } + + return elements; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + 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/ui/dialog/paint-servers.h b/src/ui/dialog/paint-servers.h new file mode 100644 index 0000000..80e2c0f --- /dev/null +++ b/src/ui/dialog/paint-servers.h @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Paint Servers dialog + */ +/* Authors: + * Valentin Ionita + * Rafael Siejakowski <rs@rs-math.net> + * + * Copyright (C) 2019 Valentin Ionita + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_PAINT_SERVERS_H +#define INKSCAPE_UI_DIALOG_PAINT_SERVERS_H + +#include <glibmm/i18n.h> +#include <gtkmm.h> + +#include "display/drawing.h" +#include "ui/dialog/dialog-base.h" + +class SPObject; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class PaintServersColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + Gtk::TreeModelColumn<Glib::ustring> id; + Gtk::TreeModelColumn<Glib::ustring> paint; + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> pixbuf; + Gtk::TreeModelColumn<Glib::ustring> document; + + PaintServersColumns() + { + add(id); + add(paint); + add(pixbuf); + add(document); + } +}; + +struct PaintDescription +{ + /** Pointer to the document from which the paint originates */ + SPDocument *source_document = nullptr; + + /** Title of the document from which the paint originates, or "Current document" */ + Glib::ustring doc_title; + + /** ID of the the paint server within the document */ + Glib::ustring id; + + /** URL of the paint within the document */ + Glib::ustring url; + + /** Bitmap preview of the paint */ + Glib::RefPtr<Gdk::Pixbuf> bitmap; + + PaintDescription(SPDocument *source_doc, Glib::ustring title, Glib::ustring const &&paint_url) + : source_document{source_doc} + , doc_title{std::move(title)} + , id{} // id will be filled in when generating the bitmap + , url{paint_url} + , bitmap{nullptr} + {} + + /** Two paints are considered the same if they have the same urls */ + bool operator==(PaintDescription const &other) const { return url == other.url; } +}; + +/** + * This dialog serves as a preview for different types of paint servers, + * currently only predefined. It can set the fill or stroke of the selected + * object to the to the paint server you select. + * + * Patterns and hatches are loaded from the preferences paths and displayed + * for each document, for all documents and for the current document. + */ + +class PaintServersDialog : public DialogBase +{ +public: + ~PaintServersDialog() override; + static PaintServersDialog &getInstance() { return *new PaintServersDialog(); } + + void documentReplaced() override; + +private: + // No default constructor, noncopyable, nonassignable + PaintServersDialog(); + PaintServersDialog(PaintServersDialog const &d) = delete; + PaintServersDialog operator=(PaintServersDialog const &d) = delete; + + void _cleanupUnused(); + void _createPaints(std::vector<PaintDescription> &collection); + PaintDescription _descriptionFromIterator(Gtk::ListStore::iterator const &iter) const; + std::vector<SPObject *> extract_elements(SPObject *item); + Glib::RefPtr<Gdk::Pixbuf> get_pixbuf(SPDocument *, Glib::ustring const &, Glib::ustring &); + void _instantiatePaint(PaintDescription &paint); + void _loadFromCurrentDocument(); + void _loadPaintsFromDocument(SPDocument *document, std::vector<PaintDescription> &output); + void _loadStockPaints(); + void _regenerateAll(); + void onPaintClicked(const Gtk::TreeModel::Path &path); + void onPaintSourceDocumentChanged(); + void on_target_changed(); + + bool target_selected; ///< whether setting fill (true) or stroke (false) + const Glib::ustring ALLDOCS; + const Glib::ustring CURRENTDOC; + std::map<Glib::ustring, Glib::RefPtr<Gtk::ListStore>> store; + Glib::ustring current_store; + std::map<Glib::ustring, SPDocument *> document_map; + SPDocument *preview_document; + Inkscape::Drawing renderDrawing; + Gtk::ComboBoxText *dropdown; + Gtk::IconView *icon_view; + Gtk::ComboBoxText *target_dropdown; + PaintServersColumns const columns; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // SEEN INKSCAPE_UI_DIALOG_PAINT_SERVERS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/polar-arrange-tab.cpp b/src/ui/dialog/polar-arrange-tab.cpp new file mode 100644 index 0000000..f3f645f --- /dev/null +++ b/src/ui/dialog/polar-arrange-tab.cpp @@ -0,0 +1,409 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @brief Arranges Objects into a Circle/Ellipse + */ +/* Authors: + * Declara Denis + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gtkmm/messagedialog.h> + +#include <2geom/transforms.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "preferences.h" + +#include "object/sp-ellipse.h" +#include "object/sp-item-transform.h" + +#include "ui/dialog/polar-arrange-tab.h" +#include "ui/dialog/tile.h" +#include "ui/icon-names.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +PolarArrangeTab::PolarArrangeTab(ArrangeDialog *parent_) + : parent(parent_), + parametersTable(), + centerY("", C_("Polar arrange tab", "Y coordinate of the center"), UNIT_TYPE_LINEAR), + centerX("", C_("Polar arrange tab", "X coordinate of the center"), centerY), + radiusY("", C_("Polar arrange tab", "Y coordinate of the radius"), UNIT_TYPE_LINEAR), + radiusX("", C_("Polar arrange tab", "X coordinate of the radius"), radiusY), + angleY("", C_("Polar arrange tab", "Ending angle"), UNIT_TYPE_RADIAL), + angleX("", C_("Polar arrange tab", "Starting angle"), angleY) +{ + anchorPointLabel.set_text(C_("Polar arrange tab", "Anchor point:")); + anchorPointLabel.set_halign(Gtk::ALIGN_START); + pack_start(anchorPointLabel, false, false); + + anchorBoundingBoxRadio.set_label(C_("Polar arrange tab", "Objects' bounding boxes:")); + anchorRadioGroup = anchorBoundingBoxRadio.get_group(); + anchorBoundingBoxRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_anchor_radio_changed)); + pack_start(anchorBoundingBoxRadio, false, false); + + pack_start(anchorSelector, false, false); + + anchorObjectPivotRadio.set_label(C_("Polar arrange tab", "Objects' rotational centers")); + anchorObjectPivotRadio.set_group(anchorRadioGroup); + anchorObjectPivotRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_anchor_radio_changed)); + pack_start(anchorObjectPivotRadio, false, false); + + arrangeOnLabel.set_text(C_("Polar arrange tab", "Arrange on:")); + arrangeOnLabel.set_halign(Gtk::ALIGN_START); + pack_start(arrangeOnLabel, false, false); + + arrangeOnFirstCircleRadio.set_label(C_("Polar arrange tab", "First selected circle/ellipse/arc")); + arrangeRadioGroup = arrangeOnFirstCircleRadio.get_group(); + arrangeOnFirstCircleRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed)); + pack_start(arrangeOnFirstCircleRadio, false, false); + + arrangeOnLastCircleRadio.set_label(C_("Polar arrange tab", "Last selected circle/ellipse/arc")); + arrangeOnLastCircleRadio.set_group(arrangeRadioGroup); + arrangeOnLastCircleRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed)); + pack_start(arrangeOnLastCircleRadio, false, false); + + arrangeOnParametersRadio.set_label(C_("Polar arrange tab", "Parameterized:")); + arrangeOnParametersRadio.set_group(arrangeRadioGroup); + arrangeOnParametersRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed)); + pack_start(arrangeOnParametersRadio, false, false); + + centerLabel.set_text(C_("Polar arrange tab", "Center X/Y:")); + parametersTable.attach(centerLabel, 0, 0, 1, 1); + centerX.setDigits(2); + centerX.setIncrements(0.2, 0); + centerX.setRange(-10000, 10000); + centerX.setValue(0, "px"); + centerY.setDigits(2); + centerY.setIncrements(0.2, 0); + centerY.setRange(-10000, 10000); + centerY.setValue(0, "px"); + parametersTable.attach(centerX, 1, 0, 1, 1); + parametersTable.attach(centerY, 2, 0, 1, 1); + + radiusLabel.set_text(C_("Polar arrange tab", "Radius X/Y:")); + parametersTable.attach(radiusLabel, 0, 1, 1, 1); + radiusX.setDigits(2); + radiusX.setIncrements(0.2, 0); + radiusX.setRange(0.001, 10000); + radiusX.setValue(100, "px"); + radiusY.setDigits(2); + radiusY.setIncrements(0.2, 0); + radiusY.setRange(0.001, 10000); + radiusY.setValue(100, "px"); + parametersTable.attach(radiusX, 1, 1, 1, 1); + parametersTable.attach(radiusY, 2, 1, 1, 1); + + angleLabel.set_text(_("Angle X/Y:")); + parametersTable.attach(angleLabel, 0, 2, 1, 1); + angleX.setDigits(2); + angleX.setIncrements(0.2, 0); + angleX.setRange(-10000, 10000); + angleX.setValue(0, "°"); + angleY.setDigits(2); + angleY.setIncrements(0.2, 0); + angleY.setRange(-10000, 10000); + angleY.setValue(180, "°"); + parametersTable.attach(angleX, 1, 2, 1, 1); + parametersTable.attach(angleY, 2, 2, 1, 1); + parametersTable.set_row_spacing(4); + parametersTable.set_column_spacing(4); + pack_start(parametersTable, false, false); + + rotateObjectsCheckBox.set_label(_("Rotate objects")); + rotateObjectsCheckBox.set_active(true); + pack_start(rotateObjectsCheckBox, false, false); + + centerX.set_sensitive(false); + centerY.set_sensitive(false); + angleX.set_sensitive(false); + angleY.set_sensitive(false); + radiusX.set_sensitive(false); + radiusY.set_sensitive(false); + + set_border_width(4); + + parametersTable.show_all(); + parametersTable.set_no_show_all(); + parametersTable.hide(); +} + +/** + * This function rotates an item around a given point by a given amount + * @param item item to rotate + * @param center center of the rotation to perform + * @param rotation amount to rotate the object by + */ +static void rotateAround(SPItem *item, Geom::Point center, Geom::Rotate const &rotation) +{ + Geom::Translate const s(center); + Geom::Affine affine = Geom::Affine(s).inverse() * Geom::Affine(rotation) * Geom::Affine(s); + + // Save old center + center = item->getCenter(); + + item->set_i2d_affine(item->i2dt_affine() * affine); + item->doWriteTransform(item->transform); + + if(item->isCenterSet()) + { + item->setCenter(center * affine); + item->updateRepr(); + } +} + +/** + * Calculates the angle at which to put an object given the total amount + * of objects, the index of the objects as well as the arc start and end + * points + * @param arcBegin angle at which the arc begins + * @param arcEnd angle at which the arc ends + * @param count number of objects in the selection + * @param n index of the object in the selection + */ +static float calcAngle(float arcBegin, float arcEnd, int count, int n) +{ + float arcLength = arcEnd - arcBegin; + float delta = std::abs(std::abs(arcLength) - 2*M_PI); + if(delta > 0.01) count--; // If not a complete circle, put an object also at the extremes of the arc; + + float angle = n / (float)count; + // Normalize for arcLength: + angle = angle * arcLength; + angle += arcBegin; + + return angle; +} + +/** + * Calculates the point at which an object needs to be, given the center of the ellipse, + * it's radius (x and y), as well as the angle + */ +static Geom::Point calcPoint(float cx, float cy, float rx, float ry, float angle) +{ + return Geom::Point(cx + cos(angle) * rx, cy + sin(angle) * ry); +} + +/** + * Returns the selected anchor point in desktop coordinates. If anchor + * is 0 to 8, then a bounding box point has been chosen. If it is 9 however + * the rotational center is chosen. + */ +static Geom::Point getAnchorPoint(int anchor, SPItem *item) +{ + Geom::Point source; + + Geom::OptRect bbox = item->documentVisualBounds(); + + switch(anchor) + { + case 0: // Top - Left + case 3: // Middle - Left + case 6: // Bottom - Left + source[0] = bbox->min()[Geom::X]; + break; + case 1: // Top - Middle + case 4: // Middle - Middle + case 7: // Bottom - Middle + source[0] = (bbox->min()[Geom::X] + bbox->max()[Geom::X]) / 2.0f; + break; + case 2: // Top - Right + case 5: // Middle - Right + case 8: // Bottom - Right + source[0] = bbox->max()[Geom::X]; + break; + }; + + switch(anchor) + { + case 0: // Top - Left + case 1: // Top - Middle + case 2: // Top - Right + source[1] = bbox->min()[Geom::Y]; + break; + case 3: // Middle - Left + case 4: // Middle - Middle + case 5: // Middle - Right + source[1] = (bbox->min()[Geom::Y] + bbox->max()[Geom::Y]) / 2.0f; + break; + case 6: // Bottom - Left + case 7: // Bottom - Middle + case 8: // Bottom - Right + source[1] = bbox->max()[Geom::Y]; + break; + }; + + // If using center + if(anchor == 9) + source = item->getCenter(); + else + { + source *= item->document->doc2dt(); + } + + return source; +} + +/** + * Moves an SPItem to a given location, the location is based on the given anchor point. + * @param anchor 0 to 8 are the various bounding box points like follows: + * 0 1 2 + * 3 4 5 + * 6 7 8 + * Anchor mode 9 is the rotational center of the object + * @param item Item to move + * @param p point at which to move the object + */ +static void moveToPoint(int anchor, SPItem *item, Geom::Point p) +{ + item->move_rel(Geom::Translate(p - getAnchorPoint(anchor, item))); +} + +void PolarArrangeTab::arrange() +{ + Inkscape::Selection *selection = parent->getDesktop()->getSelection(); + const std::vector<SPItem*> tmp(selection->items().begin(), selection->items().end()); + SPGenericEllipse *referenceEllipse = nullptr; // Last ellipse in selection + + bool arrangeOnEllipse = !arrangeOnParametersRadio.get_active(); + bool arrangeOnFirstEllipse = arrangeOnEllipse && arrangeOnFirstCircleRadio.get_active(); + float yaxisdir = parent->getDesktop()->yaxisdir(); + + int count = 0; + for(auto item : tmp) + { + if(arrangeOnEllipse) + { + if(!arrangeOnFirstEllipse) + { + if(SP_IS_GENERICELLIPSE(item)) + referenceEllipse = SP_GENERICELLIPSE(item); + } else { + if(SP_IS_GENERICELLIPSE(item) && referenceEllipse == nullptr) + referenceEllipse = SP_GENERICELLIPSE(item); + } + } + ++count; + } + + float cx, cy; // Center of the ellipse + float rx, ry; // Radiuses of the ellipse in x and y direction + float arcBeg, arcEnd; // begin and end angles for arcs + Geom::Affine transformation; // Any additional transformation to apply to the objects + + if(arrangeOnEllipse) + { + if(referenceEllipse == nullptr) + { + Gtk::MessageDialog dialog(_("Couldn't find an ellipse in selection"), false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_CLOSE, true); + dialog.run(); + return; + } else { + cx = referenceEllipse->cx.value; + cy = referenceEllipse->cy.value; + rx = referenceEllipse->rx.value; + ry = referenceEllipse->ry.value; + arcBeg = referenceEllipse->start; + arcEnd = referenceEllipse->end; + + transformation = referenceEllipse->i2dt_affine(); + + // We decrement the count by 1 as we are not going to lay + // out the reference ellipse + --count; + } + + } else { + // Read options from UI + cx = centerX.getValue("px"); + cy = centerY.getValue("px"); + rx = radiusX.getValue("px"); + ry = radiusY.getValue("px"); + arcBeg = angleX.getValue("rad"); + arcEnd = angleY.getValue("rad") * yaxisdir; + transformation.setIdentity(); + referenceEllipse = nullptr; + } + + int anchor = 9; + if(anchorBoundingBoxRadio.get_active()) + { + anchor = anchorSelector.getHorizontalAlignment() + + anchorSelector.getVerticalAlignment() * 3; + } + + Geom::Point realCenter = Geom::Point(cx, cy) * transformation; + + int i = 0; + for(auto item : tmp) + { + // Ignore the reference ellipse if any + if(item != referenceEllipse) + { + float angle = calcAngle(arcBeg, arcEnd, count, i); + Geom::Point newLocation = calcPoint(cx, cy, rx, ry, angle) * transformation; + + moveToPoint(anchor, item, newLocation); + + if(rotateObjectsCheckBox.get_active()) { + // Calculate the angle by which to rotate each object + angle = -atan2f(-yaxisdir * (newLocation.x() - realCenter.x()), -yaxisdir * (newLocation.y() - realCenter.y())); + rotateAround(item, newLocation, Geom::Rotate(angle)); + } + + ++i; + } + } + + DocumentUndo::done(parent->getDesktop()->getDocument(), _("Arrange on ellipse"), INKSCAPE_ICON("dialog-align-and-distribute")); +} + +void PolarArrangeTab::updateSelection() +{ +} + +void PolarArrangeTab::on_arrange_radio_changed() +{ + bool arrangeParametric = arrangeOnParametersRadio.get_active(); + + centerX.set_sensitive(arrangeParametric); + centerY.set_sensitive(arrangeParametric); + + angleX.set_sensitive(arrangeParametric); + angleY.set_sensitive(arrangeParametric); + + radiusX.set_sensitive(arrangeParametric); + radiusY.set_sensitive(arrangeParametric); + + parametersTable.set_visible(arrangeParametric); +} + +void PolarArrangeTab::on_anchor_radio_changed() +{ + bool anchorBoundingBox = anchorBoundingBoxRadio.get_active(); + + anchorSelector.set_sensitive(anchorBoundingBox); +} + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/polar-arrange-tab.h b/src/ui/dialog/polar-arrange-tab.h new file mode 100644 index 0000000..0c5acde --- /dev/null +++ b/src/ui/dialog/polar-arrange-tab.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @brief Arranges Objects into a Circle/Ellipse + */ +/* Authors: + * Declara Denis + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_POLAR_ARRANGE_TAB_H +#define INKSCAPE_UI_DIALOG_POLAR_ARRANGE_TAB_H + +#include "ui/widget/scalar-unit.h" +#include "ui/widget/anchor-selector.h" +#include "ui/dialog/arrange-tab.h" + +#include <gtkmm/radiobutton.h> +#include <gtkmm/radiobuttongroup.h> +#include <gtkmm/grid.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ArrangeDialog; + +/** + * PolarArrangeTab is a Tab displayed in the Arrange dialog and contains + * enables the user to arrange objects on a circular or elliptical shape + */ +class PolarArrangeTab : public ArrangeTab { +public: + PolarArrangeTab(ArrangeDialog *parent_); + ~PolarArrangeTab() override = default;; + + /** + * Do the actual arrangement + */ + void arrange() override; + + /** + * Respond to selection change + */ + void updateSelection(); + + void on_anchor_radio_changed(); + void on_arrange_radio_changed(); + +private: + PolarArrangeTab(PolarArrangeTab const &d) = delete; // no copy + void operator=(PolarArrangeTab const &d) = delete; // no assign + + ArrangeDialog *parent; + + Gtk::Label anchorPointLabel; + + Gtk::RadioButtonGroup anchorRadioGroup; + Gtk::RadioButton anchorBoundingBoxRadio; + Gtk::RadioButton anchorObjectPivotRadio; + Inkscape::UI::Widget::AnchorSelector anchorSelector; + + Gtk::Label arrangeOnLabel; + + Gtk::RadioButtonGroup arrangeRadioGroup; + Gtk::RadioButton arrangeOnFirstCircleRadio; + Gtk::RadioButton arrangeOnLastCircleRadio; + Gtk::RadioButton arrangeOnParametersRadio; + + Gtk::Grid parametersTable; + + Gtk::Label centerLabel; + Inkscape::UI::Widget::ScalarUnit centerY; + Inkscape::UI::Widget::ScalarUnit centerX; + + Gtk::Label radiusLabel; + Inkscape::UI::Widget::ScalarUnit radiusY; + Inkscape::UI::Widget::ScalarUnit radiusX; + + Gtk::Label angleLabel; + Inkscape::UI::Widget::ScalarUnit angleY; + Inkscape::UI::Widget::ScalarUnit angleX; + + Gtk::CheckButton rotateObjectsCheckBox; + + +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif /* INKSCAPE_UI_DIALOG_POLAR_ARRANGE_TAB_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/ui/dialog/print.cpp b/src/ui/dialog/print.cpp new file mode 100644 index 0000000..0cbc49c --- /dev/null +++ b/src/ui/dialog/print.cpp @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Print dialog. + */ +/* Authors: + * Kees Cook <kees@outflux.net> + * Abhishek Sharma + * Patrick McDermott + * + * Copyright (C) 2007 Kees Cook + * Copyright (C) 2017 Patrick McDermott + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> + +#include <gtkmm.h> + +#include "inkscape.h" +#include "preferences.h" +#include "print.h" + +#include "extension/internal/cairo-render-context.h" +#include "extension/internal/cairo-renderer.h" +#include "document.h" + +#include "util/units.h" +#include "helper/png-write.h" +#include "svg/svg-color.h" + +#include <glibmm/i18n.h> + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +Print::Print(SPDocument *doc, SPItem *base) : + _doc (doc), + _base (base) +{ + g_assert (_doc); + g_assert (_base); + + _printop = Gtk::PrintOperation::create(); + + // set up dialog title, based on document name + const Glib::ustring jobname = _doc->getDocumentName() ? _doc->getDocumentName() : _("SVG Document"); + Glib::ustring title = _("Print"); + title += " "; + title += jobname; + _printop->set_job_name(title); + + _printop->set_unit(Gtk::UNIT_POINTS); + Glib::RefPtr<Gtk::PageSetup> page_setup = Gtk::PageSetup::create(); + + // Default to a custom paper size, in case we can't find a more specific size + gdouble doc_width = _doc->getWidth().value("pt"); + gdouble doc_height = _doc->getHeight().value("pt"); + page_setup->set_paper_size( + Gtk::PaperSize("custom", "custom", doc_width, doc_height, Gtk::UNIT_POINTS)); + + // Some print drivers, like the EPSON's ESC/P-R CUPS driver, don't accept custom + // page sizes, so we'll try to find a known page size. + // GTK+'s known paper sizes always have a longer height than width, so we'll rotate + // the page and set its orientation to landscape as necessary in order to match a paper size. + // Unfortunately, some printers, like Epilog laser cutters, don't understand landscape + // mode. + // As a compromise, we'll only rotate the page if we actually find a matching paper size, + // since laser cutter beds tend to be custom sizes. + Gtk::PageOrientation orientation = Gtk::PAGE_ORIENTATION_PORTRAIT; + if (_doc->getWidth().value("pt") > _doc->getHeight().value("pt")) { + orientation = Gtk::PAGE_ORIENTATION_REVERSE_LANDSCAPE; + std::swap(doc_width, doc_height); + } + + // attempt to match document size against known paper sizes + std::vector<Gtk::PaperSize> known_sizes = Gtk::PaperSize::get_paper_sizes(false); + for (auto& size : known_sizes) { + if (fabs(size.get_width(Gtk::UNIT_POINTS) - doc_width) >= 1.0) { + // width (short edge) doesn't match + continue; + } + if (fabs(size.get_height(Gtk::UNIT_POINTS) - doc_height) >= 1.0) { + // height (short edge) doesn't match + continue; + } + // size matches + page_setup->set_paper_size(size); + page_setup->set_orientation(orientation); + break; + } + + _printop->set_default_page_setup(page_setup); + _printop->set_use_full_page(true); + + // set up signals + _workaround._doc = _doc; + _workaround._base = _base; + _workaround._tab = &_tab; + _printop->signal_create_custom_widget().connect(sigc::mem_fun(*this, &Print::create_custom_widget)); + _printop->signal_begin_print().connect(sigc::mem_fun(*this, &Print::begin_print)); + _printop->signal_draw_page().connect(sigc::mem_fun(*this, &Print::draw_page)); + + // build custom preferences tab + _printop->set_custom_tab_label(_("Rendering")); +} + +void Print::draw_page(const Glib::RefPtr<Gtk::PrintContext>& context, int /*page_nr*/) +{ + // TODO: If the user prints multiple copies we render the whole page for each copy + // It would be more efficient to render the page once (e.g. in "begin_print") + // and simply print this result as often as necessary + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + //printf("%s %d\n",__FUNCTION__, page_nr); + + if (_workaround._tab->as_bitmap()) { + // Render as exported PNG + prefs->setBool("/dialogs/printing/asbitmap", true); + gdouble width = (_workaround._doc)->getWidth().value("px"); + gdouble height = (_workaround._doc)->getHeight().value("px"); + gdouble dpi = _workaround._tab->bitmap_dpi(); + prefs->setDouble("/dialogs/printing/dpi", dpi); + + std::string tmp_png; + std::string tmp_base = "inkscape-print-png-XXXXXX"; + + int tmp_fd; + if ( (tmp_fd = Glib::file_open_tmp(tmp_png, tmp_base)) >= 0) { + close(tmp_fd); + + guint32 bgcolor = 0x00000000; + Inkscape::XML::Node *nv = _workaround._doc->getReprNamedView(); + if (nv && nv->attribute("pagecolor")){ + bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00); + } + if (nv && nv->attribute("inkscape:pageopacity")){ + double opacity = nv->getAttributeDouble("inkscape:pageopacity", 1.0); + bgcolor |= SP_COLOR_F_TO_U(opacity); + } + + sp_export_png_file(_workaround._doc, tmp_png.c_str(), 0.0, 0.0, + width, height, + (unsigned long)(Inkscape::Util::Quantity::convert(width, "px", "in") * dpi), + (unsigned long)(Inkscape::Util::Quantity::convert(height, "px", "in") * dpi), + dpi, dpi, bgcolor, nullptr, nullptr, true, std::vector<SPItem*>()); + + // This doesn't seem to work: + //context->set_cairo_context ( Cairo::Context::create (Cairo::ImageSurface::create_from_png (tmp_png) ), dpi, dpi ); + // + // so we'll use a surface pattern blat instead... + // + // but the C++ interface isn't implemented in cairomm: + //context->get_cairo_context ()->set_source_surface(Cairo::ImageSurface::create_from_png (tmp_png) ); + // + // so do it in C: + { + auto png = Cairo::ImageSurface::create_from_png(tmp_png); + auto pattern = Cairo::SurfacePattern::create(png); + auto cr = context->get_cairo_context(); + auto m = cr->get_matrix(); + cr->scale(Inkscape::Util::Quantity::convert(1, "in", "pt") / dpi, + Inkscape::Util::Quantity::convert(1, "in", "pt") / dpi); + // FIXME: why is the origin offset?? + cr->set_source(pattern); + cr->paint(); + cr->set_matrix(m); + } + + // Clean up + unlink (tmp_png.c_str()); + } + else { + g_warning("%s", _("Could not open temporary PNG for bitmap printing")); + } + } + else { + // Render as vectors + prefs->setBool("/dialogs/printing/asbitmap", false); + Inkscape::Extension::Internal::CairoRenderer renderer; + Inkscape::Extension::Internal::CairoRenderContext *ctx = renderer.createContext(); + + // ctx->setPSLevel(CAIRO_PS_LEVEL_3); + ctx->setTextToPath(false); + ctx->setFilterToBitmap(true); + ctx->setBitmapResolution(72); + + auto cr = context->get_cairo_context(); + auto surface = cr->get_target(); + auto ctm = cr->get_matrix(); + + bool ret = ctx->setSurfaceTarget(surface->cobj(), true, &ctm); + if (ret) { + ret = renderer.setupDocument (ctx, _workaround._doc, TRUE, 0., nullptr); + if (ret) { + renderer.renderItem(ctx, _workaround._base); + ctx->finish(false); // do not finish the cairo_surface_t - it's owned by our GtkPrintContext! + } + else { + g_warning("%s", _("Could not set up Document")); + } + } + else { + g_warning("%s", _("Failed to set CairoRenderContext")); + } + + // Clean up + renderer.destroyContext(ctx); + } + +} + +Gtk::Widget *Print::create_custom_widget() +{ + //printf("%s\n",__FUNCTION__); + return &_tab; +} + +void Print::begin_print(const Glib::RefPtr<Gtk::PrintContext>&) +{ + //printf("%s\n",__FUNCTION__); + _printop->set_n_pages(1); +} + +Gtk::PrintOperationResult Print::run(Gtk::PrintOperationAction, Gtk::Window &parent_window) +{ + // Remember to restore the previous print settings + _printop->set_print_settings(SP_ACTIVE_DESKTOP->printer_settings._gtk_print_settings); + + try { + Gtk::PrintOperationResult res = _printop->run(Gtk::PRINT_OPERATION_ACTION_PRINT_DIALOG, parent_window); + + // Save printer settings (but only on success) + if (res == Gtk::PRINT_OPERATION_RESULT_APPLY) { + SP_ACTIVE_DESKTOP->printer_settings._gtk_print_settings = _printop->get_print_settings(); + } + + return res; + } catch (const Glib::Error &e) { + g_warning("Failed to print '%s': %s", _doc->getDocumentName(), e.what().c_str()); + } + + return Gtk::PRINT_OPERATION_RESULT_ERROR; +} + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/print.h b/src/ui/dialog/print.h new file mode 100644 index 0000000..d015210 --- /dev/null +++ b/src/ui/dialog/print.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Print dialog + */ +/* Authors: + * Kees Cook <kees@outflux.net> + * + * Copyright (C) 2007 Kees Cook + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_PRINT_H +#define INKSCAPE_UI_DIALOG_PRINT_H + +#include "ui/widget/rendering-options.h" +#include <gtkmm/printoperation.h> // GtkMM + +class SPItem; +class SPDocument; + + +/* + * gtk 2.12.0 has a bug (http://bugzilla.gnome.org/show_bug.cgi?id=482089) + * where it fails to correctly deal with gtkmm signal management. As a result + * we have call gtk directly instead of doing a much cleaner version of + * this printing dialog, using full gtkmmification. (The bug was fixed + * in 2.12.1, so when the Inkscape gtk minimum version is bumped there, + * we can revert Inkscape commit 16865. + */ +struct workaround_gtkmm +{ + SPDocument *_doc; + SPItem *_base; + Inkscape::UI::Widget::RenderingOptions *_tab; +}; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +struct PrinterSettings { + Glib::RefPtr<Gtk::PrintSettings> _gtk_print_settings; +}; + +class Print { +public: + Print(SPDocument *doc, SPItem *base); + Gtk::PrintOperationResult run(Gtk::PrintOperationAction, Gtk::Window &parent_window); + +protected: + +private: + Glib::RefPtr<Gtk::PrintOperation> _printop; + SPDocument *_doc; + SPItem *_base; + Inkscape::UI::Widget::RenderingOptions _tab; + + struct workaround_gtkmm _workaround; + + void draw_page(const Glib::RefPtr<Gtk::PrintContext>& context, int /*page_nr*/); + Gtk::Widget *create_custom_widget(); + void begin_print(const Glib::RefPtr<Gtk::PrintContext>&); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_PRINT_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/ui/dialog/prototype.cpp b/src/ui/dialog/prototype.cpp new file mode 100644 index 0000000..97649c8 --- /dev/null +++ b/src/ui/dialog/prototype.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A bare minimum example of deriving from Inkscape::UI:Widget::Panel. + * + * Author: + * Tavmjong Bah + * + * Copyright (C) Tavmjong Bah <tavmjong@free.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef DEBUG + +#include "prototype.h" + +#include "document.h" +#include "inkscape-application.h" + +// Only for use in demonstration widget. +#include "object/sp-root.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +Prototype::Prototype() + : DialogBase("/dialogs/prototype", "Prototype") +{ + // A widget for demonstration that displays the current SVG's id. + _label = Gtk::manage(new Gtk::Label(_name)); + _label->set_line_wrap(); + + _debug_button.set_name("PrototypeDebugButton"); + _debug_button.set_hexpand(); + _debug_button.signal_clicked().connect(sigc::mem_fun(*this, &Prototype::on_click)); + + _debug_button.add(*_label); + add(_debug_button); +} + +void Prototype::documentReplaced() +{ + if (document && document->getRoot()) { + const gchar *root_id = document->getRoot()->getId(); + Glib::ustring label_string("Document's SVG id: "); + label_string += (root_id ? root_id : "null"); + _label->set_label(label_string); + } +} + +void Prototype::selectionChanged(Inkscape::Selection *selection) +{ + if (!selection) { + return; + } + + // Update demonstration widget. + Glib::ustring label = _label->get_text() + "\nSelection changed to "; + SPObject* object = selection->single(); + if (object) { + label = label + object->getId(); + } else { + object = selection->activeContext(); + + if (object) { + label = label + object->getId(); + } else { + label = label + "unknown"; + } + } + + _label->set_label(label); +} + +void Prototype::on_click() +{ + Gtk::Window *window = dynamic_cast<Gtk::Window *>(get_toplevel()); + if (window) { + std::cout << "Dialog is part of: " << window->get_name() << " (" << window->get_title() << ")" << std::endl; + } else { + std::cerr << "Prototype::on_click(): Dialog not attached to window!" << std::endl; + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // DEBUG + +/* + 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/ui/dialog/prototype.h b/src/ui/dialog/prototype.h new file mode 100644 index 0000000..9bf5b59 --- /dev/null +++ b/src/ui/dialog/prototype.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A bare minimum example of deriving from Inkscape::UI:Widget::Panel. + * + * Author: + * Tavmjong Bah + * + * Copyright (C) Tavmjong Bah <tavmjong@free.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_PROTOTYPE_PANEL_H +#define SEEN_PROTOTYPE_PANEL_H + +#ifdef DEBUG + +#include <iostream> + +#include "selection.h" +#include "ui/dialog/dialog-base.h" + +// Only to display status. +#include <gtkmm/label.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A panel that does almost nothing! + */ +class Prototype : public DialogBase +{ +public: + ~Prototype() override { std::cout << "Prototype::~Prototype()" << std::endl; } + static Prototype &getInstance() { return *new Prototype(); } + + void documentReplaced(SPDocument *document) override; + void selectionChanged(Inkscape::Selection *selection) override; + +private: + // No default constructor, noncopyable, nonassignable + Prototype(); + Prototype(Prototype const &d) = delete; + Prototype operator=(Prototype const &d) = delete; + + // Just for example + Gtk::Label *_label; + Gtk::Button _debug_button; // For printing to console. + virtual void on_click(); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // DEBUG + +#endif // SEEN_PROTOTYPE_PANEL_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/ui/dialog/save-template-dialog.cpp b/src/ui/dialog/save-template-dialog.cpp new file mode 100644 index 0000000..a240f61 --- /dev/null +++ b/src/ui/dialog/save-template-dialog.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "save-template-dialog.h" +#include "file.h" +#include "io/resource.h" + +#include <glibmm/i18n.h> +#include <gtkmm/builder.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/dialog.h> +#include <gtkmm/entry.h> +#include <gtkmm/window.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +SaveTemplate::SaveTemplate(Gtk::Window &parent) { + + std::string gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-save-template.glade"); + Glib::RefPtr<Gtk::Builder> builder; + try { + builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("GtkBuilder file loading failed for save template dialog"); + return; + } + + builder->get_widget("dialog", dialog); + builder->get_widget("name", name); + builder->get_widget("author", author); + builder->get_widget("description", description); + builder->get_widget("keywords", keywords); + builder->get_widget("set-default", set_default_template); + + name->signal_changed().connect(sigc::mem_fun(*this, &SaveTemplate::on_name_changed)); + + dialog->add_button(_("Cancel"), Gtk::RESPONSE_CANCEL); + dialog->add_button(_("Save"), Gtk::RESPONSE_OK); + + dialog->set_response_sensitive(Gtk::RESPONSE_OK, false); + dialog->set_default_response(Gtk::RESPONSE_CANCEL); + + dialog->set_transient_for(parent); + dialog->show_all(); +} + +void SaveTemplate::on_name_changed() { + + bool has_text = name->get_text_length() != 0; + dialog->set_response_sensitive(Gtk::RESPONSE_OK, has_text); +} + +void SaveTemplate::save_template(Gtk::Window &parent) { + + sp_file_save_template(parent, name->get_text(), author->get_text(), description->get_text(), + keywords->get_text(), set_default_template->get_active()); +} + +void SaveTemplate::save_document_as_template(Gtk::Window &parent) { + + SaveTemplate dialog(parent); + int response = dialog.dialog->run(); + + switch (response) { + case Gtk::RESPONSE_OK: + dialog.save_template(parent); + break; + default: + break; + } + + dialog.dialog->close(); +} + +} +} +} diff --git a/src/ui/dialog/save-template-dialog.h b/src/ui/dialog/save-template-dialog.h new file mode 100644 index 0000000..eb702ca --- /dev/null +++ b/src/ui/dialog/save-template-dialog.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_SEEN_UI_DIALOG_SAVE_TEMPLATE_H +#define INKSCAPE_SEEN_UI_DIALOG_SAVE_TEMPLATE_H + +#include <glibmm/refptr.h> + +namespace Gtk { +class Builder; +class CheckButton; +class Dialog; +class Entry; +class Window; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class SaveTemplate +{ + +public: + + static void save_document_as_template(Gtk::Window &parentWindow); + +protected: + + void on_name_changed(); + +private: + + Gtk::Dialog *dialog; + + Gtk::Entry *name; + Gtk::Entry *author; + Gtk::Entry *description; + Gtk::Entry *keywords; + + Gtk::CheckButton *set_default_template; + + SaveTemplate(Gtk::Window &parent); + void save_template(Gtk::Window &parent); + +}; +} +} +} +#endif diff --git a/src/ui/dialog/selectorsdialog.cpp b/src/ui/dialog/selectorsdialog.cpp new file mode 100644 index 0000000..30765fd --- /dev/null +++ b/src/ui/dialog/selectorsdialog.cpp @@ -0,0 +1,1350 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for CSS selectors + */ +/* Authors: + * Kamalpreet Kaur Grewal + * Tavmjong Bah + * Jabiertxof + * + * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com> + * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "selectorsdialog.h" + +#include <map> +#include <regex> +#include <utility> + +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include "attribute-rel-svg.h" +#include "document-undo.h" +#include "inkscape.h" +#include "selection.h" +#include "style.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/widget/iconrenderer.h" + +#include "util/trim.h" + +#include "xml/attribute-record.h" +#include "xml/node-observer.h" +#include "xml/sp-css-attr.h" + + +// G_MESSAGES_DEBUG=DEBUG_SELECTORSDIALOG gdb ./inkscape +// #define DEBUG_SELECTORSDIALOG +// #define G_LOG_DOMAIN "SELECTORSDIALOG" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +// Keeps a watch on style element +class SelectorsDialog::NodeObserver : public Inkscape::XML::NodeObserver { + public: + NodeObserver(SelectorsDialog *selectorsdialog) + : _selectorsdialog(selectorsdialog) + { + g_debug("SelectorsDialog::NodeObserver: Constructor"); + }; + + void notifyContentChanged(Inkscape::XML::Node &node, + Inkscape::Util::ptr_shared old_content, + Inkscape::Util::ptr_shared new_content) override; + + SelectorsDialog *_selectorsdialog; +}; + + +void SelectorsDialog::NodeObserver::notifyContentChanged(Inkscape::XML::Node & /*node*/, + Inkscape::Util::ptr_shared /*old_content*/, + Inkscape::Util::ptr_shared /*new_content*/) +{ + + g_debug("SelectorsDialog::NodeObserver::notifyContentChanged"); + _selectorsdialog->_scrollock = true; + _selectorsdialog->_updating = false; + _selectorsdialog->_readStyleElement(); + _selectorsdialog->_selectRow(); +} + + +// Keeps a watch for new/removed/changed nodes +// (Must update objects that selectors match.) +class SelectorsDialog::NodeWatcher : public Inkscape::XML::NodeObserver { + public: + NodeWatcher(SelectorsDialog *selectorsdialog) + : _selectorsdialog(selectorsdialog) + { + g_debug("SelectorsDialog::NodeWatcher: Constructor"); + }; + + void notifyChildAdded( Inkscape::XML::Node &/*node*/, + Inkscape::XML::Node &child, + Inkscape::XML::Node */*prev*/ ) override + { + _selectorsdialog->_nodeAdded(child); + } + + void notifyChildRemoved( Inkscape::XML::Node &/*node*/, + Inkscape::XML::Node &child, + Inkscape::XML::Node */*prev*/ ) override + { + _selectorsdialog->_nodeRemoved(child); + } + + void notifyAttributeChanged( Inkscape::XML::Node &node, + GQuark qname, + Util::ptr_shared /*old_value*/, + Util::ptr_shared /*new_value*/ ) override { + + static GQuark const CODE_id = g_quark_from_static_string("id"); + static GQuark const CODE_class = g_quark_from_static_string("class"); + + if (qname == CODE_id || qname == CODE_class) { + _selectorsdialog->_nodeChanged(node); + } + } + + SelectorsDialog *_selectorsdialog; +}; + +void SelectorsDialog::_nodeAdded(Inkscape::XML::Node &node) +{ + _readStyleElement(); + _selectRow(); +} + +void SelectorsDialog::_nodeRemoved(Inkscape::XML::Node &repr) +{ + if (_textNode == &repr) { + _textNode = nullptr; + } + + _readStyleElement(); + _selectRow(); +} + +void SelectorsDialog::_nodeChanged(Inkscape::XML::Node &object) +{ + + g_debug("SelectorsDialog::NodeChanged"); + + _scrollock = true; + + _readStyleElement(); + _selectRow(); +} + +SelectorsDialog::TreeStore::TreeStore() = default; + + +/** + * Allow dragging only selectors. + */ +bool SelectorsDialog::TreeStore::row_draggable_vfunc(const Gtk::TreeModel::Path &path) const +{ + g_debug("SelectorsDialog::TreeStore::row_draggable_vfunc"); + + auto unconstThis = const_cast<SelectorsDialog::TreeStore *>(this); + const_iterator iter = unconstThis->get_iter(path); + if (iter) { + Gtk::TreeModel::Row row = *iter; + bool is_draggable = row[_selectorsdialog->_mColumns._colType] == SELECTOR; + return is_draggable; + } + return Gtk::TreeStore::row_draggable_vfunc(path); +} + +/** + * Allow dropping only in between other selectors. + */ +bool SelectorsDialog::TreeStore::row_drop_possible_vfunc(const Gtk::TreeModel::Path &dest, + const Gtk::SelectionData &selection_data) const +{ + g_debug("SelectorsDialog::TreeStore::row_drop_possible_vfunc"); + + Gtk::TreeModel::Path dest_parent = dest; + dest_parent.up(); + return dest_parent.empty(); +} + + +// This is only here to handle updating style element after a drag and drop. +void SelectorsDialog::TreeStore::on_row_deleted(const TreeModel::Path &path) +{ + if (_selectorsdialog->_updating) + return; // Don't write if we deleted row (other than from DND) + + g_debug("on_row_deleted"); + _selectorsdialog->_writeStyleElement(); + _selectorsdialog->_readStyleElement(); +} + + +Glib::RefPtr<SelectorsDialog::TreeStore> SelectorsDialog::TreeStore::create(SelectorsDialog *selectorsdialog) +{ + g_debug("SelectorsDialog::TreeStore::create"); + + SelectorsDialog::TreeStore *store = new SelectorsDialog::TreeStore(); + store->_selectorsdialog = selectorsdialog; + store->set_column_types(store->_selectorsdialog->_mColumns); + return Glib::RefPtr<SelectorsDialog::TreeStore>(store); +} + +/** + * Constructor + * A treeview and a set of two buttons are added to the dialog. _addSelector + * adds selectors to treeview. _delSelector deletes the selector from the dialog. + * Any addition/deletion of the selectors updates XML style element accordingly. + */ +SelectorsDialog::SelectorsDialog() + : DialogBase("/dialogs/selectors", "Selectors") + , _updating(false) + , _textNode(nullptr) + , _scrollpos(0) + , _scrollock(false) +{ + g_debug("SelectorsDialog::SelectorsDialog"); + + m_nodewatcher.reset(new SelectorsDialog::NodeWatcher(this)); + m_styletextwatcher.reset(new SelectorsDialog::NodeObserver(this)); + + // Tree + Inkscape::UI::Widget::IconRenderer * addRenderer = manage( + new Inkscape::UI::Widget::IconRenderer() ); + addRenderer->add_icon("edit-delete"); + addRenderer->add_icon("list-add"); + addRenderer->add_icon("empty-icon"); + _store = TreeStore::create(this); + _treeView.set_model(_store); + + // ALWAYS be a single selection widget + _treeView.get_selection()->set_mode(Gtk::SELECTION_SINGLE); + + _treeView.set_headers_visible(false); + _treeView.enable_model_drag_source(); + _treeView.enable_model_drag_dest( Gdk::ACTION_MOVE ); + int addCol = _treeView.append_column("", *addRenderer) - 1; + Gtk::TreeViewColumn *col = _treeView.get_column(addCol); + if ( col ) { + col->add_attribute(addRenderer->property_icon(), _mColumns._colType); + } + + Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText()); + addCol = _treeView.append_column("CSS Selector", *label) - 1; + col = _treeView.get_column(addCol); + if (col) { + col->add_attribute(label->property_text(), _mColumns._colSelector); + col->add_attribute(label->property_weight(), _mColumns._colSelected); + } + _treeView.set_expander_column(*(_treeView.get_column(1))); + + + // Signal handlers + _treeView.signal_button_release_event().connect( // Needs to be release, not press. + sigc::mem_fun(*this, &SelectorsDialog::_handleButtonEvent), false); + + _treeView.signal_button_release_event().connect_notify( + sigc::mem_fun(*this, &SelectorsDialog::_buttonEventsSelectObjs), false); + + _treeView.signal_row_expanded().connect(sigc::mem_fun(*this, &SelectorsDialog::_rowExpand)); + + _treeView.signal_row_collapsed().connect(sigc::mem_fun(*this, &SelectorsDialog::_rowCollapse)); + + _showWidgets(); + + show_all(); +} + + +void SelectorsDialog::_vscroll() +{ + if (!_scrollock) { + _scrollpos = _vadj->get_value(); + } else { + _vadj->set_value(_scrollpos); + _scrollock = false; + } +} + +void SelectorsDialog::_showWidgets() +{ + // Pack widgets + g_debug("SelectorsDialog::_showWidgets"); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool dir = prefs->getBool("/dialogs/selectors/vertical", true); + _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL); + _selectors_box.set_orientation(Gtk::ORIENTATION_VERTICAL); + _selectors_box.set_name("SelectorsDialog"); + _scrolled_window_selectors.add(_treeView); + _scrolled_window_selectors.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _scrolled_window_selectors.set_overlay_scrolling(false); + _vadj = _scrolled_window_selectors.get_vadjustment(); + _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_vscroll)); + _selectors_box.pack_start(_scrolled_window_selectors, Gtk::PACK_EXPAND_WIDGET); + /* Gtk::Label *dirtogglerlabel = Gtk::manage(new Gtk::Label(_("Paned vertical"))); + dirtogglerlabel->get_style_context()->add_class("inksmall"); + _direction.property_active() = dir; + _direction.property_active().signal_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_toggleDirection)); + _direction.get_style_context()->add_class("inkswitch"); */ + _styleButton(_create, "list-add", "Add a new CSS Selector"); + _create.signal_clicked().connect(sigc::mem_fun(*this, &SelectorsDialog::_addSelector)); + _styleButton(_del, "list-remove", "Remove a CSS Selector"); + _button_box.pack_start(_create, Gtk::PACK_SHRINK); + _button_box.pack_start(_del, Gtk::PACK_SHRINK); + Gtk::RadioButton::Group group; + Gtk::RadioButton *_horizontal = Gtk::manage(new Gtk::RadioButton()); + Gtk::RadioButton *_vertical = Gtk::manage(new Gtk::RadioButton()); + _horizontal->set_image_from_icon_name(INKSCAPE_ICON("horizontal")); + _vertical->set_image_from_icon_name(INKSCAPE_ICON("vertical")); + _horizontal->set_group(group); + _vertical->set_group(group); + _vertical->set_active(dir); + _vertical->signal_toggled().connect( + sigc::bind(sigc::mem_fun(*this, &SelectorsDialog::_toggleDirection), _vertical)); + _horizontal->property_draw_indicator() = false; + _vertical->property_draw_indicator() = false; + _button_box.pack_end(*_horizontal, false, false, 0); + _button_box.pack_end(*_vertical, false, false, 0); + _del.signal_clicked().connect(sigc::mem_fun(*this, &SelectorsDialog::_delSelector)); + _del.hide(); + _style_dialog = Gtk::manage(new StyleDialog); + _style_dialog->set_name("StyleDialog"); + _paned.pack1(*_style_dialog, Gtk::SHRINK); + _paned.pack2(_selectors_box, true, true); + _paned.set_wide_handle(true); + Gtk::Box *contents = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + contents->pack_start(_paned, Gtk::PACK_EXPAND_WIDGET); + contents->pack_start(_button_box, false, false, 0); + contents->set_valign(Gtk::ALIGN_FILL); + contents->child_property_fill(_paned); + pack_start(*contents, Gtk::PACK_EXPAND_WIDGET); + show_all(); + _updating = true; + _paned.property_position() = 200; + _updating = false; + set_size_request(320, -1); + set_name("SelectorsAndStyleDialog"); +} + +void SelectorsDialog::_toggleDirection(Gtk::RadioButton *vertical) +{ + g_debug("SelectorsDialog::_toggleDirection"); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool dir = vertical->get_active(); + prefs->setBool("/dialogs/selectors/vertical", dir); + _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL); + _paned.check_resize(); + int widthpos = _paned.property_max_position() - _paned.property_min_position(); + prefs->setInt("/dialogs/selectors/panedpos", widthpos / 2); + _paned.property_position() = widthpos / 2; +} + +/** + * @return Inkscape::XML::Node* pointing to a style element's text node. + * Returns the style element's text node. If there is no style element, one is created. + * Ditto for text node. + */ +Inkscape::XML::Node *SelectorsDialog::_getStyleTextNode(bool create_if_missing) +{ + g_debug("SelectorsDialog::_getStyleTextNode"); + + auto textNode = Inkscape::get_first_style_text_node(m_root, create_if_missing); + + if (_textNode != textNode) { + if (_textNode) { + _textNode->removeObserver(*m_styletextwatcher); + } + + _textNode = textNode; + + if (_textNode) { + _textNode->addObserver(*m_styletextwatcher); + } + } + + return textNode; +} + +/** + * Fill the Gtk::TreeStore from the svg:style element. + */ +void SelectorsDialog::_readStyleElement() +{ + g_debug("SelectorsDialog::_readStyleElement(): updating %s", (_updating ? "true" : "false")); + + if (_updating) return; // Don't read if we wrote style element. + _updating = true; + _scrollock = true; + Inkscape::XML::Node * textNode = _getStyleTextNode(); + + // Get content from style text node. + std::string content = (textNode && textNode->content()) ? textNode->content() : ""; + + // Remove end-of-lines (check it works on Windoze). + content.erase(std::remove(content.begin(), content.end(), '\n'), content.end()); + + // Remove comments (/* xxx */) +#if 0 + while(content.find("/*") != std::string::npos) { + size_t start = content.find("/*"); + content.erase(start, (content.find("*\/", start) - start) +2); + } +#endif + + // First split into selector/value chunks. + // An attempt to use Glib::Regex failed. A C++11 version worked but + // reportedly has problems on Windows. Using split_simple() is simpler + // and probably faster. + // + // Glib::RefPtr<Glib::Regex> regex1 = + // Glib::Regex::create("([^\\{]+)\\{([^\\{]+)\\}"); + // + // Glib::MatchInfo minfo; + // regex1->match(content, minfo); + + // Split on curly brackets. Even tokens are selectors, odd are values. + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[}{]", content); + + // If text node is empty, return (avoids problem with negative below). + if (tokens.size() == 0) { + _store->clear(); + _updating = false; + return; + } + _treeView.show_all(); + std::vector<std::pair<Glib::ustring, bool>> expanderstatus; + for (unsigned i = 0; i < tokens.size() - 1; i += 2) { + Glib::ustring selector = tokens[i]; + Util::trim(selector, ","); // Remove leading/trailing spaces and commas + std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector); + if (!selectordata.empty()) { + selector = selectordata.back(); + } + selector = _style_dialog->fixCSSSelectors(selector); + for (auto &row : _store->children()) { + Glib::ustring selectorold = row[_mColumns._colSelector]; + if (selectorold == selector) { + expanderstatus.emplace_back(selector, row[_mColumns._colExpand]); + } + } + } + _store->clear(); + bool rewrite = false; + + + for (unsigned i = 0; i < tokens.size()-1; i += 2) { + Glib::ustring selector = tokens[i]; + Util::trim(selector, ","); // Remove leading/trailing spaces and commas + std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector); + for (auto selectoritem : selectordata) { + if (selectordata[selectordata.size() - 1] == selectoritem) { + selector = selectoritem; + } else { + Gtk::TreeModel::Row row = *(_store->append()); + row[_mColumns._colSelector] = selectoritem + ";"; + row[_mColumns._colExpand] = false; + row[_mColumns._colType] = OTHER; + row[_mColumns._colObj] = nullptr; + row[_mColumns._colProperties] = ""; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + } + } + Glib::ustring selector_old = selector; + selector = _style_dialog->fixCSSSelectors(selector); + if (selector_old != selector) { + rewrite = true; + } + + if (selector.empty() || selector == "* > .inkscapehacktmp") { + continue; + } + std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[,]+", selector); + coltype colType = SELECTOR; + + Glib::ustring properties; + // Check to make sure we do have a value to match selector. + if ((i+1) < tokens.size()) { + properties = tokens[i+1]; + } else { + std::cerr << "SelectorsDialog::_readStyleElement(): Missing values " + "for last selector!" + << std::endl; + } + Util::trim(properties); + bool colExpand = false; + for (auto rowstatus : expanderstatus) { + if (selector == rowstatus.first) { + colExpand = rowstatus.second; + } + } + std::vector<Glib::ustring> properties_data = Glib::Regex::split_simple(";", properties); + Gtk::TreeModel::Row row = *(_store->append()); + row[_mColumns._colSelector] = selector; + row[_mColumns._colExpand] = colExpand; + row[_mColumns._colType] = colType; + row[_mColumns._colObj] = nullptr; + row[_mColumns._colProperties] = properties; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + // Add as children, objects that match selector. + for (auto &obj : _getObjVec(selector)) { + auto *id = obj->getId(); + if (!id) + continue; + Gtk::TreeModel::Row childrow = *(_store->append(row->children())); + childrow[_mColumns._colSelector] = "#" + Glib::ustring(id); + childrow[_mColumns._colExpand] = false; + childrow[_mColumns._colType] = colType == OBJECT; + childrow[_mColumns._colObj] = obj; + childrow[_mColumns._colProperties] = ""; // Unused + childrow[_mColumns._colVisible] = true; // Unused + childrow[_mColumns._colSelected] = 400; + } + } + + + _updating = false; + if (rewrite) { + _writeStyleElement(); + } + _scrollock = false; + _vadj->set_value(std::min(_scrollpos, _vadj->get_upper())); +} + +void SelectorsDialog::_rowExpand(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) +{ + g_debug("SelectorsDialog::_row_expand()"); + Gtk::TreeModel::Row row = *iter; + row[_mColumns._colExpand] = true; +} + +void SelectorsDialog::_rowCollapse(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) +{ + g_debug("SelectorsDialog::_row_collapse()"); + Gtk::TreeModel::Row row = *iter; + row[_mColumns._colExpand] = false; +} +/** + * Update the content of the style element as selectors (or objects) are added/removed. + */ +void SelectorsDialog::_writeStyleElement() +{ + + if (_updating) { + return; + } + + g_debug("SelectorsDialog::_writeStyleElement"); + + _scrollock = true; + _updating = true; + Glib::ustring styleContent = ""; + for (auto& row: _store->children()) { + Glib::ustring selector = row[_mColumns._colSelector]; +#if 0 + Util::trim(selector, ","); + row[_mColumns._colSelector] = selector; +#endif + if (row[_mColumns._colType] == OTHER) { + styleContent = selector + styleContent; + } else { + styleContent = styleContent + selector + " { " + row[_mColumns._colProperties] + " }\n"; + } + } + // We could test if styleContent is empty and then delete the style node here but there is no + // harm in keeping it around ... + Inkscape::XML::Node *textNode = _getStyleTextNode(true); + bool empty = false; + if (styleContent.empty()) { + empty = true; + styleContent = "* > .inkscapehacktmp{}"; + } + textNode->setContent(styleContent.c_str()); + if (empty) { + styleContent = ""; + textNode->setContent(styleContent.c_str()); + } + textNode->setContent(styleContent.c_str()); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, _("Edited style element."), INKSCAPE_ICON("dialog-selectors")); + + _updating = false; + _scrollock = false; + _vadj->set_value(std::min(_scrollpos, _vadj->get_upper())); + g_debug("SelectorsDialog::_writeStyleElement(): | %s |", styleContent.c_str()); +} + +Glib::ustring SelectorsDialog::_getSelectorClasses(Glib::ustring selector) +{ + g_debug("SelectorsDialog::_getSelectorClasses"); + + std::pair<Glib::ustring, Glib::ustring> result; + std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[ ]+", selector); + selector = tokensplus[tokensplus.size() - 1]; + // Erase any comma/space + Util::trim(selector, ","); + Glib::ustring toparse = Glib::ustring(selector); + selector = Glib::ustring(""); + auto i = toparse.find("."); + if (i == std::string::npos) { + return ""; + } + if (toparse[0] != '.' && toparse[0] != '#') { + i = std::min(toparse.find("#"), toparse.find(".")); + Glib::ustring tag = toparse.substr(0, i); + if (!SPAttributeRelSVG::isSVGElement(tag)) { + return selector; + } + if (i != std::string::npos) { + toparse.erase(0, i); + } + } + i = toparse.find("#"); + if (i != std::string::npos) { + toparse.erase(i, 1); + } + auto j = toparse.find("#"); + if (j != std::string::npos) { + return selector; + } + if (i != std::string::npos) { + toparse.insert(i, "#"); + if (i) { + Glib::ustring post = toparse.substr(0, i); + Glib::ustring pre = toparse.substr(i, toparse.size() - i); + toparse = pre + post; + } + auto k = toparse.find("."); + if (k != std::string::npos) { + toparse = toparse.substr(k, toparse.size() - k); + } + } + return toparse; +} + +/** + * @param row + * Add selected objects on the desktop to the selector corresponding to 'row'. + */ +void SelectorsDialog::_addToSelector(Gtk::TreeModel::Row row) +{ + g_debug("SelectorsDialog::_addToSelector: Entrance"); + if (*row) { + // Store list of selected elements on desktop (not to be confused with selector). + _updating = true; + if (row[_mColumns._colType] == OTHER) { + return; + } + Inkscape::Selection *selection = getDesktop()->getSelection(); + std::vector<SPObject *> toAddObjVec(selection->objects().begin(), selection->objects().end()); + Glib::ustring multiselector = row[_mColumns._colSelector]; + row[_mColumns._colExpand] = true; + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", multiselector); + for (auto &obj : toAddObjVec) { + auto *id = obj->getId(); + if (!id) + continue; + for (auto tok : tokens) { + Glib::ustring clases = _getSelectorClasses(tok); + if (!clases.empty()) { + _insertClass(obj, clases); + std::vector<SPObject *> currentobjs = _getObjVec(multiselector); + bool removeclass = true; + for (auto currentobj : currentobjs) { + if (g_strcmp0(currentobj->getId(), id) == 0) { + removeclass = false; + } + } + if (removeclass) { + _removeClass(obj, clases); + } + } + } + std::vector<SPObject *> currentobjs = _getObjVec(multiselector); + bool insertid = true; + for (auto currentobj : currentobjs) { + if (g_strcmp0(currentobj->getId(), id) == 0) { + insertid = false; + } + } + if (insertid) { + multiselector = multiselector + ",#" + id; + } + Gtk::TreeModel::Row childrow = *(_store->prepend(row->children())); + childrow[_mColumns._colSelector] = "#" + Glib::ustring(id); + childrow[_mColumns._colExpand] = false; + childrow[_mColumns._colType] = OBJECT; + childrow[_mColumns._colObj] = obj; + childrow[_mColumns._colProperties] = ""; // Unused + childrow[_mColumns._colVisible] = true; // Unused + childrow[_mColumns._colSelected] = 400; + } + row[_mColumns._colSelector] = multiselector; + _updating = false; + + // Add entry to style element + for (auto &obj : toAddObjVec) { + Glib::ustring css_str = ""; + SPCSSAttr *css = sp_repr_css_attr_new(); + SPCSSAttr *css_selector = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style")); + Glib::ustring selprops = row[_mColumns._colProperties]; + sp_repr_css_attr_add_from_string(css_selector, selprops.c_str()); + for (const auto & iter : css_selector->attributeList()) { + gchar const *key = g_quark_to_string(iter.key); + css->removeAttribute(key); + } + sp_repr_css_write_string(css, css_str); + sp_repr_css_attr_unref(css); + sp_repr_css_attr_unref(css_selector); + obj->getRepr()->setAttribute("style", css_str); + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + _writeStyleElement(); + } +} + +/** + * @param row + * Remove the object corresponding to 'row' from the parent selector. + */ +void SelectorsDialog::_removeFromSelector(Gtk::TreeModel::Row row) +{ + g_debug("SelectorsDialog::_removeFromSelector: Entrance"); + if (*row) { + _scrollock = true; + _updating = true; + SPObject *obj = nullptr; + Glib::ustring objectLabel = row[_mColumns._colSelector]; + Gtk::TreeModel::iterator iter = row->parent(); + if (iter) { + Gtk::TreeModel::Row parent = *iter; + Glib::ustring multiselector = parent[_mColumns._colSelector]; + Util::trim(multiselector, ","); + obj = _getObjVec(objectLabel)[0]; + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", multiselector); + Glib::ustring selector = ""; + for (auto tok : tokens) { + if (tok.empty()) { + continue; + } + // TODO: handle when other selectors has the removed class applied to maybe not remove + Glib::ustring clases = _getSelectorClasses(tok); + if (!clases.empty()) { + _removeClass(obj, tok, true); + } + auto i = tok.find(row[_mColumns._colSelector]); + if (i == std::string::npos) { + selector = selector.empty() ? tok : selector + "," + tok; + } + } + Util::trim(selector); + if (selector.empty()) { + _store->erase(parent); + + } else { + _store->erase(row); + parent[_mColumns._colSelector] = selector; + parent[_mColumns._colExpand] = true; + parent[_mColumns._colObj] = nullptr; + } + } + _updating = false; + + // Add entry to style element + _writeStyleElement(); + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + _scrollock = false; + _vadj->set_value(std::min(_scrollpos, _vadj->get_upper())); + } +} + + +/** + * @param sel + * @return This function returns a comma separated list of ids for objects in input vector. + * It is used in creating an 'id' selector. It relies on objects having 'id's. + */ +Glib::ustring SelectorsDialog::_getIdList(std::vector<SPObject *> sel) +{ + g_debug("SelectorsDialog::_getIdList"); + + Glib::ustring str; + for (auto& obj: sel) { + char const *id = obj->getId(); + if (id) { + if (!str.empty()) { + str.append(", "); + } + str.append("#").append(id); + } + } + return str; +} + +/** + * @param selector: a valid CSS selector string. + * @return objVec: a vector of pointers to SPObject's the selector matches. + * Return a vector of all objects that selector matches. + */ +std::vector<SPObject *> SelectorsDialog::_getObjVec(Glib::ustring selector) +{ + + g_debug("SelectorsDialog::_getObjVec: | %s |", selector.c_str()); + + g_assert(selector.find(";") == Glib::ustring::npos); + + return getDesktop()->getDocument()->getObjectsBySelector(selector); +} + + +/** + * @param objs: list of objects to insert class + * @param class: class to insert + * Insert a class name into objects' 'class' attribute. + */ +void SelectorsDialog::_insertClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className) +{ + g_debug("SelectorsDialog::_insertClass"); + + for (auto& obj: objVec) { + _insertClass(obj, className); + } +} + +/** + * @param objs: list of objects to insert class + * @param class: class to insert + * Insert a class name into objects' 'class' attribute. + */ +void SelectorsDialog::_insertClass(SPObject *obj, const Glib::ustring &className) +{ + g_debug("SelectorsDialog::_insertClass"); + + Glib::ustring classAttr = Glib::ustring(""); + if (obj->getRepr()->attribute("class")) { + classAttr = obj->getRepr()->attribute("class"); + } + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[.]+", className); + std::sort(tokens.begin(), tokens.end()); + tokens.erase(std::unique(tokens.begin(), tokens.end()), tokens.end()); + std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[\\s]+", classAttr); + for (auto tok : tokens) { + bool exist = false; + for (auto &tokenplus : tokensplus) { + if (tokenplus == tok) { + exist = true; + } + } + if (!exist) { + classAttr = classAttr.empty() ? tok : classAttr + " " + tok; + } + } + obj->getRepr()->setAttribute("class", classAttr); +} + +/** + * @param objs: list of objects to insert class + * @param class: class to insert + * Insert a class name into objects' 'class' attribute. + */ +void SelectorsDialog::_removeClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className, bool all) +{ + g_debug("SelectorsDialog::_removeClass"); + + for (auto &obj : objVec) { + _removeClass(obj, className, all); + } +} + +/** + * @param objs: list of objects to insert class + * @param class: class to insert + * Insert a class name into objects' 'class' attribute. + */ +void SelectorsDialog::_removeClass(SPObject *obj, const Glib::ustring &className, bool all) // without "." +{ + g_debug("SelectorsDialog::_removeClass"); + + if (obj->getRepr()->attribute("class")) { + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[.]+", className); + Glib::ustring classAttr = obj->getRepr()->attribute("class"); + Glib::ustring classAttrRestore = classAttr; + bool notfound = false; + for (auto tok : tokens) { + auto i = classAttr.find(tok); + if (i != std::string::npos) { + classAttr.erase(i, tok.length()); + } else { + notfound = true; + } + } + if (all && notfound) { + classAttr = classAttrRestore; + } + Util::trim(classAttr, ","); + if (classAttr.empty()) { + obj->getRepr()->removeAttribute("class"); + } else { + obj->getRepr()->setAttribute("class", classAttr); + } + } +} + + +/** + * @param eventX + * @param eventY + * This function selects objects in the drawing corresponding to the selector + * selected in the treeview. + */ +void SelectorsDialog::_selectObjects(int eventX, int eventY) +{ + g_debug("SelectorsDialog::_selectObjects: %d, %d", eventX, eventY); + Gtk::TreeViewColumn *col = _treeView.get_column(1); + Gtk::TreeModel::Path path; + int x2 = 0; + int y2 = 0; + // To do: We should be able to do this via passing in row. + if (_treeView.get_path_at_pos(eventX, eventY, path, col, x2, y2)) { + if (_lastpath.size() && _lastpath == path) { + return; + } + if (col == _treeView.get_column(1) && x2 > 25) { + getDesktop()->selection->clear(); + Gtk::TreeModel::iterator iter = _store->get_iter(path); + if (iter) { + Gtk::TreeModel::Row row = *iter; + if (row[_mColumns._colObj]) { + getDesktop()->selection->add(row[_mColumns._colObj]); + } + Gtk::TreeModel::Children children = row.children(); + if (children.empty() || children.size() == 1) { + _del.show(); + } + for (auto child : row.children()) { + Gtk::TreeModel::Row child_row = *child; + if (child[_mColumns._colObj]) { + getDesktop()->selection->add(child[_mColumns._colObj]); + } + } + } + _lastpath = path; + } + } +} + +/** + * This function opens a dialog to add a selector. The dialog is prefilled + * with an 'id' selector containing a list of the id's of selected objects + * or with a 'class' selector if no objects are selected. + */ +void SelectorsDialog::_addSelector() +{ + g_debug("SelectorsDialog::_addSelector: Entrance"); + _scrollock = true; + // Store list of selected elements on desktop (not to be confused with selector). + Inkscape::Selection* selection = getDesktop()->getSelection(); + std::vector<SPObject *> objVec( selection->objects().begin(), + selection->objects().end() ); + + // ==== Create popup dialog ==== + Gtk::Dialog *textDialogPtr = new Gtk::Dialog(); + textDialogPtr->property_modal() = true; + textDialogPtr->property_title() = _("CSS selector"); + textDialogPtr->property_window_position() = Gtk::WIN_POS_CENTER_ON_PARENT; + textDialogPtr->add_button(_("Cancel"), Gtk::RESPONSE_CANCEL); + textDialogPtr->add_button(_("Add"), Gtk::RESPONSE_OK); + + Gtk::Entry *textEditPtr = manage ( new Gtk::Entry() ); + textEditPtr->signal_activate().connect( + sigc::bind<Gtk::Dialog *>(sigc::mem_fun(*this, &SelectorsDialog::_closeDialog), textDialogPtr)); + textDialogPtr->get_content_area()->pack_start(*textEditPtr, Gtk::PACK_SHRINK); + + Gtk::Label *textLabelPtr = manage(new Gtk::Label(_("Invalid CSS selector."))); + textDialogPtr->get_content_area()->pack_start(*textLabelPtr, Gtk::PACK_SHRINK); + + /** + * By default, the entrybox contains 'Class1' as text. However, if object(s) + * is(are) selected and user clicks '+' at the bottom of dialog, the + * entrybox will have the id(s) of the selected objects as text. + */ + if (getDesktop()->getSelection()->isEmpty()) { + textEditPtr->set_text(".Class1"); + } else { + textEditPtr->set_text(_getIdList(objVec)); + } + + Gtk::Requisition sreq1, sreq2; + textDialogPtr->get_preferred_size(sreq1, sreq2); + int minWidth = 200; + int minHeight = 100; + minWidth = (sreq2.width > minWidth ? sreq2.width : minWidth ); + minHeight = (sreq2.height > minHeight ? sreq2.height : minHeight); + textDialogPtr->set_size_request(minWidth, minHeight); + textEditPtr->show(); + textLabelPtr->hide(); + textDialogPtr->show(); + + + // ==== Get response ==== + int result = -1; + bool invalid = true; + Glib::ustring selectorValue; + Glib::ustring originalValue; + while (invalid) { + result = textDialogPtr->run(); + if (result != Gtk::RESPONSE_OK) { // Cancel, close dialog, etc. + textDialogPtr->hide(); + delete textDialogPtr; + return; + } + /** + * @brief selectorName + * This string stores selector name. The text from entrybox is saved as name + * for selector. If the entrybox is empty, the text (thus selectorName) is + * set to ".Class1" + */ + originalValue = Glib::ustring(textEditPtr->get_text()); + selectorValue = _style_dialog->fixCSSSelectors(originalValue); + _del.show(); + if (originalValue.find("@import ") == std::string::npos && selectorValue.empty()) { + textLabelPtr->show(); + } else { + invalid = false; + } + } + delete textDialogPtr; + // ==== Handle response ==== + // If class selector, add selector name to class attribute for each object + Util::trim(selectorValue, ","); + if (originalValue.find("@import ") != std::string::npos) { + Gtk::TreeModel::Row row = *(_store->prepend()); + row[_mColumns._colSelector] = originalValue; + row[_mColumns._colExpand] = false; + row[_mColumns._colType] = OTHER; + row[_mColumns._colObj] = nullptr; + row[_mColumns._colProperties] = ""; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + } else { + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", selectorValue); + for (auto &obj : objVec) { + for (auto tok : tokens) { + Glib::ustring clases = _getSelectorClasses(tok); + if (clases.empty()) { + continue; + } + _insertClass(obj, clases); + std::vector<SPObject *> currentobjs = _getObjVec(selectorValue); + bool removeclass = true; + for (auto currentobj : currentobjs) { + if (currentobj == obj) { + removeclass = false; + } + } + if (removeclass) { + _removeClass(obj, clases); + } + } + } + Gtk::TreeModel::Row row = *(_store->prepend()); + row[_mColumns._colExpand] = true; + row[_mColumns._colType] = SELECTOR; + row[_mColumns._colSelector] = selectorValue; + row[_mColumns._colObj] = nullptr; + row[_mColumns._colProperties] = ""; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + for (auto &obj : _getObjVec(selectorValue)) { + auto *id = obj->getId(); + if (!id) + continue; + Gtk::TreeModel::Row childrow = *(_store->prepend(row->children())); + childrow[_mColumns._colSelector] = "#" + Glib::ustring(id); + childrow[_mColumns._colExpand] = false; + childrow[_mColumns._colType] = OBJECT; + childrow[_mColumns._colObj] = obj; + childrow[_mColumns._colProperties] = ""; // Unused + childrow[_mColumns._colVisible] = true; // Unused + childrow[_mColumns._colSelected] = 400; + } + } + // Add entry to style element + _writeStyleElement(); + _scrollock = false; + _vadj->set_value(std::min(_scrollpos, _vadj->get_upper())); +} + +void SelectorsDialog::_closeDialog(Gtk::Dialog *textDialogPtr) { textDialogPtr->response(Gtk::RESPONSE_OK); } + +/** + * This function deletes selector when '-' at the bottom is clicked. + * Note: If deleting a class selector, class attributes are NOT changed. + */ +void SelectorsDialog::_delSelector() +{ + g_debug("SelectorsDialog::_delSelector"); + + _scrollock = true; + Glib::RefPtr<Gtk::TreeSelection> refTreeSelection = _treeView.get_selection(); + Gtk::TreeModel::iterator iter = refTreeSelection->get_selected(); + if (iter) { + _vscroll(); + Gtk::TreeModel::Row row = *iter; + if (row.children().size() > 2) { + return; + } + _updating = true; + _store->erase(iter); + _updating = false; + _writeStyleElement(); + _del.hide(); + _scrollock = false; + _vadj->set_value(std::min(_scrollpos, _vadj->get_upper())); + } +} + +/** + * @param event + * @return + * Handles the event when '+' button in front of a selector name is clicked or when a '-' button in + * front of a child object is clicked. In the first case, the selected objects on the desktop (if + * any) are added as children of the selector in the treeview. In the latter case, the object + * corresponding to the row is removed from the selector. + */ +bool SelectorsDialog::_handleButtonEvent(GdkEventButton *event) +{ + g_debug("SelectorsDialog::_handleButtonEvent: Entrance"); + if (event->type == GDK_BUTTON_RELEASE && event->button == 1) { + _scrollock = true; + Gtk::TreeViewColumn *col = nullptr; + Gtk::TreeModel::Path path; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + + if (_treeView.get_path_at_pos(x, y, path, col, x2, y2)) { + if (col == _treeView.get_column(0)) { + _vscroll(); + Gtk::TreeModel::iterator iter = _store->get_iter(path); + Gtk::TreeModel::Row row = *iter; + if (!row.parent()) { + _addToSelector(row); + } else { + _removeFromSelector(row); + } + _vadj->set_value(std::min(_scrollpos, _vadj->get_upper())); + } + } + } + return false; +} + +// ------------------------------------------------------------------- + +class PropertyData +{ +public: + PropertyData() = default;; + PropertyData(Glib::ustring name) : _name(std::move(name)) {}; + + void _setSheetValue(Glib::ustring value) { _sheetValue = value; }; + void _setAttrValue(Glib::ustring value) { _attrValue = value; }; + Glib::ustring _getName() { return _name; }; + Glib::ustring _getSheetValue() { return _sheetValue; }; + Glib::ustring _getAttrValue() { return _attrValue; }; + +private: + Glib::ustring _name; + Glib::ustring _sheetValue; + Glib::ustring _attrValue; +}; + +// ------------------------------------------------------------------- + +SelectorsDialog::~SelectorsDialog() +{ + removeObservers(); + _style_dialog->setDesktop(nullptr); +} + +void SelectorsDialog::update() +{ + _style_dialog->update(); +} + +void SelectorsDialog::desktopReplaced() +{ + _style_dialog->setDesktop(getDesktop()); +} + +void SelectorsDialog::removeObservers() +{ + if (_textNode) { + _textNode->removeObserver(*m_styletextwatcher); + _textNode = nullptr; + } + if (m_root) { + m_root->removeSubtreeObserver(*m_nodewatcher); + m_root = nullptr; + } +} + +void SelectorsDialog::documentReplaced() +{ + removeObservers(); + if (auto document = getDocument()) { + m_root = document->getReprRoot(); + m_root->addSubtreeObserver(*m_nodewatcher); + } + selectionChanged(getSelection()); +} + +void SelectorsDialog::selectionChanged(Selection *selection) +{ + _lastpath.clear(); + _readStyleElement(); + _selectRow(); +} + +/** + * @param event + * This function detects single or double click on a selector in any row. Clicking + * on a selector selects the matching objects on the desktop. A double click will + * in addition open the CSS dialog. + */ +void SelectorsDialog::_buttonEventsSelectObjs(GdkEventButton *event) +{ + g_debug("SelectorsDialog::_buttonEventsSelectObjs"); + if (event->type == GDK_BUTTON_RELEASE && event->button == 1) { + _updating = true; + _del.show(); + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + _selectObjects(x, y); + _updating = false; + _selectRow(); + } +} + + +/** + * This function selects the row in treeview corresponding to an object selected + * in the drawing. If more than one row matches, the first is chosen. + */ +void SelectorsDialog::_selectRow() +{ + _scrollock = true; + g_debug("SelectorsDialog::_selectRow: updating: %s", (_updating ? "true" : "false")); + _del.hide(); + std::vector<Gtk::TreeModel::Path> selectedrows = _treeView.get_selection()->get_selected_rows(); + if (selectedrows.size() == 1) { + Gtk::TreeModel::Row row = *_store->get_iter(selectedrows[0]); + if (!row->parent() && row->children().size() < 2) { + _del.show(); + } + if (row) { + _style_dialog->setCurrentSelector(row[_mColumns._colSelector]); + } + } else if (selectedrows.size() == 0) { + _del.show(); + } + if (_updating || !getDesktop()) return; // Avoid updating if we have set row via dialog. + + Gtk::TreeModel::Children children = _store->children(); + Inkscape::Selection* selection = getDesktop()->getSelection(); + SPObject *obj = nullptr; + if (!selection->isEmpty()) { + obj = selection->objects().back(); + } else { + _style_dialog->setCurrentSelector(""); + } + for (auto row : children) { + row[_mColumns._colSelected] = 400; + Gtk::TreeModel::Children subchildren = row->children(); + for (auto subrow : subchildren) { + subrow[_mColumns._colSelected] = 400; + } + } + + // Sort selection for matching. + std::vector<SPObject *> selected_objs( + selection->objects().begin(), selection->objects().end()); + std::sort(selected_objs.begin(), selected_objs.end()); + + for (auto row : children) { + // Recalculate the selector, in real time. + auto row_children = _getObjVec(row[_mColumns._colSelector]); + std::sort(row_children.begin(), row_children.end()); + + // If all selected objects are in the css-selector, select it. + if (row_children == selected_objs) { + row[_mColumns._colSelected] = 700; + } + + Gtk::TreeModel::Children subchildren = row->children(); + + for (auto subrow : subchildren) { + if (subrow[_mColumns._colObj] && selection->includes(subrow[_mColumns._colObj])) { + subrow[_mColumns._colSelected] = 700; + } + if (row[_mColumns._colExpand]) { + _treeView.expand_to_path(Gtk::TreePath(row)); + } + } + if (row[_mColumns._colExpand]) { + _treeView.expand_to_path(Gtk::TreePath(row)); + } + } + _vadj->set_value(std::min(_scrollpos, _vadj->get_upper())); +} + +/** + * @param btn + * @param iconName + * @param tooltip + * Set the style of '+' and '-' buttons at the bottom of dialog. + */ +void SelectorsDialog::_styleButton(Gtk::Button &btn, char const *iconName, char const *tooltip) +{ + g_debug("SelectorsDialog::_styleButton"); + + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show(child); + btn.add(*manage(Glib::wrap(child))); + btn.set_relief(Gtk::RELIEF_NONE); + btn.set_tooltip_text (tooltip); +} + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/selectorsdialog.h b/src/ui/dialog/selectorsdialog.h new file mode 100644 index 0000000..1e14cfe --- /dev/null +++ b/src/ui/dialog/selectorsdialog.h @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for CSS selectors + */ +/* Authors: + * Kamalpreet Kaur Grewal + * Tavmjong Bah + * + * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com> + * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SELECTORSDIALOG_H +#define SELECTORSDIALOG_H + +#include <gtkmm/dialog.h> +#include <gtkmm/paned.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/switch.h> +#include <gtkmm/treemodelfilter.h> +#include <gtkmm/treeselection.h> +#include <gtkmm/treestore.h> +#include <gtkmm/treeview.h> +#include <memory> +#include <vector> + +#include "ui/dialog/dialog-base.h" +#include "ui/dialog/styledialog.h" +#include "xml/helper-observer.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * @brief The SelectorsDialog class + * A list of CSS selectors will show up in this dialog. This dialog allows one to + * add and delete selectors. Elements can be added to and removed from the selectors + * in the dialog. Selection of any selector row selects the matching objects in + * the drawing and vice-versa. (Only simple selectors supported for now.) + * + * This class must keep two things in sync: + * 1. The text node of the style element. + * 2. The Gtk::TreeModel. + */ +class SelectorsDialog : public DialogBase +{ +public: + // No default constructor, noncopyable, nonassignable + SelectorsDialog(); + ~SelectorsDialog() override; + SelectorsDialog(SelectorsDialog const &d) = delete; + SelectorsDialog operator=(SelectorsDialog const &d) = delete; + static SelectorsDialog &getInstance() { return *new SelectorsDialog(); } + + void update() override; + void desktopReplaced() override; + void documentReplaced() override; + void selectionChanged(Selection *selection) override; + + private: + // Monitor <style> element for changes. + class NodeObserver; + + void removeObservers(); + + // Monitor all objects for addition/removal/attribute change + class NodeWatcher; + enum SelectorType { CLASS, ID, TAG }; + void _nodeAdded( Inkscape::XML::Node &repr ); + void _nodeRemoved( Inkscape::XML::Node &repr ); + void _nodeChanged( Inkscape::XML::Node &repr ); + // Data structure + enum coltype { OBJECT, SELECTOR, OTHER }; + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns() { + add(_colSelector); + add(_colExpand); + add(_colType); + add(_colObj); + add(_colProperties); + add(_colVisible); + add(_colSelected); + } + Gtk::TreeModelColumn<Glib::ustring> _colSelector; // Selector or matching object id. + Gtk::TreeModelColumn<bool> _colExpand; // Open/Close store row. + Gtk::TreeModelColumn<gint> _colType; // Selector row or child object row. + Gtk::TreeModelColumn<SPObject *> _colObj; // Matching object (if any). + Gtk::TreeModelColumn<Glib::ustring> _colProperties; // List of properties. + Gtk::TreeModelColumn<bool> _colVisible; // Make visible or not. + Gtk::TreeModelColumn<gint> _colSelected; // Make selected. + }; + ModelColumns _mColumns; + + // Override Gtk::TreeStore to control drag-n-drop (only allow dragging and dropping of selectors). + // See: https://developer.gnome.org/gtkmm-tutorial/stable/sec-treeview-examples.html.en + // + // TreeStore implements simple drag and drop (DND) but there appears no way to know when a DND + // has been completed (other than doing the whole DND ourselves). As a hack, we use + // on_row_deleted to trigger write of style element. + class TreeStore : public Gtk::TreeStore { + protected: + TreeStore(); + bool row_draggable_vfunc(const Gtk::TreeModel::Path& path) const override; + bool row_drop_possible_vfunc(const Gtk::TreeModel::Path& path, + const Gtk::SelectionData& selection_data) const override; + void on_row_deleted(const TreeModel::Path& path) override; + + public: + static Glib::RefPtr<SelectorsDialog::TreeStore> create(SelectorsDialog *styledialog); + + private: + SelectorsDialog *_selectorsdialog; + }; + + // TreeView + Glib::RefPtr<Gtk::TreeModelFilter> _modelfilter; + Glib::RefPtr<TreeStore> _store; + Gtk::TreeView _treeView; + Gtk::TreeModel::Path _lastpath; + // Widgets + StyleDialog *_style_dialog; + Gtk::Paned _paned; + Glib::RefPtr<Gtk::Adjustment> _vadj; + Gtk::Box _button_box; + Gtk::Box _selectors_box; + Gtk::ScrolledWindow _scrolled_window_selectors; + + Gtk::Button _del; + Gtk::Button _create; + // Reading and writing the style element. + Inkscape::XML::Node *_getStyleTextNode(bool create_if_missing = false); + void _readStyleElement(); + void _writeStyleElement(); + + // Update watchers + std::unique_ptr<Inkscape::XML::NodeObserver> m_nodewatcher; + std::unique_ptr<Inkscape::XML::NodeObserver> m_styletextwatcher; + + // Manipulate Tree + void _addToSelector(Gtk::TreeModel::Row row); + void _removeFromSelector(Gtk::TreeModel::Row row); + Glib::ustring _getIdList(std::vector<SPObject *>); + std::vector<SPObject *> _getObjVec(Glib::ustring selector); + void _insertClass(const std::vector<SPObject *>& objVec, const Glib::ustring& className); + void _insertClass(SPObject *obj, const Glib::ustring &className); + void _removeClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className, bool all = false); + void _removeClass(SPObject *obj, const Glib::ustring &className, bool all = false); + void _toggleDirection(Gtk::RadioButton *vertical); + void _showWidgets(); + + void _selectObjects(int, int); + // Variables + double _scrollpos; + bool _scrollock; + bool _updating; // Prevent cyclic actions: read <-> write, select via dialog <-> via desktop + Inkscape::XML::Node *m_root = nullptr; + Inkscape::XML::Node *_textNode; // Track so we know when to add a NodeObserver. + + void _rowExpand(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path); + void _rowCollapse(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path); + void _closeDialog(Gtk::Dialog *textDialogPtr); + + Inkscape::XML::SignalObserver _objObserver; // Track object in selected row (for style change). + + // Signal and handlers - Internal + void _addSelector(); + void _delSelector(); + static Glib::ustring _getSelectorClasses(Glib::ustring selector); + bool _handleButtonEvent(GdkEventButton *event); + void _buttonEventsSelectObjs(GdkEventButton *event); + void _selectRow(); // Select row in tree when selection changed. + void _vscroll(); + + // GUI + void _styleButton(Gtk::Button& btn, char const* iconName, char const* tooltip); +}; + +} // namespace Dialogc +} // namespace UI +} // namespace Inkscape + +#endif // SELECTORSDIALOG_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/ui/dialog/spellcheck.cpp b/src/ui/dialog/spellcheck.cpp new file mode 100644 index 0000000..41df154 --- /dev/null +++ b/src/ui/dialog/spellcheck.cpp @@ -0,0 +1,761 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Spellcheck dialog. + */ +/* Authors: + * bulia byak <bulia@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2009 Authors + * + * 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 "spellcheck.h" + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-stack.h" +#include "layer-manager.h" +#include "selection-chemistry.h" +#include "text-editing.h" + +#include "display/control/canvas-item-rect.h" + +#include "object/sp-defs.h" +#include "object/sp-flowtext.h" +#include "object/sp-object.h" +#include "object/sp-root.h" +#include "object/sp-string.h" +#include "object/sp-text.h" +#include "object/sp-tref.h" + +#include "ui/dialog/dialog-container.h" +#include "ui/dialog/inkscape-preferences.h" // for PREFS_PAGE_SPELLCHECK +#include "ui/icon-names.h" +#include "ui/tools/text-tool.h" + +#include <glibmm/i18n.h> + +#ifdef _WIN32 +#include <windows.h> +#endif + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * Get the list of installed dictionaries/languages + */ +std::vector<LanguagePair> SpellCheck::get_available_langs() +{ + std::vector<LanguagePair> langs; + +#if WITH_GSPELL + // TODO: write a gspellmm library. + // TODO: why is this not const? + GList *list = const_cast<GList *>(gspell_language_get_available()); + g_list_foreach(list, [](gpointer data, gpointer user_data) { + GspellLanguage *language = reinterpret_cast<GspellLanguage*>(data); + std::vector<LanguagePair> *langs = reinterpret_cast<std::vector<LanguagePair>*>(user_data); + const gchar *name = gspell_language_get_name(language); + const gchar *code = gspell_language_get_code(language); + langs->emplace_back(name, code); + }, &langs); +#endif + + return langs; +} + +static void show_spellcheck_preferences_dialog() +{ + Inkscape::Preferences::get()->setInt("/dialogs/preferences/page", PREFS_PAGE_SPELLCHECK); + SP_ACTIVE_DESKTOP->getContainer()->new_dialog("Spellcheck"); +} + +SpellCheck::SpellCheck() + : DialogBase("/dialogs/spellcheck/", "Spellcheck") + , _text(nullptr) + , _layout(nullptr) + , _stops(0) + , _adds(0) + , _working(false) + , _local_change(false) + , _prefs(nullptr) + , accept_button(_("_Accept"), true) + , ignoreonce_button(_("_Ignore once"), true) + , ignore_button(_("_Ignore"), true) + , add_button(_("A_dd"), true) + , dictionary_label(_("Language")) + , dictionary_hbox(Gtk::ORIENTATION_HORIZONTAL, 0) + , stop_button(_("_Stop"), true) + , start_button(_("_Start"), true) + , suggestion_hbox(Gtk::ORIENTATION_HORIZONTAL) + , changebutton_vbox(Gtk::ORIENTATION_VERTICAL) +{ + _prefs = Inkscape::Preferences::get(); + + banner_hbox.set_layout(Gtk::BUTTONBOX_START); + banner_hbox.add(banner_label); + + if (_langs.empty()) { + _langs = get_available_langs(); + + if (_langs.empty()) { + banner_label.set_markup(Glib::ustring::compose("<i>%1</i>", _("No dictionaries installed"))); + } + } + + scrolled_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + scrolled_window.set_shadow_type(Gtk::SHADOW_IN); + scrolled_window.set_size_request(120, 96); + scrolled_window.add(tree_view); + + model = Gtk::ListStore::create(tree_columns); + tree_view.set_model(model); + tree_view.append_column(_("Suggestions:"), tree_columns.suggestions); + + if (!_langs.empty()) { + for (const LanguagePair &pair : _langs) { + dictionary_combo.append(pair.second, pair.first); + } + // Set previously set language (or the first item) + if(!dictionary_combo.set_active_id(_prefs->getString("/dialogs/spellcheck/lang"))) { + dictionary_combo.set_active(0); + } + } + + accept_button.set_tooltip_text(_("Accept the chosen suggestion")); + ignoreonce_button.set_tooltip_text(_("Ignore this word only once")); + ignore_button.set_tooltip_text(_("Ignore this word in this session")); + add_button.set_tooltip_text(_("Add this word to the chosen dictionary")); + pref_button.set_tooltip_text(_("Preferences")); + pref_button.set_image_from_icon_name("preferences-system"); + + dictionary_hbox.pack_start(dictionary_label, false, false, 6); + dictionary_hbox.pack_start(dictionary_combo, true, true, 0); + dictionary_hbox.pack_start(pref_button, false, false, 0); + + changebutton_vbox.set_spacing(4); + changebutton_vbox.pack_start(accept_button, false, false, 0); + changebutton_vbox.pack_start(ignoreonce_button, false, false, 0); + changebutton_vbox.pack_start(ignore_button, false, false, 0); + changebutton_vbox.pack_start(add_button, false, false, 0); + + suggestion_hbox.pack_start (scrolled_window, true, true, 4); + suggestion_hbox.pack_end (changebutton_vbox, false, false, 0); + + stop_button.set_tooltip_text(_("Stop the check")); + start_button.set_tooltip_text(_("Start the check")); + + actionbutton_hbox.set_layout(Gtk::BUTTONBOX_END); + actionbutton_hbox.set_spacing(4); + actionbutton_hbox.add(stop_button); + actionbutton_hbox.add(start_button); + + /* + * Main dialog + */ + set_spacing(6); + pack_start (banner_hbox, false, false, 0); + pack_start (suggestion_hbox, true, true, 0); + pack_start (dictionary_hbox, false, false, 0); + pack_start (action_sep, false, false, 6); + pack_start (actionbutton_hbox, false, false, 0); + + /* + * Signal handlers + */ + accept_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onAccept)); + ignoreonce_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onIgnoreOnce)); + ignore_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onIgnore)); + add_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onAdd)); + start_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onStart)); + stop_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onStop)); + tree_view.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SpellCheck::onTreeSelectionChange)); + dictionary_combo.signal_changed().connect(sigc::mem_fun(*this, &SpellCheck::onLanguageChanged)); + pref_button.signal_clicked().connect(sigc::ptr_fun(show_spellcheck_preferences_dialog)); + + show_all_children (); + + tree_view.set_sensitive(false); + accept_button.set_sensitive(false); + ignore_button.set_sensitive(false); + ignoreonce_button.set_sensitive(false); + add_button.set_sensitive(false); + stop_button.set_sensitive(false); +} + +SpellCheck::~SpellCheck() +{ + clearRects(); + disconnect(); +} + +void SpellCheck::documentReplaced() +{ + if (_working) { + // Stop and start on the new desktop + finished(); + onStart(); + } +} + +void SpellCheck::clearRects() +{ + for(auto rect : _rects) { + rect->hide(); + delete rect; + } + _rects.clear(); +} + +void SpellCheck::disconnect() +{ + if (_release_connection) { + _release_connection.disconnect(); + } + if (_modified_connection) { + _modified_connection.disconnect(); + } +} + +void SpellCheck::allTextItems (SPObject *r, std::vector<SPItem *> &l, bool hidden, bool locked) +{ + if (SP_IS_DEFS(r)) + return; // we're not interested in items in defs + + if (!strcmp(r->getRepr()->name(), "svg:metadata")) { + return; // we're not interested in metadata + } + + if (auto desktop = getDesktop()) { + for (auto& child: r->children) { + if (auto item = dynamic_cast<SPItem *>(&child)) { + if (!child.cloned && !desktop->layerManager().isLayer(item)) { + if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) { + if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) + l.push_back(item); + } + } + } + allTextItems (&child, l, hidden, locked); + } + } + return; +} + +bool +SpellCheck::textIsValid (SPObject *root, SPItem *text) +{ + std::vector<SPItem*> l; + allTextItems (root, l, false, true); + return (std::find(l.begin(), l.end(), text) != l.end()); +} + +bool SpellCheck::compareTextBboxes(SPItem const *i1, SPItem const *i2)//returns a<b +{ + Geom::OptRect bbox1 = i1->documentVisualBounds(); + Geom::OptRect bbox2 = i2->documentVisualBounds(); + if (!bbox1 || !bbox2) { + return false; + } + + // vector between top left corners + Geom::Point diff = bbox1->min() - bbox2->min(); + + return diff[Geom::Y] == 0 ? (diff[Geom::X] < 0) : (diff[Geom::Y] < 0); +} + +// We regenerate and resort the list every time, because user could have changed it while the +// dialog was waiting +SPItem *SpellCheck::getText (SPObject *root) +{ + std::vector<SPItem*> l; + allTextItems (root, l, false, true); + std::sort(l.begin(),l.end(),SpellCheck::compareTextBboxes); + + for (auto item:l) { + if(_seen_objects.insert(item).second) + return item; + } + return nullptr; +} + +void +SpellCheck::nextText() +{ + disconnect(); + + _text = getText(_root); + if (_text) { + + _modified_connection = _text->connectModified(sigc::mem_fun(*this, &SpellCheck::onObjModified)); + _release_connection = _text->connectRelease(sigc::mem_fun(*this, &SpellCheck::onObjReleased)); + + _layout = te_get_layout (_text); + _begin_w = _layout->begin(); + } + _end_w = _begin_w; + _word.clear(); +} + +void SpellCheck::deleteSpeller() { +} + +bool SpellCheck::updateSpeller() { +#if WITH_GSPELL + auto lang = dictionary_combo.get_active_id(); + if (!lang.empty()) { + const GspellLanguage *language = gspell_language_lookup(lang.c_str()); + _checker = gspell_checker_new(language); + } + + return _checker != nullptr; +#else + return false; +#endif +} + +void SpellCheck::onStart() +{ + if (!getDocument()) + return; + + start_button.set_sensitive(false); + + _stops = 0; + _adds = 0; + clearRects(); + + if (!updateSpeller()) + return; + + _root = getDocument()->getRoot(); + + // empty the list of objects we've checked + _seen_objects.clear(); + + // grab first text + nextText(); + + _working = true; + + doSpellcheck(); +} + +void +SpellCheck::finished () +{ + deleteSpeller(); + + clearRects(); + disconnect(); + + tree_view.unset_model(); + tree_view.set_sensitive(false); + accept_button.set_sensitive(false); + ignore_button.set_sensitive(false); + ignoreonce_button.set_sensitive(false); + add_button.set_sensitive(false); + stop_button.set_sensitive(false); + start_button.set_sensitive(true); + + { + gchar *label; + if (_stops) + label = g_strdup_printf(_("<b>Finished</b>, <b>%d</b> words added to dictionary"), _adds); + else + label = g_strdup_printf("%s", _("<b>Finished</b>, nothing suspicious found")); + banner_label.set_markup(label); + g_free(label); + } + + _seen_objects.clear(); + + _root = nullptr; + + _working = false; +} + +bool +SpellCheck::nextWord() +{ + auto desktop = getDesktop(); + if (!_working || !desktop) + return false; + + if (!_text) { + finished(); + return false; + } + _word.clear(); + + while (_word.size() == 0) { + _begin_w = _end_w; + + if (!_layout || _begin_w == _layout->end()) { + nextText(); + return false; + } + + if (!_layout->isStartOfWord(_begin_w)) { + _begin_w.nextStartOfWord(); + } + + _end_w = _begin_w; + _end_w.nextEndOfWord(); + _word = sp_te_get_string_multiline (_text, _begin_w, _end_w); + } + + // try to link this word with the next if separated by ' + SPObject *char_item = nullptr; + Glib::ustring::iterator text_iter; + _layout->getSourceOfCharacter(_end_w, &char_item, &text_iter); + if (SP_IS_STRING(char_item)) { + int this_char = *text_iter; + if (this_char == '\'' || this_char == 0x2019) { + Inkscape::Text::Layout::iterator end_t = _end_w; + end_t.nextCharacter(); + _layout->getSourceOfCharacter(end_t, &char_item, &text_iter); + if (SP_IS_STRING(char_item)) { + int this_char = *text_iter; + if (g_ascii_isalpha(this_char)) { // 's + _end_w.nextEndOfWord(); + _word = sp_te_get_string_multiline (_text, _begin_w, _end_w); + } + } + } + } + + // skip words containing digits + if (_prefs->getInt(_prefs_path + "ignorenumbers") != 0) { + bool digits = false; + for (unsigned int i : _word) { + if (g_unichar_isdigit(i)) { + digits = true; + break; + } + } + if (digits) { + return false; + } + } + + // skip ALL-CAPS words + if (_prefs->getInt(_prefs_path + "ignoreallcaps") != 0) { + bool allcaps = true; + for (unsigned int i : _word) { + if (!g_unichar_isupper(i)) { + allcaps = false; + break; + } + } + if (allcaps) { + return false; + } + } + + int have = 0; + +#if WITH_GSPELL + if (_checker) { + GError *error = nullptr; + have += gspell_checker_check_word(_checker, _word.c_str(), -1, &error); + } +#endif /* WITH_GSPELL */ + + if (have == 0) { // not found in any! + _stops ++; + + // display it in window + { + gchar *label = g_strdup_printf(_("Not in dictionary: <b>%s</b>"), _word.c_str()); + banner_label.set_markup(label); + g_free(label); + } + + tree_view.set_sensitive(true); + ignore_button.set_sensitive(true); + ignoreonce_button.set_sensitive(true); + add_button.set_sensitive(true); + stop_button.set_sensitive(true); + + // draw rect + std::vector<Geom::Point> points = + _layout->createSelectionShape(_begin_w, _end_w, _text->i2dt_affine()); + if (points.size() >= 4) { // We may not have a single quad if this is a clipped part of text on path; + // in that case skip drawing the rect + Geom::Point tl, br; + tl = br = points.front(); + for (auto & point : points) { + if (point[Geom::X] < tl[Geom::X]) + tl[Geom::X] = point[Geom::X]; + if (point[Geom::Y] < tl[Geom::Y]) + tl[Geom::Y] = point[Geom::Y]; + if (point[Geom::X] > br[Geom::X]) + br[Geom::X] = point[Geom::X]; + if (point[Geom::Y] > br[Geom::Y]) + br[Geom::Y] = point[Geom::Y]; + } + + // expand slightly + Geom::Rect area = Geom::Rect(tl, br); + double mindim = fabs(tl[Geom::Y] - br[Geom::Y]); + if (fabs(tl[Geom::X] - br[Geom::X]) < mindim) + mindim = fabs(tl[Geom::X] - br[Geom::X]); + area.expandBy(MAX(0.05 * mindim, 1)); + + // Create canvas item rect with red stroke. (TODO: a quad could allow non-axis aligned rects.) + auto rect = new Inkscape::CanvasItemRect(desktop->getCanvasSketch(), area); + rect->set_stroke(0xff0000ff); + rect->show(); + _rects.push_back(rect); + + // scroll to make it all visible + Geom::Point const center = desktop->current_center(); + area.expandBy(0.5 * mindim); + Geom::Point scrollto; + double dist = 0; + for (unsigned corner = 0; corner < 4; corner ++) { + if (Geom::L2(area.corner(corner) - center) > dist) { + dist = Geom::L2(area.corner(corner) - center); + scrollto = area.corner(corner); + } + } + desktop->scroll_to_point (scrollto, 1.0); + } + + // select text; if in Text tool, position cursor to the beginning of word + // unless it is already in the word + if (desktop->selection->singleItem() != _text) { + desktop->selection->set (_text); + } + + if (dynamic_cast<Inkscape::UI::Tools::TextTool *>(desktop->event_context)) { + Inkscape::Text::Layout::iterator *cursor = + sp_text_context_get_cursor_position(SP_TEXT_CONTEXT(desktop->event_context), _text); + if (!cursor) // some other text is selected there + desktop->selection->set (_text); + else if (*cursor <= _begin_w || *cursor >= _end_w) + sp_text_context_place_cursor (SP_TEXT_CONTEXT(desktop->event_context), _text, _begin_w); + } + +#if WITH_GSPELL + + // get suggestions + model = Gtk::ListStore::create(tree_columns); + tree_view.set_model(model); + unsigned n_sugg = 0; + + if (_checker) { + GSList *list = gspell_checker_get_suggestions(_checker, _word.c_str(), -1); + std::vector<std::string> suggs; + + // TODO: use a better API for that, or figure out how to make gspellmm. + g_slist_foreach(list, [](gpointer data, gpointer user_data) { + const gchar *suggestion = reinterpret_cast<const gchar*>(data); + std::vector<std::string> *suggs = reinterpret_cast<std::vector<std::string>*>(user_data); + suggs->push_back(suggestion); + }, &suggs); + g_slist_free_full(list, g_free); + + Gtk::TreeModel::iterator iter; + for (std::string sugg : suggs) { + iter = model->append(); + Gtk::TreeModel::Row row = *iter; + row[tree_columns.suggestions] = sugg; + + // select first suggestion + if (++n_sugg == 1) { + tree_view.get_selection()->select(iter); + } + } + } + + accept_button.set_sensitive(n_sugg > 0); + +#endif /* WITH_GSPELL */ + + return true; + + } + return false; +} + + + +void +SpellCheck::deleteLastRect () +{ + if (!_rects.empty()) { + _rects.back()->hide(); + delete _rects.back(); + _rects.pop_back(); + } +} + +void SpellCheck::doSpellcheck () +{ + if (_langs.empty()) { + return; + } + + banner_label.set_markup(_("<i>Checking...</i>")); + + while (_working) + if (nextWord()) + break; +} + +void SpellCheck::onTreeSelectionChange() +{ + accept_button.set_sensitive(true); +} + +void SpellCheck::onObjModified (SPObject* /* blah */, unsigned int /* bleh */) +{ + if (_local_change) { // this was a change by this dialog, i.e. an Accept, skip it + _local_change = false; + return; + } + + if (_working && _root) { + // user may have edited the text we're checking; try to do the most sensible thing in this + // situation + + // just in case, re-get text's layout + _layout = te_get_layout (_text); + + // re-get the word + _layout->validateIterator(&_begin_w); + _end_w = _begin_w; + _end_w.nextEndOfWord(); + Glib::ustring word_new = sp_te_get_string_multiline (_text, _begin_w, _end_w); + if (word_new != _word) { + _end_w = _begin_w; + deleteLastRect (); + doSpellcheck (); // recheck this word and go ahead if it's ok + } + } +} + +void SpellCheck::onObjReleased (SPObject* /* blah */) +{ + if (_working && _root) { + // the text object was deleted + deleteLastRect (); + nextText(); + doSpellcheck (); // get next text and continue + } +} + +void SpellCheck::onAccept () +{ + // insert chosen suggestion + + Glib::RefPtr<Gtk::TreeSelection> selection = tree_view.get_selection(); + Gtk::TreeModel::iterator iter = selection->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring sugg = row[tree_columns.suggestions]; + + if (sugg.length() > 0) { + //g_print("chosen: %s\n", sugg); + _local_change = true; + sp_te_replace(_text, _begin_w, _end_w, sugg.c_str()); + // find the end of the word anew + _end_w = _begin_w; + _end_w.nextEndOfWord(); + DocumentUndo::done(getDocument(), _("Fix spelling"), INKSCAPE_ICON("draw-text")); + } + } + + deleteLastRect(); + doSpellcheck(); +} + +void +SpellCheck::onIgnore () +{ +#if WITH_GSPELL + if (_checker) { + gspell_checker_add_word_to_session(_checker, _word.c_str(), -1); + } +#endif /* WITH_GSPELL */ + + deleteLastRect(); + doSpellcheck(); +} + +void +SpellCheck::onIgnoreOnce () +{ + deleteLastRect(); + doSpellcheck(); +} + +void +SpellCheck::onAdd () +{ + _adds++; + +#if WITH_GSPELL + if (_checker) { + gspell_checker_add_word_to_personal(_checker, _word.c_str(), -1); + } +#endif /* WITH_GSPELL */ + + deleteLastRect(); + doSpellcheck(); +} + +void +SpellCheck::onStop () +{ + finished(); +} + +void SpellCheck::onLanguageChanged() +{ + // First, save language for next load + auto lang = dictionary_combo.get_active_id(); + _prefs->setString("/dialogs/spellcheck/lang", lang); + + if (!_working) { + onStart(); + return; + } + + if (!updateSpeller()) { + return; + } + + // recheck current word + _end_w = _begin_w; + deleteLastRect(); + doSpellcheck(); +} +} +} +} + +/* + 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/ui/dialog/spellcheck.h b/src/ui/dialog/spellcheck.h new file mode 100644 index 0000000..ddef42f --- /dev/null +++ b/src/ui/dialog/spellcheck.h @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Spellcheck dialog + */ +/* Authors: + * bulia byak <bulia@users.sf.net> + * + * Copyright (C) 2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SPELLCHECK_H +#define SEEN_SPELLCHECK_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <gtkmm/box.h> +#include <gtkmm/button.h> +#include <gtkmm/buttonbox.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/liststore.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/separator.h> +#include <gtkmm/treeview.h> +#include <set> +#include <vector> + +#include "text-editing.h" +#include "ui/dialog/dialog-base.h" +#include "ui/widget/scrollprotected.h" + +#if WITH_GSPELL +#include <gspell/gspell.h> +#endif /* WITH_GSPELL */ + +class SPObject; +class SPItem; +class SPCanvasItem; + +namespace Inkscape { +class Preferences; +class CanvasItemRect; + +namespace UI { +namespace Dialog { + +using LanguagePair = std::pair<std::string, std::string>; + +/** + * + * A dialog widget to checking spelling of text elements in the document + * Uses gspell and one of the languages set in the users preference file + * + */ +class SpellCheck : public DialogBase +{ +public: + SpellCheck (); + ~SpellCheck () override; + + static SpellCheck &getInstance() { return *new SpellCheck(); } + + static std::vector<LanguagePair> get_available_langs(); + +private: + void documentReplaced() override; + + /** + * Remove the highlight rectangle form the canvas + */ + void clearRects(); + + /** + * Release handlers to the selected item + */ + void disconnect(); + + /** + * Returns a list of all the text items in the SPObject + */ + void allTextItems (SPObject *r, std::vector<SPItem *> &l, bool hidden, bool locked); + + /** + * Is text inside the SPOject's tree + */ + bool textIsValid (SPObject *root, SPItem *text); + + /** + * Compare the visual bounds of 2 SPItems referred to by a and b + */ + static bool compareTextBboxes(SPItem const *i1, SPItem const *i2); + SPItem *getText (SPObject *root); + void nextText (); + + /** + * Cleanup after spellcheck is finished + */ + void finished (); + + /** + * Find the next word to spell check + */ + bool nextWord(); + void deleteLastRect (); + void doSpellcheck (); + + /** + * Update speller from language combobox + * @return true if update was successful + */ + bool updateSpeller(); + void deleteSpeller(); + + /** + * Accept button clicked + */ + void onAccept (); + + /** + * Ignore button clicked + */ + void onIgnore (); + + /** + * Ignore once button clicked + */ + void onIgnoreOnce (); + + /** + * Add button clicked + */ + void onAdd (); + + /** + * Stop button clicked + */ + void onStop (); + + /** + * Start button clicked + */ + void onStart (); + + /** + * Language selection changed + */ + void onLanguageChanged(); + + /** + * Selected object modified on canvas + */ + void onObjModified (SPObject* /* blah */, unsigned int /* bleh */); + + /** + * Selected object removed from canvas + */ + void onObjReleased (SPObject* /* blah */); + + /** + * Selection in suggestions text view changed + */ + void onTreeSelectionChange(); + + SPObject *_root; + +#if WITH_GSPELL + GspellChecker *_checker = nullptr; +#endif /* WITH_GSPELL */ + + /** + * list of canvasitems (currently just rects) that mark misspelled things on canvas + */ + std::vector<Inkscape::CanvasItemRect *> _rects; + + /** + * list of text objects we have already checked in this session + */ + std::set<SPItem *> _seen_objects; + + /** + * the object currently being checked + */ + SPItem *_text; + + /** + * current objects layout + */ + Inkscape::Text::Layout const *_layout; + + /** + * iterators for the start and end of the current word + */ + Inkscape::Text::Layout::iterator _begin_w; + Inkscape::Text::Layout::iterator _end_w; + + /** + * the word we're checking + */ + Glib::ustring _word; + + /** + * counters for the number of stops and dictionary adds + */ + int _stops; + int _adds; + + /** + * true if we are in the middle of a check + */ + bool _working; + + /** + * connect to the object being checked in case it is modified or deleted by user + */ + sigc::connection _modified_connection; + sigc::connection _release_connection; + + /** + * true if the spell checker dialog has changed text, to suppress modified callback + */ + bool _local_change; + + Inkscape::Preferences *_prefs; + + std::vector<LanguagePair> _langs; + + /* + * Dialogs widgets + */ + Gtk::Label banner_label; + Gtk::ButtonBox banner_hbox; + Gtk::ScrolledWindow scrolled_window; + Gtk::TreeView tree_view; + Glib::RefPtr<Gtk::ListStore> model; + + Gtk::Box suggestion_hbox; + Gtk::Box changebutton_vbox; + Gtk::Button accept_button; + Gtk::Button ignoreonce_button; + Gtk::Button ignore_button; + + Gtk::Button add_button; + Gtk::Button pref_button; + Gtk::Label dictionary_label; + Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText> dictionary_combo; + Gtk::Box dictionary_hbox; + Gtk::Separator action_sep; + Gtk::Button stop_button; + Gtk::Button start_button; + Gtk::ButtonBox actionbutton_hbox; + + class TreeColumns : public Gtk::TreeModel::ColumnRecord + { + public: + TreeColumns() + { + add(suggestions); + } + ~TreeColumns() override = default; + Gtk::TreeModelColumn<Glib::ustring> suggestions; + }; + TreeColumns tree_columns; +}; +} +} +} + +#endif /* !SEEN_SPELLCHECK_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/ui/dialog/startup.cpp b/src/ui/dialog/startup.cpp new file mode 100644 index 0000000..21510cf --- /dev/null +++ b/src/ui/dialog/startup.cpp @@ -0,0 +1,849 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for the about screen + * + * Copyright (C) Martin Owens 2019 <doctormo@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "startup.h" + +#include <fstream> +#include <glibmm/i18n.h> +#include <streambuf> +#include <string> + +#include "color-rgba.h" +#include "file.h" +#include "inkscape-application.h" +#include "inkscape-version-info.h" +#include "inkscape-version.h" +#include "inkscape.h" +#include "io/resource.h" +#include "object/sp-namedview.h" +#include "preferences.h" +#include "ui/dialog/filedialog.h" +#include "ui/shortcuts.h" +#include "ui/themes.h" +#include "ui/util.h" +#include "util/units.h" + +using namespace Inkscape::IO; +using namespace Inkscape::UI::View; +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class NameIdCols: public Gtk::TreeModel::ColumnRecord { + public: + // These types must match those for the model in the .glade file + NameIdCols() { + this->add(this->col_name); + this->add(this->col_id); + } + Gtk::TreeModelColumn<Glib::ustring> col_name; + Gtk::TreeModelColumn<Glib::ustring> col_id; +}; + +class CanvasCols: public Gtk::TreeModel::ColumnRecord { + public: + // These types must match those for the model in the .glade file + CanvasCols() { + this->add(this->id); + this->add(this->name); + this->add(this->icon_filename); + this->add(this->pagecolor); + this->add(this->checkered); + this->add(this->bordercolor); + this->add(this->shadow); + } + Gtk::TreeModelColumn<Glib::ustring> id; + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<Glib::ustring> icon_filename; + Gtk::TreeModelColumn<Glib::ustring> pagecolor; + Gtk::TreeModelColumn<bool> checkered; + Gtk::TreeModelColumn<Glib::ustring> bordercolor; + Gtk::TreeModelColumn<bool> shadow; +}; + +class TemplateCols: public Gtk::TreeModel::ColumnRecord { + public: + // These types must match those for the model in the .glade file + TemplateCols() { + this->add(this->name); + this->add(this->icon); + this->add(this->filename); + this->add(this->width); + this->add(this->height); + } + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<Glib::ustring> icon; + Gtk::TreeModelColumn<Glib::ustring> filename; + Gtk::TreeModelColumn<Glib::ustring> width; + Gtk::TreeModelColumn<Glib::ustring> height; +}; + + +class ThemeCols: public Gtk::TreeModel::ColumnRecord { + public: + // These types must match those for the model in the .glade file + ThemeCols() { + this->add(this->id); + this->add(this->name); + this->add(this->theme); + this->add(this->icons); + this->add(this->base); + this->add(this->base_dark); + this->add(this->success); + this->add(this->warn); + this->add(this->error); + this->add(this->symbolic); + this->add(this->smallicons); + this->add(this->enabled); + } + Gtk::TreeModelColumn<Glib::ustring> id; + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<Glib::ustring> theme; + Gtk::TreeModelColumn<Glib::ustring> icons; + Gtk::TreeModelColumn<Glib::ustring> base; + Gtk::TreeModelColumn<Glib::ustring> base_dark; + Gtk::TreeModelColumn<Glib::ustring> success; + Gtk::TreeModelColumn<Glib::ustring> warn; + Gtk::TreeModelColumn<Glib::ustring> error; + Gtk::TreeModelColumn<bool> symbolic; + Gtk::TreeModelColumn<bool> smallicons; + Gtk::TreeModelColumn<bool> enabled; +}; + +/** + * Color is store as a string in the form #RRGGBBAA, '0' means "unset" + * + * @param color - The string color from glade. + */ +unsigned int get_color_value(const Glib::ustring color) +{ + Gdk::RGBA gdk_color = Gdk::RGBA(color); + ColorRGBA sp_color(gdk_color.get_red(), gdk_color.get_green(), + gdk_color.get_blue(), gdk_color.get_alpha()); + return sp_color.getIntValue(); +} + +StartScreen::StartScreen() + : Gtk::Dialog() +{ + set_can_focus(true); + grab_focus(); + set_can_default(true); + grab_default(); + set_urgency_hint(true); // Draw user's attention to this window! + set_modal(true); + set_position(Gtk::WIN_POS_CENTER_ALWAYS); + set_default_size(700, 360); + + Glib::ustring gladefile = Resource::get_filename(Resource::UIS, "inkscape-start.glade"); + + try { + builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_error("Glade file loading failed for boot screen"); + // cleanup? + } + + // Get window from Glade file. + builder->get_widget("start-screen-window", window); + set_name("start-screen-window"); + set_title(Inkscape::inkscape_version()); + + // Get references to various widgets used globally. + builder->get_widget("tabs", tabs); + builder->get_widget("kinds", kinds); + builder->get_widget("banner", banners); + builder->get_widget("themes", themes); + builder->get_widget("recent_treeview", recent_treeview); + + // Get references to various widget used locally. (In order of appearance.) + Gtk::ComboBox* canvas = nullptr; + Gtk::ComboBox* keys = nullptr; + Gtk::Button* save = nullptr; + Gtk::Button* thanks = nullptr; + Gtk::Button* close_btn = nullptr; + Gtk::Button* new_btn = nullptr; + Gtk::Button* show_toggle = nullptr; + Gtk::Switch* dark_toggle = nullptr; + builder->get_widget("canvas", canvas); + builder->get_widget("keys", keys); + builder->get_widget("save", save); + builder->get_widget("thanks", thanks); + builder->get_widget("show_toggle", show_toggle); + builder->get_widget("dark_toggle", dark_toggle); + builder->get_widget("load", load_btn); + builder->get_widget("new", new_btn); + builder->get_widget("close_window", close_btn); + + // Unparent to move to our dialog window. + auto parent = banners->get_parent(); + parent->remove(*banners); + parent->remove(*tabs); + + // Add signals and setup things. + auto prefs = Inkscape::Preferences::get(); + + tabs->signal_switch_page().connect(sigc::mem_fun(*this, &StartScreen::notebook_switch)); + + // Setup the lists of items + enlist_recent_files(); + enlist_keys(); + filter_themes(); + set_active_combo("themes", prefs->getString("/options/boot/theme")); + set_active_combo("canvas", prefs->getString("/options/boot/canvas")); + + // initialise dark depending on prefs and background + refresh_dark_switch(); + + // Welcome! tab + std::string welcome_text_file = Resource::get_filename_string(Resource::SCREENS, "start-welcome-text.svg", true); + Gtk::Image *welcome_text; + builder->get_widget("welcome_text", welcome_text); + welcome_text->set(welcome_text_file); + + canvas->signal_changed().connect(sigc::mem_fun(*this, &StartScreen::canvas_changed)); + keys->signal_changed().connect(sigc::mem_fun(*this, &StartScreen::keyboard_changed)); + themes->signal_changed().connect(sigc::mem_fun(*this, &StartScreen::theme_changed)); + dark_toggle->property_active().signal_changed().connect(sigc::mem_fun(*this, &StartScreen::theme_changed)); + save->signal_clicked().connect(sigc::bind<Gtk::Button *>(sigc::mem_fun(*this, &StartScreen::notebook_next), save)); + + // "Supported by You" tab + thanks->signal_clicked().connect(sigc::bind<Gtk::Button *>(sigc::mem_fun(*this, &StartScreen::notebook_next), thanks)); + + // "Time to Draw" tab + recent_treeview->signal_row_activated().connect(sigc::hide(sigc::hide((sigc::mem_fun(*this, &StartScreen::load_document))))); + recent_treeview->get_selection()->signal_changed().connect(sigc::mem_fun(*this, &StartScreen::on_recent_changed)); + kinds->signal_switch_page().connect(sigc::mem_fun(*this, &StartScreen::on_kind_changed)); + load_btn->set_sensitive(true); + + for (auto widget : kinds->get_children()) { + auto container = dynamic_cast<Gtk::Container *>(widget); + if (container) { + widget = container->get_children()[0]; + } + auto template_list = dynamic_cast<Gtk::IconView *>(widget); + if (template_list) { + template_list->signal_selection_changed().connect([=]() { response(GTK_RESPONSE_APPLY); }); + } + } + + show_toggle->signal_clicked().connect(sigc::mem_fun(*this, &StartScreen::show_toggle)); + load_btn->signal_clicked().connect(sigc::mem_fun(*this, &StartScreen::load_document)); + new_btn->signal_clicked().connect([=] { response(GTK_RESPONSE_APPLY); }); + close_btn->signal_clicked().connect([=] { response(GTK_RESPONSE_CANCEL); }); + + // Reparent to our dialog window + set_titlebar(*banners); + Gtk::Box* box = get_content_area(); + box->add(*tabs); + + // Show the first tab ONLY on the first run for this version + std::string opt_shown = "/options/boot/shown/ver"; + opt_shown += Inkscape::version_string_without_revision; + _first_open = prefs->getBool(opt_shown, false); + if(!_first_open) { + theme_changed(); + tabs->set_current_page(0); + prefs->setBool(opt_shown, true); + } else { + tabs->set_current_page(2); + notebook_switch(nullptr, 2); + } + + set_modal(true); + set_position(Gtk::WIN_POS_CENTER_ALWAYS); + property_resizable() = false; + set_default_size(700, 360); + show(); +} + +StartScreen::~StartScreen() +{ + // These are "owned" by builder... don't delete them! + banners->get_parent()->remove(*banners); + tabs->get_parent()->remove(*tabs); +} + +/** + * Return the active row of the named combo box. + * + * @param widget_name - The name of the widget in the glade file + * @return Gtk Row object ready for use. + * @throws Three errors depending on where it failed. + */ +Gtk::TreeModel::Row +StartScreen::active_combo(std::string widget_name) +{ + Gtk::ComboBox *combo; + builder->get_widget(widget_name, combo); + if (!combo) throw 1; + Gtk::TreeModel::iterator iter = combo->get_active(); + if (!iter) throw 2; + Gtk::TreeModel::Row row = *iter; + if (!row) throw 3; + return row; +} + +/** + * Set the active item in the combo based on the unique_id (column set in glade) + * + * @param widget_name - The name of the widget in the glade file + * @param unique_id - The column id to activate, sets to first item if blank. + */ +void +StartScreen::set_active_combo(std::string widget_name, std::string unique_id) +{ + Gtk::ComboBox *combo; + builder->get_widget(widget_name, combo); + if (combo) { + if (unique_id.empty()) { + combo->set_active(0); // Select the first + } else if(!combo->set_active_id(unique_id)) { + combo->set_active(-1); // Select nothing + } + } +} + +/** + * When a notbook is switched, reveal the right banner image (gtk signal). + */ +void +StartScreen::notebook_switch(Gtk::Widget *tab, guint page_num) +{ + int page = 0; + for (auto banner : banners->get_children()) { + if (auto revealer = dynamic_cast<Gtk::Revealer *>(banner)) { + revealer->set_reveal_child(page == page_num); + page++; + } + } +} + +void +StartScreen::enlist_recent_files() +{ + NameIdCols cols; + if (!recent_treeview) return; + // We're not sure why we have to ask C for the TreeStore object + auto store = Glib::wrap(GTK_LIST_STORE(gtk_tree_view_get_model(recent_treeview->gobj()))); + store->clear(); + + // Open [other] + Gtk::TreeModel::Row first_row = *(store->append()); + first_row[cols.col_name] = _("Browse for other files..."); + first_row[cols.col_id] = ""; + recent_treeview->get_selection()->select(store->get_path(first_row)); + + Glib::RefPtr<Gtk::RecentManager> manager = Gtk::RecentManager::get_default(); + for (auto item : manager->get_items()) { + if (item->has_application(g_get_prgname()) + || item->has_application("org.inkscape.Inkscape") + || item->has_application("inkscape") + || item->has_application("inkscape.exe") + ) { + // This uri is a GVFS uri, so parse it with that or it will fail. + auto file = Gio::File::create_for_uri(item->get_uri()); + std::string path = file->get_path(); + if (!path.empty() && Glib::file_test(path, Glib::FILE_TEST_IS_REGULAR) + && item->get_mime_type() == "image/svg+xml") { + Gtk::TreeModel::Row row = *(store->append()); + row[cols.col_name] = item->get_display_name(); + row[cols.col_id] = item->get_uri(); + } + } + } +} + +/** + * Called when a new recent document is selected. + */ +void +StartScreen::on_recent_changed() +{ + // TODO: In the future this is where previews and other information can be loaded. +} + +/** + * Called when the left side tabs are changed. + */ +void +StartScreen::on_kind_changed(Gtk::Widget *tab, guint page_num) +{ + if (page_num == 0) { + load_btn->show(); + } else { + load_btn->hide(); + } +} + +/** + * Called when new button clicked or template is double clicked, or escape pressed. + */ +void +StartScreen::new_document() +{ + Glib::ustring filename = sp_file_default_template_uri(); + Glib::ustring width = ""; + Glib::ustring height = ""; + + // Find requested file name. + Glib::RefPtr<Gio::File> file; + if (kinds) { + Gtk::Widget *selected_widget = kinds->get_children()[kinds->get_current_page()]; + auto container = dynamic_cast<Gtk::Container *>(selected_widget); + if (container) { + selected_widget = container->get_children()[0]; + } + + auto template_list = dynamic_cast<Gtk::IconView *>(selected_widget); + if (template_list) { + auto items = template_list->get_selected_items(); + if (!items.empty()) { + auto iter = template_list->get_model()->get_iter(items[0]); + Gtk::TreeModel::Row row = *iter; + if (row) { + TemplateCols cols; + Glib::ustring template_filename = row[cols.filename]; + if (!(template_filename == "-")) { + filename = Resource::get_filename_string( + Resource::TEMPLATES, template_filename.c_str(), true); + } + // This isn't used on opening, just for checking it's existence. + file = Gio::File::create_for_path(filename); + width = row[cols.width]; + height = row[cols.height]; + } + } + } + } + + if (!file) { + // Failure to open, so open up a new document instead. + file = Gio::File::create_for_path(filename); + } + + if (!file) { + // We're really messed up... so give up! + std::cerr << "StartScreen::load_document(): Failed to find: " << filename << std::endl; + return; + } + + // Now we have filename, open document. + auto app = InkscapeApplication::instance(); + + // If it was a template file, modify the document according to user's input. + _document = app->document_new (filename); + auto nv = _document->getNamedView(); + + if (!width.empty()) { + // Set the width, height and default display units for the selected template + auto q_width = unit_table.parseQuantity(width); + _document->setWidthAndHeight(q_width, unit_table.parseQuantity(height), true); + nv->setAttribute("inkscape:document-units", q_width.unit->abbr); + _document->setDocumentScale(1.0); + } + + DocumentUndo::clearUndo(_document); + _document->setModifiedSinceSave(false); +} + +/** + * Called when load button clicked. + */ +void +StartScreen::load_document() +{ + NameIdCols cols; + auto prefs = Inkscape::Preferences::get(); + auto app = InkscapeApplication::instance(); + + if (!recent_treeview) + return; + + auto iter = recent_treeview->get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + if (row) { + Glib::ustring _file = row[cols.col_id]; + Glib::RefPtr<Gio::File> file; + + if (!_file.empty()) { + file = Gio::File::create_for_uri(_file); + } else { + Glib::ustring open_path = prefs->getString("/dialogs/open/path"); + if (open_path.empty()) { + open_path = g_get_home_dir(); + open_path.append(G_DIR_SEPARATOR_S); + } + + // Browse for file instead + auto browser = Inkscape::UI::Dialog::FileOpenDialog::create( + *this, open_path, Inkscape::UI::Dialog::SVG_TYPES, _("Open a different file")); + + if (browser->show()) { + prefs->setString("/dialogs/open/path", browser->getCurrentDirectory()); + file = Gio::File::create_for_path(browser->getFilename()); + delete browser; + } else { + delete browser; + return; // Cancel + } + } + + // Now we have filename, open document. + bool canceled = false; + _document = app->document_open(file, &canceled); + + if (!canceled && _document) { + // We're done, hand back to app. + response(GTK_RESPONSE_OK); + } + } + } +} + +/** + * When a button needs to go to the next notebook page. + */ +void +StartScreen::notebook_next(Gtk::Widget *button) +{ + int page = tabs->get_current_page(); + if (page == 2) { + response(GTK_RESPONSE_CANCEL); // Only occurs from keypress. + } else { + tabs->set_current_page(page + 1); + } +} + +/** + * When a key is pressed in the main window. + */ +bool +StartScreen::on_key_press_event(GdkEventKey* event) +{ +#ifdef GDK_WINDOWING_QUARTZ + // On macOS only, if user press Cmd+Q => exit + if (event->keyval == 'q' && event->state == (GDK_MOD2_MASK | GDK_META_MASK)) { + close(); + return false; + } +#endif + switch (event->keyval) { + case GDK_KEY_Escape: + // Prevent loading any selected items + response(GTK_RESPONSE_CANCEL); + return true; + case GDK_KEY_Return: + notebook_next(nullptr); + return true; + } + + return false; +} + +void +StartScreen::on_response(int response_id) +{ + if (response_id == GTK_RESPONSE_DELETE_EVENT) { + // Don't open a window for force closing. + return; + } + if (response_id == GTK_RESPONSE_CANCEL) { + kinds = nullptr; + if (_first_open) { + // Disable the screen if the user cancels on their first run. + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/boot/enabled", false); + } + } + if (response_id != GTK_RESPONSE_OK) { + // Most actions cause a new document to appear. + new_document(); + } +} + +void +StartScreen::show_toggle() +{ + Gtk::ToggleButton *button; + builder->get_widget("show_toggle", button); + if (button) { + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/boot/enabled", button->get_active()); + } else { + g_warning("Can't find toggle button widget."); + } +} + +/** + * Refresh theme in-place so user can see a semi-preview. This theme selection + * is not meant to be perfect, but hint to the user that they can set the + * theme if they want. + * + * @param theme_name - The name of the theme to load. + */ +void +StartScreen::refresh_theme(Glib::ustring theme_name) +{ + auto const screen = Gdk::Screen::get_default(); + if (INKSCAPE.themecontext->getContrastThemeProvider()) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getContrastThemeProvider()); + } + auto settings = Gtk::Settings::get_default(); + + auto prefs = Inkscape::Preferences::get(); + + settings->property_gtk_theme_name() = theme_name; + settings->property_gtk_application_prefer_dark_theme() = prefs->getBool("/theme/preferDarkTheme", true); + settings->property_gtk_icon_theme_name() = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", "")); + + if (prefs->getBool("/theme/symbolicIcons", false)) { + get_style_context()->add_class("symbolic"); + get_style_context()->remove_class("regular"); + } else { + get_style_context()->add_class("regular"); + get_style_context()->remove_class("symbolic"); + } + + if (INKSCAPE.themecontext->getColorizeProvider()) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider()); + } + if (!prefs->getBool("/theme/symbolicDefaultHighColors", false)) { + Gtk::CssProvider::create(); + Glib::ustring css_str = INKSCAPE.themecontext->get_symbolic_colors(); + try { + INKSCAPE.themecontext->getColorizeProvider()->load_from_data(css_str); + } catch (const Gtk::CssProviderError &ex) { + g_critical("CSSProviderError::load_from_data(): failed to load '%s'\n(%s)", css_str.c_str(), ex.what().c_str()); + } + Gtk::StyleContext::add_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider(), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + // set dark switch and disable if there is no prefer option for dark + refresh_dark_switch(); + + INKSCAPE.themecontext->getChangeThemeSignal().emit(); +} + +/** + * Set the theme, icon pack and other theme options from a set defined + * in the glade file. The combo box has a number of columns with the needed + * data describing how to set up the theme. + */ +void +StartScreen::theme_changed() +{ + auto prefs = Inkscape::Preferences::get(); + + ThemeCols cols; + try { + auto row = active_combo("themes"); + Glib::ustring theme_id = row[cols.id]; + if (theme_id == "custom") return; + prefs->setString("/options/boot/theme", row[cols.id]); + + // Update theme from combo. + Glib::ustring icons = row[cols.icons]; + prefs->setBool("/toolbox/tools/small", row[cols.smallicons]); + prefs->setString("/theme/gtkTheme", row[cols.theme]); + prefs->setString("/theme/iconTheme", icons); + prefs->setBool("/theme/symbolicIcons", row[cols.symbolic]); + + Gtk::Switch* dark_toggle = nullptr; + builder->get_widget("dark_toggle", dark_toggle); + bool is_dark = dark_toggle->get_active(); + prefs->setBool("/theme/preferDarkTheme", is_dark); + prefs->setBool("/theme/darkTheme", is_dark); + // Symbolic icon colours + if (get_color_value(row[cols.base]) == 0) { + prefs->setBool("/theme/symbolicDefaultBaseColors", true); + prefs->setBool("/theme/symbolicDefaultHighColors", true); + } else { + Glib::ustring prefix = "/theme/" + icons; + prefs->setBool("/theme/symbolicDefaultBaseColors", false); + prefs->setBool("/theme/symbolicDefaultHighColors", false); + if (is_dark) { + prefs->setUInt(prefix + "/symbolicBaseColor", get_color_value(row[cols.base_dark])); + } else { + prefs->setUInt(prefix + "/symbolicBaseColor", get_color_value(row[cols.base])); + } + prefs->setUInt(prefix + "/symbolicSuccessColor", get_color_value(row[cols.success])); + prefs->setUInt(prefix + "/symbolicWarningColor", get_color_value(row[cols.warn])); + prefs->setUInt(prefix + "/symbolicErrorColor", get_color_value(row[cols.error])); + } + + refresh_theme(prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", ""))); + } catch(int e) { + g_warning("Couldn't find theme value."); + } +} + +/** + * Called when the canvas dropdown changes. + */ +void +StartScreen::canvas_changed() +{ + CanvasCols cols; + try { + auto row = active_combo("canvas"); + + auto prefs = Inkscape::Preferences::get(); + prefs->setString("/options/boot/canvas", row[cols.id]); + + Gdk::RGBA gdk_color = Gdk::RGBA(row[cols.pagecolor]); + SPColor sp_color(gdk_color.get_red(), gdk_color.get_green(), gdk_color.get_blue()); + prefs->setString("/template/base/pagecolor", sp_color.toString()); + prefs->setDouble("/template/base/pageopacity", gdk_color.get_alpha()); + + Gdk::RGBA gdk_border = Gdk::RGBA(row[cols.bordercolor]); + SPColor sp_border(gdk_border.get_red(), gdk_border.get_green(), gdk_border.get_blue()); + prefs->setString("/template/base/bordercolor", sp_border.toString()); + prefs->setDouble("/template/base/borderopacity", gdk_border.get_alpha()); + + prefs->setBool("/template/base/pagecheckerboard", row[cols.checkered]); + prefs->setInt("/template/base/pageshadow", row[cols.shadow] ? 2 : 0); + + } catch(int e) { + g_warning("Couldn't find canvas value."); + } +} + +void +StartScreen::filter_themes() +{ + ThemeCols cols; + // We need to disable themes which aren't available. + auto store = Glib::wrap(GTK_LIST_STORE(gtk_combo_box_get_model(themes->gobj()))); + auto available = INKSCAPE.themecontext->get_available_themes(); + + // Detect use of custom theme here, detect defaults used in many systems. + auto settings = Gtk::Settings::get_default(); + Glib::ustring theme_name = settings->property_gtk_theme_name(); + Glib::ustring icons_name = settings->property_gtk_icon_theme_name(); + + bool has_system_theme = false; + if (theme_name != "Adwaita" || icons_name != "hicolor") { + has_system_theme = true; + /* Enable if/when we want custom to be the default. + if (prefs->getString("/options/boot/theme").empty()) { + prefs->setString("/options/boot/theme", "system") + theme_changed(); + }*/ + } + + for(auto row : store->children()) { + Glib::ustring theme = row[cols.theme]; + if (!row[cols.enabled]) { + // Available themes; We only "enable" them, we don't disable them. + row[cols.enabled] = available.find(theme) != available.end(); + } else if(row[cols.id] == "system" && !has_system_theme) { + // Disable system theme option if not available. + row[cols.enabled] = false; + } + } +} + +void +StartScreen::enlist_keys() +{ + NameIdCols cols; + Gtk::ComboBox *keys; + builder->get_widget("keys", keys); + if (!keys) return; + + auto store = Glib::wrap(GTK_LIST_STORE(gtk_combo_box_get_model(keys->gobj()))); + store->clear(); + + for(auto item: Inkscape::Shortcuts::get_file_names()){ + Gtk::TreeModel::Row row = *(store->append()); + row[cols.col_name] = item.first; + row[cols.col_id] = item.second; + } + + auto prefs = Inkscape::Preferences::get(); + auto current = prefs->getString("/options/kbshortcuts/shortcutfile"); + if (current.empty()) { + current = "inkscape.xml"; + } + keys->set_active_id(current); +} + +/** + * Set the keys file based on the keys set in the enlist above + */ +void +StartScreen::keyboard_changed() +{ + NameIdCols cols; + try { + auto row = active_combo("keys"); + auto prefs = Inkscape::Preferences::get(); + Glib::ustring set_to = row[cols.col_id]; + prefs->setString("/options/kbshortcuts/shortcutfile", set_to); + Inkscape::Shortcuts::getInstance().init(); + + Gtk::InfoBar* keys_warning; + builder->get_widget("keys_warning", keys_warning); + if (set_to != "inkscape.xml" && set_to != "default.xml") { + keys_warning->set_message_type(Gtk::MessageType::MESSAGE_WARNING); + keys_warning->show(); + } else { + keys_warning->hide(); + } + } catch(int e) { + g_warning("Couldn't find keys value."); + } +} + +/** + * Set Dark Switch based on current selected theme. + * We will disable switch if current theme doesn't have prefer dark theme option. + */ + +void StartScreen::refresh_dark_switch() +{ + auto prefs = Inkscape::Preferences::get(); + + Gtk::Container *window = dynamic_cast<Gtk::Container *>(get_toplevel()); + bool dark = INKSCAPE.themecontext->isCurrentThemeDark(window); + prefs->setBool("/theme/preferDarkTheme", dark); + prefs->setBool("/theme/darkTheme", dark); + + auto themes = INKSCAPE.themecontext->get_available_themes(); + Glib::ustring current_theme = prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", "")); + + Gtk::Switch *dark_toggle = nullptr; + builder->get_widget("dark_toggle", dark_toggle); + + if (!themes[current_theme]) { + dark_toggle->set_sensitive(false); + } else { + dark_toggle->set_sensitive(true); + } + dark_toggle->set_active(dark); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/startup.h b/src/ui/dialog/startup.h new file mode 100644 index 0000000..b555aae --- /dev/null +++ b/src/ui/dialog/startup.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for the start screen + * + * Copyright (C) Martin Owens 2020 <doctormo@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef STARTSCREEN_H +#define STARTSCREEN_H + +#include <gtkmm.h> + +class SPDocument; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class StartScreen : public Gtk::Dialog { + +public: + StartScreen(); + ~StartScreen() override; + + SPDocument* get_document() { return _document; } + +protected: + bool on_key_press_event(GdkEventKey* event) override; + void on_response(int response_id) override; + +private: + void notebook_next(Gtk::Widget *button); + Gtk::TreeModel::Row active_combo(std::string widget_name); + void set_active_combo(std::string widget_name, std::string unique_id); + void show_toggle(); + void enlist_recent_files(); + void enlist_keys(); + void filter_themes(); + void keyboard_changed(); + void notebook_switch(Gtk::Widget *tab, guint page_num); + + void theme_changed(); + void canvas_changed(); + void refresh_theme(Glib::ustring theme_name); + void refresh_dark_switch(); + + void new_document(); + void load_document(); + void on_recent_changed(); + void on_kind_changed(Gtk::Widget *tab, guint page_num); + + +private: + Glib::RefPtr<Gtk::Builder> builder; + Gtk::Window *window = nullptr; + Gtk::Notebook *tabs = nullptr; + Gtk::Notebook *kinds = nullptr; + Gtk::Fixed *banners = nullptr; + Gtk::ComboBox *themes = nullptr; + Gtk::TreeView *recent_treeview = nullptr; + Gtk::Button *load_btn = nullptr; + + SPDocument* _document = nullptr; + + bool _first_open = false; +}; + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // STARTSCREEN_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/ui/dialog/styledialog.cpp b/src/ui/dialog/styledialog.cpp new file mode 100644 index 0000000..1b05e78 --- /dev/null +++ b/src/ui/dialog/styledialog.cpp @@ -0,0 +1,1609 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for CSS styles + */ +/* Authors: + * Kamalpreet Kaur Grewal + * Tavmjong Bah + * Jabiertxof + * + * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com> + * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "styledialog.h" + +#include <map> +#include <regex> +#include <utility> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include "attribute-rel-svg.h" +#include "attributes.h" +#include "document-undo.h" +#include "inkscape.h" +#include "io/resource.h" +#include "selection.h" +#include "style-internal.h" +#include "style.h" + +#include "svg/svg-color.h" +#include "ui/icon-loader.h" +#include "ui/widget/iconrenderer.h" +#include "util/trim.h" +#include "xml/attribute-record.h" +#include "xml/node-observer.h" +#include "xml/sp-css-attr.h" + +// G_MESSAGES_DEBUG=DEBUG_STYLEDIALOG gdb ./inkscape +// #define DEBUG_STYLEDIALOG +// #define G_LOG_DOMAIN "STYLEDIALOG" + +using Inkscape::DocumentUndo; + +namespace Inkscape { + +/** + * Get the first <style> element's first text node. If no such node exists and + * `create_if_missing` is false, then return NULL. + * + * Only finds <style> elements in root or in root-level <defs>. + */ +XML::Node *get_first_style_text_node(XML::Node *root, bool create_if_missing) +{ + static GQuark const CODE_svg_style = g_quark_from_static_string("svg:style"); + static GQuark const CODE_svg_defs = g_quark_from_static_string("svg:defs"); + + XML::Node *styleNode = nullptr; + XML::Node *textNode = nullptr; + + if (!root) { + return nullptr; + } + + for (auto *node = root->firstChild(); node; node = node->next()) { + if (node->code() == CODE_svg_defs) { + textNode = get_first_style_text_node(node, false); + if (textNode != nullptr) { + return textNode; + } + } + + if (node->code() == CODE_svg_style) { + styleNode = node; + break; + } + } + + if (styleNode == nullptr) { + if (!create_if_missing) + return nullptr; + + styleNode = root->document()->createElement("svg:style"); + root->addChild(styleNode, nullptr); + Inkscape::GC::release(styleNode); + } + + for (auto *node = styleNode->firstChild(); node; node = node->next()) { + if (node->type() == XML::NodeType::TEXT_NODE) { + textNode = node; + break; + } + } + + if (textNode == nullptr) { + if (!create_if_missing) + return nullptr; + + textNode = root->document()->createTextNode(""); + styleNode->appendChild(textNode); + Inkscape::GC::release(textNode); + } + + return textNode; +} + +namespace UI { +namespace Dialog { + +// Keeps a watch on style element +class StyleDialog::NodeObserver : public Inkscape::XML::NodeObserver { + public: + NodeObserver(StyleDialog *styledialog) + : _styledialog(styledialog) + { + g_debug("StyleDialog::NodeObserver: Constructor"); + }; + + void notifyContentChanged(Inkscape::XML::Node &node, Inkscape::Util::ptr_shared old_content, + Inkscape::Util::ptr_shared new_content) override; + + StyleDialog *_styledialog; +}; + + +void StyleDialog::NodeObserver::notifyContentChanged(Inkscape::XML::Node & /*node*/, + Inkscape::Util::ptr_shared /*old_content*/, + Inkscape::Util::ptr_shared /*new_content*/) +{ + + g_debug("StyleDialog::NodeObserver::notifyContentChanged"); + _styledialog->_updating = false; + _styledialog->readStyleElement(); +} + + +// Keeps a watch for new/removed/changed nodes +// (Must update objects that selectors match.) +class StyleDialog::NodeWatcher : public Inkscape::XML::NodeObserver { + public: + NodeWatcher(StyleDialog *styledialog) + : _styledialog(styledialog) + { + g_debug("StyleDialog::NodeWatcher: Constructor"); + }; + + void notifyChildAdded(Inkscape::XML::Node & /*node*/, Inkscape::XML::Node &child, + Inkscape::XML::Node * /*prev*/) override + { + _styledialog->_nodeAdded(child); + } + + void notifyChildRemoved(Inkscape::XML::Node & /*node*/, Inkscape::XML::Node &child, + Inkscape::XML::Node * /*prev*/) override + { + _styledialog->_nodeRemoved(child); + } + void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark qname, Util::ptr_shared /*old_value*/, + Util::ptr_shared /*new_value*/) override + { + static GQuark const CODE_id = g_quark_from_static_string("id"); + static GQuark const CODE_class = g_quark_from_static_string("class"); + static GQuark const CODE_style = g_quark_from_static_string("style"); + + if (qname == CODE_id || qname == CODE_class || qname == CODE_style) { + _styledialog->_nodeChanged(node); + } + } + + StyleDialog *_styledialog; +}; + +void StyleDialog::_nodeAdded(Inkscape::XML::Node &node) +{ + if (!getShowing()) { + return; + } + readStyleElement(); +} + +void StyleDialog::_nodeRemoved(Inkscape::XML::Node &repr) +{ + if (!getShowing()) { + return; + } + if (_textNode == &repr) { + _textNode = nullptr; + } + + readStyleElement(); +} + +void StyleDialog::_nodeChanged(Inkscape::XML::Node &object) +{ + if (!getShowing()) { + return; + } + g_debug("StyleDialog::_nodeChanged"); + readStyleElement(); +} + +/** + * Constructor + * A treeview and a set of two buttons are added to the dialog. _addSelector + * adds selectors to treeview. _delSelector deletes the selector from the dialog. + * Any addition/deletion of the selectors updates XML style element accordingly. + */ +StyleDialog::StyleDialog() + : DialogBase("/dialogs/style", "Style") + , _updating(false) + , _textNode(nullptr) + , _scrollpos(0) + , _deleted_pos(0) + , _deletion(false) +{ + g_debug("StyleDialog::StyleDialog"); + + m_nodewatcher.reset(new StyleDialog::NodeWatcher(this)); + m_styletextwatcher.reset(new StyleDialog::NodeObserver(this)); + + // Pack widgets + _mainBox.pack_start(_scrolledWindow, Gtk::PACK_EXPAND_WIDGET); + _scrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _styleBox.set_orientation(Gtk::ORIENTATION_VERTICAL); + _styleBox.set_valign(Gtk::ALIGN_START); + _scrolledWindow.add(_styleBox); + _scrolledWindow.set_overlay_scrolling(false); + _vadj = _scrolledWindow.get_vadjustment(); + _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &StyleDialog::_vscroll)); + _mainBox.set_orientation(Gtk::ORIENTATION_VERTICAL); + + pack_start(_mainBox, Gtk::PACK_EXPAND_WIDGET); +} + +StyleDialog::~StyleDialog() +{ + removeObservers(); +} + +void StyleDialog::_vscroll() +{ + if (!_scrollock) { + _scrollpos = _vadj->get_value(); + } else { + _vadj->set_value(_scrollpos); + _scrollock = false; + } +} + +Glib::ustring StyleDialog::fixCSSSelectors(Glib::ustring selector) +{ + g_debug("SelectorsDialog::fixCSSSelectors"); + Util::trim(selector); + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", selector); + CRSelector *cr_selector = cr_selector_parse_from_buf((guchar const *)selector.c_str(), CR_UTF_8); + for (auto token : tokens) { + Util::trim(token); + std::vector<Glib::ustring> subtokens = Glib::Regex::split_simple("[ ]+", token); + for (auto subtoken : subtokens) { + Util::trim(subtoken); + CRSelector *cr_selector = cr_selector_parse_from_buf((guchar const *)subtoken.c_str(), CR_UTF_8); + gchar *selectorchar = reinterpret_cast<gchar *>(cr_selector_to_string(cr_selector)); + if (selectorchar) { + Glib::ustring toadd = Glib::ustring(selectorchar); + g_free(selectorchar); + if (toadd[0] != '.' && toadd[0] != '#' && toadd.size() > 1) { + auto i = std::min(toadd.find("#"), toadd.find(".")); + Glib::ustring tag = toadd; + if (i != std::string::npos) { + tag = tag.substr(0, i); + } + if (!SPAttributeRelSVG::isSVGElement(tag)) { + if (tokens.size() == 1) { + tag = "." + tag; + return tag; + } else { + return ""; + } + } + } + } + } + } + if (cr_selector) { + return selector; + } + return ""; +} + +void StyleDialog::_reload() { readStyleElement(); } + +/** + * @return Inkscape::XML::Node* pointing to a style element's text node. + * Returns the style element's text node. If there is no style element, one is created. + * Ditto for text node. + */ +Inkscape::XML::Node *StyleDialog::_getStyleTextNode(bool create_if_missing) +{ + g_debug("StyleDialog::_getStyleTextNoded"); + + auto textNode = Inkscape::get_first_style_text_node(m_root, create_if_missing); + + if (_textNode != textNode) { + if (_textNode) { + _textNode->removeObserver(*m_styletextwatcher); + } + + _textNode = textNode; + + if (_textNode) { + _textNode->addObserver(*m_styletextwatcher); + } + } + + return textNode; +} + + +Glib::RefPtr<Gtk::TreeModel> StyleDialog::_selectTree(Glib::ustring selector) +{ + g_debug("StyleDialog::_selectTree"); + + Gtk::Label *selectorlabel; + Glib::RefPtr<Gtk::TreeModel> model; + for (auto fullstyle : _styleBox.get_children()) { + Gtk::Box *style = dynamic_cast<Gtk::Box *>(fullstyle); + for (auto stylepart : style->get_children()) { + switch (style->child_property_position(*stylepart)) { + case 0: { + Gtk::Box *selectorbox = dynamic_cast<Gtk::Box *>(stylepart); + for (auto styleheader : selectorbox->get_children()) { + if (!selectorbox->child_property_position(*styleheader)) { + selectorlabel = dynamic_cast<Gtk::Label *>(styleheader); + } + } + break; + } + case 1: { + Glib::ustring wdg_selector = selectorlabel->get_text(); + if (wdg_selector == selector) { + Gtk::TreeView *treeview = dynamic_cast<Gtk::TreeView *>(stylepart); + if (treeview) { + return treeview->get_model(); + } + } + break; + } + default: + break; + } + } + } + return model; +} + +void StyleDialog::setCurrentSelector(Glib::ustring current_selector) +{ + g_debug("StyleDialog::setCurrentSelector"); + _current_selector = current_selector; + readStyleElement(); +} + +// copied from style.cpp:1499 +static bool is_url(char const *p) +{ + if (p == nullptr) + return false; + /** \todo + * FIXME: I'm not sure if this applies to SVG as well, but CSS2 says any URIs + * in property values must start with 'url('. + */ + return (g_ascii_strncasecmp(p, "url(", 4) == 0); +} + +/** + * Fill the Gtk::TreeStore from the svg:style element. + */ +void StyleDialog::readStyleElement() +{ + g_debug("StyleDialog::readStyleElement"); + + auto document = getDocument(); + if (_updating || !document) + return; // Don't read if we wrote style element. + _updating = true; + _scrollock = true; + Inkscape::XML::Node *textNode = _getStyleTextNode(); + + // Get content from style text node. + std::string content = (textNode && textNode->content()) ? textNode->content() : ""; + + // Remove end-of-lines (check it works on Windoze). + content.erase(std::remove(content.begin(), content.end(), '\n'), content.end()); + + // Remove comments (/* xxx */) + + bool breakme = false; + size_t start = content.find("/*"); + size_t open = content.find("{", start + 1); + size_t close = content.find("}", start + 1); + size_t end = content.find("*/", close + 1); + while (!breakme) { + if (open == std::string::npos || close == std::string::npos || end == std::string::npos) { + breakme = true; + break; + } + while (open < close) { + open = content.find("{", close + 1); + close = content.find("}", close + 1); + end = content.find("*/", close + 1); + size_t reopen = content.find("{", close + 1); + if (open == std::string::npos || end == std::string::npos || end < reopen) { + if (end < reopen) { + content = content.erase(start, end - start + 2); + } else { + breakme = true; + } + break; + } + } + start = content.find("/*", start + 1); + open = content.find("{", start + 1); + close = content.find("}", start + 1); + end = content.find("*/", close + 1); + } + + // First split into selector/value chunks. + // An attempt to use Glib::Regex failed. A C++11 version worked but + // reportedly has problems on Windows. Using split_simple() is simpler + // and probably faster. + // + // Glib::RefPtr<Glib::Regex> regex1 = + // Glib::Regex::create("([^\\{]+)\\{([^\\{]+)\\}"); + // + // Glib::MatchInfo minfo; + // regex1->match(content, minfo); + + // Split on curly brackets. Even tokens are selectors, odd are values. + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[}{]", content); + _owner_style.clear(); + // If text node is empty, return (avoids problem with negative below). + + for (auto child : _styleBox.get_children()) { + _styleBox.remove(*child); + delete child; + } + Inkscape::Selection *selection = getSelection(); + SPObject *obj = nullptr; + if (selection->objects().size() == 1) { + obj = selection->objects().back(); + } + if (!obj) { + obj = document->getXMLDialogSelectedObject(); + if (obj && !obj->getRepr()) { + obj = nullptr; // treat detached object as no selection + } + } + + auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-css.glade"); + Glib::RefPtr<Gtk::Builder> _builder; + try { + _builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + gint selectorpos = 0; + Gtk::Box *css_selector_container; + _builder->get_widget("CSSSelectorContainer", css_selector_container); + Gtk::Label *css_selector; + _builder->get_widget("CSSSelector", css_selector); + Gtk::EventBox *css_selector_event_add; + _builder->get_widget("CSSSelectorEventAdd", css_selector_event_add); + css_selector_event_add->add_events(Gdk::BUTTON_RELEASE_MASK); + css_selector->set_text("element"); + Gtk::TreeView *css_tree; + _builder->get_widget("CSSTree", css_tree); + css_tree->get_style_context()->add_class("style_element"); + Glib::RefPtr<Gtk::TreeStore> store = Gtk::TreeStore::create(_mColumns); + css_tree->set_model(store); + css_selector_event_add->signal_button_release_event().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *, Glib::ustring, gint>( + sigc::mem_fun(*this, &StyleDialog::_addRow), store, css_tree, "style_properties", selectorpos)); + Inkscape::UI::Widget::IconRenderer *addRenderer = manage(new Inkscape::UI::Widget::IconRenderer()); + addRenderer->add_icon("edit-delete"); + int addCol = css_tree->append_column(" ", *addRenderer) - 1; + Gtk::TreeViewColumn *col = css_tree->get_column(addCol); + if (col) { + addRenderer->signal_activated().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_onPropDelete), store)); + } + Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText()); + label->property_placeholder_text() = _("property"); + label->property_editable() = true; + label->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *>( + sigc::mem_fun(*this, &StyleDialog::_nameEdited), store, css_tree)); + label->signal_editing_started().connect(sigc::mem_fun(*this, &StyleDialog::_startNameEdit)); + addCol = css_tree->append_column(" ", *label) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->set_resizable(true); + col->add_attribute(label->property_text(), _mColumns._colName); + } + Gtk::CellRendererText *value = Gtk::manage(new Gtk::CellRendererText()); + value->property_placeholder_text() = _("value"); + value->property_editable() = true; + value->signal_edited().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_valueEdited), store)); + value->signal_editing_started().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_startValueEdit), store)); + addCol = css_tree->append_column(" ", *value) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->add_attribute(value->property_text(), _mColumns._colValue); + col->set_expand(true); + col->add_attribute(value->property_strikethrough(), _mColumns._colStrike); + } + Inkscape::UI::Widget::IconRenderer *urlRenderer = manage(new Inkscape::UI::Widget::IconRenderer()); + urlRenderer->add_icon("empty-icon"); + urlRenderer->add_icon("edit-redo"); + int urlCol = css_tree->append_column(" ", *urlRenderer) - 1; + Gtk::TreeViewColumn *urlcol = css_tree->get_column(urlCol); + if (urlcol) { + urlcol->set_min_width(40); + urlcol->set_max_width(40); + urlRenderer->signal_activated().connect(sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onLinkObj), store)); + urlcol->add_attribute(urlRenderer->property_icon(), _mColumns._colLinked); + } + std::map<Glib::ustring, Glib::ustring> attr_prop; + Gtk::TreeModel::Path path; + bool empty = true; + if (obj && obj->getRepr()->attribute("style")) { + Glib::ustring style = obj->getRepr()->attribute("style"); + attr_prop = parseStyle(style); + for (auto iter : obj->style->properties()) { + if (attr_prop.count(iter->name())) { + auto value = attr_prop[iter->name()]; + empty = false; + Gtk::TreeModel::Row row = *(store->prepend()); + row[_mColumns._colSelector] = "style_properties"; + row[_mColumns._colSelectorPos] = 0; + row[_mColumns._colActive] = true; + row[_mColumns._colName] = iter->name(); + row[_mColumns._colValue] = value; + row[_mColumns._colStrike] = false; + row[_mColumns._colOwner] = Glib::ustring("Current value"); + row[_mColumns._colHref] = nullptr; + row[_mColumns._colLinked] = false; + if (is_url(value.c_str())) { + auto id = value.substr(5, value.size() - 6); + SPObject *elemref = nullptr; + if ((elemref = document->getObjectById(id.c_str()))) { + row[_mColumns._colHref] = elemref; + row[_mColumns._colLinked] = true; + } + } + _addOwnerStyle(iter->name(), "style attribute"); + } + } + // this is to fix a bug on cairo win: + // https://gitlab.freedesktop.org/cairo/cairo/issues/338 + // TODO: check if inkscape min cairo version has applied the patch proposed and remove (3 times) + if (empty) { + css_tree->hide(); + } + _styleBox.pack_start(*css_selector_container, Gtk::PACK_EXPAND_WIDGET); + } + selectorpos++; + if (tokens.size() == 0) { + _updating = false; + return; + } + for (unsigned i = 0; i < tokens.size() - 1; i += 2) { + Glib::ustring selector = tokens[i]; + Util::trim(selector); // Remove leading/trailing spaces + // Get list of objects selector matches + std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector); + Glib::ustring selector_orig = selector; + if (!selectordata.empty()) { + selector = selectordata.back(); + } + std::vector<SPObject *> objVec = _getObjVec(selector); + if (obj) { + bool stop = true; + for (auto objel : objVec) { + if (objel == obj) { + stop = false; + } + } + if (stop) { + _updating = false; + selectorpos++; + continue; + } + } + if (!obj && _current_selector != "" && _current_selector != selector) { + _updating = false; + selectorpos++; + continue; + } + if (!obj) { + bool present = false; + for (auto objv : objVec) { + for (auto objsel : selection->objects()) { + if (objv == objsel) { + present = true; + break; + } + } + if (present) { + break; + } + } + if (!present) { + _updating = false; + selectorpos++; + continue; + } + } + Glib::ustring properties; + // Check to make sure we do have a value to match selector. + if ((i + 1) < tokens.size()) { + properties = tokens[i + 1]; + } else { + std::cerr << "StyleDialog::readStyleElement: Missing values " + "for last selector!" + << std::endl; + } + Glib::RefPtr<Gtk::Builder> _builder; + try { + _builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + Gtk::Box *css_selector_container; + _builder->get_widget("CSSSelectorContainer", css_selector_container); + Gtk::Label *css_selector; + _builder->get_widget("CSSSelector", css_selector); + Gtk::EventBox *css_selector_event_box; + _builder->get_widget("CSSSelectorEventBox", css_selector_event_box); + Gtk::Entry *css_edit_selector; + _builder->get_widget("CSSEditSelector", css_edit_selector); + Gtk::EventBox *css_selector_event_add; + _builder->get_widget("CSSSelectorEventAdd", css_selector_event_add); + css_selector_event_add->add_events(Gdk::BUTTON_RELEASE_MASK); + css_selector->set_text(selector); + Gtk::TreeView *css_tree; + _builder->get_widget("CSSTree", css_tree); + css_tree->get_style_context()->add_class("style_sheet"); + Glib::RefPtr<Gtk::TreeStore> store = Gtk::TreeStore::create(_mColumns); + css_tree->set_model(store); + // I comment this feature, is working but seems obscure to understand + // the user can edit selector name in current implementation + /* css_selector_event_box->signal_button_release_event().connect( + sigc::bind(sigc::mem_fun(*this, &StyleDialog::_selectorStartEdit), css_selector, css_edit_selector)); + css_edit_selector->signal_key_press_event().connect(sigc::bind( + sigc::mem_fun(*this, &StyleDialog::_selectorEditKeyPress), store, css_selector, css_edit_selector)); + css_edit_selector->signal_activate().connect( + sigc::bind(sigc::mem_fun(*this, &StyleDialog::_selectorActivate), store, css_selector, css_edit_selector)); + */ + Inkscape::UI::Widget::IconRenderer *addRenderer = manage(new Inkscape::UI::Widget::IconRenderer()); + addRenderer->add_icon("edit-delete"); + int addCol = css_tree->append_column(" ", *addRenderer) - 1; + Gtk::TreeViewColumn *col = css_tree->get_column(addCol); + if (col) { + addRenderer->signal_activated().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_onPropDelete), store)); + } + Gtk::CellRendererToggle *isactive = Gtk::manage(new Gtk::CellRendererToggle()); + isactive->property_activatable() = true; + addCol = css_tree->append_column(" ", *isactive) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->add_attribute(isactive->property_active(), _mColumns._colActive); + isactive->signal_toggled().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_activeToggled), store)); + } + Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText()); + label->property_placeholder_text() = _("property"); + label->property_editable() = true; + label->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *>( + sigc::mem_fun(*this, &StyleDialog::_nameEdited), store, css_tree)); + label->signal_editing_started().connect(sigc::mem_fun(*this, &StyleDialog::_startNameEdit)); + addCol = css_tree->append_column(" ", *label) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->set_resizable(true); + col->add_attribute(label->property_text(), _mColumns._colName); + } + Gtk::CellRendererText *value = Gtk::manage(new Gtk::CellRendererText()); + value->property_editable() = true; + value->property_placeholder_text() = _("value"); + value->signal_edited().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_valueEdited), store)); + value->signal_editing_started().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_startValueEdit), store)); + addCol = css_tree->append_column(" ", *value) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->add_attribute(value->property_text(), _mColumns._colValue); + col->add_attribute(value->property_strikethrough(), _mColumns._colStrike); + } + Glib::ustring style = properties; + Glib::ustring comments = ""; + while (style.find("/*") != std::string::npos) { + size_t beg = style.find("/*"); + size_t end = style.find("*/"); + if (end != std::string::npos && beg != std::string::npos) { + comments = comments.append(style, beg + 2, end - beg - 2); + style = style.erase(beg, end - beg + 2); + } + } + std::map<Glib::ustring, Glib::ustring> attr_prop_styleshet = parseStyle(style); + std::map<Glib::ustring, Glib::ustring> attr_prop_styleshet_comments = parseStyle(comments); + std::map<Glib::ustring, std::pair<Glib::ustring, bool>> result_props; + for (auto styled : attr_prop_styleshet) { + result_props[styled.first] = std::make_pair(styled.second, true); + } + for (auto styled : attr_prop_styleshet_comments) { + result_props[styled.first] = std::make_pair(styled.second, false); + } + empty = true; + css_selector_event_add->signal_button_release_event().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *, Glib::ustring, gint>( + sigc::mem_fun(*this, &StyleDialog::_addRow), store, css_tree, selector_orig, selectorpos)); + if (obj) { + for (auto iter : result_props) { + empty = false; + Gtk::TreeIter iterstore = store->append(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iterstore; + Gtk::TreeModel::Row row = *(iterstore); + row[_mColumns._colSelector] = selector_orig; + row[_mColumns._colSelectorPos] = selectorpos; + row[_mColumns._colActive] = iter.second.second; + row[_mColumns._colName] = iter.first; + row[_mColumns._colValue] = iter.second.first; + const Glib::ustring value = row[_mColumns._colValue]; + if (iter.second.second) { + Glib::ustring val = ""; + for (auto iterprop : obj->style->properties()) { + if (iterprop->style_src != SPStyleSrc::UNSET && iterprop->name() == iter.first) { + val = iterprop->get_value(); + break; + } + } + guint32 r1 = 0; // if there's no color, return black + r1 = sp_svg_read_color(value.c_str(), r1); + guint32 r2 = 0; // if there's no color, return black + r2 = sp_svg_read_color(val.c_str(), r2); + if (attr_prop.count(iter.first) || (value != val && (r1 == 0 || r1 != r2))) { + row[_mColumns._colStrike] = true; + row[_mColumns._colOwner] = Glib::ustring(""); + } else { + row[_mColumns._colStrike] = false; + row[_mColumns._colOwner] = Glib::ustring("Current value"); + _addOwnerStyle(iter.first, selector); + } + } else { + row[_mColumns._colStrike] = true; + Glib::ustring tooltiptext = _("This value is commented out."); + row[_mColumns._colOwner] = tooltiptext; + } + } + } else { + for (auto iter : result_props) { + empty = false; + Gtk::TreeModel::Row row = *(store->prepend()); + row[_mColumns._colSelector] = selector_orig; + row[_mColumns._colSelectorPos] = selectorpos; + row[_mColumns._colActive] = iter.second.second; + row[_mColumns._colName] = iter.first; + row[_mColumns._colValue] = iter.second.first; + row[_mColumns._colStrike] = false; + row[_mColumns._colOwner] = Glib::ustring("Stylesheet value"); + } + } + if (empty) { + css_tree->hide(); + } + _styleBox.pack_start(*css_selector_container, Gtk::PACK_EXPAND_WIDGET); + selectorpos++; + } + try { + _builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for filter effect dialog."); + return; + } + _builder->get_widget("CSSSelector", css_selector); + css_selector->set_text("element.attributes"); + _builder->get_widget("CSSSelectorContainer", css_selector_container); + _builder->get_widget("CSSSelectorEventAdd", css_selector_event_add); + css_selector_event_add->add_events(Gdk::BUTTON_RELEASE_MASK); + store = Gtk::TreeStore::create(_mColumns); + _builder->get_widget("CSSTree", css_tree); + css_tree->get_style_context()->add_class("style_attribute"); + css_tree->set_model(store); + css_selector_event_add->signal_button_release_event().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *, Glib::ustring, gint>( + sigc::mem_fun(*this, &StyleDialog::_addRow), store, css_tree, "attributes", selectorpos)); + bool hasattributes = false; + empty = true; + if (obj) { + for (auto iter : obj->style->properties()) { + if (iter->style_src != SPStyleSrc::UNSET) { + auto key = iter->id(); + if (key != SPAttr::FONT && key != SPAttr::D && key != SPAttr::MARKER) { + const gchar *attr = obj->getRepr()->attribute(iter->name().c_str()); + if (attr) { + if (!hasattributes) { + Inkscape::UI::Widget::IconRenderer *addRenderer = + manage(new Inkscape::UI::Widget::IconRenderer()); + addRenderer->add_icon("edit-delete"); + int addCol = css_tree->append_column(" ", *addRenderer) - 1; + Gtk::TreeViewColumn *col = css_tree->get_column(addCol); + if (col) { + addRenderer->signal_activated().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>>( + sigc::mem_fun(*this, &StyleDialog::_onPropDelete), store)); + } + Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText()); + label->property_placeholder_text() = _("property"); + label->property_editable() = true; + label->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *>( + sigc::mem_fun(*this, &StyleDialog::_nameEdited), store, css_tree)); + label->signal_editing_started().connect(sigc::mem_fun(*this, &StyleDialog::_startNameEdit)); + addCol = css_tree->append_column(" ", *label) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->set_resizable(true); + col->add_attribute(label->property_text(), _mColumns._colName); + } + Gtk::CellRendererText *value = Gtk::manage(new Gtk::CellRendererText()); + value->property_placeholder_text() = _("value"); + value->property_editable() = true; + value->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>>( + sigc::mem_fun(*this, &StyleDialog::_valueEdited), store)); + value->signal_editing_started().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>>( + sigc::mem_fun(*this, &StyleDialog::_startValueEdit), store)); + + addCol = css_tree->append_column(" ", *value) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->add_attribute(value->property_text(), _mColumns._colValue); + col->add_attribute(value->property_strikethrough(), _mColumns._colStrike); + } + } + empty = false; + Gtk::TreeIter iterstore = store->prepend(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iterstore; + Gtk::TreeModel::Row row = *(iterstore); + row[_mColumns._colSelector] = "attributes"; + row[_mColumns._colSelectorPos] = selectorpos; + row[_mColumns._colActive] = true; + row[_mColumns._colName] = iter->name(); + row[_mColumns._colValue] = attr; + if (_owner_style.find(iter->name()) != _owner_style.end()) { + row[_mColumns._colStrike] = true; + Glib::ustring tooltiptext = Glib::ustring(""); + row[_mColumns._colOwner] = tooltiptext; + } else { + row[_mColumns._colStrike] = false; + row[_mColumns._colOwner] = Glib::ustring("Current value"); + _addOwnerStyle(iter->name(), "inline attributes"); + } + hasattributes = true; + } + } + } + } + if (empty) { + css_tree->hide(); + } + if (!hasattributes) { + for (auto widg : css_selector_container->get_children()) { + delete widg; + } + } + _styleBox.pack_start(*css_selector_container, Gtk::PACK_EXPAND_WIDGET); + } + for (auto selector : _styleBox.get_children()) { + Gtk::Box *box = dynamic_cast<Gtk::Box *>(&selector[0]); + if (box) { + std::vector<Gtk::Widget *> childs = box->get_children(); + if (childs.size() > 1) { + Gtk::TreeView *css_tree = dynamic_cast<Gtk::TreeView *>(childs[1]); + if (css_tree) { + Glib::RefPtr<Gtk::TreeModel> model = css_tree->get_model(); + if (model) { + model->foreach_iter(sigc::mem_fun(*this, &StyleDialog::_on_foreach_iter)); + } + } + } + } + } + if (obj) { + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + _mainBox.show_all_children(); + _updating = false; +} + +bool StyleDialog::_selectorStartEdit(GdkEventButton *event, Gtk::Label *selector, Gtk::Entry *selector_edit) +{ + g_debug("StyleDialog::_selectorStartEdit"); + if (event->type == GDK_BUTTON_RELEASE && event->button == 1) { + selector->hide(); + selector_edit->set_text(selector->get_text()); + selector_edit->show(); + } + return false; +} + +/* void StyleDialog::_selectorActivate(Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector, Gtk::Entry +*selector_edit) +{ + g_debug("StyleDialog::_selectorEditKeyPress"); + Glib::ustring newselector = fixCSSSelectors(selector_edit->get_text()); + if (newselector.empty()) { + selector_edit->get_style_context()->add_class("system_error_color"); + return; + } + _writeStyleElement(store, selector->get_text(), selector_edit->get_text()); +} */ + +bool StyleDialog::_selectorEditKeyPress(GdkEventKey *event, Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector, + Gtk::Entry *selector_edit) +{ + g_debug("StyleDialog::_selectorEditKeyPress"); + switch (event->keyval) { + case GDK_KEY_Escape: + selector->show(); + selector_edit->hide(); + selector_edit->get_style_context()->remove_class("system_error_color"); + break; + } + return false; +} + +bool StyleDialog::_on_foreach_iter(const Gtk::TreeModel::iterator &iter) +{ + g_debug("StyleDialog::_on_foreach_iter"); + + Gtk::TreeModel::Row row = *(iter); + Glib::ustring owner = row[_mColumns._colOwner]; + if (owner.empty()) { + Glib::ustring value = _owner_style[row[_mColumns._colName]]; + Glib::ustring tooltiptext = Glib::ustring(_("Current value")); + if (!value.empty()) { + tooltiptext = Glib::ustring::compose(_("Used in %1"), _owner_style[row[_mColumns._colName]]); + row[_mColumns._colStrike] = true; + } else { + row[_mColumns._colStrike] = false; + } + row[_mColumns._colOwner] = tooltiptext; + } + return false; +} + +void StyleDialog::_onLinkObj(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store) +{ + g_debug("StyleDialog::_onLinkObj"); + + Gtk::TreeModel::Row row = *store->get_iter(path); + if (row && row[_mColumns._colLinked]) { + SPObject *linked = row[_mColumns._colHref]; + if (linked) { + auto selection = getSelection(); + getDocument()->setXMLDialogSelectedObject(linked); + selection->clear(); + selection->set(linked); + } + } +} + +/** + * @brief StyleDialog::_onPropDelete + * @param event + * @return true + * Delete the attribute from the style + */ +void StyleDialog::_onPropDelete(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store) +{ + g_debug("StyleDialog::_onPropDelete"); + Gtk::TreeModel::Row row = *store->get_iter(path); + if (row) { + Glib::ustring selector = row[_mColumns._colSelector]; + row[_mColumns._colName] = ""; + _deleted_pos = row[_mColumns._colSelectorPos]; + store->erase(row); + _deletion = true; + _writeStyleElement(store, selector); + } +} + +void StyleDialog::_addOwnerStyle(Glib::ustring name, Glib::ustring selector) +{ + g_debug("StyleDialog::_addOwnerStyle"); + + if (_owner_style.find(name) == _owner_style.end()) { + _owner_style[name] = selector; + } +} + + +/** + * @brief StyleDialog::parseStyle + * + * Convert a style string into a vector map. This should be moved to style.cpp + * + */ +std::map<Glib::ustring, Glib::ustring> StyleDialog::parseStyle(Glib::ustring style_string) +{ + g_debug("StyleDialog::parseStyle"); + + std::map<Glib::ustring, Glib::ustring> ret; + + Util::trim(style_string); // We'd use const, but we need to trip spaces + std::vector<Glib::ustring> props = r_props->split(style_string); + + for (auto token : props) { + Util::trim(token); + + if (token.empty()) + break; + std::vector<Glib::ustring> pair = r_pair->split(token); + + if (pair.size() > 1) { + ret[pair[0]] = pair[1]; + } + } + return ret; +} + + +/** + * Update the content of the style element as selectors (or objects) are added/removed. + */ +void StyleDialog::_writeStyleElement(Glib::RefPtr<Gtk::TreeStore> store, Glib::ustring selector, + Glib::ustring new_selector) +{ + g_debug("StyleDialog::_writeStyleElemen"); + auto selection = getSelection(); + if (_updating && selection) + return; + _scrollock = true; + SPObject *obj = nullptr; + if (selection->objects().size() == 1) { + obj = selection->objects().back(); + } + if (!obj) { + obj = getDocument()->getXMLDialogSelectedObject(); + } + if (selection->objects().size() < 2 && !obj) { + readStyleElement(); + return; + } + _updating = true; + gint selectorpos = 0; + std::string styleContent = ""; + if (selector != "style_properties" && selector != "attributes") { + if (!new_selector.empty()) { + selector = new_selector; + } + std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector); + for (auto selectoritem : selectordata) { + if (selectordata[selectordata.size() - 1] == selectoritem) { + selector = selectoritem; + } else { + styleContent = styleContent + selectoritem + ";\n"; + } + } + styleContent.append("\n").append(selector.raw()).append(" { \n"); + } + selectorpos = _deleted_pos; + for (auto &row : store->children()) { + selector = row[_mColumns._colSelector]; + selectorpos = row[_mColumns._colSelectorPos]; + const char *opencomment = ""; + const char *closecomment = ""; + if (selector != "style_properties" && selector != "attributes") { + opencomment = row[_mColumns._colActive] ? " " : " /*"; + closecomment = row[_mColumns._colActive] ? "\n" : "*/\n"; + } + Glib::ustring const &name = row[_mColumns._colName]; + Glib::ustring const &value = row[_mColumns._colValue]; + if (!(name.empty() && value.empty())) { + styleContent = styleContent + opencomment + name.raw() + ":" + value.raw() + ";" + closecomment; + } + } + if (selector != "style_properties" && selector != "attributes") { + styleContent = styleContent + "}"; + } + if (selector == "style_properties") { + _updating = true; + obj->getRepr()->setAttribute("style", styleContent); + _updating = false; + } else if (selector == "attributes") { + for (auto iter : obj->style->properties()) { + auto key = iter->id(); + if (key != SPAttr::FONT && key != SPAttr::D && key != SPAttr::MARKER) { + const gchar *attr = obj->getRepr()->attribute(iter->name().c_str()); + if (attr) { + _updating = true; + obj->getRepr()->removeAttribute(iter->name()); + _updating = false; + } + } + } + for (auto &row : store->children()) { + Glib::ustring const &name = row[_mColumns._colName]; + Glib::ustring const &value = row[_mColumns._colValue]; + if (!(name.empty() && value.empty())) { + _updating = true; + obj->getRepr()->setAttribute(name, value); + _updating = false; + } + } + } else if (!selector.empty()) { // styleshet + // We could test if styleContent is empty and then delete the style node here but there is no + // harm in keeping it around ... + + std::string pos = std::to_string(selectorpos); + std::string selectormatch = "("; + for (; selectorpos > 1; selectorpos--) { + selectormatch = selectormatch + "[^\\}]*?\\}"; + } + selectormatch = selectormatch + ")([^\\}]*?\\})((.|\n)*)"; + + Inkscape::XML::Node *textNode = _getStyleTextNode(true); + std::regex e(selectormatch.c_str()); + std::string content = (textNode->content() ? textNode->content() : ""); + std::string result; + std::regex_replace(std::back_inserter(result), content.begin(), content.end(), e, "$1" + styleContent + "$3"); + bool empty = false; + if (result.empty()) { + empty = true; + result = "* > .inkscapehacktmp{}"; + } + textNode->setContent(result.c_str()); + if (empty) { + textNode->setContent(""); + } + } + _updating = false; + readStyleElement(); + for (auto iter : getDocument()->getObjectsBySelector(selector)) { + iter->style->readFromObject(iter); + iter->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + DocumentUndo::done(SP_ACTIVE_DOCUMENT, _("Edited style element."), ""); + + g_debug("StyleDialog::_writeStyleElement(): | %s |", styleContent.c_str()); +} + +bool StyleDialog::_addRow(GdkEventButton *evt, Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeView *css_tree, + Glib::ustring selector, gint pos) +{ + g_debug("StyleDialog::_addRow"); + + if (evt->type == GDK_BUTTON_RELEASE && evt->button == 1) { + Gtk::TreeIter iter = store->prepend(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter; + Gtk::TreeModel::Row row = *(iter); + row[_mColumns._colSelector] = selector; + row[_mColumns._colSelectorPos] = pos; + row[_mColumns._colActive] = true; + row[_mColumns._colName] = ""; + row[_mColumns._colValue] = ""; + row[_mColumns._colStrike] = false; + gint col = 2; + if (pos < 1) { + col = 1; + } + css_tree->show(); + css_tree->set_cursor(path, *(css_tree->get_column(col)), true); + grab_focus(); + return true; + } + return false; +} + +void StyleDialog::_setAutocompletion(Gtk::Entry *entry, SPStyleEnum const cssenum[]) +{ + g_debug("StyleDialog::_setAutocompletion"); + + Glib::RefPtr<Gtk::ListStore> completionModel = Gtk::ListStore::create(_mCSSData); + Glib::RefPtr<Gtk::EntryCompletion> entry_completion = Gtk::EntryCompletion::create(); + entry_completion->set_model(completionModel); + entry_completion->set_text_column (_mCSSData._colCSSData); + entry_completion->set_minimum_key_length(0); + entry_completion->set_popup_completion(true); + gint counter = 0; + const char * key = cssenum[counter].key; + while (key) { + Gtk::TreeModel::Row row = *(completionModel->prepend()); + row[_mCSSData._colCSSData] = Glib::ustring(key); + counter++; + key = cssenum[counter].key; + } + entry->set_completion(entry_completion); +} +/*Hardcode values non in enum*/ +void StyleDialog::_setAutocompletion(Gtk::Entry *entry, Glib::ustring name) +{ + g_debug("StyleDialog::_setAutocompletion"); + + Glib::RefPtr<Gtk::ListStore> completionModel = Gtk::ListStore::create(_mCSSData); + Glib::RefPtr<Gtk::EntryCompletion> entry_completion = Gtk::EntryCompletion::create(); + entry_completion->set_model(completionModel); + entry_completion->set_text_column(_mCSSData._colCSSData); + entry_completion->set_minimum_key_length(0); + entry_completion->set_popup_completion(true); + if (name == "paint-order") { + Gtk::TreeModel::Row row = *(completionModel->append()); + row[_mCSSData._colCSSData] = Glib::ustring("fill markers stroke"); + row = *(completionModel->append()); + row[_mCSSData._colCSSData] = Glib::ustring("fill stroke markers"); + row = *(completionModel->append()); + row[_mCSSData._colCSSData] = Glib::ustring("stroke markers fill"); + row = *(completionModel->append()); + row[_mCSSData._colCSSData] = Glib::ustring("stroke fill markers"); + row = *(completionModel->append()); + row[_mCSSData._colCSSData] = Glib::ustring("markers fill stroke"); + row = *(completionModel->append()); + row[_mCSSData._colCSSData] = Glib::ustring("markers stroke fill"); + } + entry->set_completion(entry_completion); +} + +void +StyleDialog::_startValueEdit(Gtk::CellEditable* cell, const Glib::ustring& path, Glib::RefPtr<Gtk::TreeStore> store) +{ + g_debug("StyleDialog::_startValueEdit"); + _deletion = false; + _scrollock = true; + Gtk::TreeModel::Row row = *store->get_iter(path); + if (row) { + Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell); + Glib::ustring name = row[_mColumns._colName]; + if (name == "paint-order") { + _setAutocompletion(entry, name); + } else if (name == "fill-rule") { + _setAutocompletion(entry, enum_fill_rule); + } else if (name == "stroke-linecap") { + _setAutocompletion(entry, enum_stroke_linecap); + } else if (name == "stroke-linejoin") { + _setAutocompletion(entry, enum_stroke_linejoin); + } else if (name == "font-style") { + _setAutocompletion(entry, enum_font_style); + } else if (name == "font-variant") { + _setAutocompletion(entry, enum_font_variant); + } else if (name == "font-weight") { + _setAutocompletion(entry, enum_font_weight); + } else if (name == "font-stretch") { + _setAutocompletion(entry, enum_font_stretch); + } else if (name == "font-variant-position") { + _setAutocompletion(entry, enum_font_variant_position); + } else if (name == "text-align") { + _setAutocompletion(entry, enum_text_align); + } else if (name == "text-transform") { + _setAutocompletion(entry, enum_text_transform); + } else if (name == "text-anchor") { + _setAutocompletion(entry, enum_text_anchor); + } else if (name == "white-space") { + _setAutocompletion(entry, enum_white_space); + } else if (name == "direction") { + _setAutocompletion(entry, enum_direction); + } else if (name == "baseline-shift") { + _setAutocompletion(entry, enum_baseline_shift); + } else if (name == "visibility") { + _setAutocompletion(entry, enum_visibility); + } else if (name == "overflow") { + _setAutocompletion(entry, enum_overflow); + } else if (name == "display") { + _setAutocompletion(entry, enum_display); + } else if (name == "shape-rendering") { + _setAutocompletion(entry, enum_shape_rendering); + } else if (name == "color-rendering") { + _setAutocompletion(entry, enum_color_rendering); + } else if (name == "overflow") { + _setAutocompletion(entry, enum_overflow); + } else if (name == "clip-rule") { + _setAutocompletion(entry, enum_clip_rule); + } else if (name == "color-interpolation") { + _setAutocompletion(entry, enum_color_interpolation); + } + entry->signal_key_release_event().connect( + sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onValueKeyReleased), entry)); + entry->signal_key_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onValueKeyPressed), entry)); + } +} + +void StyleDialog::_startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path) +{ + _deletion = false; + g_debug("StyleDialog::_startNameEdit"); + _scrollock = true; + Glib::RefPtr<Gtk::ListStore> completionModel = Gtk::ListStore::create(_mCSSData); + Glib::RefPtr<Gtk::EntryCompletion> entry_completion = Gtk::EntryCompletion::create(); + entry_completion->set_model(completionModel); + entry_completion->set_text_column(_mCSSData._colCSSData); + entry_completion->set_minimum_key_length(1); + entry_completion->set_popup_completion(true); + for (auto prop : sp_attribute_name_list(true)) { + Gtk::TreeModel::Row row = *(completionModel->append()); + row[_mCSSData._colCSSData] = prop; + } + Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell); + entry->set_completion(entry_completion); + entry->signal_key_release_event().connect( + sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onNameKeyReleased), entry)); + entry->signal_key_press_event().connect(sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onNameKeyPressed), entry)); +} + + +gboolean sp_styledialog_store_move_to_next(gpointer data) +{ + StyleDialog *styledialog = reinterpret_cast<StyleDialog *>(data); + if (!styledialog->_deletion) { + auto selection = styledialog->_current_css_tree->get_selection(); + Gtk::TreeIter iter = *(selection->get_selected()); + Gtk::TreeModel::Path model = (Gtk::TreeModel::Path)iter; + if (model == styledialog->_current_path) { + styledialog->_current_css_tree->set_cursor(styledialog->_current_path, *styledialog->_current_value_col, + true); + } + } + return FALSE; +} + +/** + * @brief StyleDialog::nameEdited + * @param event + * @return + * Called when the name is edited in the TreeView editable column + */ +void StyleDialog::_nameEdited(const Glib::ustring &path, const Glib::ustring &name, Glib::RefPtr<Gtk::TreeStore> store, + Gtk::TreeView *css_tree) +{ + g_debug("StyleDialog::_nameEdited"); + + _scrollock = true; + Gtk::TreeModel::Row row = *store->get_iter(path); + _current_path = (Gtk::TreeModel::Path)*store->get_iter(path); + + if (row) { + _current_css_tree = css_tree; + Glib::ustring finalname = name; + auto i = finalname.find_first_of(";:="); + if (i != std::string::npos) { + finalname.erase(i, name.size() - i); + } + gint pos = row[_mColumns._colSelectorPos]; + bool write = false; + if (row[_mColumns._colName] != finalname && row[_mColumns._colValue] != "") { + write = true; + } + Glib::ustring selector = row[_mColumns._colSelector]; + Glib::ustring value = row[_mColumns._colValue]; + bool is_attr = selector == "attributes"; + Glib::ustring old_name = row[_mColumns._colName]; + row[_mColumns._colName] = finalname; + if (finalname.empty() && value.empty()) { + _deleted_pos = row[_mColumns._colSelectorPos]; + store->erase(row); + } + gint col = 3; + if (pos < 1 || is_attr) { + col = 2; + } + _current_value_col = css_tree->get_column(col); + if (write && old_name != name) { + _writeStyleElement(store, selector); + /* + I think is better comment this, is enough update on value change + if (selector != "style_properties" && selector != "attributes") { + std::vector<SPObject *> objs = _getObjVec(selector); + for (auto obj : objs){ + Glib::ustring css_str = ""; + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style")); + css->removeAttribute(name); + sp_repr_css_write_string(css, css_str); + obj->getRepr()->setAttributeOrRemoveIfEmpty("style", css_str); + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } */ + } else { + g_timeout_add(50, &sp_styledialog_store_move_to_next, this); + grab_focus(); + } + } +} + +/** + * @brief StyleDialog::valueEdited + * @param event + * @return + * Called when the value is edited in the TreeView editable column + */ +void StyleDialog::_valueEdited(const Glib::ustring &path, const Glib::ustring &value, + Glib::RefPtr<Gtk::TreeStore> store) +{ + g_debug("StyleDialog::_valueEdited"); + + _scrollock = true; + + Gtk::TreeModel::Row row = *store->get_iter(path); + if (row) { + Glib::ustring finalvalue = value; + auto i = std::min(finalvalue.find(";"), finalvalue.find(":")); + if (i != std::string::npos) { + finalvalue.erase(i, finalvalue.size() - i); + } + Glib::ustring old_value = row[_mColumns._colValue]; + if (old_value == finalvalue) { + return; + } + row[_mColumns._colValue] = finalvalue; + Glib::ustring selector = row[_mColumns._colSelector]; + Glib::ustring name = row[_mColumns._colName]; + if (name.empty() && finalvalue.empty()) { + _deleted_pos = row[_mColumns._colSelectorPos]; + store->erase(row); + } + _writeStyleElement(store, selector); + if (selector != "style_properties" && selector != "attributes") { + std::vector<SPObject *> objs = _getObjVec(selector); + for (auto obj : objs) { + Glib::ustring css_str = ""; + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style")); + css->removeAttribute(name); + sp_repr_css_write_string(css, css_str); + obj->getRepr()->setAttribute("style", css_str); + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } + } +} + +void StyleDialog::_activeToggled(const Glib::ustring &path, Glib::RefPtr<Gtk::TreeStore> store) +{ + g_debug("StyleDialog::_activeToggled"); + + _scrollock = true; + Gtk::TreeModel::Row row = *store->get_iter(path); + if (row) { + row[_mColumns._colActive] = !row[_mColumns._colActive]; + Glib::ustring selector = row[_mColumns._colSelector]; + _writeStyleElement(store, selector); + } +} + +bool StyleDialog::_onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onNameKeyReleased"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + entry->editing_done(); + ret = true; + break; + } + return ret; +} + +bool StyleDialog::_onNameKeyReleased(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onNameKeyReleased"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_equal: + case GDK_KEY_colon: + entry->editing_done(); + ret = true; + break; + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_semicolon: { + Glib::ustring text = entry->get_text(); + auto i = std::min(text.find(";"), text.find(":")); + if (i != std::string::npos) { + entry->editing_done(); + ret = true; + } + break; + } + } + return ret; +} + +bool StyleDialog::_onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onValueKeyReleased"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + entry->editing_done(); + ret = true; + break; + } + return ret; +} + +bool StyleDialog::_onValueKeyReleased(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onValueKeyReleased"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_semicolon: + entry->editing_done(); + ret = true; + break; + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_colon: { + Glib::ustring text = entry->get_text(); + auto i = std::min(text.find(";"), text.find(":")); + if (i != std::string::npos) { + entry->editing_done(); + ret = true; + } + break; + } + } + return ret; +} + +/** + * @param selector: a valid CSS selector string. + * @return objVec: a vector of pointers to SPObject's the selector matches. + * Return a vector of all objects that selector matches. + */ +std::vector<SPObject *> StyleDialog::_getObjVec(Glib::ustring selector) +{ + g_debug("StyleDialog::_getObjVec"); + + g_assert(selector.find(";") == Glib::ustring::npos); + + return getDocument()->getObjectsBySelector(selector); +} + +void StyleDialog::_closeDialog(Gtk::Dialog *textDialogPtr) { textDialogPtr->response(Gtk::RESPONSE_OK); } + + +void StyleDialog::removeObservers() +{ + if (_textNode) { + _textNode->removeObserver(*m_styletextwatcher); + _textNode = nullptr; + } + if (m_root) { + m_root->removeSubtreeObserver(*m_nodewatcher); + m_root = nullptr; + } +} + +/** + * Handle document replaced. (Happens when a default document is immediately replaced by another + * document in a new window.) + */ +void StyleDialog::documentReplaced() +{ + removeObservers(); + if (auto document = getDocument()) { + m_root = document->getReprRoot(); + m_root->addSubtreeObserver(*m_nodewatcher); + } + readStyleElement(); +} + +/* + * Handle a change in which objects are selected in a document. + */ +void StyleDialog::selectionChanged(Selection * /*selection*/) +{ + _scrollpos = 0; + _vadj->set_value(0); + // Sometimes the selection changes because inkscape is closing. + if (getDesktop()) { + readStyleElement(); + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/styledialog.h b/src/ui/dialog/styledialog.h new file mode 100644 index 0000000..dd2171e --- /dev/null +++ b/src/ui/dialog/styledialog.h @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for CSS selectors + */ +/* Authors: + * Kamalpreet Kaur Grewal + * Tavmjong Bah + * + * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com> + * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef STYLEDIALOG_H +#define STYLEDIALOG_H + +#include <glibmm/regex.h> +#include <gtkmm/adjustment.h> +#include <gtkmm/builder.h> +#include <gtkmm/celleditable.h> +#include <gtkmm/cellrenderercombo.h> +#include <gtkmm/dialog.h> +#include <gtkmm/entry.h> +#include <gtkmm/entrycompletion.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/liststore.h> +#include <gtkmm/paned.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/switch.h> +#include <gtkmm/tooltip.h> +#include <gtkmm/treemodelfilter.h> +#include <gtkmm/treeselection.h> +#include <gtkmm/treestore.h> +#include <gtkmm/treeview.h> +#include <gtkmm/viewport.h> +#include <memory> +#include <vector> + +#include "style-enums.h" +#include "ui/dialog/dialog-base.h" +#include "xml/helper-observer.h" + +namespace Inkscape { + +XML::Node *get_first_style_text_node(XML::Node *root, bool create_if_missing); + +namespace UI { +namespace Dialog { + +/** + * @brief The StyleDialog class + * A list of CSS selectors will show up in this dialog. This dialog allows one to + * add and delete selectors. Elements can be added to and removed from the selectors + * in the dialog. Selection of any selector row selects the matching objects in + * the drawing and vice-versa. (Only simple selectors supported for now.) + * + * This class must keep two things in sync: + * 1. The text node of the style element. + * 2. The Gtk::TreeModel. + */ +class StyleDialog : public DialogBase +{ +public: + // No default constructor, noncopyable, nonassignable + StyleDialog(); + ~StyleDialog() override; + StyleDialog(StyleDialog const &d) = delete; + StyleDialog operator=(StyleDialog const &d) = delete; + + void documentReplaced() override; + void selectionChanged(Selection *selection) override; + + static StyleDialog &getInstance() { return *new StyleDialog(); } + void setCurrentSelector(Glib::ustring current_selector); + Gtk::TreeView *_current_css_tree; + Gtk::TreeViewColumn *_current_value_col; + Gtk::TreeModel::Path _current_path; + bool _deletion; + Glib::ustring fixCSSSelectors(Glib::ustring selector); + void readStyleElement(); + + private: + // Monitor <style> element for changes. + class NodeObserver; + // Monitor all objects for addition/removal/attribute change + class NodeWatcher; + Glib::RefPtr<Glib::Regex> r_props = Glib::Regex::create("\\s*;\\s*"); + Glib::RefPtr<Glib::Regex> r_pair = Glib::Regex::create("\\s*:\\s*"); + void _nodeAdded(Inkscape::XML::Node &repr); + void _nodeRemoved(Inkscape::XML::Node &repr); + void _nodeChanged(Inkscape::XML::Node &repr); + void removeObservers(); + /* void _stylesheetChanged( Inkscape::XML::Node &repr ); */ + // Data structure + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns() + { + add(_colActive); + add(_colName); + add(_colValue); + add(_colStrike); + add(_colSelector); + add(_colSelectorPos); + add(_colOwner); + add(_colLinked); + add(_colHref); + } + Gtk::TreeModelColumn<bool> _colActive; // Active or inactive property + Gtk::TreeModelColumn<Glib::ustring> _colName; // Name of the property. + Gtk::TreeModelColumn<Glib::ustring> _colValue; // Value of the property. + Gtk::TreeModelColumn<bool> _colStrike; // Property not used, overloaded + Gtk::TreeModelColumn<Glib::ustring> _colSelector; // Style or matching object id. + Gtk::TreeModelColumn<gint> _colSelectorPos; // Position of the selector to handle dup selectors + Gtk::TreeModelColumn<Glib::ustring> _colOwner; // Store the owner of the property for popup + Gtk::TreeModelColumn<bool> _colLinked; // Other object linked + Gtk::TreeModelColumn<SPObject *> _colHref; // Is going to another object + }; + ModelColumns _mColumns; + + class CSSData : public Gtk::TreeModel::ColumnRecord { + public: + CSSData() { add(_colCSSData); } + Gtk::TreeModelColumn<Glib::ustring> _colCSSData; // Name of the property. + }; + CSSData _mCSSData; + guint _deleted_pos; + // Widgets + Gtk::ScrolledWindow _scrolledWindow; + Glib::RefPtr<Gtk::Adjustment> _vadj; + Gtk::Box _mainBox; + Gtk::Box _styleBox; + // Reading and writing the style element. + Inkscape::XML::Node *_getStyleTextNode(bool create_if_missing = false); + Glib::RefPtr<Gtk::TreeModel> _selectTree(Glib::ustring selector); + void _writeStyleElement(Glib::RefPtr<Gtk::TreeStore> store, Glib::ustring selector, + Glib::ustring new_selector = ""); + // void _selectorActivate(Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector, Gtk::Entry *selector_edit); + bool _selectorEditKeyPress(GdkEventKey *event, Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector, + Gtk::Entry *selector_edit); + bool _selectorStartEdit(GdkEventButton *event, Gtk::Label *selector, Gtk::Entry *selector_edit); + void _activeToggled(const Glib::ustring &path, Glib::RefPtr<Gtk::TreeStore> store); + bool _addRow(GdkEventButton *evt, Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeView *css_tree, + Glib::ustring selector, gint pos); + void _onPropDelete(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store); + void _nameEdited(const Glib::ustring &path, const Glib::ustring &name, Glib::RefPtr<Gtk::TreeStore> store, + Gtk::TreeView *css_tree); + bool _onNameKeyReleased(GdkEventKey *event, Gtk::Entry *entry); + bool _onValueKeyReleased(GdkEventKey *event, Gtk::Entry *entry); + bool _onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry); + bool _onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry); + void _onLinkObj(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store); + void _valueEdited(const Glib::ustring &path, const Glib::ustring &value, Glib::RefPtr<Gtk::TreeStore> store); + void _startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path); + + void _startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path, Glib::RefPtr<Gtk::TreeStore> store); + void _setAutocompletion(Gtk::Entry *entry, SPStyleEnum const cssenum[]); + void _setAutocompletion(Gtk::Entry *entry, Glib::ustring name); + bool _on_foreach_iter(const Gtk::TreeModel::iterator &iter); + void _reload(); + void _vscroll(); + bool _scrollock; + double _scrollpos; + Glib::ustring _current_selector; + + // Update watchers + std::unique_ptr<Inkscape::XML::NodeObserver> m_nodewatcher; + std::unique_ptr<Inkscape::XML::NodeObserver> m_styletextwatcher; + + // Manipulate Tree + std::vector<SPObject *> _getObjVec(Glib::ustring selector); + std::map<Glib::ustring, Glib::ustring> parseStyle(Glib::ustring style_string); + std::map<Glib::ustring, Glib::ustring> _owner_style; + void _addOwnerStyle(Glib::ustring name, Glib::ustring selector); + // Variables + Inkscape::XML::Node *m_root = nullptr; + Inkscape::XML::Node *_textNode; // Track so we know when to add a NodeObserver. + bool _updating; // Prevent cyclic actions: read <-> write, select via dialog <-> via desktop + + void _closeDialog(Gtk::Dialog *textDialogPtr); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // STYLEDIALOG_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/ui/dialog/svg-fonts-dialog.cpp b/src/ui/dialog/svg-fonts-dialog.cpp new file mode 100644 index 0000000..e7a34ff --- /dev/null +++ b/src/ui/dialog/svg-fonts-dialog.cpp @@ -0,0 +1,1795 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG Fonts dialog - implementation. + */ +/* Authors: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2008 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <message-stack.h> +#include <sstream> +#include <iomanip> + +#include <gtkmm/scale.h> +#include <gtkmm/notebook.h> +#include <gtkmm/expander.h> +#include <gtkmm/imagemenuitem.h> +#include <glibmm/stringutils.h> +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "document-undo.h" +#include "layer-manager.h" +#include "selection.h" +#include "svg-fonts-dialog.h" + +#include "display/nr-svgfonts.h" +#include "include/gtkmm_version.h" +#include "object/sp-defs.h" +#include "object/sp-font-face.h" +#include "object/sp-font.h" +#include "object/sp-glyph-kerning.h" +#include "object/sp-glyph.h" +#include "object/sp-guide.h" +#include "object/sp-missing-glyph.h" +#include "object/sp-path.h" +#include "svg/svg.h" +#include "util/units.h" +#include "xml/repr.h" +#include "document.h" + +SvgFontDrawingArea::SvgFontDrawingArea(): + _x(0), + _y(0), + _svgfont(nullptr), + _text() +{ +} + +void SvgFontDrawingArea::set_svgfont(SvgFont* svgfont){ + _svgfont = svgfont; +} + +void SvgFontDrawingArea::set_text(Glib::ustring text){ + _text = text; + redraw(); +} + +void SvgFontDrawingArea::set_size(int x, int y){ + _x = x; + _y = y; + ((Gtk::Widget*) this)->set_size_request(_x, _y); +} + +void SvgFontDrawingArea::redraw(){ + ((Gtk::Widget*) this)->queue_draw(); +} + +bool SvgFontDrawingArea::on_draw(const Cairo::RefPtr<Cairo::Context> &cr) { + if (_svgfont){ + cr->set_font_face( Cairo::RefPtr<Cairo::FontFace>(new Cairo::FontFace(_svgfont->get_font_face(), false /* does not have reference */)) ); + cr->set_font_size (_y-20); + cr->move_to (10, 10); + auto context = get_style_context(); + Gdk::RGBA fg = context->get_color(get_state_flags()); + cr->set_source_rgb(fg.get_red(), fg.get_green(), fg.get_blue()); + // crash on macos: https://gitlab.com/inkscape/inkscape/-/issues/266 + try { + cr->show_text(_text.c_str()); + } + catch (std::exception& ex) { + g_warning("Error drawing custom SVG font text: %s", ex.what()); + } + } + return true; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +void SvgGlyphRenderer::render_vfunc( + const Cairo::RefPtr<Cairo::Context>& cr, Gtk::Widget& widget, + const Gdk::Rectangle& background_area, const Gdk::Rectangle& cell_area, Gtk::CellRendererState flags) { + + if (!_font || !_tree) return; + + cr->set_font_face(Cairo::RefPtr<Cairo::FontFace>(new Cairo::FontFace(_font->get_font_face(), false /* does not have reference */))); + cr->set_font_size(_font_size); + Glib::ustring glyph = _property_glyph.get_value(); + Cairo::TextExtents ext; + cr->get_text_extents(glyph, ext); + cr->move_to(cell_area.get_x() + (_width - ext.width) / 2, cell_area.get_y() + 1); + auto context = _tree->get_style_context(); + Gtk::StateFlags sflags = _tree->get_state_flags(); + if (flags & Gtk::CELL_RENDERER_SELECTED) { + sflags |= Gtk::STATE_FLAG_SELECTED; + } + Gdk::RGBA fg = context->get_color(sflags); + cr->set_source_rgb(fg.get_red(), fg.get_green(), fg.get_blue()); + // crash on macos: https://gitlab.com/inkscape/inkscape/-/issues/266 + try { + cr->show_text(glyph); + } + catch (std::exception& ex) { + g_warning("Error drawing custom SVG font glyphs: %s", ex.what()); + } +} + +bool SvgGlyphRenderer::activate_vfunc( + GdkEvent* event, Gtk::Widget& widget, const Glib::ustring& path, const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, Gtk::CellRendererState flags) { + + Glib::ustring glyph = _property_glyph.get_value(); + _signal_clicked.emit(event, glyph); + return false; +} + +SvgFontsDialog::AttrEntry::AttrEntry(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttr attr) +{ + this->dialog = d; + this->attr = attr; + entry.set_tooltip_text(tooltip); + _label = Gtk::make_managed<Gtk::Label>(lbl); + _label->show(); + _label->set_halign(Gtk::ALIGN_START); + entry.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::AttrEntry::on_attr_changed)); +} + +void SvgFontsDialog::AttrEntry::set_text(const char* t){ + if (!t) return; + entry.set_text(t); +} + +// 'font-family' has a problem as it is also a presentation attribute for <text> +void SvgFontsDialog::AttrEntry::on_attr_changed(){ + if (dialog->_update.pending()) return; + + SPObject* o = nullptr; + for (auto& node: dialog->get_selected_spfont()->children) { + switch(this->attr){ + case SPAttr::FONT_FAMILY: + if (SP_IS_FONTFACE(&node)){ + o = &node; + continue; + } + break; + default: + o = nullptr; + } + } + + const gchar* name = (const gchar*)sp_attribute_name(this->attr); + if(name && o) { + o->setAttribute((const gchar*) name, this->entry.get_text()); + o->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + + Glib::ustring undokey = "svgfonts:"; + undokey += name; + DocumentUndo::maybeDone(o->document, undokey.c_str(), _("Set SVG Font attribute"), ""); + } + +} + +SvgFontsDialog::AttrSpin::AttrSpin(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttr attr) +{ + this->dialog = d; + this->attr = attr; + spin.set_tooltip_text(tooltip); + spin.show(); + _label = Gtk::make_managed<Gtk::Label>(lbl); + _label->show(); + _label->set_halign(Gtk::ALIGN_START); + spin.set_range(0, 4096); + spin.set_increments(10, 0); + spin.signal_value_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::AttrSpin::on_attr_changed)); +} + +void SvgFontsDialog::AttrSpin::set_range(double low, double high){ + spin.set_range(low, high); +} + +void SvgFontsDialog::AttrSpin::set_value(double v){ + spin.set_value(v); +} + +void SvgFontsDialog::AttrSpin::on_attr_changed(){ + if (dialog->_update.pending()) return; + + SPObject* o = nullptr; + switch (this->attr) { + + // <font> attributes + case SPAttr::HORIZ_ORIGIN_X: + case SPAttr::HORIZ_ORIGIN_Y: + case SPAttr::HORIZ_ADV_X: + case SPAttr::VERT_ORIGIN_X: + case SPAttr::VERT_ORIGIN_Y: + case SPAttr::VERT_ADV_Y: + o = this->dialog->get_selected_spfont(); + break; + + // <font-face> attributes + case SPAttr::UNITS_PER_EM: + case SPAttr::ASCENT: + case SPAttr::DESCENT: + case SPAttr::CAP_HEIGHT: + case SPAttr::X_HEIGHT: + for (auto& node: dialog->get_selected_spfont()->children){ + if (SP_IS_FONTFACE(&node)){ + o = &node; + continue; + } + } + break; + + default: + o = nullptr; + } + + const gchar* name = (const gchar*)sp_attribute_name(this->attr); + if(name && o) { + std::ostringstream temp; + temp << this->spin.get_value(); + o->setAttribute(name, temp.str()); + o->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + + Glib::ustring undokey = "svgfonts:"; + undokey += name; + DocumentUndo::maybeDone(o->document, undokey.c_str(), _("Set SVG Font attribute"), ""); + } + +} + +Gtk::Box* SvgFontsDialog::AttrCombo(gchar* lbl, const SPAttr /*attr*/){ + Gtk::Box* hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + hbox->add(* Gtk::manage(new Gtk::Label(lbl)) ); + hbox->add(* Gtk::manage(new Gtk::ComboBox()) ); + hbox->show_all(); + return hbox; +} + +/*** SvgFontsDialog ***/ + +GlyphComboBox::GlyphComboBox() { +} + +void GlyphComboBox::update(SPFont* spfont){ + if (!spfont) return; + + // remove wrapping - it has severe performance penalty for appending items + set_wrap_width(0); + + this->remove_all(); + + for (auto& node: spfont->children) { + if (SP_IS_GLYPH(&node)){ + this->append((static_cast<SPGlyph*>(&node))->unicode); + } + } + + // set desired wrpping now + set_wrap_width(4); +} + +void SvgFontsDialog::on_kerning_value_changed(){ + if (!get_selected_kerning_pair()) { + return; + } + + //TODO: I am unsure whether this is the correct way of calling SPDocumentUndo::maybe_done + Glib::ustring undokey = "svgfonts:hkern:k:"; + undokey += this->kerning_pair->u1->attribute_string(); + undokey += ":"; + undokey += this->kerning_pair->u2->attribute_string(); + + //slider values increase from right to left so that they match the kerning pair preview + + //XML Tree being directly used here while it shouldn't be. + this->kerning_pair->setAttribute("k", Glib::Ascii::dtostr(get_selected_spfont()->horiz_adv_x - kerning_slider->get_value())); + DocumentUndo::maybeDone(getDocument(), undokey.c_str(), _("Adjust kerning value"), ""); + + //populate_kerning_pairs_box(); + kerning_preview.redraw(); + _font_da.redraw(); +} + +void SvgFontsDialog::glyphs_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _GlyphsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void SvgFontsDialog::kerning_pairs_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _KerningPairsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void SvgFontsDialog::fonts_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _FontsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void SvgFontsDialog::sort_glyphs(SPFont* font) { + if (!font) return; + + { + auto scoped(_update.block()); + font->sort_glyphs(); + } + update_glyphs(); +} + +// return U+<code> ... string +Glib::ustring create_unicode_name(const Glib::ustring& unicode, int max_chars) { + std::ostringstream ost; + if (unicode.empty()) { + ost << "-"; + } + else { + auto it = unicode.begin(); + for (int i = 0; i < max_chars && it != unicode.end(); ++i) { + if (i > 0) { + ost << " "; + } + unsigned int code = *it++; + ost << "U+" << std::hex << std::uppercase << std::setw(6) << std::setfill('0') << code; + } + if (it != unicode.end()) { + ost << "..."; // there's more, but we skip them + } + } + return ost.str(); +} + +// synthetic name consists for unicode hex numbers derived from glyph's "unicode" attribute +Glib::ustring get_glyph_synthetic_name(const SPGlyph& glyph) { + auto unicode_name = create_unicode_name(glyph.unicode, 3); + // U+<code> plus character + return unicode_name + " " + glyph.unicode; +} + +// full name consists of user-defined name combined with synthetic one +Glib::ustring get_glyph_full_name(const SPGlyph& glyph) { + auto name = get_glyph_synthetic_name(glyph); + if (!glyph.glyph_name.empty()) { + // unicode name first, followed by user name - for sorting layers + return name + " " + glyph.glyph_name; + } + else { + return name; + } +} + +// look for a layer by its label; looking only in direct sublayers of 'root_layer' +SPItem* find_layer(SPDesktop* desktop, SPObject* root_layer, const Glib::ustring& name) { + if (!desktop) return nullptr; + + const auto& layers = desktop->layerManager(); + auto root = root_layer == nullptr ? layers.currentRoot() : root_layer; + if (!root) return nullptr; + + // check only direct child layers + auto it = std::find_if(root->children.begin(), root->children.end(), [&](SPObject& obj) { + return layers.isLayer(&obj) && obj.label() && strcmp(obj.label(), name.c_str()) == 0; + }); + if (it != root->children.end()) { + return static_cast<SPItem*>(&*it); + } + + return nullptr; // not found +} + +std::vector<SPGroup*> get_direct_sublayers(SPObject* layer) { + std::vector<SPGroup*> layers; + if (!layer) return layers; + + for (auto&& item : layer->children) { + if (auto l = LayerManager::asLayer(&item)) { + layers.push_back(l); + } + } + + return layers; +} + +void rename_glyph_layer(SPDesktop* desktop, SPItem* layer, const Glib::ustring& font, const Glib::ustring& name) { + if (!desktop || !layer || font.empty() || name.empty()) return; + + auto parent_layer = find_layer(desktop, desktop->layerManager().currentRoot(), font); + if (!parent_layer) return; + + // before renaming the layer find new place to move it into to keep sorted order intact + auto glyph_layers = get_direct_sublayers(parent_layer); + + auto it = std::lower_bound(glyph_layers.rbegin(), glyph_layers.rend(), name, [&](auto&& layer, const Glib::ustring n) { + auto label = layer->label(); + if (!label) return false; + + Glib::ustring temp(label); + return std::lexicographical_compare(temp.begin(), temp.end(), n.begin(), n.end()); + }); + SPObject* after = nullptr; + if (it != glyph_layers.rend()) { + after = *it; + } + + // SPItem changeOrder messes up inserting into first position, so dropping to Node level + if (layer != after && parent_layer->getRepr() && layer->getRepr()) { + parent_layer->getRepr()->changeOrder(layer->getRepr(), after ? after->getRepr() : nullptr); + } + + desktop->layerManager().renameLayer(layer, name.c_str(), false); +} + +SPItem* get_layer_for_glyph(SPDesktop* desktop, const Glib::ustring& font, const Glib::ustring& name) { + if (!desktop || name.empty() || font.empty()) return nullptr; + + auto parent_layer = find_layer(desktop, desktop->layerManager().currentRoot(), font); + if (!parent_layer) return nullptr; + + return find_layer(desktop, parent_layer, name); +} + +SPItem* get_or_create_layer_for_glyph(SPDesktop* desktop, const Glib::ustring& font, const Glib::ustring& name) { + if (!desktop || name.empty() || font.empty()) return nullptr; + + auto& layers = desktop->layerManager(); + auto parent_layer = find_layer(desktop, layers.currentRoot(), font); + if (!parent_layer) { + // create a new layer for a font + parent_layer = static_cast<SPItem*>(create_layer(layers.currentRoot(), layers.currentRoot(), Inkscape::LayerRelativePosition::LPOS_CHILD)); + if (!parent_layer) return nullptr; + + layers.renameLayer(parent_layer, font.c_str(), false); + } + + if (auto layer = find_layer(desktop, parent_layer, name)) { + return layer; + } + + // find the right place for a new layer, so they appear sorted + auto glyph_layers = get_direct_sublayers(parent_layer); + // auto& glyph_layers = parent_layer->children; + auto it = std::lower_bound(glyph_layers.rbegin(), glyph_layers.rend(), name, [&](auto&& layer, const Glib::ustring n) { + auto label = layer->label(); + if (!label) return false; + + Glib::ustring temp(label); + return std::lexicographical_compare(temp.begin(), temp.end(), n.begin(), n.end()); + }); + SPObject* insert = parent_layer; + Inkscape::LayerRelativePosition pos = Inkscape::LayerRelativePosition::LPOS_ABOVE; + if (it != glyph_layers.rend()) { + insert = *it; + } + else { + // auto first = std::find_if(glyph_layers.begin(), glyph_layers.end(), [&](auto&& obj) { + // return layers.isLayer(&obj); + // }); + if (!glyph_layers.empty()) { + insert = glyph_layers.front(); + pos = Inkscape::LayerRelativePosition::LPOS_BELOW; + } + } + + // create a new layer for a glyph + auto layer = create_layer(parent_layer, insert, pos); + if (!layer) return nullptr; + + layers.renameLayer(layer, name.c_str(), false); + + DocumentUndo::done(desktop->getDocument(), _("Add layer"), ""); + return dynamic_cast<SPItem*>(layer); +} + +void SvgFontsDialog::create_glyphs_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem) +{ + // - edit glyph (show its layer) + // - sort glyphs and their layers + // - remove current glyph + auto mi = Gtk::make_managed<Gtk::MenuItem>(_("_Edit current glyph"), true); + mi->show(); + mi->signal_activate().connect([=](){ + edit_glyph(get_selected_glyph()); + }); + _GlyphsContextMenu.append(*mi); + + mi = Gtk::make_managed<Gtk::SeparatorMenuItem>(); + mi->show(); + _GlyphsContextMenu.append(*mi); + + mi = Gtk::make_managed<Gtk::MenuItem>(_("_Sort glyphs"), true); + mi->show(); + mi->signal_activate().connect([=](){ + sort_glyphs(get_selected_spfont()); + }); + _GlyphsContextMenu.append(*mi); + + mi = Gtk::make_managed<Gtk::SeparatorMenuItem>(); + mi->show(); + _GlyphsContextMenu.append(*mi); + + mi = Gtk::make_managed<Gtk::MenuItem>(_("_Remove"), true); + _GlyphsContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + + _GlyphsContextMenu.accelerate(parent); +} + +void SvgFontsDialog::create_kerning_pairs_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem) +{ + auto mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _KerningPairsContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _KerningPairsContextMenu.accelerate(parent); +} + +void SvgFontsDialog::create_fonts_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem) +{ + auto mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _FontsContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _FontsContextMenu.accelerate(parent); +} + +void SvgFontsDialog::update_sensitiveness(){ + if (get_selected_spfont()){ + _grid.set_sensitive(true); + glyphs_vbox.set_sensitive(true); + kerning_vbox.set_sensitive(true); + } else { + _grid.set_sensitive(false); + glyphs_vbox.set_sensitive(false); + kerning_vbox.set_sensitive(false); + } +} + +Glib::ustring get_font_label(SPFont* font) { + if (!font) return Glib::ustring(); + + const gchar* label = font->label(); + const gchar* id = font->getId(); + return Glib::ustring(label ? label : (id ? id : "font")); +}; + +/** Add all fonts in the getDocument() to the combobox. + * This function is called when new document is selected as well as when SVG "definition" section changes. + * Try to detect if font(s) have actually been modified to eliminate some expensive refreshes. + */ +void SvgFontsDialog::update_fonts(bool document_replaced) +{ + std::vector<SPObject*> fonts; + if (auto document = getDocument()) { + fonts = document->getResourceList( "font" ); + } + + auto children = _model->children(); + bool equal = false; + bool selected_font = false; + + // compare model and resources + if (!document_replaced && children.size() == fonts.size()) { + equal = true; // assume they are the same + auto it = fonts.begin(); + for (auto&& node : children) { + SPFont* sp_font = node[_columns.spfont]; + if (it == fonts.end() || *it != sp_font) { + // difference detected; update model + equal = false; + break; + } + ++it; + } + } + + // rebuild model if list of fonts is different + if (!equal) { + _model->clear(); + for (auto font : fonts) { + Gtk::TreeModel::Row row = *_model->append(); + SPFont* f = SP_FONT(font); + row[_columns.spfont] = f; + row[_columns.svgfont] = new SvgFont(f); + row[_columns.label] = get_font_label(f); + } + if (!fonts.empty()) { + // select a font, this dialog is disabled without a font + auto selection = _FontsList.get_selection(); + if (selection) { + selection->select(_model->get_iter("0")); + selected_font = true; + } + } + } + else { + // list of fonts is the same, but attributes may have changed + auto it = fonts.begin(); + for (auto&& node : children) { + if (auto font = dynamic_cast<SPFont*>(*it++)) { + node[_columns.label] = get_font_label(font); + } + } + } + + if (document_replaced && !selected_font) { + // replace fonts, they are stale + font_selected(nullptr, nullptr); + } + else { + update_sensitiveness(); + } +} + +void SvgFontsDialog::on_preview_text_changed(){ + _font_da.set_text(_preview_entry.get_text()); +} + +void SvgFontsDialog::on_kerning_pair_selection_changed(){ + SPGlyphKerning* kern = get_selected_kerning_pair(); + if (!kern) { + kerning_preview.set_text(""); + return; + } + Glib::ustring str; + str += kern->u1->sample_glyph(); + str += kern->u2->sample_glyph(); + + kerning_preview.set_text(str); + this->kerning_pair = kern; + + //slider values increase from right to left so that they match the kerning pair preview + kerning_slider->set_value(get_selected_spfont()->horiz_adv_x - kern->k); +} + +void SvgFontsDialog::update_global_settings_tab(){ + SPFont* font = get_selected_spfont(); + if (!font) { + //TODO: perhaps reset all values when there's no font + _familyname_entry->set_text(""); + return; + } + + _horiz_adv_x_spin->set_value(font->horiz_adv_x); + _horiz_origin_x_spin->set_value(font->horiz_origin_x); + _horiz_origin_y_spin->set_value(font->horiz_origin_y); + + for (auto& obj: font->children) { + if (SP_IS_FONTFACE(&obj)){ + _familyname_entry->set_text((SP_FONTFACE(&obj))->font_family); + _units_per_em_spin->set_value((SP_FONTFACE(&obj))->units_per_em); + _ascent_spin->set_value((SP_FONTFACE(&obj))->ascent); + _descent_spin->set_value((SP_FONTFACE(&obj))->descent); + _x_height_spin->set_value((SP_FONTFACE(&obj))->x_height); + _cap_height_spin->set_value((SP_FONTFACE(&obj))->cap_height); + } + } +} + +void SvgFontsDialog::font_selected(SvgFont* svgfont, SPFont* spfont) { + // in update + auto scoped(_update.block()); + + first_glyph.update(spfont); + second_glyph.update(spfont); + kerning_preview.set_svgfont(svgfont); + _font_da.set_svgfont(svgfont); + _font_da.redraw(); + _glyph_renderer->set_svg_font(svgfont); + _glyph_cell_renderer->set_svg_font(svgfont); + + kerning_slider->set_range(0, spfont ? spfont->horiz_adv_x : 0); + kerning_slider->set_draw_value(false); + kerning_slider->set_value(0); + + update_global_settings_tab(); + populate_glyphs_box(); + populate_kerning_pairs_box(); + update_sensitiveness(); +} + +void SvgFontsDialog::on_font_selection_changed(){ + SPFont* spfont = get_selected_spfont(); + SvgFont* svgfont = get_selected_svgfont(); + font_selected(svgfont, spfont); +} + +SPGlyphKerning* SvgFontsDialog::get_selected_kerning_pair() +{ + Gtk::TreeModel::iterator i = _KerningPairsList.get_selection()->get_selected(); + if(i) + return (*i)[_KerningPairsListColumns.spnode]; + return nullptr; +} + +SvgFont* SvgFontsDialog::get_selected_svgfont() +{ + Gtk::TreeModel::iterator i = _FontsList.get_selection()->get_selected(); + if(i) + return (*i)[_columns.svgfont]; + return nullptr; +} + +SPFont* SvgFontsDialog::get_selected_spfont() +{ + Gtk::TreeModel::iterator i = _FontsList.get_selection()->get_selected(); + if(i) + return (*i)[_columns.spfont]; + return nullptr; +} + +Gtk::TreeModel::iterator SvgFontsDialog::get_selected_glyph_iter() { + if (_GlyphsListScroller.get_visible()) { + if (auto selection = _GlyphsList.get_selection()) { + Gtk::TreeModel::iterator it = selection->get_selected(); + return it; + } + } + else { + std::vector<Gtk::TreePath> selected = _glyphs_grid.get_selected_items(); + if (selected.size() == 1) { + Gtk::ListStore::iterator it = _GlyphsListStore->get_iter(selected.front()); + return it; + } + } + return Gtk::TreeModel::iterator(); +} + +SPGlyph* SvgFontsDialog::get_selected_glyph() +{ + if (auto it = get_selected_glyph_iter()) { + return (*it)[_GlyphsListColumns.glyph_node]; + } + return nullptr; +} + +void SvgFontsDialog::set_selected_glyph(SPGlyph* glyph) { + if (!glyph) return; + + _GlyphsListStore->foreach_iter([=](const Gtk::TreeModel::iterator& it) { + if (it->get_value(_GlyphsListColumns.glyph_node) == glyph) { + if (auto selection = _GlyphsList.get_selection()) { + selection->select(it); + } + auto selected_item = _GlyphsListStore->get_path(it); + _glyphs_grid.select_path(selected_item); + return true; // stop + } + return false; // continue + }); +} + +SPGuide* get_guide(SPDocument& doc, const Glib::ustring& id) { + auto object = doc.getObjectById(id); + if (!object) return nullptr; + + // get guide line + if (auto guide = dynamic_cast<SPGuide*>(object)) { + return guide; + } + // remove colliding object + object->deleteObject(); + return nullptr; +} + +SPGuide* create_guide(SPDocument& doc, double x0, double y0, double x1, double y1) { + return SPGuide::createSPGuide(&doc, Geom::Point(x0, y1), Geom::Point(x1, y1)); +} + +void set_up_typography_canvas(SPDocument* document, double em, double asc, double cap, double xheight, double des) { + if (!document || em <= 0) return; + + // set size and viewbox + auto size = Inkscape::Util::Quantity(em, "px"); + bool change_size = false; + document->setWidthAndHeight(size, size, change_size); + document->setViewBox(Geom::Rect::from_xywh(0, 0, em, em)); + + // baseline + double base = des; + double ascPos = base + asc; + double capPos = base + cap; + double xPos = base + xheight; + double desPos = base - des; + + if (!document->is_yaxisdown()) { + base = size.quantity - des; + ascPos = base - asc; + capPos = base - cap; + xPos = base - xheight; + desPos = base + des; + } + + // add/move guide lines + struct { double pos; const char* name; const char* id; } guides[5] = { + {ascPos, _("ascender"), "ink-font-guide-ascender"}, + {capPos, _("caps"), "ink-font-guide-caps"}, + {xPos, _("x-height"), "ink-font-guide-x-height"}, + {base, _("baseline"), "ink-font-guide-baseline"}, + {desPos, _("descender"), "ink-font-guide-descender"}, + }; + + double left = 0; + double right = em; + + for (auto&& g : guides) { + double y = em - g.pos; + auto guide = get_guide(*document, g.id); + if (guide) { + guide->set_locked(false, true); + guide->moveto(Geom::Point(left, y), true); + } + else { + guide = create_guide(*document, left, y, right, y); + guide->getRepr()->setAttributeOrRemoveIfEmpty("id", g.id); + } + guide->set_label(g.name, true); + guide->set_locked(true, true); + } + + DocumentUndo::done(document, _("Set up typography canvas"), ""); +} + +const int MARGIN_SPACE = 4; + +Gtk::Box* SvgFontsDialog::global_settings_tab(){ + + _fonts_scroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + _fonts_scroller.add(_FontsList); + _fonts_scroller.set_hexpand(); + _fonts_scroller.show(); + _header_box.set_column_spacing(MARGIN_SPACE); + _header_box.set_row_spacing(MARGIN_SPACE); + _header_box.attach(_fonts_scroller, 0, 0, 1, 3); + _header_box.attach(*Gtk::make_managed<Gtk::Label>(), 1, 0); + _header_box.attach(_add, 1, 1); + _header_box.attach(_remove, 1, 2); + _header_box.set_margin_bottom(MARGIN_SPACE); + _header_box.set_margin_end(MARGIN_SPACE); + _add.set_valign(Gtk::ALIGN_CENTER); + _remove.set_valign(Gtk::ALIGN_CENTER); + _remove.set_halign(Gtk::ALIGN_CENTER); + _add.set_image_from_icon_name("list-add", Gtk::ICON_SIZE_BUTTON); + _remove.set_image_from_icon_name("list-remove", Gtk::ICON_SIZE_BUTTON); + + global_vbox.pack_start(_header_box, false, false); + + _font_label = new Gtk::Label(Glib::ustring("<b>") + _("Font Attributes") + "</b>", Gtk::ALIGN_START, Gtk::ALIGN_CENTER); + _horiz_adv_x_spin = new AttrSpin( this, (gchar*) _("Horizontal advance X:"), _("Default glyph width for horizontal text"), SPAttr::HORIZ_ADV_X); + _horiz_origin_x_spin = new AttrSpin( this, (gchar*) _("Horizontal origin X:"), _("Default X-coordinate of the origin of a glyph (for horizontal text)"), SPAttr::HORIZ_ORIGIN_X); + _horiz_origin_y_spin = new AttrSpin( this, (gchar*) _("Horizontal origin Y:"), _("Default Y-coordinate of the origin of a glyph (for horizontal text)"), SPAttr::HORIZ_ORIGIN_Y); + _font_face_label = new Gtk::Label(Glib::ustring("<b>") + _("Font face attributes") + "</b>", Gtk::ALIGN_START, Gtk::ALIGN_CENTER); + _familyname_entry = new AttrEntry(this, (gchar*) _("Family name:"), _("Name of the font as it appears in font selectors and css font-family properties"), SPAttr::FONT_FAMILY); + _units_per_em_spin = new AttrSpin( this, (gchar*) _("Em-size:"), _("Display units per <italic>em</italic> (nominally width of 'M' character)"), SPAttr::UNITS_PER_EM); + _ascent_spin = new AttrSpin( this, (gchar*) _("Ascender:"), _("Amount of space taken up by ascenders like the tall line on the letter 'h'"), SPAttr::ASCENT); + _cap_height_spin = new AttrSpin( this, (gchar*) _("Caps height:"), _("The height of a capital letter above the baseline like the letter 'H' or 'I'"), SPAttr::CAP_HEIGHT); + _x_height_spin = new AttrSpin( this, (gchar*) _("x-height:"), _("The height of a lower-case letter above the baseline like the letter 'x'"), SPAttr::X_HEIGHT); + _descent_spin = new AttrSpin( this, (gchar*) _("Descender:"), _("Amount of space taken up by descenders like the tail on the letter 'g'"), SPAttr::DESCENT); + + //_descent_spin->set_range(-4096,0); + _font_label->set_use_markup(); + _font_face_label->set_use_markup(); + + _grid.set_column_spacing(MARGIN_SPACE); + _grid.set_row_spacing(MARGIN_SPACE); + _grid.set_margin_start(MARGIN_SPACE); + _grid.set_margin_bottom(MARGIN_SPACE); + const int indent = 2 * MARGIN_SPACE; + int row = 0; + + _grid.attach(*_font_label, 0, row++, 2); + SvgFontsDialog::AttrSpin* font[] = {_horiz_adv_x_spin, _horiz_origin_x_spin, _horiz_origin_y_spin}; + for (auto spin : font) { + spin->get_label()->set_margin_start(indent); + _grid.attach(*spin->get_label(), 0, row); + _grid.attach(*spin->getSpin(), 1, row++); + } + + _grid.attach(*_font_face_label, 0, row++, 2); + _familyname_entry->get_label()->set_margin_start(indent); + _familyname_entry->get_entry()->set_margin_end(MARGIN_SPACE); + _grid.attach(*_familyname_entry->get_label(), 0, row); + _grid.attach(*_familyname_entry->get_entry(), 1, row++, 2); + + SvgFontsDialog::AttrSpin* face[] = {_units_per_em_spin, _ascent_spin, _cap_height_spin, _x_height_spin, _descent_spin}; + for (auto spin : face) { + spin->get_label()->set_margin_start(indent); + _grid.attach(*spin->get_label(), 0, row); + _grid.attach(*spin->getSpin(), 1, row++); + } + auto setup = Gtk::make_managed<Gtk::Button>(_("Set up canvas")); + _grid.attach(*setup, 0, row++, 2); + setup->set_halign(Gtk::ALIGN_START); + setup->signal_clicked().connect([=](){ + // set up typography canvas + set_up_typography_canvas( + getDocument(), + _units_per_em_spin->getSpin()->get_value(), + _ascent_spin->getSpin()->get_value(), + _cap_height_spin->getSpin()->get_value(), + _x_height_spin->getSpin()->get_value(), + _descent_spin->getSpin()->get_value() + ); + }); + + global_vbox.set_border_width(2); + global_vbox.pack_start(_grid, false, true); + +/* global_vbox->add(*AttrCombo((gchar*) _("Style:"), SPAttr::FONT_STYLE)); + global_vbox->add(*AttrCombo((gchar*) _("Variant:"), SPAttr::FONT_VARIANT)); + global_vbox->add(*AttrCombo((gchar*) _("Weight:"), SPAttr::FONT_WEIGHT)); +*/ + return &global_vbox; +} + +void SvgFontsDialog::set_glyph_row(const Gtk::TreeRow& row, SPGlyph& glyph) { + auto unicode_name = create_unicode_name(glyph.unicode, 3); + row[_GlyphsListColumns.glyph_node] = &glyph; + row[_GlyphsListColumns.glyph_name] = glyph.glyph_name; + row[_GlyphsListColumns.unicode] = glyph.unicode; + row[_GlyphsListColumns.UplusCode] = unicode_name; + row[_GlyphsListColumns.advance] = glyph.horiz_adv_x; + row[_GlyphsListColumns.name_markup] = "<small>" + Glib::Markup::escape_text(get_glyph_synthetic_name(glyph)) + "</small>"; +} + +void +SvgFontsDialog::populate_glyphs_box() +{ + if (!_GlyphsListStore) return; + + _GlyphsListStore->freeze_notify(); + + // try to keep selected glyph + Gtk::TreeModel::Path selected_item; + if (auto selected = get_selected_glyph_iter()) { + selected_item = _GlyphsListStore->get_path(selected); + } + _GlyphsListStore->clear(); + + SPFont* spfont = get_selected_spfont(); + _glyphs_observer.set(spfont); + + if (spfont) { + for (auto& node: spfont->children) { + if (SP_IS_GLYPH(&node)) { + auto& glyph = static_cast<SPGlyph&>(node); + Gtk::TreeModel::Row row = *_GlyphsListStore->append(); + set_glyph_row(row, glyph); + } + } + + if (!selected_item.empty()) { + if (auto selection = _GlyphsList.get_selection()) { + selection->select(selected_item); + _GlyphsList.scroll_to_row(selected_item); + } + _glyphs_grid.select_path(selected_item); + } + } + + _GlyphsListStore->thaw_notify(); +} + +void +SvgFontsDialog::populate_kerning_pairs_box() +{ + if (!_KerningPairsListStore) return; + + _KerningPairsListStore->clear(); + + if (SPFont* spfont = get_selected_spfont()) { + for (auto& node: spfont->children) { + if (SP_IS_HKERN(&node)){ + Gtk::TreeModel::Row row = *(_KerningPairsListStore->append()); + row[_KerningPairsListColumns.first_glyph] = (static_cast<SPGlyphKerning*>(&node))->u1->attribute_string().c_str(); + row[_KerningPairsListColumns.second_glyph] = (static_cast<SPGlyphKerning*>(&node))->u2->attribute_string().c_str(); + row[_KerningPairsListColumns.kerning_value] = (static_cast<SPGlyphKerning*>(&node))->k; + row[_KerningPairsListColumns.spnode] = static_cast<SPGlyphKerning*>(&node); + } + } + } +} + +// update existing glyph in the tree model +void SvgFontsDialog::update_glyph(SPGlyph* glyph) { + if (_update.pending() || !glyph) return; + + _GlyphsListStore->foreach_iter([&](const Gtk::TreeModel::iterator& it) { + if (it->get_value(_GlyphsListColumns.glyph_node) == glyph) { + const Gtk::TreeRow& row = *it; + set_glyph_row(row, *glyph); + return true; // stop + } + return false; // continue + }); +} + +void SvgFontsDialog::update_glyphs(SPGlyph* changed_glyph) { + if (_update.pending()) return; + + SPFont* font = get_selected_spfont(); + if (!font) return; + + if (changed_glyph) { + update_glyph(changed_glyph); + } + else { + populate_glyphs_box(); + } + + populate_kerning_pairs_box(); + refresh_svgfont(); +} + +void SvgFontsDialog::refresh_svgfont() { + if (auto font = get_selected_svgfont()) { + font->refresh(); + } + _font_da.redraw(); +} + +void SvgFontsDialog::add_glyph(){ + auto document = getDocument(); + if (!document) return; + auto font = get_selected_spfont(); + if (!font) return; + + auto glyphs = _GlyphsListStore->children(); + // initialize "unicode" field; if there are glyphs look for the last one and take next unicode + gunichar unicode = ' '; + if (!glyphs.empty()) { + const auto& last = glyphs[glyphs.size() - 1]; + if (SPGlyph* last_glyph = last[_GlyphsListColumns.glyph_node]) { + const Glib::ustring& code = last_glyph->unicode; + if (!code.empty()) { + auto value = code[0]; + // skip control chars 7f-9f + if (value == 0x7e) value = 0x9f; + // wrap around + if (value == 0x10ffff) value = 0x1f; + unicode = value + 1; + } + } + } + auto str = Glib::ustring(1, unicode); + + // empty name to begin with + SPGlyph* glyph = font->create_new_glyph("", str.c_str()); + DocumentUndo::done(document, _("Add glyph"), ""); + + // select newly added glyph + set_selected_glyph(glyph); +} + +double get_font_units_per_em(const SPFont* font) { + double units_per_em = 0.0; + if (font) { + for (auto& obj: font->children) { + if (SP_IS_FONTFACE(&obj)){ + //XML Tree being directly used here while it shouldn't be. + units_per_em = obj.getRepr()->getAttributeDouble("units-per-em", units_per_em); + break; + } + } + } + return units_per_em; +} + +Geom::PathVector flip_coordinate_system(Geom::PathVector pathv, const SPFont* font, double units_per_em) { + if (!font) return pathv; + + if (units_per_em <= 0) { + g_warning("Units per em not defined, path will be misplaced."); + } + + double baseline_offset = units_per_em - font->horiz_origin_y; + // This matrix flips y-axis and places the origin at baseline + Geom::Affine m(1, 0, 0, -1, 0, baseline_offset); + return pathv * m; +} + +void SvgFontsDialog::set_glyph_description_from_selected_path() { + auto font = get_selected_spfont(); + if (!font) return; + + auto selection = getSelection(); + if (!selection) + return; + + Inkscape::MessageStack *msgStack = getDesktop()->getMessageStack(); + if (selection->isEmpty()){ + char *msg = _("Select a <b>path</b> to define the curves of a glyph"); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + return; + } + + Inkscape::XML::Node* node = selection->xmlNodes().front(); + if (!node) return;//TODO: should this be an assert? + if (!node->matchAttributeName("d") || !node->attribute("d")){ + char *msg = _("The selected object does not have a <b>path</b> description."); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + return; + } //TODO: //Is there a better way to tell it to to the user? + + SPGlyph* glyph = get_selected_glyph(); + if (!glyph){ + char *msg = _("No glyph selected in the SVGFonts dialog."); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + return; + } + + Geom::PathVector pathv = sp_svg_read_pathv(node->attribute("d")); + + auto units_per_em = get_font_units_per_em(font); + //XML Tree being directly used here while it shouldn't be. + glyph->setAttribute("d", sp_svg_write_path(flip_coordinate_system(pathv, font, units_per_em))); + DocumentUndo::done(getDocument(), _("Set glyph curves"), ""); + + update_glyphs(glyph); +} + +void SvgFontsDialog::missing_glyph_description_from_selected_path(){ + auto font = get_selected_spfont(); + if (!font) return; + + auto selection = getSelection(); + if (!selection) + return; + + Inkscape::MessageStack *msgStack = getDesktop()->getMessageStack(); + if (selection->isEmpty()){ + char *msg = _("Select a <b>path</b> to define the curves of a glyph"); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + return; + } + + Inkscape::XML::Node* node = selection->xmlNodes().front(); + if (!node) return;//TODO: should this be an assert? + if (!node->matchAttributeName("d") || !node->attribute("d")){ + char *msg = _("The selected object does not have a <b>path</b> description."); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + return; + } //TODO: //Is there a better way to tell it to the user? + + Geom::PathVector pathv = sp_svg_read_pathv(node->attribute("d")); + + auto units_per_em = get_font_units_per_em(font); + for (auto& obj: font->children) { + if (SP_IS_MISSING_GLYPH(&obj)){ + //XML Tree being directly used here while it shouldn't be. + obj.setAttribute("d", sp_svg_write_path(flip_coordinate_system(pathv, font, units_per_em))); + DocumentUndo::done(getDocument(), _("Set glyph curves"), ""); + } + } + + refresh_svgfont(); +} + +void SvgFontsDialog::reset_missing_glyph_description(){ + for (auto& obj: get_selected_spfont()->children) { + if (SP_IS_MISSING_GLYPH(&obj)){ + //XML Tree being directly used here while it shouldn't be. + obj.setAttribute("d", "M0,0h1000v1024h-1000z"); + DocumentUndo::done(getDocument(), _("Reset missing-glyph"), ""); + } + } + refresh_svgfont(); +} + +void change_glyph_attribute(SPDesktop* desktop, SPGlyph& glyph, std::function<void ()> change) { + assert(glyph.parent); + + auto name = get_glyph_full_name(glyph); + auto font_label = glyph.parent->label(); + auto layer = get_layer_for_glyph(desktop, font_label, name); + + change(); + + if (!layer) return; + + name = get_glyph_full_name(glyph); + font_label = glyph.parent->label(); + rename_glyph_layer(desktop, layer, font_label, name); +} + +void SvgFontsDialog::glyph_name_edit(const Glib::ustring&, const Glib::ustring& str){ + SPGlyph* glyph = get_selected_glyph(); + if (!glyph) return; + + if (glyph->glyph_name == str) return; // no change + + change_glyph_attribute(getDesktop(), *glyph, [=](){ + //XML Tree being directly used here while it shouldn't be. + glyph->setAttribute("glyph-name", str); + + DocumentUndo::done(getDocument(), _("Edit glyph name"), ""); + update_glyphs(glyph); + }); +} + +void SvgFontsDialog::glyph_unicode_edit(const Glib::ustring&, const Glib::ustring& str){ + SPGlyph* glyph = get_selected_glyph(); + if (!glyph) return; + + if (glyph->unicode == str) return; // no change + + change_glyph_attribute(getDesktop(), *glyph, [=]() { + // XML Tree being directly used here while it shouldn't be. + glyph->setAttribute("unicode", str); + + DocumentUndo::done(getDocument(), _("Set glyph unicode"), ""); + update_glyphs(glyph); + }); +} + +void SvgFontsDialog::glyph_advance_edit(const Glib::ustring&, const Glib::ustring& str){ + SPGlyph* glyph = get_selected_glyph(); + if (!glyph) return; + + if (auto val = glyph->getAttribute("horiz-adv-x")) { + if (str == val) return; // no change + } + + //XML Tree being directly used here while it shouldn't be. + std::istringstream is(str.raw()); + double value; + // Check if input valid + if ((is >> value)) { + glyph->setAttribute("horiz-adv-x", str); + DocumentUndo::done(getDocument(), _("Set glyph advance"), ""); + + update_glyphs(glyph); + } else { + std::cerr << "SvgFontDialog::glyph_advance_edit: Error in input: " << str << std::endl; + } +} + +void SvgFontsDialog::remove_selected_font(){ + SPFont* font = get_selected_spfont(); + if (!font) return; + + //XML Tree being directly used here while it shouldn't be. + sp_repr_unparent(font->getRepr()); + DocumentUndo::done(getDocument(), _("Remove font"), ""); + + update_fonts(false); +} + +void SvgFontsDialog::remove_selected_glyph(){ + SPGlyph* glyph = get_selected_glyph(); + if (!glyph) return; + + //XML Tree being directly used here while it shouldn't be. + sp_repr_unparent(glyph->getRepr()); + DocumentUndo::done(getDocument(), _("Remove glyph"), ""); + + update_glyphs(); +} + +void SvgFontsDialog::remove_selected_kerning_pair() { + SPGlyphKerning* pair = get_selected_kerning_pair(); + if (!pair) return; + + //XML Tree being directly used here while it shouldn't be. + sp_repr_unparent(pair->getRepr()); + DocumentUndo::done(getDocument(), _("Remove kerning pair"), ""); + + update_glyphs(); +} + +Inkscape::XML::Node* create_path_from_glyph(const SPGlyph& glyph) { + Geom::PathVector pathv = sp_svg_read_pathv(glyph.getAttribute("d")); + auto path = glyph.document->getReprDoc()->createElement("svg:path"); + // auto path = new SPPath(); + auto font = dynamic_cast<SPFont*>(glyph.parent); + auto units_per_em = get_font_units_per_em(font); + path->setAttribute("d", sp_svg_write_path(flip_coordinate_system(pathv, font, units_per_em))); + return path; +} + +// switch to a glyph layer (and create this dedicated layer if necessary) +void SvgFontsDialog::edit_glyph(SPGlyph* glyph) { + if (!glyph || !glyph->parent) return; + + auto desktop = getDesktop(); + if (!desktop) return; + auto document = getDocument(); + if (!document) return; + + // glyph's full name to match layer name + auto name = get_glyph_full_name(*glyph); + if (name.empty()) return; + // font's name to match parent layer name + auto font_label = get_font_label(dynamic_cast<SPFont*>(glyph->parent)); + if (font_label.empty()) return; + + auto layer = get_or_create_layer_for_glyph(desktop, font_label, name); + if (!layer) return; + + // is layer empty? + if (!layer->hasChildren()) { + // since layer is empty try to initialize it by copying font glyph into it + auto path = create_path_from_glyph(*glyph); + if (path) { + // layer->attach(path, nullptr); + layer->addChild(path); + } + } + + auto& layers = desktop->layerManager(); + // set layer as "solo" - only one visible and unlocked + if (layers.isLayer(layer) && layer != layers.currentRoot()) { + layers.setCurrentLayer(layer, true); + layers.toggleLayerSolo(layer, true); + layers.toggleLockOtherLayers(layer, true); + DocumentUndo::done(document, _("Toggle layer solo"), ""); + } +} + +void SvgFontsDialog::set_glyphs_view_mode(bool list) { + if (list) { + _glyphs_icon_scroller.hide(); + _GlyphsListScroller.show(); + } + else { + _GlyphsListScroller.hide(); + _glyphs_icon_scroller.show(); + } +} + +Gtk::Box* SvgFontsDialog::glyphs_tab() { + _GlyphsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::glyphs_list_button_release)); + _glyphs_grid.signal_button_release_event().connect_notify([=](GdkEventButton* event){ glyphs_list_button_release(event); }); + create_glyphs_popup_menu(_GlyphsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_glyph)); + + auto missing_glyph = Gtk::make_managed<Gtk::Expander>(); + missing_glyph->set_label(_("Missing glyph")); + Gtk::Box* missing_glyph_hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4)); + missing_glyph->add(*missing_glyph_hbox); + missing_glyph->set_valign(Gtk::ALIGN_CENTER); + + missing_glyph_hbox->set_hexpand(false); + missing_glyph_hbox->pack_start(missing_glyph_button, false,false); + missing_glyph_hbox->pack_start(missing_glyph_reset_button, false,false); + + missing_glyph_button.set_label(_("From selection")); + missing_glyph_button.set_margin_top(MARGIN_SPACE); + missing_glyph_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::missing_glyph_description_from_selected_path)); + missing_glyph_reset_button.set_label(_("Reset")); + missing_glyph_reset_button.set_margin_top(MARGIN_SPACE); + missing_glyph_reset_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::reset_missing_glyph_description)); + + glyphs_vbox.set_border_width(4); + glyphs_vbox.set_spacing(4); + + _GlyphsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _GlyphsListScroller.add(_GlyphsList); + fix_inner_scroll(&_GlyphsListScroller); + _GlyphsList.set_model(_GlyphsListStore); + _GlyphsList.set_enable_search(false); + + _glyph_renderer = Gtk::manage(new SvgGlyphRenderer()); + const int size = 20; // arbitrarily chosen to keep glyphs small but still legible + _glyph_renderer->set_font_size(size * 9 / 10); + _glyph_renderer->set_cell_size(size * 3 / 2, size); + _glyph_renderer->set_tree(&_GlyphsList); + _glyph_renderer->signal_clicked().connect([=](const GdkEvent*, const Glib::ustring& unicodes) { + // set preview: show clicked glyph only + _preview_entry.set_text(unicodes); + }); + auto col_index = _GlyphsList.append_column(_("Glyph"), *_glyph_renderer) - 1; + if (auto column = _GlyphsList.get_column(col_index)) { + column->add_attribute(_glyph_renderer->property_glyph(), _GlyphsListColumns.unicode); + } + _GlyphsList.append_column_editable(_("Name"), _GlyphsListColumns.glyph_name); + _GlyphsList.append_column_editable(_("Characters"), _GlyphsListColumns.unicode); + _GlyphsList.append_column(_("Unicode"), _GlyphsListColumns.UplusCode); + _GlyphsList.append_column_numeric_editable(_("Advance"), _GlyphsListColumns.advance, "%.2f"); + _GlyphsList.show(); + _GlyphsList.signal_row_activated().connect([=](const Gtk::TreeModel::Path& path, Gtk::TreeViewColumn*) { + edit_glyph(get_selected_glyph()); + }); + + Gtk::Box* hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4)); + add_glyph_button.set_image_from_icon_name("list-add"); + add_glyph_button.set_tooltip_text(_("Add new glyph")); + add_glyph_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_glyph)); + remove_glyph_button.set_image_from_icon_name("list-remove"); + remove_glyph_button.set_tooltip_text(_("Delete current glyph")); + remove_glyph_button.signal_clicked().connect([=](){ remove_selected_glyph(); }); + + glyph_from_path_button.set_label(_("Get curves")); + glyph_from_path_button.set_always_show_image(); + glyph_from_path_button.set_image_from_icon_name("glyph-copy-from"); + glyph_from_path_button.set_tooltip_text(_("Get curves from selection to replace current glyph")); + glyph_from_path_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::set_glyph_description_from_selected_path)); + + auto edit = Gtk::make_managed<Gtk::Button>(); + edit->set_label(_("Edit")); + edit->set_always_show_image(); + edit->set_image_from_icon_name("edit"); + edit->set_tooltip_text(_("Switch to a layer with the same name as current glyph")); + edit->signal_clicked().connect([=]() { + edit_glyph(get_selected_glyph()); + }); + + hb->pack_start(glyph_from_path_button, false, false); + hb->pack_start(*edit, false, false); + hb->pack_end(remove_glyph_button, false, false); + hb->pack_end(add_glyph_button, false, false); + + _glyph_cell_renderer = Gtk::manage(new SvgGlyphRenderer()); + _glyph_cell_renderer->set_tree(&_glyphs_grid); + const int cell_width = 70; + const int cell_height = 50; + _glyph_cell_renderer->set_cell_size(cell_width, cell_height); + _glyph_cell_renderer->set_font_size(cell_height * 8 / 10); // font size: 80% of height + _glyphs_icon_scroller.add(_glyphs_grid); + _glyphs_icon_scroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _glyphs_grid.set_name("GlyphsGrid"); + _glyphs_grid.set_model(_GlyphsListStore); + _glyphs_grid.set_item_width(cell_width); + _glyphs_grid.set_selection_mode(Gtk::SELECTION_SINGLE); + _glyphs_grid.show_all_children(); + _glyphs_grid.set_margin(0); + _glyphs_grid.set_item_padding(0); + _glyphs_grid.set_row_spacing(0); + _glyphs_grid.set_column_spacing(0); + _glyphs_grid.set_columns(-1); + _glyphs_grid.set_markup_column(_GlyphsListColumns.name_markup); + _glyphs_grid.pack_start(*_glyph_cell_renderer); + _glyphs_grid.add_attribute(*_glyph_cell_renderer, "glyph", _GlyphsListColumns.unicode); + _glyphs_grid.show(); + _glyphs_grid.signal_item_activated().connect([=](const Gtk::TreeModel::Path& path) { + edit_glyph(get_selected_glyph()); + }); + + // keep selection in sync between the two views: list and grid + _glyphs_grid.signal_selection_changed().connect([=]() { + if (_glyphs_icon_scroller.get_visible()) { + if (auto selected = get_selected_glyph_iter()) { + if (auto selection = _GlyphsList.get_selection()) { + selection->select(selected); + } + } + } + }); + if (auto selection = _GlyphsList.get_selection()) { + selection->signal_changed().connect([=]() { + if (_GlyphsListScroller.get_visible()) { + if (auto selected = get_selected_glyph_iter()) { + auto selected_item = _GlyphsListStore->get_path(selected); + _glyphs_grid.select_path(selected_item); + } + } + }); + } + + // display mode switching buttons + auto hbox = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 4); + Gtk::RadioButtonGroup group; + auto list = Gtk::make_managed<Gtk::RadioButton>(group); + list->set_mode(false); + list->set_image_from_icon_name("glyph-list"); + list->set_tooltip_text(_("Glyph list view")); + list->set_valign(Gtk::ALIGN_START); + list->signal_toggled().connect([=]() { set_glyphs_view_mode(true); }); + auto grid = Gtk::make_managed<Gtk::RadioButton>(group); + grid->set_mode(false); + grid->set_image_from_icon_name("glyph-grid"); + grid->set_tooltip_text(_("Glyph grid view")); + grid->set_valign(Gtk::ALIGN_START); + grid->signal_toggled().connect([=]() { set_glyphs_view_mode(false); }); + hbox->pack_start(*missing_glyph); + hbox->pack_end(*grid, false, false); + hbox->pack_end(*list, false, false); + + glyphs_vbox.pack_start(*hb, false, false); + glyphs_vbox.pack_start(_GlyphsListScroller, true, true); + glyphs_vbox.pack_start(_glyphs_icon_scroller, true, true); + glyphs_vbox.pack_start(*hbox, false,false); + + _GlyphsListScroller.set_no_show_all(); + _glyphs_icon_scroller.set_no_show_all(); + (_show_glyph_list ? list : grid)->set_active(); + set_glyphs_view_mode(_show_glyph_list); + + for (auto&& col : _GlyphsList.get_columns()) { + col->set_resizable(); + } + + static_cast<Gtk::CellRendererText*>(_GlyphsList.get_column_cell_renderer(ColName))->signal_edited().connect( + sigc::mem_fun(*this, &SvgFontsDialog::glyph_name_edit)); + + static_cast<Gtk::CellRendererText*>(_GlyphsList.get_column_cell_renderer(ColString))->signal_edited().connect( + sigc::mem_fun(*this, &SvgFontsDialog::glyph_unicode_edit)); + + static_cast<Gtk::CellRendererText*>(_GlyphsList.get_column_cell_renderer(ColAdvance))->signal_edited().connect( + sigc::mem_fun(*this, &SvgFontsDialog::glyph_advance_edit)); + + _glyphs_observer.signal_changed().connect([=]() { update_glyphs(); }); + + return &glyphs_vbox; +} + +void SvgFontsDialog::add_kerning_pair(){ + if (first_glyph.get_active_text() == "" || + second_glyph.get_active_text() == "") return; + + //look for this kerning pair on the currently selected font + this->kerning_pair = nullptr; + for (auto& node: get_selected_spfont()->children) { + //TODO: It is not really correct to get only the first byte of each string. + //TODO: We should also support vertical kerning + if (SP_IS_HKERN(&node) && (static_cast<SPGlyphKerning*>(&node))->u1->contains((gchar) first_glyph.get_active_text().c_str()[0]) + && (static_cast<SPGlyphKerning*>(&node))->u2->contains((gchar) second_glyph.get_active_text().c_str()[0]) ){ + this->kerning_pair = static_cast<SPGlyphKerning*>(&node); + continue; + } + } + + if (this->kerning_pair) return; //We already have this kerning pair + + Inkscape::XML::Document *xml_doc = getDocument()->getReprDoc(); + + // create a new hkern node + Inkscape::XML::Node *repr = xml_doc->createElement("svg:hkern"); + + repr->setAttribute("u1", first_glyph.get_active_text()); + repr->setAttribute("u2", second_glyph.get_active_text()); + repr->setAttribute("k", "0"); + + // Append the new hkern node to the current font + get_selected_spfont()->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + + // get corresponding object + this->kerning_pair = SP_HKERN( getDocument()->getObjectByRepr(repr) ); + + // select newly added pair + if (auto selection = _KerningPairsList.get_selection()) { + _KerningPairsListStore->foreach_iter([=](const Gtk::TreeModel::iterator& it) { + if (it->get_value(_KerningPairsListColumns.spnode) == kerning_pair) { + selection->select(it); + return true; // stop + } + return false; // continue + }); + } + + DocumentUndo::done(getDocument(), _("Add kerning pair"), ""); +} + +Gtk::Box* SvgFontsDialog::kerning_tab(){ + _KerningPairsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::kerning_pairs_list_button_release)); + create_kerning_pairs_popup_menu(_KerningPairsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_kerning_pair)); + +//Kerning Setup: + kerning_vbox.set_border_width(4); + kerning_vbox.set_spacing(4); + // kerning_vbox.add(*Gtk::manage(new Gtk::Label(_("Kerning Setup")))); + Gtk::Box* kerning_selector = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + kerning_selector->pack_start(*Gtk::manage(new Gtk::Label(_("Select glyphs:"))), false, false); + kerning_selector->pack_start(first_glyph, false, false, MARGIN_SPACE / 2); + kerning_selector->pack_start(second_glyph, false, false, MARGIN_SPACE / 2); + kerning_selector->pack_start(add_kernpair_button, false, false, MARGIN_SPACE / 2); + add_kernpair_button.set_label(_("Add pair")); + add_kernpair_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_kerning_pair)); + _KerningPairsList.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_kerning_pair_selection_changed)); + kerning_slider->signal_value_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_kerning_value_changed)); + + kerning_vbox.pack_start(*kerning_selector, false,false); + + kerning_vbox.pack_start(_KerningPairsListScroller, true,true); + _KerningPairsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _KerningPairsListScroller.add(_KerningPairsList); + _KerningPairsList.set_model(_KerningPairsListStore); + _KerningPairsList.append_column(_("First glyph"), _KerningPairsListColumns.first_glyph); + _KerningPairsList.append_column(_("Second glyph"), _KerningPairsListColumns.second_glyph); +// _KerningPairsList.append_column_numeric_editable(_("Kerning Value"), _KerningPairsListColumns.kerning_value, "%f"); + + kerning_vbox.pack_start((Gtk::Widget&) kerning_preview, false,false); + + // kerning_slider has a big handle. Extra padding added + Gtk::Box* kerning_amount_hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 8)); + kerning_vbox.pack_start(*kerning_amount_hbox, false,false); + kerning_amount_hbox->pack_start(*Gtk::manage(new Gtk::Label(_("Kerning value:"))), false,false); + kerning_amount_hbox->pack_start(*kerning_slider, true,true); + + kerning_preview.set_size(-1, 150 + 20); + _font_da.set_size(-1, 60 + 20); + + return &kerning_vbox; +} + +SPFont *new_font(SPDocument *document) +{ + g_return_val_if_fail(document != nullptr, NULL); + + SPDefs *defs = document->getDefs(); + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + // create a new font + Inkscape::XML::Node *repr = xml_doc->createElement("svg:font"); + + //By default, set the horizontal advance to 1000 units + repr->setAttribute("horiz-adv-x", "1000"); + + // Append the new font node to defs + defs->getRepr()->appendChild(repr); + + // add some default values + Inkscape::XML::Node *fontface; + fontface = xml_doc->createElement("svg:font-face"); + fontface->setAttribute("units-per-em", "1000"); + fontface->setAttribute("ascent", "750"); + fontface->setAttribute("cap-height", "600"); + fontface->setAttribute("x-height", "400"); + fontface->setAttribute("descent", "200"); + repr->appendChild(fontface); + + //create a missing glyph + Inkscape::XML::Node *mg; + mg = xml_doc->createElement("svg:missing-glyph"); + mg->setAttribute("d", "M0,0h1000v1000h-1000z"); + repr->appendChild(mg); + + // get corresponding object + SPFont *f = SP_FONT( document->getObjectByRepr(repr) ); + + g_assert(f != nullptr); + g_assert(SP_IS_FONT(f)); + Inkscape::GC::release(mg); + Inkscape::GC::release(repr); + return f; +} + +void set_font_family(SPFont* font, char* str){ + if (!font) return; + for (auto& obj: font->children) { + if (SP_IS_FONTFACE(&obj)){ + //XML Tree being directly used here while it shouldn't be. + obj.setAttribute("font-family", str); + } + } + + DocumentUndo::done(font->document, _("Set font family"), ""); +} + +void SvgFontsDialog::add_font(){ + SPDocument* doc = this->getDesktop()->getDocument(); + SPFont* font = new_font(doc); + + const int count = _model->children().size(); + std::ostringstream os, os2; + os << _("font") << " " << count; + font->setLabel(os.str().c_str()); + + os2 << "SVGFont " << count; + for (auto& obj: font->children) { + if (SP_IS_FONTFACE(&obj)){ + //XML Tree being directly used here while it shouldn't be. + obj.setAttribute("font-family", os2.str()); + } + } + + update_fonts(false); + on_font_selection_changed(); + + DocumentUndo::done(doc, _("Add font"), ""); +} + +SvgFontsDialog::SvgFontsDialog() + : DialogBase("/dialogs/svgfonts", "SVGFonts") + , global_vbox(Gtk::ORIENTATION_VERTICAL) + , glyphs_vbox(Gtk::ORIENTATION_VERTICAL) + , kerning_vbox(Gtk::ORIENTATION_VERTICAL) +{ + kerning_slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL)); + + // kerning pairs store + _KerningPairsListStore = Gtk::ListStore::create(_KerningPairsListColumns); + + // list of glyphs in a current font; this store is reused if there are multiple fonts + _GlyphsListStore = Gtk::ListStore::create(_GlyphsListColumns); + + // List of SVGFonts declared in a document: + _model = Gtk::ListStore::create(_columns); + _FontsList.set_model(_model); + _FontsList.set_enable_search(false); + _FontsList.append_column_editable(_("_Fonts"), _columns.label); + _FontsList.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_font_selection_changed)); + // connect to the cell renderer's edit signal; there's also model's row_changed, but it is less specific + if (auto renderer = dynamic_cast<Gtk::CellRendererText*>(_FontsList.get_column_cell_renderer(0))) { + // commit font names when user edits them + renderer->signal_edited().connect([=](const Glib::ustring& path, const Glib::ustring& new_name) { + if (auto it = _model->get_iter(path)) { + auto font = it->get_value(_columns.spfont); + font->setLabel(new_name.c_str()); + Glib::ustring undokey = "svgfonts:fontName"; + DocumentUndo::maybeDone(font->document, undokey.c_str(), _("Set SVG font name"), ""); + } + }); + } + + _add.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_font)); + _remove.signal_clicked().connect([=](){ remove_selected_font(); }); + + Gtk::Notebook *tabs = Gtk::manage(new Gtk::Notebook()); + tabs->set_scrollable(); + + tabs->append_page(*global_settings_tab(), _("_Global settings"), true); + tabs->append_page(*glyphs_tab(), _("_Glyphs"), true); + tabs->append_page(*kerning_tab(), _("_Kerning"), true); + tabs->signal_switch_page().connect([=](Gtk::Widget*, guint page) { + if (page == 2) { + // update kerning glyph combos + if (SPFont* font = get_selected_spfont()) { + first_glyph.update(font); + second_glyph.update(font); + } + } + }); + + pack_start(*tabs, true, true, 0); + + // Text Preview: + _preview_entry.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_preview_text_changed)); + pack_start((Gtk::Widget&) _font_da, false, false); + _preview_entry.set_text(_("Sample text")); + _font_da.set_text(_("Sample text")); + + Gtk::Box* preview_entry_hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, MARGIN_SPACE)); + pack_start(*preview_entry_hbox, false, false); // Non-latin characters may need more height. + preview_entry_hbox->pack_start(*Gtk::manage(new Gtk::Label(_("Preview text:"))), false, false); + preview_entry_hbox->pack_start(_preview_entry, true, true); + preview_entry_hbox->set_margin_bottom(MARGIN_SPACE); + preview_entry_hbox->set_margin_start(MARGIN_SPACE); + preview_entry_hbox->set_margin_end(MARGIN_SPACE); + + _FontsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::fonts_list_button_release)); + create_fonts_popup_menu(_FontsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_font)); + + show_all(); +} + +void SvgFontsDialog::documentReplaced() +{ + _defs_observer_connection.disconnect(); + if (auto document = getDocument()) { + _defs_observer.set(document->getDefs()); + _defs_observer_connection = _defs_observer.signal_changed().connect([=](){ update_fonts(false); }); + } + update_fonts(true); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/svg-fonts-dialog.h b/src/ui/dialog/svg-fonts-dialog.h new file mode 100644 index 0000000..1138bc5 --- /dev/null +++ b/src/ui/dialog/svg-fonts-dialog.h @@ -0,0 +1,386 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG Fonts dialog + */ +/* Authors: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2008 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_SVG_FONTS_H +#define INKSCAPE_UI_DIALOG_SVG_FONTS_H + +#include <2geom/pathvector.h> +#include <gtkmm/box.h> +#include <gtkmm/grid.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/drawingarea.h> +#include <gtkmm/entry.h> +#include <gtkmm/iconview.h> +#include <gtkmm/liststore.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/treeview.h> + +#include "attributes.h" +#include "helper/auto-connection.h" +#include "ui/operation-blocker.h" +#include "ui/dialog/dialog-base.h" +#include "ui/widget/spinbutton.h" +#include "xml/helper-observer.h" + +namespace Gtk { +class Scale; +} + +class SPGlyph; +class SPGlyphKerning; +class SvgFont; + +class SvgFontDrawingArea : Gtk::DrawingArea{ +public: + SvgFontDrawingArea(); + void set_text(Glib::ustring); + void set_svgfont(SvgFont*); + void set_size(int x, int y); + void redraw(); +private: + int _x,_y; + SvgFont* _svgfont; + Glib::ustring _text; + bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override; +}; + +class SPFont; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class GlyphComboBox : public Gtk::ComboBoxText { +public: + GlyphComboBox(); + void update(SPFont*); +}; + +// cell text renderer for SVG font glyps (relying on Cairo "user font"); +// it can accept mouse clicks and report them via signal_clicked() +class SvgGlyphRenderer : public Gtk::CellRenderer { +public: + SvgGlyphRenderer() : + Glib::ObjectBase(typeid(CellRenderer)), + Gtk::CellRenderer(), + _property_active(*this, "active", true), + _property_activatable(*this, "activatable", true), + _property_glyph(*this, "glyph", "") { + + property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; + } + + ~SvgGlyphRenderer() override = default; + + Glib::PropertyProxy<Glib::ustring> property_glyph() { return _property_glyph.get_proxy(); } + Glib::PropertyProxy<bool> property_active() { return _property_active.get_proxy(); } + Glib::PropertyProxy<bool> property_activatable() { return _property_activatable.get_proxy(); } + + sigc::signal<void (const GdkEvent*, const Glib::ustring&)>& signal_clicked() { + return _signal_clicked; + } + + void set_svg_font(SvgFont* font) { + _font = font; + } + + void set_font_size(int size) { + _font_size = size; + } + + void set_tree(Gtk::Widget* tree) { + _tree = tree; + } + + void set_cell_size(int w, int h) { + _width = w; + _height = h; + } + + int get_width() const { + return _width; + } + + void render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr, Gtk::Widget& widget, const Gdk::Rectangle& background_area, const Gdk::Rectangle& cell_area, Gtk::CellRendererState flags) override; + + bool activate_vfunc(GdkEvent* event, Gtk::Widget& widget, const Glib::ustring& path, const Gdk::Rectangle& background_area, const Gdk::Rectangle& cell_area, Gtk::CellRendererState flags) override; + + void get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const override { + min_w = nat_w = _width; + } + + void get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const override { + min_h = nat_h = _height; + } + +private: + int _width = 0; + int _height = 0; + int _font_size = 0; + Glib::Property<Glib::ustring> _property_glyph; + Glib::Property<bool> _property_active; + Glib::Property<bool> _property_activatable; + SvgFont* _font = nullptr; + Gtk::Widget* _tree = nullptr; + sigc::signal<void (const GdkEvent*, const Glib::ustring&)> _signal_clicked; +}; + + +class SvgFontsDialog : public DialogBase +{ +public: + SvgFontsDialog(); + ~SvgFontsDialog() override {}; + + static SvgFontsDialog &getInstance() { return *new SvgFontsDialog(); } + + void documentReplaced() override; + + void update_fonts(bool document_replaced); + SvgFont* get_selected_svgfont(); + SPFont* get_selected_spfont(); + SPGlyph* get_selected_glyph(); + SPGlyphKerning* get_selected_kerning_pair(); + + //TODO: these methods should be private, right?! + void on_font_selection_changed(); + void on_kerning_pair_selection_changed(); + void on_preview_text_changed(); + void on_kerning_pair_changed(); + void on_kerning_value_changed(); + void on_setfontdata_changed(); + void add_font(); + + // Used for font-family + class AttrEntry + { + public: + AttrEntry(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttr attr); + void set_text(const char*); + Gtk::Entry* get_entry() { return &entry; } + Gtk::Label* get_label() { return _label; } + private: + SvgFontsDialog* dialog; + void on_attr_changed(); + Gtk::Entry entry; + SPAttr attr; + Gtk::Label* _label; + }; + + class AttrSpin + { + public: + AttrSpin(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttr attr); + void set_value(double v); + void set_range(double low, double high); + Inkscape::UI::Widget::SpinButton* getSpin() { return &spin; } + Gtk::Label* get_label() { return _label; } + private: + SvgFontsDialog* dialog; + void on_attr_changed(); + Inkscape::UI::Widget::SpinButton spin; + SPAttr attr; + Gtk::Label* _label; + }; + + OperationBlocker _update; + +private: + void update_glyphs(SPGlyph* changed_glyph = nullptr); + void update_glyph(SPGlyph* glyph); + void set_glyph_row(const Gtk::TreeRow& row, SPGlyph& glyph); + void refresh_svgfont(); + void update_sensitiveness(); + void update_global_settings_tab(); + void populate_glyphs_box(); + void populate_kerning_pairs_box(); + void set_glyph_description_from_selected_path(); + void missing_glyph_description_from_selected_path(); + void reset_missing_glyph_description(); + void add_glyph(); + void glyph_unicode_edit(const Glib::ustring&, const Glib::ustring&); + void glyph_name_edit( const Glib::ustring&, const Glib::ustring&); + void glyph_advance_edit(const Glib::ustring&, const Glib::ustring&); + void remove_selected_glyph(); + void remove_selected_font(); + void remove_selected_kerning_pair(); + void font_selected(SvgFont* svgfont, SPFont* spfont); + + void add_kerning_pair(); + + void create_glyphs_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem); + void glyphs_list_button_release(GdkEventButton* event); + + void create_fonts_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem); + void fonts_list_button_release(GdkEventButton* event); + + void create_kerning_pairs_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem); + void kerning_pairs_list_button_release(GdkEventButton* event); + + Gtk::TreeModel::iterator get_selected_glyph_iter(); + void set_selected_glyph(SPGlyph* glyph); + void edit_glyph(SPGlyph* glyph); + void sort_glyphs(SPFont* font); + + Inkscape::XML::SignalObserver _defs_observer; //in order to update fonts + Inkscape::XML::SignalObserver _glyphs_observer; + Inkscape::auto_connection _defs_observer_connection; + + Gtk::Box* AttrCombo(gchar* lbl, const SPAttr attr); + Gtk::Box* global_settings_tab(); + + // <font> + Gtk::Label* _font_label; + AttrSpin* _horiz_adv_x_spin; + AttrSpin* _horiz_origin_x_spin; + AttrSpin* _horiz_origin_y_spin; + + // <font-face> + Gtk::Label* _font_face_label; + AttrEntry* _familyname_entry; + AttrSpin* _units_per_em_spin; + AttrSpin* _ascent_spin; + AttrSpin* _descent_spin; + AttrSpin* _cap_height_spin; + AttrSpin* _x_height_spin; + + Gtk::Box* kerning_tab(); + Gtk::Box* glyphs_tab(); + Gtk::Button _add; + Gtk::Button _remove; + Gtk::Button add_glyph_button; + Gtk::Button remove_glyph_button; + Gtk::Button glyph_from_path_button; + Gtk::Button missing_glyph_button; + Gtk::Button missing_glyph_reset_button; + + class Columns : public Gtk::TreeModel::ColumnRecord + { + public: + Columns() { + add(spfont); + add(svgfont); + add(label); + } + + Gtk::TreeModelColumn<SPFont*> spfont; + Gtk::TreeModelColumn<SvgFont*> svgfont; + Gtk::TreeModelColumn<Glib::ustring> label; + }; + Glib::RefPtr<Gtk::ListStore> _model; + Columns _columns; + Gtk::TreeView _FontsList; + Gtk::ScrolledWindow _fonts_scroller; + + class GlyphsColumns : public Gtk::TreeModel::ColumnRecord + { + public: + GlyphsColumns() { + add(glyph_node); + add(glyph_name); + add(unicode); + add(UplusCode); + add(advance); + add(name_markup); + } + + Gtk::TreeModelColumn<SPGlyph*> glyph_node; + Gtk::TreeModelColumn<Glib::ustring> glyph_name; + Gtk::TreeModelColumn<Glib::ustring> unicode; + Gtk::TreeModelColumn<Glib::ustring> UplusCode; + Gtk::TreeModelColumn<double> advance; + Gtk::TreeModelColumn<Glib::ustring> name_markup; + }; + enum GlyphColumnIndex { ColGlyph, ColName, ColString, ColUplusCode, ColAdvance }; + GlyphsColumns _GlyphsListColumns; + Glib::RefPtr<Gtk::ListStore> _GlyphsListStore; + Gtk::TreeView _GlyphsList; + Gtk::ScrolledWindow _GlyphsListScroller; + Gtk::ScrolledWindow _glyphs_icon_scroller; + Gtk::IconView _glyphs_grid; + SvgGlyphRenderer* _glyph_renderer = nullptr; + SvgGlyphRenderer* _glyph_cell_renderer = nullptr; + + class KerningPairColumns : public Gtk::TreeModel::ColumnRecord + { + public: + KerningPairColumns() { + add(first_glyph); + add(second_glyph); + add(kerning_value); + add(spnode); + } + + Gtk::TreeModelColumn<Glib::ustring> first_glyph; + Gtk::TreeModelColumn<Glib::ustring> second_glyph; + Gtk::TreeModelColumn<double> kerning_value; + Gtk::TreeModelColumn<SPGlyphKerning *> spnode; + }; + + KerningPairColumns _KerningPairsListColumns; + Glib::RefPtr<Gtk::ListStore> _KerningPairsListStore; + Gtk::TreeView _KerningPairsList; + Gtk::ScrolledWindow _KerningPairsListScroller; + Gtk::Button add_kernpair_button; + + Gtk::Grid _header_box; + Gtk::Grid _grid; + Gtk::Box global_vbox; + Gtk::Box glyphs_vbox; + Gtk::Box kerning_vbox; + Gtk::Entry _preview_entry; + bool _show_glyph_list = true; + void set_glyphs_view_mode(bool list); + + Gtk::Menu _FontsContextMenu; + Gtk::Menu _GlyphsContextMenu; + Gtk::Menu _KerningPairsContextMenu; + + SvgFontDrawingArea _font_da, kerning_preview; + GlyphComboBox first_glyph, second_glyph; + SPGlyphKerning* kerning_pair; + Inkscape::UI::Widget::SpinButton setwidth_spin; + Gtk::Scale* kerning_slider; + + class EntryWidget : public Gtk::Box + { + public: + EntryWidget() + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) { + this->add(this->_label); + this->add(this->_entry); + } + void set_label(const gchar* l){ + this->_label.set_text(l); + } + private: + Gtk::Label _label; + Gtk::Entry _entry; + }; + EntryWidget _font_family, _font_variant; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //#ifndef INKSCAPE_UI_DIALOG_SVG_FONTS_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/ui/dialog/svg-preview.cpp b/src/ui/dialog/svg-preview.cpp new file mode 100644 index 0000000..2d6cc4e --- /dev/null +++ b/src/ui/dialog/svg-preview.cpp @@ -0,0 +1,477 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Implementation of the file dialog interfaces defined in filedialogimpl.h. + */ +/* Authors: + * Bob Jamison + * Joel Holdsworth + * Bruno Dilly + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * + * Copyright (C) 2004-2007 Bob Jamison + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2007-2008 Joel Holdsworth + * Copyright (C) 2004-2007 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iostream> +#include <fstream> + +#include <glibmm/i18n.h> +#include <glib/gstdio.h> // GStatBuf + +#include "svg-preview.h" + +#include "document.h" +#include "ui/view/svg-view-widget.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/*######################################################################### +### SVG Preview Widget +#########################################################################*/ + +bool SVGPreview::setDocument(SPDocument *doc) +{ + if (viewer) { + viewer->setDocument(doc); + } else { + viewer = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc)); + pack_start(*viewer, true, true); + } + + if (document) { + delete document; + } + document = doc; + + show_all(); + + return true; +} + + +bool SVGPreview::setFileName(Glib::ustring &theFileName) +{ + Glib::ustring fileName = theFileName; + + fileName = Glib::filename_to_utf8(fileName); + + /** + * I don't know why passing false to keepalive is bad. But it + * prevents the display of an svg with a non-ascii filename + */ + SPDocument *doc = SPDocument::createNewDoc(fileName.c_str(), true); + if (!doc) { + g_warning("SVGView: error loading document '%s'\n", fileName.c_str()); + return false; + } + + setDocument(doc); + + return true; +} + + + +bool SVGPreview::setFromMem(char const *xmlBuffer) +{ + if (!xmlBuffer) + return false; + + gint len = (gint)strlen(xmlBuffer); + SPDocument *doc = SPDocument::createNewDocFromMem(xmlBuffer, len, false); + if (!doc) { + g_warning("SVGView: error loading buffer '%s'\n", xmlBuffer); + return false; + } + + setDocument(doc); + + return true; +} + + + +void SVGPreview::showImage(Glib::ustring &theFileName) +{ + Glib::ustring fileName = theFileName; + + // Let's get real width and height from SVG file. These are template + // files so we assume they are well formed. + + // std::cout << "SVGPreview::showImage: " << theFileName << std::endl; + std::string width; + std::string height; + + /*##################################### + # LET'S HAVE SOME FUN WITH SVG! + # Instead of just loading an image, why + # don't we make a lovely little svg and + # display it nicely? + #####################################*/ + + // Arbitrary size of svg doc -- rather 'portrait' shaped + gint previewWidth = 400; + gint previewHeight = 600; + + // Get some image info. Smart pointer does not need to be deleted + Glib::RefPtr<Gdk::Pixbuf> img(nullptr); + try + { + img = Gdk::Pixbuf::create_from_file(fileName); + } + catch (const Glib::FileError &e) + { + g_message("caught Glib::FileError in SVGPreview::showImage"); + return; + } + catch (const Gdk::PixbufError &e) + { + g_message("Gdk::PixbufError in SVGPreview::showImage"); + return; + } + catch (...) + { + g_message("Caught ... in SVGPreview::showImage"); + return; + } + + gint imgWidth = img->get_width(); + gint imgHeight = img->get_height(); + + Glib::ustring svg = ".svg"; + if (hasSuffix(fileName, svg)) { + std::ifstream input(theFileName.c_str()); + if( !input ) { + std::cerr << "SVGPreview::showImage: Failed to open file: " << theFileName << std::endl; + } else { + + Glib::ustring token; + + Glib::MatchInfo match_info; + Glib::RefPtr<Glib::Regex> regex1 = Glib::Regex::create("width=\"(.*)\""); + Glib::RefPtr<Glib::Regex> regex2 = Glib::Regex::create("height=\"(.*)\""); + + while( !input.eof() && (height.empty() || width.empty()) ) { + + input >> token; + // std::cout << "|" << token << "|" << std::endl; + + if (regex1->match(token, match_info)) { + width = match_info.fetch(1).raw(); + } + + if (regex2->match(token, match_info)) { + height = match_info.fetch(1).raw(); + } + + } + } + } + + // TODO: replace int to string conversion with std::to_string when fully C++11 compliant + if (height.empty() || width.empty()) { + std::ostringstream s_width; + std::ostringstream s_height; + s_width << imgWidth; + s_height << imgHeight; + width = s_width.str(); + height = s_height.str(); + } + + // Find the minimum scale to fit the image inside the preview area + double scaleFactorX = (0.9 * (double)previewWidth) / ((double)imgWidth); + double scaleFactorY = (0.9 * (double)previewHeight) / ((double)imgHeight); + double scaleFactor = scaleFactorX; + if (scaleFactorX > scaleFactorY) + scaleFactor = scaleFactorY; + + // Now get the resized values + gint scaledImgWidth = (int)(scaleFactor * (double)imgWidth); + gint scaledImgHeight = (int)(scaleFactor * (double)imgHeight); + + // center the image on the area + gint imgX = (previewWidth - scaledImgWidth) / 2; + gint imgY = (previewHeight - scaledImgHeight) / 2; + + // wrap a rectangle around the image + gint rectX = imgX - 1; + gint rectY = imgY - 1; + gint rectWidth = scaledImgWidth + 2; + gint rectHeight = scaledImgHeight + 2; + + // Our template. Modify to taste + gchar const *xformat = R"A( +<svg width="%d" height="%d" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <rect width="100%" height="100%" style="fill:#eeeeee"/> + <image x="%d" y="%d" width="%d" height="%d" xlink:href="%s"/> + <rect x="%d" y="%d" width="%d" height="%d" style="fill:none;stroke:black"/> + <text x="50%" y="55%" style="font-family:sans-serif;font-size:24px;text-anchor:middle">%s x %s</text> +</svg> +)A"; + + // if (!Glib::get_charset()) //If we are not utf8 + fileName = Glib::filename_to_utf8(fileName); + // Filenames in xlinks are decoded, so any % will break without this. + auto encodedName = Glib::uri_escape_string(fileName); + + // Fill in the template + /* FIXME: Do proper XML quoting for fileName. */ + gchar *xmlBuffer = + g_strdup_printf(xformat, previewWidth, previewHeight, imgX, imgY, scaledImgWidth, scaledImgHeight, + encodedName.c_str(), rectX, rectY, rectWidth, rectHeight, width.c_str(), height.c_str() ); + + // g_message("%s\n", xmlBuffer); + + // now show it! + setFromMem(xmlBuffer); + g_free(xmlBuffer); +} + + + +void SVGPreview::showNoPreview() +{ + // Are we already showing it? + if (showingNoPreview) + return; + + // Our template. Modify to taste + gchar const *xformat = R"B( +<svg width="400" height="600" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <g transform="translate(-160,90)" style="opacity:0.10"> + <path style="fill:white" + d="M 397.64309 320.25301 L 280.39197 282.517 L 250.74227 124.83447 L 345.08225 + 29.146783 L 393.59996 46.667064 L 483.89679 135.61619 L 397.64309 320.25301 z"/> + <path d="M 476.95792 339.17168 C 495.78197 342.93607 499.54842 356.11361 495.78197 359.87802 + C 492.01856 363.6434 482.6065 367.40781 475.07663 361.76014 C 467.54478 + 356.11361 467.54478 342.93607 476.95792 339.17168 z" + id="droplet01" /> + <path d="M 286.46194 340.42914 C 284.6277 340.91835 269.30405 327.71337 257.16909 333.8338 + C 245.03722 339.95336 236.89276 353.65666 248.22676 359.27982 C 259.56184 364.90298 + 267.66433 358.41867 277.60113 351.44119 C 287.53903 344.46477 + 287.18046 343.1206 286.46194 340.42914 z" + id= "droplet02"/> + <path d="M 510.35756 306.92856 C 520.59494 304.36879 544.24333 306.92856 540.47688 321.98634 + C 536.71354 337.04806 504.71297 331.39827 484.00371 323.87156 C 482.12141 + 308.81083 505.53237 308.13423 510.35756 306.92856 z" + id="droplet03"/> + <path d="M 359.2403 21.362537 C 347.92693 21.362537 336.6347 25.683095 327.96556 34.35223 + L 173.87387 188.41466 C 165.37697 196.9114 161.1116 207.95813 160.94269 219.04577 + L 160.88418 219.04577 C 160.88418 219.08524 160.94076 219.12322 160.94269 219.16279 + C 160.94033 219.34888 160.88418 219.53256 160.88418 219.71865 L 161.14748 219.71865 + C 164.0966 230.93917 240.29699 245.24198 248.79866 253.74346 C 261.63771 266.58263 + 199.5652 276.01151 212.4041 288.85074 C 225.24316 301.68979 289.99433 313.6933 302.8346 + 326.53254 C 315.67368 339.37161 276.5961 353.04289 289.43532 365.88196 C 302.27439 + 378.72118 345.40201 362.67257 337.5908 396.16198 C 354.92909 413.50026 391.10302 + 405.2208 415.32417 387.88252 C 428.16323 375.04345 390.6948 376.17577 403.53397 + 363.33668 C 416.37304 350.49745 448.78128 350.4282 476.08902 319.71589 C 465.09739 + 302.62116 429.10801 295.34136 441.94719 282.50217 C 454.78625 269.66311 479.74708 + 276.18423 533.60644 251.72479 C 559.89837 239.78398 557.72636 230.71459 557.62567 + 219.71865 C 557.62356 219.48727 557.62567 219.27892 557.62567 219.04577 L 557.56716 + 219.04577 C 557.3983 207.95812 553.10345 196.9114 544.60673 188.41466 L 390.54428 + 34.35223 C 381.87515 25.683095 370.55366 21.362537 359.2403 21.362537 z M 357.92378 + 41.402939 C 362.95327 41.533963 367.01541 45.368018 374.98006 50.530832 L 447.76915 + 104.50827 C 448.56596 105.02498 449.32484 105.564 450.02187 106.11735 C 450.7189 106.67062 + 451.3556 107.25745 451.95277 107.84347 C 452.54997 108.42842 453.09281 109.01553 453.59111 + 109.62808 C 454.08837 110.24052 454.53956 110.86661 454.93688 111.50048 C 455.33532 112.13538 + 455.69164 112.78029 455.9901 113.43137 C 456.28877 114.08363 456.52291 114.75639 456.7215 + 115.42078 C 456.92126 116.08419 457.08982 116.73973 457.18961 117.41019 C 457.28949 + 118.08184 457.33588 118.75535 457.33588 119.42886 L 414.21245 98.598549 L 409.9118 + 131.16055 L 386.18512 120.04324 L 349.55654 144.50131 L 335.54288 96.1703 L 317.4919 + 138.4453 L 267.08369 143.47735 L 267.63956 121.03795 C 267.63956 115.64823 296.69685 + 77.915899 314.39075 68.932902 L 346.77721 45.674327 C 351.55594 42.576634 354.90608 + 41.324327 357.92378 41.402939 z M 290.92738 261.61333 C 313.87149 267.56365 339.40299 + 275.37038 359.88393 275.50997 L 360.76161 284.72563 C 343.2235 282.91785 306.11346 + 274.45012 297.36372 269.98057 L 290.92738 261.61333 z" + id="mountainDroplet"/> + </g> + <text xml:space="preserve" x="200" y="320" + style="font-size:32px;font-weight:bold;text-anchor:middle">%s</text> +</svg> +)B"; + + // Fill in the template + gchar *xmlBuffer = g_strdup_printf(xformat, _("No preview")); + + // g_message("%s\n", xmlBuffer); + + // Now show it! + setFromMem(xmlBuffer); + g_free(xmlBuffer); + showingNoPreview = true; +} + + +/** + * Inform the user that the svg file is too large to be displayed. + * This does not check for sizes of embedded images (yet) + */ +void SVGPreview::showTooLarge(long fileLength) +{ + // Our template. Modify to taste + gchar const *xformat = R"C( +<svg width="400" height="600" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <g transform="translate(-160,90)" style="opacity:0.10"> + <path style="fill:white" + d="M 397.64309 320.25301 L 280.39197 282.517 L 250.74227 124.83447 L 345.08225 + 29.146783 L 393.59996 46.667064 L 483.89679 135.61619 L 397.64309 320.25301 z"/> + <path d="M 476.95792 339.17168 C 495.78197 342.93607 499.54842 356.11361 495.78197 359.87802 + C 492.01856 363.6434 482.6065 367.40781 475.07663 361.76014 C 467.54478 + 356.11361 467.54478 342.93607 476.95792 339.17168 z" + id="droplet01" /> + <path d="M 286.46194 340.42914 C 284.6277 340.91835 269.30405 327.71337 257.16909 333.8338 + C 245.03722 339.95336 236.89276 353.65666 248.22676 359.27982 C 259.56184 364.90298 + 267.66433 358.41867 277.60113 351.44119 C 287.53903 344.46477 + 287.18046 343.1206 286.46194 340.42914 z" + id= "droplet02"/> + <path d="M 510.35756 306.92856 C 520.59494 304.36879 544.24333 306.92856 540.47688 321.98634 + C 536.71354 337.04806 504.71297 331.39827 484.00371 323.87156 C 482.12141 + 308.81083 505.53237 308.13423 510.35756 306.92856 z" + id="droplet03"/> + <path d="M 359.2403 21.362537 C 347.92693 21.362537 336.6347 25.683095 327.96556 34.35223 + L 173.87387 188.41466 C 165.37697 196.9114 161.1116 207.95813 160.94269 219.04577 + L 160.88418 219.04577 C 160.88418 219.08524 160.94076 219.12322 160.94269 219.16279 + C 160.94033 219.34888 160.88418 219.53256 160.88418 219.71865 L 161.14748 219.71865 + C 164.0966 230.93917 240.29699 245.24198 248.79866 253.74346 C 261.63771 266.58263 + 199.5652 276.01151 212.4041 288.85074 C 225.24316 301.68979 289.99433 313.6933 302.8346 + 326.53254 C 315.67368 339.37161 276.5961 353.04289 289.43532 365.88196 C 302.27439 + 378.72118 345.40201 362.67257 337.5908 396.16198 C 354.92909 413.50026 391.10302 + 405.2208 415.32417 387.88252 C 428.16323 375.04345 390.6948 376.17577 403.53397 + 363.33668 C 416.37304 350.49745 448.78128 350.4282 476.08902 319.71589 C 465.09739 + 302.62116 429.10801 295.34136 441.94719 282.50217 C 454.78625 269.66311 479.74708 + 276.18423 533.60644 251.72479 C 559.89837 239.78398 557.72636 230.71459 557.62567 + 219.71865 C 557.62356 219.48727 557.62567 219.27892 557.62567 219.04577 L 557.56716 + 219.04577 C 557.3983 207.95812 553.10345 196.9114 544.60673 188.41466 L 390.54428 + 34.35223 C 381.87515 25.683095 370.55366 21.362537 359.2403 21.362537 z M 357.92378 + 41.402939 C 362.95327 41.533963 367.01541 45.368018 374.98006 50.530832 L 447.76915 + 104.50827 C 448.56596 105.02498 449.32484 105.564 450.02187 106.11735 C 450.7189 106.67062 + 451.3556 107.25745 451.95277 107.84347 C 452.54997 108.42842 453.09281 109.01553 453.59111 + 109.62808 C 454.08837 110.24052 454.53956 110.86661 454.93688 111.50048 C 455.33532 112.13538 + 455.69164 112.78029 455.9901 113.43137 C 456.28877 114.08363 456.52291 114.75639 456.7215 + 115.42078 C 456.92126 116.08419 457.08982 116.73973 457.18961 117.41019 C 457.28949 + 118.08184 457.33588 118.75535 457.33588 119.42886 L 414.21245 98.598549 L 409.9118 + 131.16055 L 386.18512 120.04324 L 349.55654 144.50131 L 335.54288 96.1703 L 317.4919 + 138.4453 L 267.08369 143.47735 L 267.63956 121.03795 C 267.63956 115.64823 296.69685 + 77.915899 314.39075 68.932902 L 346.77721 45.674327 C 351.55594 42.576634 354.90608 + 41.324327 357.92378 41.402939 z M 290.92738 261.61333 C 313.87149 267.56365 339.40299 + 275.37038 359.88393 275.50997 L 360.76161 284.72563 C 343.2235 282.91785 306.11346 + 274.45012 297.36372 269.98057 L 290.92738 261.61333 z" + id="mountainDroplet"/> + </g> + <text xml:space="preserve" x="200" y="280" + style="font-size:20px;font-weight:bold;text-anchor:middle">%.1f MB</text> + <text xml:space="preserve" x="200" y="360" + style="font-size:20px;font-weight:bold;text-anchor:middle">%s</text> +</svg> +)C"; + + + // Fill in the template + double floatFileLength = ((double)fileLength) / 1048576.0; + // printf("%ld %f\n", fileLength, floatFileLength); + + gchar *xmlBuffer = + g_strdup_printf(xformat, floatFileLength, _("Too large for preview")); + + // g_message("%s\n", xmlBuffer); + + // now show it! + setFromMem(xmlBuffer); + g_free(xmlBuffer); +} + +bool SVGPreview::set(Glib::ustring &fileName, int dialogType) +{ + + if (!Glib::file_test(fileName, Glib::FILE_TEST_EXISTS)) { + showNoPreview(); + return false; + } + + if (Glib::file_test(fileName, Glib::FILE_TEST_IS_DIR)) { + showNoPreview(); + return false; + } + + if (Glib::file_test(fileName, Glib::FILE_TEST_IS_REGULAR)) { + Glib::ustring fileNameUtf8 = Glib::filename_to_utf8(fileName); + gchar *fName = const_cast<gchar *>( + fileNameUtf8.c_str()); // const-cast probably not necessary? (not necessary on Windows version of stat()) + GStatBuf info; + if (g_stat(fName, &info)) // stat returns 0 upon success + { + g_warning("SVGPreview::set() : %s : %s", fName, strerror(errno)); + return false; + } + if (info.st_size > 0xA00000L) { + showingNoPreview = false; + showTooLarge(info.st_size); + return false; + } + } + + Glib::ustring svg = ".svg"; + Glib::ustring svgz = ".svgz"; + + if ((dialogType == SVG_TYPES || dialogType == IMPORT_TYPES) && + (hasSuffix(fileName, svg) || hasSuffix(fileName, svgz))) { + bool retval = setFileName(fileName); + showingNoPreview = false; + return retval; + } else if (isValidImageFile(fileName)) { + showImage(fileName); + showingNoPreview = false; + return true; + } else { + showNoPreview(); + return false; + } +} + + +SVGPreview::SVGPreview() + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) + , document(nullptr) + , viewer(nullptr) + , showingNoPreview(false) +{ + set_size_request(200, 300); +} + +SVGPreview::~SVGPreview() +{ + if (viewer) { + viewer->setDocument(nullptr); + } + delete document; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/ui/dialog/svg-preview.h b/src/ui/dialog/svg-preview.h new file mode 100644 index 0000000..55b9291 --- /dev/null +++ b/src/ui/dialog/svg-preview.h @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Implementation of the file dialog interfaces defined in filedialogimpl.h + */ +/* Authors: + * Bob Jamison + * Johan Engelen <johan@shouraizou.nl> + * Joel Holdsworth + * Bruno Dilly + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004-2008 Authors + * Copyright (C) 2004-2007 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __SVG_PREVIEW_H__ +#define __SVG_PREVIEW_H__ + +//Gtk includes +#include <gtkmm.h> + +//General includes +#include <unistd.h> +#include <sys/stat.h> +#include <cerrno> + +#include "filedialog.h" + + +namespace Gtk { +class Expander; +} + +namespace Inkscape { + class URI; + +namespace UI { + +namespace View { + class SVGViewWidget; +} + +namespace Dialog { + +/*######################################################################### +### SVG Preview Widget +#########################################################################*/ + +/** + * Simple class for displaying an SVG file in the "preview widget." + * Currently, this is just a wrapper of the sp_svg_view Gtk widget. + * Hopefully we will eventually replace with a pure Gtkmm widget. + */ +class SVGPreview : public Gtk::Box +{ +public: + + SVGPreview(); + + ~SVGPreview() override; + + bool setDocument(SPDocument *doc); + + bool setFileName(Glib::ustring &fileName); + + bool setFromMem(char const *xmlBuffer); + + bool set(Glib::ustring &fileName, int dialogType); + + bool setURI(URI &uri); + + /** + * Show image embedded in SVG + */ + void showImage(Glib::ustring &fileName); + + /** + * Show the "No preview" image + */ + void showNoPreview(); + + /** + * Show the "Too large" image + */ + void showTooLarge(long fileLength); + +private: + /** + * The svg document we are currently showing + */ + SPDocument *document; + + /** + * The sp_svg_view widget + */ + Inkscape::UI::View::SVGViewWidget *viewer; + + /** + * are we currently showing the "no preview" image? + */ + bool showingNoPreview; + +}; + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif /*__SVG_PREVIEW_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/ui/dialog/swatches.cpp b/src/ui/dialog/swatches.cpp new file mode 100644 index 0000000..65c4f4c --- /dev/null +++ b/src/ui/dialog/swatches.cpp @@ -0,0 +1,1113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Jon A. Cruz + * John Bintz + * Abhishek Sharma + * + * Copyright (C) 2005 Jon A. Cruz + * Copyright (C) 2008 John Bintz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "swatches.h" + +#include <map> +#include <algorithm> +#include <iomanip> +#include <set> + +#include <gtkmm/checkmenuitem.h> +#include <gtkmm/menu.h> +#include <gtkmm/menubutton.h> +#include <gtkmm/radiomenuitem.h> +#include <gtkmm/radiomenuitem.h> +#include <gtkmm/separatormenuitem.h> + +#include <glibmm/i18n.h> +#include <glibmm/main.h> +#include <glibmm/timer.h> +#include <glibmm/fileutils.h> +#include <glibmm/miscutils.h> + +#include "color-item.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "gradient-chemistry.h" +#include "inkscape.h" +#include "message-context.h" +#include "path-prefix.h" + +#include "actions/actions-tools.h" // Invoke gradient tool +#include "display/cairo-utils.h" +#include "extension/db.h" +#include "io/resource.h" +#include "io/sys.h" +#include "object/sp-defs.h" +#include "object/sp-gradient-reference.h" +#include "ui/dialog/dialog-container.h" +#include "ui/icon-names.h" +#include "ui/previewholder.h" +#include "ui/widget/color-palette.h" +#include "ui/widget/gradient-vector-selector.h" +#include "widgets/desktop-widget.h" +#include "widgets/ege-paint-def.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +enum { + SWATCHES_SETTINGS_SIZE = 0, + SWATCHES_SETTINGS_MODE = 1, + SWATCHES_SETTINGS_SHAPE = 2, + SWATCHES_SETTINGS_WRAP = 3, + SWATCHES_SETTINGS_BORDER = 4, + SWATCHES_SETTINGS_PALETTE = 5 +}; + +#define VBLOCK 16 +#define PREVIEW_PIXBUF_WIDTH 128 + +std::list<SwatchPage*> userSwatchPages; +std::list<SwatchPage*> systemSwatchPages; +static std::map<SPDocument*, SwatchPage*> docPalettes; +static std::vector<DocTrack*> docTrackings; +static std::map<SwatchesPanel*, SPDocument*> docPerPanel; + + +class SwatchesPanelHook : public SwatchesPanel +{ +public: + static void convertGradient( GtkMenuItem *menuitem, gpointer userData ); + static void deleteGradient( GtkMenuItem *menuitem, gpointer userData ); +}; + +static void handleClick( GtkWidget* /*widget*/, gpointer callback_data ) { + ColorItem* item = reinterpret_cast<ColorItem*>(callback_data); + if ( item ) { + item->buttonClicked(false); + } +} + +static void handleSecondaryClick( GtkWidget* /*widget*/, gint /*arg1*/, gpointer callback_data ) { + ColorItem* item = reinterpret_cast<ColorItem*>(callback_data); + if ( item ) { + item->buttonClicked(true); + } +} + +static GtkWidget* popupMenu = nullptr; +static GtkWidget *popupSubHolder = nullptr; +static GtkWidget *popupSub = nullptr; +static std::vector<Glib::ustring> popupItems; +static std::vector<GtkWidget*> popupExtras; +static ColorItem* bounceTarget = nullptr; +static SwatchesPanel* bouncePanel = nullptr; + +static void redirClick( GtkMenuItem *menuitem, gpointer /*user_data*/ ) +{ + if ( bounceTarget ) { + handleClick( GTK_WIDGET(menuitem), bounceTarget ); + } +} + +static void redirSecondaryClick( GtkMenuItem *menuitem, gpointer /*user_data*/ ) +{ + if ( bounceTarget ) { + handleSecondaryClick( GTK_WIDGET(menuitem), 0, bounceTarget ); + } +} + +static void editGradientImpl( SPDesktop* desktop, SPGradient* gr ) +{ + g_assert(desktop != nullptr); + g_assert(desktop->doc() != nullptr); + + if ( gr ) { + bool shown = false; + { + Inkscape::Selection *selection = desktop->getSelection(); + std::vector<SPItem*> const items(selection->items().begin(), selection->items().end()); + if (!items.empty()) { + SPStyle query( desktop->doc() ); + int result = objects_query_fillstroke((items), &query, true); + if ( (result == QUERY_STYLE_MULTIPLE_SAME) || (result == QUERY_STYLE_SINGLE) ) { + // could be pertinent + if (query.fill.isPaintserver()) { + SPPaintServer* server = query.getFillPaintServer(); + if ( SP_IS_GRADIENT(server) ) { + SPGradient* grad = SP_GRADIENT(server); + if ( grad->isSwatch() && grad->getId() == gr->getId()) { + desktop->getContainer()->new_dialog("FillStroke"); + shown = true; + } + } + } + } + } + } + + if (!shown) { // WHEN DOES THIS HAPPEN? + // Invoke the gradient tool + set_active_tool(desktop, "Gradient"); + } + } +} + +static void editGradient( GtkMenuItem */*menuitem*/, gpointer /*user_data*/ ) +{ + if ( bounceTarget ) { + SwatchesPanel* swp = bouncePanel; + SPDesktop* desktop = swp ? swp->getDesktop() : nullptr; + SPDocument *doc = desktop ? desktop->doc() : nullptr; + if (doc) { + std::string targetName(bounceTarget->def.descr); + std::vector<SPObject *> gradients = doc->getResourceList("gradient"); + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + if ( targetName == grad->getId() ) { + editGradientImpl( desktop, grad ); + break; + } + } + } + } +} + +void SwatchesPanelHook::convertGradient( GtkMenuItem * /*menuitem*/, gpointer userData ) +{ + if ( bounceTarget ) { + SwatchesPanel* swp = bouncePanel; + SPDesktop* desktop = swp ? swp->getDesktop() : nullptr; + SPDocument *doc = desktop ? desktop->doc() : nullptr; + gint index = GPOINTER_TO_INT(userData); + if ( doc && (index >= 0) && (static_cast<guint>(index) < popupItems.size()) ) { + Glib::ustring targetName = popupItems[index]; + std::vector<SPObject *> gradients = doc->getResourceList("gradient"); + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + + if ( targetName == grad->getId() ) { + grad->setSwatch(); + DocumentUndo::done(doc, _("Add gradient stop"), INKSCAPE_ICON("color-gradient")); + break; + } + } + } + } +} + +void SwatchesPanelHook::deleteGradient( GtkMenuItem */*menuitem*/, gpointer /*userData*/ ) +{ + if ( bounceTarget ) { + SwatchesPanel* swp = bouncePanel; + SPDesktop* desktop = swp ? swp->getDesktop() : nullptr; + sp_gradient_unset_swatch(desktop, bounceTarget->def.descr); + } +} + +static SwatchesPanel* findContainingPanel( GtkWidget *widget ) +{ + SwatchesPanel *swp = nullptr; + + std::map<GtkWidget*, SwatchesPanel*> rawObjects; + for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); it != docPerPanel.end(); ++it) { + rawObjects[GTK_WIDGET(it->first->gobj())] = it->first; + } + + for (GtkWidget* curr = widget; curr && !swp; curr = gtk_widget_get_parent(curr)) { + if (rawObjects.find(curr) != rawObjects.end()) { + swp = rawObjects[curr]; + } + } + + return swp; +} + +static void removeit( GtkWidget *widget, gpointer data ) +{ + gtk_container_remove( GTK_CONTAINER(data), widget ); +} + +/* extern'ed from color-item.cpp */ +bool colorItemHandleButtonPress(GdkEventButton* event, UI::Widget::Preview *preview, gpointer user_data) +{ + gboolean handled = FALSE; + + if ( event && (event->button == 3) && (event->type == GDK_BUTTON_PRESS) ) { + SwatchesPanel* swp = findContainingPanel( GTK_WIDGET(preview->gobj()) ); + + if ( !popupMenu ) { + popupMenu = gtk_menu_new(); + GtkWidget* child = nullptr; + + //TRANSLATORS: An item in context menu on a colour in the swatches + child = gtk_menu_item_new_with_label(_("Set fill")); + g_signal_connect( G_OBJECT(child), + "activate", + G_CALLBACK(redirClick), + user_data); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + + //TRANSLATORS: An item in context menu on a colour in the swatches + child = gtk_menu_item_new_with_label(_("Set stroke")); + + g_signal_connect( G_OBJECT(child), + "activate", + G_CALLBACK(redirSecondaryClick), + user_data); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + + child = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + popupExtras.push_back(child); + + child = gtk_menu_item_new_with_label(_("Delete")); + g_signal_connect( G_OBJECT(child), + "activate", + G_CALLBACK(SwatchesPanelHook::deleteGradient), + user_data ); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + popupExtras.push_back(child); + gtk_widget_set_sensitive( child, FALSE ); + + child = gtk_menu_item_new_with_label(_("Edit...")); + g_signal_connect( G_OBJECT(child), + "activate", + G_CALLBACK(editGradient), + user_data ); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + popupExtras.push_back(child); + + child = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + popupExtras.push_back(child); + + child = gtk_menu_item_new_with_label(_("Convert")); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + //popupExtras.push_back(child); + //gtk_widget_set_sensitive( child, FALSE ); + { + popupSubHolder = child; + popupSub = gtk_menu_new(); + gtk_menu_item_set_submenu( GTK_MENU_ITEM(child), popupSub ); + } + + gtk_widget_show_all(popupMenu); + } + + if ( user_data ) { + ColorItem* item = reinterpret_cast<ColorItem*>(user_data); + bool show = swp && (swp->getSelectedIndex() == 0); + for (auto & popupExtra : popupExtras) { + gtk_widget_set_sensitive(popupExtra, show); + } + + bounceTarget = item; + bouncePanel = swp; + popupItems.clear(); + if ( popupMenu ) { + gtk_container_foreach(GTK_CONTAINER(popupSub), removeit, popupSub); + bool processed = false; + auto *wdgt = preview->get_ancestor(SPDesktopWidget::get_type()); + if ( wdgt ) { + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(wdgt); + if ( dtw && dtw->desktop ) { + // Pick up all gradients with vectors + std::vector<SPObject *> gradients = (dtw->desktop->doc())->getResourceList("gradient"); + gint index = 0; + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + if ( grad->hasStops() && !grad->isSwatch() ) { + //gl = g_slist_prepend(gl, curr->data); + processed = true; + GtkWidget *child = gtk_menu_item_new_with_label(grad->getId()); + gtk_menu_shell_append(GTK_MENU_SHELL(popupSub), child); + + popupItems.emplace_back(grad->getId()); + g_signal_connect( G_OBJECT(child), + "activate", + G_CALLBACK(SwatchesPanelHook::convertGradient), + GINT_TO_POINTER(index) ); + index++; + } + } + + gtk_widget_show_all(popupSub); + } + } + gtk_widget_set_sensitive( popupSubHolder, processed ); + gtk_menu_popup_at_pointer(GTK_MENU(popupMenu), reinterpret_cast<GdkEvent *>(event)); + handled = TRUE; + } + } + } + + return handled; +} + + +static char* trim( char* str ) { + char* ret = str; + while ( *str && (*str == ' ' || *str == '\t') ) { + str++; + } + ret = str; + while ( *str ) { + str++; + } + str--; + while ( str >= ret && (( *str == ' ' || *str == '\t' ) || *str == '\r' || *str == '\n') ) { + *str-- = 0; + } + return ret; +} + +static void skipWhitespace( char*& str ) { + while ( *str == ' ' || *str == '\t' ) { + str++; + } +} + +static bool parseNum( char*& str, int& val ) { + val = 0; + while ( '0' <= *str && *str <= '9' ) { + val = val * 10 + (*str - '0'); + str++; + } + bool retval = !(*str == 0 || *str == ' ' || *str == '\t' || *str == '\r' || *str == '\n'); + return retval; +} + + +static +void _loadPaletteFile(Glib::ustring path, gboolean user/*=FALSE*/) +{ + auto filename = Glib::path_get_basename(path.raw()); + char block[1024]; + FILE *f = Inkscape::IO::fopen_utf8name(path.c_str(), "r"); + if ( f ) { + char* result = fgets( block, sizeof(block), f ); + if ( result ) { + if ( strncmp( "GIMP Palette", block, 12 ) == 0 ) { + bool inHeader = true; + bool hasErr = false; + + SwatchPage *onceMore = new SwatchPage(); + onceMore->_name = filename.c_str(); + + do { + result = fgets( block, sizeof(block), f ); + block[sizeof(block) - 1] = 0; + if ( result ) { + if ( block[0] == '#' ) { + // ignore comment + } else { + char *ptr = block; + // very simple check for header versus entry + while ( *ptr == ' ' || *ptr == '\t' ) { + ptr++; + } + if ( (*ptr == 0) || (*ptr == '\r') || (*ptr == '\n') ) { + // blank line. skip it. + } else if ( '0' <= *ptr && *ptr <= '9' ) { + // should be an entry link + inHeader = false; + ptr = block; + Glib::ustring name(""); + skipWhitespace(ptr); + if ( *ptr ) { + int r = 0; + int g = 0; + int b = 0; + hasErr = parseNum(ptr, r); + if ( !hasErr ) { + skipWhitespace(ptr); + hasErr = parseNum(ptr, g); + } + if ( !hasErr ) { + skipWhitespace(ptr); + hasErr = parseNum(ptr, b); + } + if ( !hasErr && *ptr ) { + char* n = trim(ptr); + if (n != nullptr && *n) { + name = g_dpgettext2(nullptr, "Palette", n); + } + if (name == "") { + name = Glib::ustring::compose("#%1%2%3", + Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), r), + Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), g), + Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), b) + ).uppercase(); + } + } + if ( !hasErr ) { + // Add the entry now + Glib::ustring nameStr(name); + ColorItem* item = new ColorItem( r, g, b, nameStr ); + onceMore->_colors.push_back(item); + } + } else { + hasErr = true; + } + } else { + if ( !inHeader ) { + // Hmmm... probably bad. Not quite the format we want? + hasErr = true; + } else { + char* sep = strchr(result, ':'); + if ( sep ) { + *sep = 0; + char* val = trim(sep + 1); + char* name = trim(result); + if ( *name ) { + if ( strcmp( "Name", name ) == 0 ) + { + onceMore->_name = val; + } + else if ( strcmp( "Columns", name ) == 0 ) + { + gchar* endPtr = nullptr; + guint64 numVal = g_ascii_strtoull( val, &endPtr, 10 ); + if ( (numVal == G_MAXUINT64) && (ERANGE == errno) ) { + // overflow + } else if ( (numVal == 0) && (endPtr == val) ) { + // failed conversion + } else { + onceMore->_prefWidth = numVal; + } + } + } else { + // error + hasErr = true; + } + } else { + // error + hasErr = true; + } + } + } + } + } + } while ( result && !hasErr ); + if ( !hasErr ) { + if (user) + userSwatchPages.push_back(onceMore); + else + systemSwatchPages.push_back(onceMore); + } else { + delete onceMore; + } + } + } + + fclose(f); + } +} + +static bool +compare_swatch_names(SwatchPage const *a, SwatchPage const *b) { + + return g_utf8_collate(a->_name.c_str(), b->_name.c_str()) < 0; +} + +static void load_palettes() +{ + static bool init_done = false; + + if (init_done) { + return; + } + init_done = true; + + for (auto &filename: Inkscape::IO::Resource::get_filenames(Inkscape::IO::Resource::PALETTES, {".gpl"})) { + bool userPalette = Inkscape::IO::file_is_writable(filename.c_str()); + _loadPaletteFile(filename, userPalette); + } + + // Sort the list of swatches by name, grouped by user/system + userSwatchPages.sort(compare_swatch_names); + systemSwatchPages.sort(compare_swatch_names); +} + +SwatchesPanel& SwatchesPanel::getInstance() +{ + return *new SwatchesPanel(); +} + + +class DocTrack +{ +public: + DocTrack(SPDocument *doc, sigc::connection &gradientRsrcChanged, sigc::connection &defsChanged, sigc::connection &defsModified) : + doc(doc->doRef()), + updatePending(false), + lastGradientUpdate(0.0), + gradientRsrcChanged(gradientRsrcChanged), + defsChanged(defsChanged), + defsModified(defsModified) + { + if ( !timer ) { + timer = new Glib::Timer(); + refreshTimer = Glib::signal_timeout().connect( sigc::ptr_fun(handleTimerCB), 33 ); + } + timerRefCount++; + } + + ~DocTrack() + { + timerRefCount--; + if ( timerRefCount <= 0 ) { + refreshTimer.disconnect(); + timerRefCount = 0; + if ( timer ) { + timer->stop(); + delete timer; + timer = nullptr; + } + } + if (doc) { + gradientRsrcChanged.disconnect(); + defsChanged.disconnect(); + defsModified.disconnect(); + } + } + + static bool handleTimerCB(); + + /** + * Checks if update should be queued or executed immediately. + * + * @return true if the update was queued and should not be immediately executed. + */ + static bool queueUpdateIfNeeded(SPDocument *doc); + + static Glib::Timer *timer; + static int timerRefCount; + static sigc::connection refreshTimer; + + std::unique_ptr<SPDocument> doc; + bool updatePending; + double lastGradientUpdate; + sigc::connection gradientRsrcChanged; + sigc::connection defsChanged; + sigc::connection defsModified; + +private: + DocTrack(DocTrack const &) = delete; // no copy + DocTrack &operator=(DocTrack const &) = delete; // no assign +}; + +Glib::Timer *DocTrack::timer = nullptr; +int DocTrack::timerRefCount = 0; +sigc::connection DocTrack::refreshTimer; + +static const double DOC_UPDATE_THREASHOLD = 0.090; + +bool DocTrack::handleTimerCB() +{ + double now = timer->elapsed(); + + std::vector<DocTrack *> needCallback; + for (auto track : docTrackings) { + if ( track->updatePending && ( (now - track->lastGradientUpdate) >= DOC_UPDATE_THREASHOLD) ) { + needCallback.push_back(track); + } + } + + for (auto track : needCallback) { + if ( std::find(docTrackings.begin(), docTrackings.end(), track) != docTrackings.end() ) { // Just in case one gets deleted while we are looping + // Note: calling handleDefsModified will call queueUpdateIfNeeded and thus update the time and flag. + SwatchesPanel::handleDefsModified(track->doc.get()); + } + } + + return true; +} + +bool DocTrack::queueUpdateIfNeeded( SPDocument *doc ) +{ + bool deferProcessing = false; + for (auto track : docTrackings) { + if ( track->doc.get() == doc ) { + double now = timer->elapsed(); + double elapsed = now - track->lastGradientUpdate; + + if ( elapsed < DOC_UPDATE_THREASHOLD ) { + deferProcessing = true; + track->updatePending = true; + } else { + track->lastGradientUpdate = now; + track->updatePending = false; + } + + break; + } + } + return deferProcessing; +} + +void SwatchesPanel::_trackDocument( SwatchesPanel *panel, SPDocument *document ) +{ + SPDocument *oldDoc = nullptr; + if (docPerPanel.find(panel) != docPerPanel.end()) { + oldDoc = docPerPanel[panel]; + if (!oldDoc) { + docPerPanel.erase(panel); // Should not be needed, but clean up just in case. + } + } + if (oldDoc != document) { + if (oldDoc) { + docPerPanel[panel] = nullptr; + bool found = false; + for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); (it != docPerPanel.end()) && !found; ++it) { + found = (it->second == document); + } + if (!found) { + for (std::vector<DocTrack*>::iterator it = docTrackings.begin(); it != docTrackings.end(); ++it){ + if ((*it)->doc.get() == oldDoc) { + delete *it; + docTrackings.erase(it); + break; + } + } + } + } + + if (document) { + bool found = false; + for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); (it != docPerPanel.end()) && !found; ++it) { + found = (it->second == document); + } + docPerPanel[panel] = document; + if (!found) { + sigc::connection conn1 = document->connectResourcesChanged( "gradient", sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleGradientsChange), document) ); + sigc::connection conn2 = document->getDefs()->connectRelease( sigc::hide(sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleDefsModified), document)) ); + sigc::connection conn3 = document->getDefs()->connectModified( sigc::hide(sigc::hide(sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleDefsModified), document))) ); + + DocTrack *dt = new DocTrack(document, conn1, conn2, conn3); + docTrackings.push_back(dt); + + if (docPalettes.find(document) == docPalettes.end()) { + SwatchPage *docPalette = new SwatchPage(); + docPalette->_name = "Auto"; + docPalettes[document] = docPalette; + } + } + // Always update the palettes if there's a document. + panel->updatePalettes(); + } + } +} + + +/** + * Constructor + */ +SwatchesPanel::SwatchesPanel(gchar const *prefsPath) + : DialogBase(prefsPath, "Swatches") + , _menu(nullptr) + , _holder(nullptr) + , _clear(nullptr) + , _remove(nullptr) + , _currentIndex(0) +{ + _palette = Gtk::manage(new Inkscape::UI::Widget::ColorPalette()); + pack_start(*_palette); + + if (_prefs_path == "/dialogs/swatches") { + _palette->set_compact(false); + } else { + _palette->set_compact(true); + } + + load_palettes(); + + _clear = new ColorItem( ege::PaintDef::CLEAR ); + _remove = new ColorItem( ege::PaintDef::NONE ); + + if (docPalettes.empty()) { + SwatchPage *docPalette = new SwatchPage(); + + docPalette->_name = "Empty"; + docPalettes[nullptr] = docPalette; + } + + if ( !systemSwatchPages.empty() || !userSwatchPages.empty()) { + SwatchPage* first = nullptr; + int index = 0; + Glib::ustring targetName; + if ( !_prefs_path.empty() ) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + targetName = prefs->getString(_prefs_path + "/palette"); + if (!targetName.empty()) { + if (targetName == "Empty") { + first = docPalettes[nullptr]; + } else { + std::vector<SwatchPage*> pages = _getSwatchSets(); + for (auto & page : pages) { + index++; + if ( page->_name == targetName ) { + first = page; + break; + } + } + } + } + } + + if ( !first ) { + first = docPalettes[nullptr]; + _currentIndex = 0; + } else { + _currentIndex = index; + } + + // restore palette settings + Inkscape::Preferences* prefs = Inkscape::Preferences::get(); + _palette->set_tile_size(prefs->getInt(_prefs_path + "/tile_size", 16)); + _palette->set_aspect(prefs->getDoubleLimited(_prefs_path + "/tile_aspect", 0.0, -2, 2)); + _palette->set_tile_border(prefs->getInt(_prefs_path + "/tile_border", 1)); + _palette->set_rows(prefs->getInt(_prefs_path + "/rows", 1)); + _palette->enable_stretch(prefs->getBool(_prefs_path + "/tile_stretch", false)); + // save settings when they change + _palette->get_settings_changed_signal().connect([=](){ + prefs->setInt(_prefs_path + "/tile_size", _palette->get_tile_size()); + prefs->setDouble(_prefs_path + "/tile_aspect", _palette->get_aspect()); + prefs->setInt(_prefs_path + "/tile_border", _palette->get_tile_border()); + prefs->setInt(_prefs_path + "/rows", _palette->get_rows()); + prefs->setBool(_prefs_path + "/tile_stretch", _palette->is_stretch_enabled()); + }); + + // switch swatch palettes + _palette->get_palette_selected_signal().connect([=](Glib::ustring name) { + std::vector<SwatchPage*> pages = _getSwatchSets(); + auto it = std::find_if(pages.begin(), pages.end(), [&](auto el){ return el->_name == name; }); + if (it != pages.end()) { + auto index = static_cast<int>(it - pages.begin()); + if (_currentIndex != index) { + _currentIndex = index; + Inkscape::Preferences* prefs = Inkscape::Preferences::get(); + prefs->setString(_prefs_path + "/palette", pages[_currentIndex]->_name); + _rebuild(); + } + } + }); + } +} + +SwatchesPanel::~SwatchesPanel() +{ + _trackDocument( this, nullptr ); + for (auto & docTracking : docTrackings){ + delete docTracking; + } + docTrackings.clear(); + + docPerPanel.erase(this); + + delete _clear; + delete _remove; +} + +/** + * Process the list of available palettes and update the list + * in the _palette widget. The widget will take care of cleaning. + */ +void SwatchesPanel::updatePalettes() +{ + std::vector<SwatchPage*> swatchSets = _getSwatchSets(); + + std::vector<Inkscape::UI::Widget::ColorPalette::palette_t> palettes; + palettes.reserve(swatchSets.size()); + for (auto curr : swatchSets) { + Inkscape::UI::Widget::ColorPalette::palette_t palette; + palette.name = curr->_name; + for (const auto& color : curr->_colors) { + if (color.def.getType() == ege::PaintDef::RGB) { + auto& c = color.def; + palette.colors.push_back( + Inkscape::UI::Widget::ColorPalette::rgb_t { c.getR() / 255.0, c.getG() / 255.0, c.getB() / 255.0 }); + } + } + palettes.push_back(palette); + } + + // pass list of available palettes + _palette->set_palettes(palettes); + _rebuild(); +} + +void SwatchesPanel::_updateSettings(int settings, int value) +{ +} + +void SwatchesPanel::documentReplaced() +{ + _trackDocument(this, getDocument()); + if (auto document = getDocument()) { + handleGradientsChange(document); + } +} + +static void recalcSwatchContents(SPDocument* doc, + boost::ptr_vector<ColorItem> &tmpColors, + std::map<ColorItem*, cairo_pattern_t*> &previewMappings, + std::map<ColorItem*, SPGradient*> &gradMappings) +{ + std::vector<SPGradient*> newList; + std::vector<SPObject *> gradients = doc->getResourceList("gradient"); + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + if ( grad->isSwatch() ) { + newList.push_back(SP_GRADIENT(gradient)); + } + } + + if ( !newList.empty() ) { + std::reverse(newList.begin(), newList.end()); + for (auto grad : newList) + { + cairo_surface_t *preview = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + PREVIEW_PIXBUF_WIDTH, VBLOCK); + cairo_t *ct = cairo_create(preview); + + Glib::ustring name( grad->getId() ); + ColorItem* item = new ColorItem( 0, 0, 0, name ); + + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_pattern_t *gradient = grad->create_preview_pattern(PREVIEW_PIXBUF_WIDTH); + cairo_set_source(ct, check); + cairo_paint(ct); + cairo_set_source(ct, gradient); + cairo_paint(ct); + + cairo_destroy(ct); + cairo_pattern_destroy(gradient); + cairo_pattern_destroy(check); + + cairo_pattern_t *prevpat = cairo_pattern_create_for_surface(preview); + cairo_surface_destroy(preview); + + previewMappings[item] = prevpat; + + tmpColors.push_back(item); + gradMappings[item] = grad; + } + } +} + +void SwatchesPanel::handleGradientsChange(SPDocument *document) +{ + SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : nullptr; + if (docPalette) { + boost::ptr_vector<ColorItem> tmpColors; + std::map<ColorItem*, cairo_pattern_t*> tmpPrevs; + std::map<ColorItem*, SPGradient*> tmpGrads; + recalcSwatchContents(document, tmpColors, tmpPrevs, tmpGrads); + + for (auto & tmpPrev : tmpPrevs) { + tmpPrev.first->setPattern(tmpPrev.second); + cairo_pattern_destroy(tmpPrev.second); + } + + for (auto & tmpGrad : tmpGrads) { + tmpGrad.first->setGradient(tmpGrad.second); + } + + docPalette->_colors.swap(tmpColors); + + _rebuildDocumentSwatch(docPalette, document); + } +} + +/** + * Figure out which SwatchesPanel instances are affected and update them. + */ +void SwatchesPanel::_rebuildDocumentSwatch(SwatchPage *docPalette, SPDocument *document) +{ + for (auto & it : docPerPanel) { + if (it.second == document) { + SwatchesPanel* swp = it.first; + std::vector<SwatchPage*> pages = swp->_getSwatchSets(); + SwatchPage* curr = pages[swp->_currentIndex]; + if (curr == docPalette) { + swp->_rebuild(); + } + } + } +} + +void SwatchesPanel::handleDefsModified(SPDocument *document) +{ + SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : nullptr; + if (docPalette && !DocTrack::queueUpdateIfNeeded(document) ) { + boost::ptr_vector<ColorItem> tmpColors; + std::map<ColorItem*, cairo_pattern_t*> tmpPrevs; + std::map<ColorItem*, SPGradient*> tmpGrads; + recalcSwatchContents(document, tmpColors, tmpPrevs, tmpGrads); + + if ( tmpColors.size() != docPalette->_colors.size() ) { + handleGradientsChange(document); + } else { + int cap = std::min(docPalette->_colors.size(), tmpColors.size()); + for (int i = 0; i < cap; i++) { + ColorItem *newColor = &tmpColors[i]; + ColorItem *oldColor = &docPalette->_colors[i]; + if ( (newColor->def.getType() != oldColor->def.getType()) || + (newColor->def.getR() != oldColor->def.getR()) || + (newColor->def.getG() != oldColor->def.getG()) || + (newColor->def.getB() != oldColor->def.getB()) ) { + oldColor->def.setRGB(newColor->def.getR(), newColor->def.getG(), newColor->def.getB()); + } + if (tmpGrads.find(newColor) != tmpGrads.end()) { + oldColor->setGradient(tmpGrads[newColor]); + } + if ( tmpPrevs.find(newColor) != tmpPrevs.end() ) { + oldColor->setPattern(tmpPrevs[newColor]); + } + } + } + + for (auto & tmpPrev : tmpPrevs) { + cairo_pattern_destroy(tmpPrev.second); + } + _rebuildDocumentSwatch(docPalette, document); + } +} + + +std::vector<SwatchPage*> SwatchesPanel::_getSwatchSets() const +{ + std::vector<SwatchPage*> tmp; + if (auto document = getDocument()) { + if (docPalettes.find(document) != docPalettes.end()) { + tmp.push_back(docPalettes[document]); + } + } + tmp.insert(tmp.end(), userSwatchPages.begin(), userSwatchPages.end()); + tmp.insert(tmp.end(), systemSwatchPages.begin(), systemSwatchPages.end()); + return tmp; +} + +void SwatchesPanel::selectionChanged(Selection *selection) +{ + auto document = getDocument(); + if (!document) + return; + + SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : nullptr; + if ( docPalette ) { + std::string fillId; + std::string strokeId; + + SPStyle tmpStyle(document); + int result = sp_desktop_query_style(getDesktop(), &tmpStyle, QUERY_STYLE_PROPERTY_FILL ); + switch (result) { + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: + case QUERY_STYLE_MULTIPLE_SAME: + { + if (tmpStyle.fill.set && tmpStyle.fill.isPaintserver()) { + SPPaintServer* server = tmpStyle.getFillPaintServer(); + if ( SP_IS_GRADIENT(server) ) { + SPGradient* target = nullptr; + SPGradient* grad = SP_GRADIENT(server); + + if ( grad->isSwatch() ) { + target = grad; + } else if ( grad->ref ) { + SPGradient *tmp = grad->ref->getObject(); + if ( tmp && tmp->isSwatch() ) { + target = tmp; + } + } + if ( target ) { + //XML Tree being used directly here while it shouldn't be + gchar const* id = target->getRepr()->attribute("id"); + if ( id ) { + fillId = id; + } + } + } + } + break; + } + } + + result = sp_desktop_query_style(getDesktop(), &tmpStyle, QUERY_STYLE_PROPERTY_STROKE); + switch (result) { + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: + case QUERY_STYLE_MULTIPLE_SAME: + { + if (tmpStyle.stroke.set && tmpStyle.stroke.isPaintserver()) { + SPPaintServer* server = tmpStyle.getStrokePaintServer(); + if ( SP_IS_GRADIENT(server) ) { + SPGradient* target = nullptr; + SPGradient* grad = SP_GRADIENT(server); + if ( grad->isSwatch() ) { + target = grad; + } else if ( grad->ref ) { + SPGradient *tmp = grad->ref->getObject(); + if ( tmp && tmp->isSwatch() ) { + target = tmp; + } + } + if ( target ) { + //XML Tree being used directly here while it shouldn't be + gchar const* id = target->getRepr()->attribute("id"); + if ( id ) { + strokeId = id; + } + } + } + } + break; + } + } + + for (auto & _color : docPalette->_colors) { + ColorItem* item = &_color; + bool isFill = (fillId == item->def.descr); + bool isStroke = (strokeId == item->def.descr); + item->setState( isFill, isStroke ); + } + } +} + +void SwatchesPanel::_rebuild() +{ + std::vector<SwatchPage*> pages = _getSwatchSets(); + SwatchPage* curr = pages[_currentIndex]; + + std::vector<Widget*> palette; + palette.reserve(curr->_colors.size() + 1); + palette.push_back(_remove->createWidget()); + for (auto & _color : curr->_colors) { + palette.push_back(_color.createWidget()); + } + _palette->set_colors(palette); + _palette->set_selected(curr->_name); +} + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/swatches.h b/src/ui/dialog/swatches.h new file mode 100644 index 0000000..477a7a9 --- /dev/null +++ b/src/ui/dialog/swatches.h @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Color swatches dialog + */ +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2005 Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_DIALOGS_SWATCHES_H +#define SEEN_DIALOGS_SWATCHES_H + +#include "ui/dialog/dialog-base.h" + +namespace Gtk { + class Menu; + class MenuItem; + class CheckMenuItem; +} + +namespace Inkscape { +namespace UI { + +class PreviewHolder; + +namespace Widget { + class ColorPalette; +} + +namespace Dialog { + +class ColorItem; +class SwatchPage; +class DocTrack; + +/** + * A panel that displays paint swatches. + * + * It comes in two flavors, depending on the prefsPath argument passed to + * the constructor: the default "/dialog/swatches" is just a regular panel; + * the "/embedded/swatches/" is the horizontal color swatches at the bottom + * of window. + */ +class SwatchesPanel : public DialogBase +{ +public: + SwatchesPanel(gchar const* prefsPath = "/dialogs/swatches"); + ~SwatchesPanel() override; + + void updatePalettes(); + void documentReplaced() override; + static SwatchesPanel& getInstance(); + virtual int getSelectedIndex() {return _currentIndex;} // temporary + +protected: + static void handleGradientsChange(SPDocument *document); + + virtual void _rebuild(); + + virtual std::vector<SwatchPage*> _getSwatchSets() const; + +private: + SwatchesPanel(SwatchesPanel const &) = delete; // no copy + SwatchesPanel &operator=(SwatchesPanel const &) = delete; // no assign + + void _build_menu(); + + void selectionChanged(Selection *selection) override; + static void _rebuildDocumentSwatch(SwatchPage *docPalette, SPDocument *document); + static void _trackDocument( SwatchesPanel *panel, SPDocument *document ); + static void handleDefsModified(SPDocument *document); + + PreviewHolder* _holder; + ColorItem* _clear; + ColorItem* _remove; + int _currentIndex; + Inkscape::UI::Widget::ColorPalette* _palette; + + void _regItem(Gtk::MenuItem* item, int id); + + void _updateSettings(int settings, int value); + + void _wrapToggled(Gtk::CheckMenuItem *toggler); + + Gtk::Menu *_menu; + + friend class DocTrack; +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + + + +#endif // SEEN_SWATCHES_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/ui/dialog/symbols.cpp b/src/ui/dialog/symbols.cpp new file mode 100644 index 0000000..147958b --- /dev/null +++ b/src/ui/dialog/symbols.cpp @@ -0,0 +1,1354 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Symbols dialog. + */ +/* Authors: + * Copyright (C) 2012 Tavmjong Bah + * + * 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 "symbols.h" + +#include <iostream> +#include <algorithm> +#include <locale> +#include <sstream> +#include <fstream> +#include <regex> + +#include <glibmm/i18n.h> +#include <glibmm/markup.h> +#include <glibmm/regex.h> +#include <glibmm/stringutils.h> + +#include "document.h" +#include "inkscape.h" +#include "path-prefix.h" +#include "selection.h" + +#include "display/cairo-utils.h" +#include "include/gtkmm_version.h" +#include "io/resource.h" +#include "io/sys.h" +#include "object/sp-defs.h" +#include "object/sp-root.h" +#include "object/sp-symbol.h" +#include "object/sp-use.h" +#include "ui/cache/svg_preview_cache.h" +#include "ui/clipboard.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/widget/scrollprotected.h" + +#ifdef WITH_LIBVISIO + #include <libvisio/libvisio.h> + + #include <librevenge-stream/librevenge-stream.h> + + using librevenge::RVNGFileStream; + using librevenge::RVNGString; + using librevenge::RVNGStringVector; + using librevenge::RVNGPropertyList; + using librevenge::RVNGSVGDrawingGenerator; +#endif + + +namespace Inkscape { +namespace UI { + +namespace Dialog { + +// See: http://developer.gnome.org/gtkmm/stable/classGtk_1_1TreeModelColumnRecord.html +class SymbolColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + + Gtk::TreeModelColumn<Glib::ustring> symbol_id; + Gtk::TreeModelColumn<Glib::ustring> symbol_title; + Gtk::TreeModelColumn<Glib::ustring> symbol_doc_title; + Gtk::TreeModelColumn< Glib::RefPtr<Gdk::Pixbuf> > symbol_image; + + + SymbolColumns() { + add(symbol_id); + add(symbol_title); + add(symbol_doc_title); + add(symbol_image); + } +}; + +SymbolColumns* SymbolsDialog::getColumns() +{ + SymbolColumns* columns = new SymbolColumns(); + return columns; +} + +/** + * Constructor + */ +SymbolsDialog::SymbolsDialog(gchar const *prefsPath) + : DialogBase(prefsPath, "Symbols") + , store(Gtk::ListStore::create(*getColumns())) + , all_docs_processed(false) + , icon_view(nullptr) + , preview_document(nullptr) + , gtk_connections() + , CURRENTDOC(_("Current document")) + , ALLDOCS(_("All symbol sets")) +{ + /******************** Table *************************/ + auto table = new Gtk::Grid(); + + table->set_margin_start(3); + table->set_margin_end(3); + table->set_margin_top(4); + // panel is a locked Gtk::VBox + pack_start(*Gtk::manage(table), Gtk::PACK_EXPAND_WIDGET); + guint row = 0; + + /******************** Symbol Sets *************************/ + Gtk::Label* label_set = new Gtk::Label(Glib::ustring(_("Symbol set")) + ": "); + table->attach(*Gtk::manage(label_set),0,row,1,1); + symbol_set = new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>(); // Fill in later + symbol_set->append(CURRENTDOC); + symbol_set->append(ALLDOCS); + symbol_set->set_active_text(CURRENTDOC); + symbol_set->set_hexpand(); + gtk_connections.emplace_back( + symbol_set->signal_changed().connect(sigc::mem_fun(*this, &SymbolsDialog::rebuild))); + + table->attach(*Gtk::manage(symbol_set),1,row,1,1); + ++row; + + /******************** Separator *************************/ + + + Gtk::Separator* separator = Gtk::manage(new Gtk::Separator()); // Search + separator->set_margin_top(10); + separator->set_margin_bottom(10); + table->attach(*Gtk::manage(separator),0,row,2,1); + + ++row; + + /******************** Search *************************/ + + search = Gtk::manage(new Gtk::SearchEntry()); // Search + search->set_tooltip_text(_("Press 'Return' to start search.")); + search->signal_key_press_event().connect_notify( sigc::mem_fun(*this, &SymbolsDialog::beforeSearch)); + search->signal_key_release_event().connect_notify(sigc::mem_fun(*this, &SymbolsDialog::unsensitive)); + + search->set_margin_bottom(6); + search->signal_search_changed().connect(sigc::mem_fun(*this, &SymbolsDialog::clearSearch)); + table->attach(*Gtk::manage(search),0,row,2,1); + search_str = ""; + + ++row; + + + /********************* Icon View **************************/ + SymbolColumns* columns = getColumns(); + + icon_view = new Gtk::IconView(static_cast<Glib::RefPtr<Gtk::TreeModel> >(store)); + //icon_view->set_text_column( columns->symbol_id ); + icon_view->set_tooltip_column( 1 ); + icon_view->set_pixbuf_column( columns->symbol_image ); + // Giving the iconview a small minimum size will help users understand + // What the dialog does. + icon_view->set_size_request( 100, 250 ); + icon_view->set_vexpand(true); + std::vector< Gtk::TargetEntry > targets; + targets.emplace_back( "application/x-inkscape-paste"); + + icon_view->enable_model_drag_source (targets, Gdk::BUTTON1_MASK, Gdk::ACTION_COPY); + gtk_connections.emplace_back( + icon_view->signal_drag_data_get().connect(sigc::mem_fun(*this, &SymbolsDialog::iconDragDataGet))); + gtk_connections.emplace_back( + icon_view->signal_selection_changed().connect(sigc::mem_fun(*this, &SymbolsDialog::iconChanged))); + + scroller = new Gtk::ScrolledWindow(); + scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + scroller->add(*Gtk::manage(icon_view)); + scroller->set_hexpand(); + scroller->set_vexpand(); + scroller->set_overlay_scrolling(false); + // here we fix scoller to allow pass the scroll to parent scroll when reach upper or lower limit + // this must be added to al scrolleing window in dialogs. We dont do auto because dialogs can be recreated + // in the dialog code so think is safer call inside + fix_inner_scroll(scroller); + overlay = new Gtk::Overlay(); + overlay->set_hexpand(); + overlay->set_vexpand(); + overlay->add(* scroller); + overlay->get_style_context()->add_class("forcebright"); + scroller->set_size_request(100, -1); + table->attach(*Gtk::manage(overlay), 0, row, 2, 1); + + /*************************Overlays******************************/ + overlay_opacity = new Gtk::Image(); + overlay_opacity->set_halign(Gtk::ALIGN_START); + overlay_opacity->set_valign(Gtk::ALIGN_START); + overlay_opacity->get_style_context()->add_class("rawstyle"); + overlay_opacity->set_no_show_all(true); + // No results + overlay_icon = sp_get_icon_image("searching", Gtk::ICON_SIZE_DIALOG); + overlay_icon->set_pixel_size(110); + overlay_icon->set_halign(Gtk::ALIGN_CENTER); + overlay_icon->set_valign(Gtk::ALIGN_START); + + overlay_icon->set_margin_top(25); + overlay_icon->set_no_show_all(true); + + overlay_title = new Gtk::Label(); + overlay_title->set_halign(Gtk::ALIGN_CENTER ); + overlay_title->set_valign(Gtk::ALIGN_START ); + overlay_title->set_justify(Gtk::JUSTIFY_CENTER); + overlay_title->set_margin_top(135); + overlay_title->set_no_show_all(true); + + overlay_desc = new Gtk::Label(); + overlay_desc->set_halign(Gtk::ALIGN_CENTER); + overlay_desc->set_valign(Gtk::ALIGN_START); + overlay_desc->set_margin_top(160); + overlay_desc->set_justify(Gtk::JUSTIFY_CENTER); + overlay_desc->set_no_show_all(true); + + overlay->add_overlay(*overlay_opacity); + overlay->add_overlay(*overlay_icon); + overlay->add_overlay(*overlay_title); + overlay->add_overlay(*overlay_desc); + + previous_height = 0; + previous_width = 0; + ++row; + + /******************** Progress *******************************/ + progress = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL); + progress_bar = Gtk::manage(new Gtk::ProgressBar()); + table->attach(*Gtk::manage(progress),0,row, 2, 1); + progress->pack_start(* progress_bar, Gtk::PACK_EXPAND_WIDGET); + progress->set_margin_top(15); + progress->set_margin_bottom(15); + progress->set_margin_start(20); + progress->set_margin_end(20); + + ++row; + + /******************** Tools *******************************/ + tools = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL); + + //tools->set_layout( Gtk::BUTTONBOX_END ); + scroller->set_hexpand(); + table->attach(*Gtk::manage(tools),0,row,2,1); + + auto add_symbol_image = Gtk::manage(sp_get_icon_image("symbol-add", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + add_symbol = Gtk::manage(new Gtk::Button()); + add_symbol->add(*add_symbol_image); + add_symbol->set_tooltip_text(_("Add Symbol from the current document.")); + add_symbol->set_relief( Gtk::RELIEF_NONE ); + add_symbol->set_focus_on_click( false ); + add_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::insertSymbol)); + tools->pack_start(* add_symbol, Gtk::PACK_SHRINK); + + auto remove_symbolImage = Gtk::manage(sp_get_icon_image("symbol-remove", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + remove_symbol = Gtk::manage(new Gtk::Button()); + remove_symbol->add(*remove_symbolImage); + remove_symbol->set_tooltip_text(_("Remove Symbol from the current document.")); + remove_symbol->set_relief( Gtk::RELIEF_NONE ); + remove_symbol->set_focus_on_click( false ); + remove_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::revertSymbol)); + tools->pack_start(* remove_symbol, Gtk::PACK_SHRINK); + + Gtk::Label* spacer = Gtk::manage(new Gtk::Label("")); + tools->pack_start(* Gtk::manage(spacer)); + + // Pack size (controls display area) + pack_size = 2; // Default 32px + + auto packMoreImage = Gtk::manage(sp_get_icon_image("pack-more", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + more = Gtk::manage(new Gtk::Button()); + more->add(*packMoreImage); + more->set_tooltip_text(_("Display more icons in row.")); + more->set_relief( Gtk::RELIEF_NONE ); + more->set_focus_on_click( false ); + more->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::packmore)); + tools->pack_start(* more, Gtk::PACK_SHRINK); + + auto packLessImage = Gtk::manage(sp_get_icon_image("pack-less", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + fewer = Gtk::manage(new Gtk::Button()); + fewer->add(*packLessImage); + fewer->set_tooltip_text(_("Display fewer icons in row.")); + fewer->set_relief( Gtk::RELIEF_NONE ); + fewer->set_focus_on_click( false ); + fewer->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::packless)); + tools->pack_start(* fewer, Gtk::PACK_SHRINK); + + // Toggle scale to fit on/off + auto fit_symbolImage = Gtk::manage(sp_get_icon_image("symbol-fit", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + fit_symbol = Gtk::manage(new Gtk::ToggleButton()); + fit_symbol->add(*fit_symbolImage); + fit_symbol->set_tooltip_text(_("Toggle 'fit' symbols in icon space.")); + fit_symbol->set_relief( Gtk::RELIEF_NONE ); + fit_symbol->set_focus_on_click( false ); + fit_symbol->set_active( true ); + fit_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::rebuild)); + tools->pack_start(* fit_symbol, Gtk::PACK_SHRINK); + + // Render size (scales symbols within display area) + scale_factor = 0; // Default 1:1 * pack_size/pack_size default + auto zoom_outImage = Gtk::manage(sp_get_icon_image("symbol-smaller", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + zoom_out = Gtk::manage(new Gtk::Button()); + zoom_out->add(*zoom_outImage); + zoom_out->set_tooltip_text(_("Make symbols smaller by zooming out.")); + zoom_out->set_relief( Gtk::RELIEF_NONE ); + zoom_out->set_focus_on_click( false ); + zoom_out->set_sensitive( false ); + zoom_out->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::zoomout)); + tools->pack_start(* zoom_out, Gtk::PACK_SHRINK); + + auto zoom_inImage = Gtk::manage(sp_get_icon_image("symbol-bigger", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + zoom_in = Gtk::manage(new Gtk::Button()); + zoom_in->add(*zoom_inImage); + zoom_in->set_tooltip_text(_("Make symbols bigger by zooming in.")); + zoom_in->set_relief( Gtk::RELIEF_NONE ); + zoom_in->set_focus_on_click( false ); + zoom_in->set_sensitive( false ); + zoom_in->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::zoomin)); + tools->pack_start(* zoom_in, Gtk::PACK_SHRINK); + + ++row; + + sensitive = true; + + preview_document = symbolsPreviewDoc(); /* Template to render symbols in */ + preview_document->ensureUpToDate(); /* Necessary? */ + key = SPItem::display_key_new(1); + renderDrawing.setRoot(preview_document->getRoot()->invoke_show(renderDrawing, key, SP_ITEM_SHOW_DISPLAY )); + + getSymbolsTitle(); + icons_found = false; +} + +SymbolsDialog::~SymbolsDialog() +{ + for (auto &connection : gtk_connections) { + connection.disconnect(); + } + gtk_connections.clear(); + idleconn.disconnect(); + + Inkscape::GC::release(preview_document); + assert(preview_document->_anchored_refcount() == 0); + delete preview_document; +} + +SymbolsDialog& SymbolsDialog::getInstance() +{ + return *new SymbolsDialog(); +} + +void SymbolsDialog::packless() { + if(pack_size < 4) { + pack_size++; + rebuild(); + } +} + +void SymbolsDialog::packmore() { + if(pack_size > 0) { + pack_size--; + rebuild(); + } +} + +void SymbolsDialog::zoomin() { + if(scale_factor < 4) { + scale_factor++; + rebuild(); + } +} + +void SymbolsDialog::zoomout() { + if(scale_factor > -8) { + scale_factor--; + rebuild(); + } +} + +void SymbolsDialog::rebuild() { + + if (!sensitive) { + return; + } + + if( fit_symbol->get_active() ) { + zoom_in->set_sensitive( false ); + zoom_out->set_sensitive( false ); + } else { + zoom_in->set_sensitive( true); + zoom_out->set_sensitive( true ); + } + store->clear(); + SPDocument* symbol_document = selectedSymbols(); + icons_found = false; + //We are not in search all docs + if (search->get_text() != _("Searching...") && search->get_text() != _("Loading all symbols...")) { + Glib::ustring current = Glib::Markup::escape_text(symbol_set->get_active_text()); + if (current == ALLDOCS && search->get_text() != "") { + searchsymbols(); + return; + } + } + if (symbol_document) { + addSymbolsInDoc(symbol_document); + } else { + showOverlay(); + } +} +void SymbolsDialog::showOverlay() { + Glib::ustring current = Glib::Markup::escape_text(symbol_set->get_active_text()); + if (current == ALLDOCS && !l.size()) + { + overlay_icon->hide(); + if (!all_docs_processed ) { + overlay_icon->show(); + overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + + Glib::ustring(_("Search in all symbol sets...")) + Glib::ustring("</span>")); + overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") + + Glib::ustring(_("The first search can be slow.")) + Glib::ustring("</span>")); + } else if (!icons_found && !search_str.empty()) { + overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No symbols found.")) + + Glib::ustring("</span>")); + overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") + + Glib::ustring(_("Try a different search term.")) + Glib::ustring("</span>")); + } else { + overlay_icon->show(); + overlay_title->set_markup(Glib::ustring("<spansize=\"large\">") + + Glib::ustring(_("Search in all symbol sets...")) + Glib::ustring("</span>")); + overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") + Glib::ustring("</span>")); + } + } else if (!number_symbols && (current != CURRENTDOC || !search_str.empty())) { + overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No symbols found.")) + + Glib::ustring("</span>")); + overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") + + Glib::ustring(_("Try a different search term,\nor switch to a different symbol set.")) + + Glib::ustring("</span>")); + } else if (!number_symbols && current == CURRENTDOC) { + overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No symbols found.")) + + Glib::ustring("</span>")); + overlay_desc->set_markup( + Glib::ustring("<span size=\"small\">") + + Glib::ustring(_("No symbols in current document.\nChoose a different symbol set\nor add a new symbol.")) + + Glib::ustring("</span>")); + } else if (!icons_found && !search_str.empty()) { + overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No symbols found.")) + + Glib::ustring("</span>")); + overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") + + Glib::ustring(_("Try a different search term,\nor switch to a different symbol set.")) + + Glib::ustring("</span>")); + } + gint width = scroller->get_allocated_width(); + gint height = scroller->get_allocated_height(); + if (previous_height != height || previous_width != width) { + previous_height = height; + previous_width = width; + overlay_opacity->set_size_request(width, height); + overlay_opacity->set(getOverlay(width, height)); + } + overlay_opacity->hide(); + overlay_icon->show(); + overlay_title->show(); + overlay_desc->show(); + if (l.size()) { + overlay_opacity->show(); + overlay_icon->hide(); + overlay_title->hide(); + overlay_desc->hide(); + } +} + +void SymbolsDialog::hideOverlay() { + overlay_opacity->hide(); + overlay_icon->hide(); + overlay_title->hide(); + overlay_desc->hide(); +} + +void SymbolsDialog::insertSymbol() { + getDesktop()->selection->toSymbol(); +} + +void SymbolsDialog::revertSymbol() { + if (auto document = getDocument()) { + if (auto symbol = dynamic_cast<SPSymbol*> (document->getObjectById(selectedSymbolId()))) { + symbol->unSymbol(); + } + Inkscape::DocumentUndo::done(document, _("Group from symbol"), ""); + } +} + +void SymbolsDialog::iconDragDataGet(const Glib::RefPtr<Gdk::DragContext>& /*context*/, Gtk::SelectionData& data, guint /*info*/, guint /*time*/) +{ + auto iconArray = icon_view->get_selected_items(); + + if( iconArray.empty() ) { + //std::cout << " iconArray empty: huh? " << std::endl; + } else { + Gtk::TreeModel::Path const & path = *iconArray.begin(); + Gtk::ListStore::iterator row = store->get_iter(path); + Glib::ustring symbol_id = (*row)[getColumns()->symbol_id]; + GdkAtom dataAtom = gdk_atom_intern( "application/x-inkscape-paste", FALSE ); + gtk_selection_data_set( data.gobj(), dataAtom, 9, (guchar*)symbol_id.c_str(), symbol_id.length() ); + } + +} + +void SymbolsDialog::defsModified(SPObject * /*object*/, guint /*flags*/) +{ + Glib::ustring doc_title = symbol_set->get_active_text(); + if (doc_title != ALLDOCS && !symbol_sets[doc_title] ) { + rebuild(); + } +} + +void SymbolsDialog::selectionChanged(Inkscape::Selection *selection) { + Glib::ustring symbol_id = selectedSymbolId(); + Glib::ustring doc_title = selectedSymbolDocTitle(); + if (!doc_title.empty()) { + SPDocument* symbol_document = symbol_sets[doc_title]; + if (!symbol_document) { + //we are in global search so get the original symbol document by title + symbol_document = selectedSymbols(); + } + if (symbol_document) { + SPObject* symbol = symbol_document->getObjectById(symbol_id); + if(symbol && !selection->includes(symbol)) { + icon_view->unselect_all(); + } + } + } +} + +void SymbolsDialog::documentReplaced() +{ + defs_modified.disconnect(); + if (auto document = getDocument()) { + defs_modified = document->getDefs()->connectModified(sigc::mem_fun(*this, &SymbolsDialog::defsModified)); + if (!symbol_sets[symbol_set->get_active_text()]) { + // Symbol set is from Current document, need to rebuild + rebuild(); + } + } +} + +SPDocument* SymbolsDialog::selectedSymbols() { + /* OK, we know symbol name... now we need to copy it to clipboard, bon chance! */ + Glib::ustring doc_title = symbol_set->get_active_text(); + if (doc_title == ALLDOCS) { + return nullptr; + } + SPDocument* symbol_document = symbol_sets[doc_title]; + if( !symbol_document ) { + symbol_document = getSymbolsSet(doc_title).second; + // Symbol must be from Current Document (this method of checking should be language independent). + if( !symbol_document ) { + // Symbol must be from Current Document (this method of + // checking should be language independent). + symbol_document = getDocument(); + add_symbol->set_sensitive( true ); + remove_symbol->set_sensitive( true ); + } else { + add_symbol->set_sensitive( false ); + remove_symbol->set_sensitive( false ); + } + } + return symbol_document; +} + +Glib::ustring SymbolsDialog::selectedSymbolId() { + + auto iconArray = icon_view->get_selected_items(); + + if( !iconArray.empty() ) { + Gtk::TreeModel::Path const & path = *iconArray.begin(); + Gtk::ListStore::iterator row = store->get_iter(path); + return (*row)[getColumns()->symbol_id]; + } + return Glib::ustring(""); +} + +Glib::ustring SymbolsDialog::selectedSymbolDocTitle() { + + auto iconArray = icon_view->get_selected_items(); + + if( !iconArray.empty() ) { + Gtk::TreeModel::Path const & path = *iconArray.begin(); + Gtk::ListStore::iterator row = store->get_iter(path); + return (*row)[getColumns()->symbol_doc_title]; + } + return Glib::ustring(""); +} + +Glib::ustring SymbolsDialog::documentTitle(SPDocument* symbol_doc) { + if (symbol_doc) { + SPRoot * root = symbol_doc->getRoot(); + gchar * title = root->title(); + if (title) { + return ellipsize(Glib::ustring(title), 33); + } + g_free(title); + } + Glib::ustring current = symbol_set->get_active_text(); + if (current == CURRENTDOC) { + return current; + } + return _("Untitled document"); +} + +void SymbolsDialog::iconChanged() { + + Glib::ustring symbol_id = selectedSymbolId(); + SPDocument* symbol_document = selectedSymbols(); + if (!symbol_document) { + //we are in global search so get the original symbol document by title + Glib::ustring doc_title = selectedSymbolDocTitle(); + if (!doc_title.empty()) { + symbol_document = symbol_sets[doc_title]; + } + } + if (symbol_document) { + SPObject* symbol = symbol_document->getObjectById(symbol_id); + + if( symbol ) { + // Find style for use in <use> + // First look for default style stored in <symbol> + gchar const* style = symbol->getAttribute("inkscape:symbol-style"); + if( !style ) { + // If no default style in <symbol>, look in documents. + if(symbol_document == getDocument()) { + style = styleFromUse(symbol_id.c_str(), symbol_document); + } else { + style = symbol_document->getReprRoot()->attribute("style"); + } + } + + ClipboardManager *cm = ClipboardManager::get(); + cm->copySymbol(symbol->getRepr(), style, symbol_document); + } + } +} + +#ifdef WITH_LIBVISIO + +// Extend libvisio's native RVNGSVGDrawingGenerator with support for extracting stencil names (to be used as ID/title) +class REVENGE_API RVNGSVGDrawingGenerator_WithTitle : public RVNGSVGDrawingGenerator { + public: + RVNGSVGDrawingGenerator_WithTitle(RVNGStringVector &output, RVNGStringVector &titles, const RVNGString &nmSpace) + : RVNGSVGDrawingGenerator(output, nmSpace) + , _titles(titles) + {} + + void startPage(const RVNGPropertyList &propList) override + { + RVNGSVGDrawingGenerator::startPage(propList); + if (propList["draw:name"]) { + _titles.append(propList["draw:name"]->getStr()); + } else { + _titles.append(""); + } + } + + private: + RVNGStringVector &_titles; +}; + +// Read Visio stencil files +SPDocument* read_vss(Glib::ustring filename, Glib::ustring name ) { + gchar *fullname; + #ifdef _WIN32 + // RVNGFileStream uses fopen() internally which unfortunately only uses ANSI encoding on Windows + // therefore attempt to convert uri to the system codepage + // even if this is not possible the alternate short (8.3) file name will be used if available + fullname = g_win32_locale_filename_from_utf8(filename.c_str()); + #else + fullname = strdup(filename.c_str()); + #endif + + RVNGFileStream input(fullname); + g_free(fullname); + + if (!libvisio::VisioDocument::isSupported(&input)) { + return nullptr; + } + RVNGStringVector output; + RVNGStringVector titles; + RVNGSVGDrawingGenerator_WithTitle generator(output, titles, "svg"); + + if (!libvisio::VisioDocument::parseStencils(&input, &generator)) { + return nullptr; + } + if (output.empty()) { + return nullptr; + } + + // prepare a valid title for the symbol file + Glib::ustring title = Glib::Markup::escape_text(name); + // prepare a valid id prefix for symbols libvisio doesn't give us a name for + Glib::RefPtr<Glib::Regex> regex1 = Glib::Regex::create("[^a-zA-Z0-9_-]"); + Glib::ustring id = regex1->replace(name, 0, "_", Glib::REGEX_MATCH_PARTIAL); + + Glib::ustring tmpSVGOutput; + tmpSVGOutput += "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"; + tmpSVGOutput += "<svg\n"; + tmpSVGOutput += " xmlns=\"http://www.w3.org/2000/svg\"\n"; + tmpSVGOutput += " xmlns:svg=\"http://www.w3.org/2000/svg\"\n"; + tmpSVGOutput += " xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n"; + tmpSVGOutput += " version=\"1.1\"\n"; + tmpSVGOutput += " style=\"fill:none;stroke:#000000;stroke-width:2\">\n"; + tmpSVGOutput += " <title>"; + tmpSVGOutput += title; + tmpSVGOutput += "</title>\n"; + tmpSVGOutput += " <defs>\n"; + + // Each "symbol" is in its own SVG file, we wrap with <symbol> and merge into one file. + for (unsigned i=0; i<output.size(); ++i) { + + std::stringstream ss; + if (titles.size() == output.size() && titles[i] != "") { + // TODO: Do we need to check for duplicated titles? + ss << regex1->replace(titles[i].cstr(), 0, "_", Glib::REGEX_MATCH_PARTIAL); + } else { + ss << id << "_" << i; + } + + tmpSVGOutput += " <symbol id=\"" + ss.str() + "\">\n"; + + if (titles.size() == output.size() && titles[i] != "") { + tmpSVGOutput += " <title>" + Glib::ustring(RVNGString::escapeXML(titles[i].cstr()).cstr()) + "</title>\n"; + } + + std::istringstream iss( output[i].cstr() ); + std::string line; + while( std::getline( iss, line ) ) { + if( line.find( "svg:svg" ) == std::string::npos ) { + tmpSVGOutput += " " + line + "\n"; + } + } + + tmpSVGOutput += " </symbol>\n"; + } + + tmpSVGOutput += " </defs>\n"; + tmpSVGOutput += "</svg>\n"; + return SPDocument::createNewDocFromMem( tmpSVGOutput.c_str(), strlen( tmpSVGOutput.c_str()), false ); + +} +#endif + +/* Hunts preference directories for symbol files */ +void SymbolsDialog::getSymbolsTitle() { + + using namespace Inkscape::IO::Resource; + Glib::ustring title; + number_docs = 0; + std::regex matchtitle (".*?<title.*?>(.*?)<(/| /)"); + for(auto &filename: get_filenames(SYMBOLS, {".svg", ".vss"})) { + if(Glib::str_has_suffix(filename, ".vss")) { + std::size_t found = filename.find_last_of("/\\"); + filename = filename.substr(found+1); + title = filename.erase(filename.rfind('.')); + if(title.empty()) { + title = _("Unnamed Symbols"); + } + symbol_sets[title]= nullptr; + ++number_docs; + } else { + std::ifstream infile(filename); + std::string line; + while (std::getline(infile, line)) { + std::string title_res = std::regex_replace (line, matchtitle,"$1",std::regex_constants::format_no_copy); + if (!title_res.empty()) { + title_res = g_dpgettext2(nullptr, "Symbol", title_res.c_str()); + symbol_sets[ellipsize(Glib::ustring(title_res), 33)]= nullptr; + ++number_docs; + break; + } + std::string::size_type position_exit = line.find ("<defs"); + if (position_exit != std::string::npos) { + std::size_t found = filename.find_last_of("/\\"); + filename = filename.substr(found+1); + title = filename.erase(filename.rfind('.')); + if(title.empty()) { + title = _("Unnamed Symbols"); + } + symbol_sets[title]= nullptr; + ++number_docs; + break; + } + } + } + } + for(auto const &symbol_document_map : symbol_sets) { + symbol_set->append(symbol_document_map.first); + } +} + +/* Hunts preference directories for symbol files */ +std::pair<Glib::ustring, SPDocument*> +SymbolsDialog::getSymbolsSet(Glib::ustring title) +{ + SPDocument* symbol_doc = nullptr; + Glib::ustring current = symbol_set->get_active_text(); + if (current == CURRENTDOC) { + return std::make_pair(CURRENTDOC, symbol_doc); + } + if (symbol_sets[title]) { + sensitive = false; + symbol_set->remove_all(); + symbol_set->append(CURRENTDOC); + symbol_set->append(ALLDOCS); + for(auto const &symbol_document_map : symbol_sets) { + if (CURRENTDOC != symbol_document_map.first) { + symbol_set->append(symbol_document_map.first); + } + } + symbol_set->set_active_text(title); + sensitive = true; + return std::make_pair(title, symbol_sets[title]); + } + using namespace Inkscape::IO::Resource; + Glib::ustring new_title; + + std::regex matchtitle (".*?<title.*?>(.*?)<(/| /)"); + for(auto &filename: get_filenames(SYMBOLS, {".svg", ".vss"})) { + if(Glib::str_has_suffix(filename, ".vss")) { +#ifdef WITH_LIBVISIO + std::size_t pos = filename.find_last_of("/\\"); + Glib::ustring filename_short = ""; + if (pos != std::string::npos) { + filename_short = filename.substr(pos+1); + } + if (filename_short == title + ".vss") { + new_title = title; + symbol_doc = read_vss(Glib::ustring(filename), title); + } +#endif + } else { + std::ifstream infile(filename); + std::string line; + while (std::getline(infile, line)) { + std::string title_res = std::regex_replace (line, matchtitle,"$1",std::regex_constants::format_no_copy); + if (!title_res.empty()) { + title_res = g_dpgettext2(nullptr, "Symbol", title_res.c_str()); + new_title = ellipsize(Glib::ustring(title_res), 33); + } + std::size_t pos = filename.find_last_of("/\\"); + Glib::ustring filename_short = ""; + if (pos != std::string::npos) { + filename_short = filename.substr(pos+1); + } + if (title == new_title || filename_short == title + ".svg") { + new_title = title; + if(Glib::str_has_suffix(filename, ".svg")) { + symbol_doc = SPDocument::createNewDoc(filename.c_str(), FALSE); + } + } + if (symbol_doc) { + break; + } + std::string::size_type position_exit = line.find ("<defs"); + if (position_exit != std::string::npos) { + break; + } + } + } + if (symbol_doc) { + break; + } + } + if(symbol_doc) { + symbol_sets.erase(title); + symbol_sets[new_title] = symbol_doc; + sensitive = false; + symbol_set->remove_all(); + symbol_set->append(CURRENTDOC); + symbol_set->append(ALLDOCS); + for(auto const &symbol_document_map : symbol_sets) { + if (CURRENTDOC != symbol_document_map.first) { + symbol_set->append(symbol_document_map.first); + } + } + symbol_set->set_active_text(new_title); + sensitive = true; + } + return std::make_pair(new_title, symbol_doc); +} + +void SymbolsDialog::symbolsInDocRecursive (SPObject *r, std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > &l, Glib::ustring doc_title) +{ + if(!r) return; + + // Stop multiple counting of same symbol + if ( dynamic_cast<SPUse *>(r) ) { + return; + } + + if ( dynamic_cast<SPSymbol *>(r)) { + Glib::ustring id = r->getAttribute("id"); + gchar * title = r->title(); + if(title) { + l[doc_title + title + id] = std::make_pair(doc_title,dynamic_cast<SPSymbol *>(r)); + } else { + l[Glib::ustring(_("notitle_")) + id] = std::make_pair(doc_title,dynamic_cast<SPSymbol *>(r)); + } + g_free(title); + } + for (auto& child: r->children) { + symbolsInDocRecursive(&child, l, doc_title); + } +} + +std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > +SymbolsDialog::symbolsInDoc( SPDocument* symbol_document, Glib::ustring doc_title) +{ + + std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > l; + if (symbol_document) { + symbolsInDocRecursive (symbol_document->getRoot(), l , doc_title); + } + return l; +} + +void SymbolsDialog::useInDoc (SPObject *r, std::vector<SPUse*> &l) +{ + + if ( dynamic_cast<SPUse *>(r) ) { + l.push_back(dynamic_cast<SPUse *>(r)); + } + + for (auto& child: r->children) { + useInDoc( &child, l ); + } +} + +std::vector<SPUse*> SymbolsDialog::useInDoc( SPDocument* useDocument) { + std::vector<SPUse*> l; + useInDoc (useDocument->getRoot(), l); + return l; +} + +// Returns style from first <use> element found that references id. +// This is a last ditch effort to find a style. +gchar const* SymbolsDialog::styleFromUse( gchar const* id, SPDocument* document) { + + gchar const* style = nullptr; + std::vector<SPUse*> l = useInDoc( document ); + for( auto use:l ) { + if ( use ) { + gchar const *href = use->getRepr()->attribute("xlink:href"); + if( href ) { + Glib::ustring href2(href); + Glib::ustring id2(id); + id2 = "#" + id2; + if( !href2.compare(id2) ) { + style = use->getRepr()->attribute("style"); + break; + } + } + } + } + return style; +} + +void SymbolsDialog::clearSearch() +{ + if(search->get_text().empty() && sensitive) { + enableWidgets(false); + search_str = ""; + store->clear(); + SPDocument* symbol_document = selectedSymbols(); + if (symbol_document) { + //We are not in search all docs + icons_found = false; + addSymbolsInDoc(symbol_document); + } else { + showOverlay(); + enableWidgets(true); + } + } +} + +void SymbolsDialog::enableWidgets(bool enable) +{ + symbol_set->set_sensitive(enable); + search->set_sensitive(enable); + tools ->set_sensitive(enable); +} + +void SymbolsDialog::beforeSearch(GdkEventKey* evt) +{ + sensitive = false; + search_str = search->get_text().lowercase(); + if (evt->keyval != GDK_KEY_Return) { + return; + } + searchsymbols(); +} + +void SymbolsDialog::searchsymbols() +{ + progress_bar->set_fraction(0.0); + enableWidgets(false); + SPDocument *symbol_document = selectedSymbols(); + if (symbol_document) { + // We are not in search all docs + search->set_text(_("Searching...")); + store->clear(); + icons_found = false; + addSymbolsInDoc(symbol_document); + } else { + idleconn.disconnect(); + idleconn = Glib::signal_idle().connect(sigc::mem_fun(*this, &SymbolsDialog::callbackAllSymbols)); + search->set_text(_("Loading all symbols...")); + } +} + +void SymbolsDialog::unsensitive(GdkEventKey* evt) +{ + sensitive = true; +} + +bool SymbolsDialog::callbackSymbols(){ + if (l.size()) { + showOverlay(); + for (auto symbol_data = l.begin(); symbol_data != l.end();) { + Glib::ustring doc_title = symbol_data->second.first; + SPSymbol * symbol = symbol_data->second.second; + counter_symbols ++; + gchar *symbol_title_char = symbol->title(); + gchar *symbol_desc_char = symbol->description(); + bool found = false; + if (symbol_title_char) { + Glib::ustring symbol_title = Glib::ustring(symbol_title_char).lowercase(); + auto pos = symbol_title.rfind(search_str); + auto pos_translated = Glib::ustring(g_dpgettext2(nullptr, "Symbol", symbol_title_char)).lowercase().rfind(search_str); + if ((pos != std::string::npos) || (pos_translated != std::string::npos)) { + found = true; + } + if (!found && symbol_desc_char) { + Glib::ustring symbol_desc = Glib::ustring(symbol_desc_char).lowercase(); + auto pos = symbol_desc.rfind(search_str); + auto pos_translated = Glib::ustring(g_dpgettext2(nullptr, "Symbol", symbol_desc_char)).lowercase().rfind(search_str); + if ((pos != std::string::npos) || (pos_translated != std::string::npos)) { + found = true; + } + } + } + if (symbol && (search_str.empty() || found)) { + addSymbol( symbol, doc_title); + icons_found = true; + } + + progress_bar->set_fraction(((100.0/number_symbols) * counter_symbols)/100.0); + symbol_data = l.erase(l.begin()); + //to get more items and best performance + int modulus = number_symbols > 200 ? 50 : (number_symbols/4); + g_free(symbol_title_char); + g_free(symbol_desc_char); + if (modulus && counter_symbols % modulus == 0 && !l.empty()) { + return true; + } + } + if (!icons_found && !search_str.empty()) { + showOverlay(); + } else { + hideOverlay(); + } + progress_bar->set_fraction(0); + sensitive = false; + search->set_text(search_str); + sensitive = true; + enableWidgets(true); + return false; + } + return true; +} + +bool SymbolsDialog::callbackAllSymbols(){ + Glib::ustring current = symbol_set->get_active_text(); + if (current == ALLDOCS && search->get_text() == _("Loading all symbols...")) { + size_t counter = 0; + std::map<Glib::ustring, SPDocument*> symbol_sets_tmp = symbol_sets; + for(auto const &symbol_document_map : symbol_sets_tmp) { + ++counter; + SPDocument* symbol_document = symbol_document_map.second; + if (symbol_document) { + continue; + } + symbol_document = getSymbolsSet(symbol_document_map.first).second; + symbol_set->set_active_text(ALLDOCS); + if (!symbol_document) { + continue; + } + progress_bar->set_fraction(((100.0/number_docs) * counter)/100.0); + return true; + } + symbol_sets_tmp.clear(); + hideOverlay(); + all_docs_processed = true; + addSymbols(); + progress_bar->set_fraction(0); + search->set_text("Searching..."); + return false; + } + return true; +} + +Glib::ustring SymbolsDialog::ellipsize(Glib::ustring data, size_t limit) { + if (data.length() > limit) { + data = data.substr(0, limit-3); + return data + "..."; + } + return data; +} + +void SymbolsDialog::addSymbolsInDoc(SPDocument* symbol_document) { + + if (!symbol_document) { + return; //Search all + } + Glib::ustring doc_title = documentTitle(symbol_document); + progress_bar->set_fraction(0.0); + counter_symbols = 0; + l = symbolsInDoc(symbol_document, doc_title); + number_symbols = l.size(); + if (!number_symbols) { + sensitive = false; + search->set_text(search_str); + sensitive = true; + enableWidgets(true); + idleconn.disconnect(); + showOverlay(); + } else { + idleconn.disconnect(); + idleconn = Glib::signal_idle().connect( sigc::mem_fun(*this, &SymbolsDialog::callbackSymbols)); + } +} + +void SymbolsDialog::addSymbols() { + store->clear(); + icons_found = false; + for(auto const &symbol_document_map : symbol_sets) { + SPDocument* symbol_document = symbol_document_map.second; + if (!symbol_document) { + continue; + } + Glib::ustring doc_title = documentTitle(symbol_document); + std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > l_tmp = symbolsInDoc(symbol_document, doc_title); + for(auto &p : l_tmp ) { + l[p.first] = p.second; + } + l_tmp.clear(); + } + counter_symbols = 0; + progress_bar->set_fraction(0.0); + number_symbols = l.size(); + if (!number_symbols) { + showOverlay(); + idleconn.disconnect(); + sensitive = false; + search->set_text(search_str); + sensitive = true; + enableWidgets(true); + } else { + idleconn.disconnect(); + idleconn = Glib::signal_idle().connect( sigc::mem_fun(*this, &SymbolsDialog::callbackSymbols)); + } +} + +void SymbolsDialog::addSymbol( SPObject* symbol, Glib::ustring doc_title) +{ + gchar const *id = symbol->getRepr()->attribute("id"); + + if (doc_title.empty()) { + doc_title = CURRENTDOC; + } else { + doc_title = g_dpgettext2(nullptr, "Symbol", doc_title.c_str()); + } + + Glib::ustring symbol_title; + gchar *title = symbol->title(); // From title element + if (title) { + symbol_title = Glib::ustring::compose("%1 (%2)", g_dpgettext2(nullptr, "Symbol", title), doc_title.c_str()); + } else { + symbol_title = Glib::ustring::compose("%1 %2 (%3)", _("Symbol without title"), Glib::ustring(id), doc_title); + } + g_free(title); + + Glib::RefPtr<Gdk::Pixbuf> pixbuf = drawSymbol( symbol ); + if( pixbuf ) { + Gtk::ListStore::iterator row = store->append(); + SymbolColumns* columns = getColumns(); + (*row)[columns->symbol_id] = Glib::ustring( id ); + (*row)[columns->symbol_title] = Glib::Markup::escape_text(symbol_title); + (*row)[columns->symbol_doc_title] = Glib::Markup::escape_text(doc_title); + (*row)[columns->symbol_image] = pixbuf; + delete columns; + } +} + +/* + * Returns image of symbol. + * + * Symbols normally are not visible. They must be referenced by a + * <use> element. A temporary document is created with a dummy + * <symbol> element and a <use> element that references the symbol + * element. Each real symbol is swapped in for the dummy symbol and + * the temporary document is rendered. + */ +Glib::RefPtr<Gdk::Pixbuf> +SymbolsDialog::drawSymbol(SPObject *symbol) +{ + // Create a copy repr of the symbol with id="the_symbol" + Inkscape::XML::Document *xml_doc = preview_document->getReprDoc(); + Inkscape::XML::Node *repr = symbol->getRepr()->duplicate(xml_doc); + repr->setAttribute("id", "the_symbol"); + + // Replace old "the_symbol" in preview_document by new. + Inkscape::XML::Node *root = preview_document->getReprRoot(); + SPObject *symbol_old = preview_document->getObjectById("the_symbol"); + if (symbol_old) { + symbol_old->deleteObject(false); + } + + SPDocument::install_reference_document scoped(preview_document, getDocument()); + + // First look for default style stored in <symbol> + gchar const* style = repr->attribute("inkscape:symbol-style"); + if(!style) { + // If no default style in <symbol>, look in documents. + if(symbol->document == getDocument()) { + gchar const *id = symbol->getRepr()->attribute("id"); + style = styleFromUse( id, symbol->document ); + } else { + style = symbol->document->getReprRoot()->attribute("style"); + } + } + + // This is for display in Symbols dialog only + if( style ) repr->setAttribute( "style", style ); + + root->appendChild(repr); + Inkscape::GC::release(repr); + + // Uncomment this to get the preview_document documents saved (useful for debugging) + // FILE *fp = fopen (g_strconcat(id, ".svg", NULL), "w"); + // sp_repr_save_stream(preview_document->getReprDoc(), fp); + // fclose (fp); + + // Make sure preview_document is up-to-date. + preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + preview_document->ensureUpToDate(); + + // Make sure we have symbol in preview_document + SPObject *object_temp = preview_document->getObjectById( "the_use" ); + + SPItem *item = dynamic_cast<SPItem *>(object_temp); + g_assert(item != nullptr); + unsigned psize = SYMBOL_ICON_SIZES[pack_size]; + + Glib::RefPtr<Gdk::Pixbuf> pixbuf(nullptr); + // We could use cache here, but it doesn't really work with the structure + // of this user interface and we've already cached the pixbuf in the gtklist + + // Find object's bbox in document. + // Note symbols can have own viewport... ignore for now. + //Geom::OptRect dbox = item->geometricBounds(); + Geom::OptRect dbox = item->documentVisualBounds(); + + if (dbox) { + /* Scale symbols to fit */ + double scale = 1.0; + double width = dbox->width(); + double height = dbox->height(); + + if( width == 0.0 ) width = 1.0; + if( height == 0.0 ) height = 1.0; + + if( fit_symbol->get_active() ) + scale = psize / ceil(std::max(width, height)); + else + scale = pow( 2.0, scale_factor/2.0 ) * psize / 32.0; + + pixbuf = Glib::wrap(render_pixbuf(renderDrawing, scale, *dbox, psize)); + } + + return pixbuf; +} + +/* + * Return empty doc to render symbols in. + * Symbols are by default not rendered so a <use> element is + * provided. + */ +SPDocument* SymbolsDialog::symbolsPreviewDoc() +{ + // BUG: <symbol> must be inside <defs> + gchar const *buffer = +"<svg xmlns=\"http://www.w3.org/2000/svg\"" +" xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\"" +" xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\"" +" xmlns:xlink=\"http://www.w3.org/1999/xlink\">" +" <defs id=\"defs\">" +" <symbol id=\"the_symbol\"/>" +" </defs>" +" <use id=\"the_use\" xlink:href=\"#the_symbol\"/>" +"</svg>"; + return SPDocument::createNewDocFromMem( buffer, strlen(buffer), FALSE ); +} + +/* + * Update image widgets + */ +Glib::RefPtr<Gdk::Pixbuf> +SymbolsDialog::getOverlay(gint width, gint height) +{ + cairo_surface_t *surface; + cairo_t *cr; + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); + cr = cairo_create (surface); + cairo_set_source_rgba(cr, 1, 1, 1, 0.75); + cairo_rectangle (cr, 0, 0, width, height); + cairo_fill (cr); + GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(surface); + cairo_destroy (cr); + return Glib::wrap(pixbuf); +} + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/symbols.h b/src/ui/dialog/symbols.h new file mode 100644 index 0000000..c507ffb --- /dev/null +++ b/src/ui/dialog/symbols.h @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Symbols dialog + */ +/* Authors: + * Tavmjong Bah, Martin Owens + * + * Copyright (C) 2012 Tavmjong Bah + * 2013 Martin Owens + * 2017 Jabiertxo Arraiza + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_SYMBOLS_H +#define INKSCAPE_UI_DIALOG_SYMBOLS_H + +#include <gtkmm.h> +#include <vector> + +#include "display/drawing.h" +#include "helper/auto-connection.h" +#include "include/gtkmm_version.h" +#include "ui/dialog/dialog-base.h" + +class SPObject; +class SPSymbol; +class SPUse; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class SymbolColumns; // For Gtk::ListStore + +/** + * A dialog that displays selectable symbols and allows users to drag or paste + * those symbols from the dialog into the document. + * + * Symbol documents are loaded from the preferences paths and displayed in a + * drop-down list to the user. The user then selects which of the symbols + * documents they want to get symbols from. The first document in the list is + * always the current document. + * + * This then updates an icon-view with all the symbols available. Selecting one + * puts it onto the clipboard. Dragging it or pasting it onto the canvas copies + * the symbol from the symbol document, into the current document and places a + * new <use element at the correct location on the canvas. + * + * Selected groups on the canvas can be added to the current document's symbols + * table, and symbols can be removed from the current document. This allows + * new symbols documents to be constructed and if saved in the prefs folder will + * make those symbols available for all future documents. + */ + +const int SYMBOL_ICON_SIZES[] = {16, 24, 32, 48, 64}; + +class SymbolsDialog : public DialogBase +{ +public: + SymbolsDialog( gchar const* prefsPath = "/dialogs/symbols" ); + ~SymbolsDialog() override; + + static SymbolsDialog& getInstance(); +private: + SymbolsDialog(SymbolsDialog const &) = delete; // no copy + SymbolsDialog &operator=(SymbolsDialog const &) = delete; // no assign + + static SymbolColumns *getColumns(); + void documentReplaced() override; + void selectionChanged(Inkscape::Selection *selection) override; + + Glib::ustring CURRENTDOC; + Glib::ustring ALLDOCS; + + void packless(); + void packmore(); + void zoomin(); + void zoomout(); + void rebuild(); + void insertSymbol(); + void revertSymbol(); + void defsModified(SPObject *object, guint flags); + SPDocument* selectedSymbols(); + Glib::ustring selectedSymbolId(); + Glib::ustring selectedSymbolDocTitle(); + void iconChanged(); + void iconDragDataGet(const Glib::RefPtr<Gdk::DragContext>& context, Gtk::SelectionData& selection_data, guint info, guint time); + void getSymbolsTitle(); + Glib::ustring documentTitle(SPDocument* doc); + std::pair<Glib::ustring, SPDocument*> getSymbolsSet(Glib::ustring title); + void addSymbol( SPObject* symbol, Glib::ustring doc_title); + SPDocument* symbolsPreviewDoc(); + void symbolsInDocRecursive (SPObject *r, std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > &l, Glib::ustring doc_title); + std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > symbolsInDoc( SPDocument* document, Glib::ustring doc_title); + void useInDoc(SPObject *r, std::vector<SPUse*> &l); + std::vector<SPUse*> useInDoc( SPDocument* document); + void beforeSearch(GdkEventKey* evt); + void unsensitive(GdkEventKey* evt); + void searchsymbols(); + void addSymbols(); + void addSymbolsInDoc(SPDocument* document); + void showOverlay(); + void hideOverlay(); + void clearSearch(); + bool callbackSymbols(); + bool callbackAllSymbols(); + void enableWidgets(bool enable); + Glib::ustring ellipsize(Glib::ustring data, size_t limit); + gchar const* styleFromUse( gchar const* id, SPDocument* document); + Glib::RefPtr<Gdk::Pixbuf> drawSymbol(SPObject *symbol); + Glib::RefPtr<Gdk::Pixbuf> getOverlay(gint width, gint height); + /* Keep track of all symbol template documents */ + std::map<Glib::ustring, SPDocument*> symbol_sets; + std::map<Glib::ustring, std::pair<Glib::ustring, SPSymbol*> > l; + // Index into sizes which is selected + int pack_size; + // Scale factor + int scale_factor; + bool sensitive; + double previous_height; + double previous_width; + bool all_docs_processed; + size_t number_docs; + size_t number_symbols; + size_t counter_symbols; + bool icons_found; + Glib::RefPtr<Gtk::ListStore> store; + Glib::ustring search_str; + Gtk::ComboBoxText* symbol_set; + Gtk::ProgressBar* progress_bar; + Gtk::Box* progress; + Gtk::SearchEntry* search; + Gtk::IconView* icon_view; + Gtk::Button* add_symbol; + Gtk::Button* remove_symbol; + Gtk::Button* zoom_in; + Gtk::Button* zoom_out; + Gtk::Button* more; + Gtk::Button* fewer; + Gtk::Box* tools; + Gtk::Overlay* overlay; + Gtk::Image* overlay_icon; + Gtk::Image* overlay_opacity; + Gtk::Label* overlay_title; + Gtk::Label* overlay_desc; + Gtk::ScrolledWindow *scroller; + Gtk::ToggleButton* fit_symbol; + Gtk::IconSize iconsize; + + SPDocument* preview_document; /* Document to render single symbol */ + + sigc::connection idleconn; + + /* For rendering the template drawing */ + unsigned key; + Inkscape::Drawing renderDrawing; + + std::vector<sigc::connection> gtk_connections; + Inkscape::auto_connection defs_modified; +}; + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + +#endif // INKSCAPE_UI_DIALOG_SYMBOLS_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/ui/dialog/template-load-tab.cpp b/src/ui/dialog/template-load-tab.cpp new file mode 100644 index 0000000..120ea25 --- /dev/null +++ b/src/ui/dialog/template-load-tab.cpp @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template abstract tab implementation + */ +/* Authors: + * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "template-widget.h" +#include "new-from-template.h" + +#include <glibmm/miscutils.h> +#include <glibmm/stringutils.h> +#include <glibmm/fileutils.h> +#include <gtkmm/messagedialog.h> +#include <gtkmm/scrolledwindow.h> +#include <iostream> + +#include "extension/extension.h" +#include "extension/db.h" +#include "inkscape.h" +#include "file.h" +#include "path-prefix.h" + +using namespace Inkscape::IO::Resource; + +namespace Inkscape { +namespace UI { + +TemplateLoadTab::TemplateLoadTab(NewFromTemplate* parent) + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) + , _current_keyword("") + , _keywords_combo(true) + , _current_search_type(ALL) + , _parent_widget(parent) + , _tlist_box(Gtk::ORIENTATION_VERTICAL) + , _search_box(Gtk::ORIENTATION_HORIZONTAL) +{ + set_border_width(10); + + _info_widget = Gtk::manage(new TemplateWidget()); + + Gtk::Label *title; + title = Gtk::manage(new Gtk::Label(_("Search:"))); + _search_box.pack_start(*title, Gtk::PACK_SHRINK); + _search_box.pack_start(_keywords_combo, Gtk::PACK_SHRINK, 5); + + _tlist_box.pack_start(_search_box, Gtk::PACK_SHRINK, 10); + + pack_start(_tlist_box, Gtk::PACK_SHRINK); + pack_start(*_info_widget, Gtk::PACK_EXPAND_WIDGET, 5); + + Gtk::ScrolledWindow *scrolled; + scrolled = Gtk::manage(new Gtk::ScrolledWindow()); + scrolled->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + scrolled->add(_tlist_view); + _tlist_box.pack_start(*scrolled, Gtk::PACK_EXPAND_WIDGET, 5); + + _keywords_combo.signal_changed().connect( + sigc::mem_fun(*this, &TemplateLoadTab::_keywordSelected)); + this->show_all(); + + _loadTemplates(); + _initLists(); +} + + +TemplateLoadTab::~TemplateLoadTab() += default; + + +void TemplateLoadTab::createTemplate() +{ + _info_widget->create(); +} + + +void TemplateLoadTab::_onRowActivated(const Gtk::TreeModel::Path &, Gtk::TreeViewColumn*) +{ + createTemplate(); + NewFromTemplate* parent = static_cast<NewFromTemplate*> (this->get_toplevel()); + parent->_onClose(); +} + +void TemplateLoadTab::_displayTemplateInfo() +{ + Glib::RefPtr<Gtk::TreeSelection> templateSelectionRef = _tlist_view.get_selection(); + if (templateSelectionRef->get_selected()) { + _current_template = (*templateSelectionRef->get_selected())[_columns.textValue]; + + _info_widget->display(_tdata[_current_template]); + _parent_widget->setCreateButtonSensitive(true); + } + +} + + +void TemplateLoadTab::_initKeywordsList() +{ + _keywords_combo.append(_("All")); + + for (const auto & _keyword : _keywords){ + _keywords_combo.append(_keyword); + } +} + + +void TemplateLoadTab::_initLists() +{ + _tlist_store = Gtk::ListStore::create(_columns); + _tlist_view.set_model(_tlist_store); + _tlist_view.append_column("", _columns.textValue); + _tlist_view.set_headers_visible(false); + + _initKeywordsList(); + _refreshTemplatesList(); + + Glib::RefPtr<Gtk::TreeSelection> templateSelectionRef = + _tlist_view.get_selection(); + templateSelectionRef->signal_changed().connect( + sigc::mem_fun(*this, &TemplateLoadTab::_displayTemplateInfo)); + + _tlist_view.signal_row_activated().connect( + sigc::mem_fun(*this, &TemplateLoadTab::_onRowActivated)); +} + +void TemplateLoadTab::_keywordSelected() +{ + _current_keyword = _keywords_combo.get_active_text(); + if (_current_keyword == ""){ + _current_keyword = _keywords_combo.get_entry_text(); + _current_search_type = USER_SPECIFIED; + } + else + _current_search_type = LIST_KEYWORD; + + if (_current_keyword == "" || _current_keyword == _("All")) + _current_search_type = ALL; + + _refreshTemplatesList(); +} + + +void TemplateLoadTab::_refreshTemplatesList() +{ + _tlist_store->clear(); + + switch (_current_search_type){ + case ALL :{ + for (auto & it : _tdata) { + Gtk::TreeModel::iterator iter = _tlist_store->append(); + Gtk::TreeModel::Row row = *iter; + row[_columns.textValue] = it.first; + } + break; + } + + case LIST_KEYWORD: { + for (auto & it : _tdata) { + if (it.second.keywords.count(_current_keyword.lowercase()) != 0){ + Gtk::TreeModel::iterator iter = _tlist_store->append(); + Gtk::TreeModel::Row row = *iter; + row[_columns.textValue] = it.first; + } + } + break; + } + + case USER_SPECIFIED : { + for (auto & it : _tdata) { + if (it.second.keywords.count(_current_keyword.lowercase()) != 0 || + it.second.display_name.lowercase().find(_current_keyword.lowercase()) != Glib::ustring::npos || + it.second.author.lowercase().find(_current_keyword.lowercase()) != Glib::ustring::npos || + it.second.short_description.lowercase().find(_current_keyword.lowercase()) != Glib::ustring::npos) + { + Gtk::TreeModel::iterator iter = _tlist_store->append(); + Gtk::TreeModel::Row row = *iter; + row[_columns.textValue] = it.first; + } + } + break; + } + } + + // reselect item + Gtk::TreeIter* item_to_select = nullptr; + for (Gtk::TreeModel::Children::iterator it = _tlist_store->children().begin(); it != _tlist_store->children().end(); ++it) { + Gtk::TreeModel::Row row = *it; + if (_current_template == row[_columns.textValue]) { + item_to_select = new Gtk::TreeIter(it); + break; + } + } + if (_tlist_store->children().size() == 1) { + delete item_to_select; + item_to_select = new Gtk::TreeIter(_tlist_store->children().begin()); + } + if (item_to_select) { + _tlist_view.get_selection()->select(*item_to_select); + delete item_to_select; + } else { + _current_template = ""; + _info_widget->clear(); + _parent_widget->setCreateButtonSensitive(false); + } +} + + +void TemplateLoadTab::_loadTemplates() +{ + for(auto &filename: get_filenames(TEMPLATES, {".svg"}, {"default."})) { + TemplateData tmp = _processTemplateFile(filename.raw()); + if (tmp.display_name != "") + _tdata[tmp.display_name] = tmp; + + } + // procedural templates + _getProceduralTemplates(); +} + + +TemplateLoadTab::TemplateData TemplateLoadTab::_processTemplateFile(const std::string &path) +{ + TemplateData result; + result.path = path; + result.is_procedural = false; + result.preview_name = ""; + + // convert path into valid template name + result.display_name = Glib::path_get_basename(path); + gsize n = 0; + while ((n = result.display_name.find_first_of("_", 0)) < Glib::ustring::npos){ + result.display_name.replace(n, 1, 1, ' '); + } + n = result.display_name.rfind(".svg"); + result.display_name.replace(n, 4, 1, ' '); + + Inkscape::XML::Document *rdoc = sp_repr_read_file(path.data(), SP_SVG_NS_URI); + if (rdoc){ + Inkscape::XML::Node *root = rdoc->root(); + if (strcmp(root->name(), "svg:svg") != 0){ // Wrong file format + return result; + } + + Inkscape::XML::Node *templateinfo = sp_repr_lookup_name(root, "inkscape:templateinfo"); + if (!templateinfo) { + templateinfo = sp_repr_lookup_name(root, "inkscape:_templateinfo"); // backwards-compatibility + } + + if (templateinfo == nullptr) // No template info + return result; + _getDataFromNode(templateinfo, result); + } + + return result; +} + +void TemplateLoadTab::_getProceduralTemplates() +{ + std::list<Inkscape::Extension::Effect *> effects; + Inkscape::Extension::db.get_effect_list(effects); + + std::list<Inkscape::Extension::Effect *>::iterator it = effects.begin(); + while (it != effects.end()){ + Inkscape::XML::Node *repr = (*it)->get_repr(); + Inkscape::XML::Node *templateinfo = sp_repr_lookup_name(repr, "inkscape:templateinfo"); + if (!templateinfo) { + templateinfo = sp_repr_lookup_name(repr, "inkscape:_templateinfo"); // backwards-compatibility + } + + if (templateinfo){ + TemplateData result; + result.display_name = (*it)->get_name(); + result.is_procedural = true; + result.path = ""; + result.tpl_effect = *it; + + _getDataFromNode(templateinfo, result, *it); + _tdata[result.display_name] = result; + } + ++it; + } +} + +// if the template data comes from a procedural template (aka Effect extension), +// attempt to translate within the extension's context (which might use a different gettext textdomain) +const char *_translate(const char* msgid, Extension::Extension *extension) +{ + if (extension) { + return extension->get_translation(msgid); + } else { + return _(msgid); + } +} + +void TemplateLoadTab::_getDataFromNode(Inkscape::XML::Node *dataNode, TemplateData &data, Extension::Extension *extension) +{ + Inkscape::XML::Node *currentData; + if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:name")) != nullptr) + data.display_name = _translate(currentData->firstChild()->content(), extension); + else if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:_name")) != nullptr) // backwards-compatibility + data.display_name = _translate(currentData->firstChild()->content(), extension); + + if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:author")) != nullptr) + data.author = currentData->firstChild()->content(); + + if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:shortdesc")) != nullptr) + data.short_description = _translate(currentData->firstChild()->content(), extension); + else if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:_shortdesc")) != nullptr) // backwards-compatibility + data.short_description = _translate(currentData->firstChild()->content(), extension); + + if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:preview")) != nullptr) + data.preview_name = currentData->firstChild()->content(); + + if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:date")) != nullptr) + data.creation_date = currentData->firstChild()->content(); + + if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:_keywords")) != nullptr){ + Glib::ustring tplKeywords = _translate(currentData->firstChild()->content(), extension); + while (!tplKeywords.empty()){ + std::size_t pos = tplKeywords.find_first_of(" "); + if (pos == Glib::ustring::npos) + pos = tplKeywords.size(); + + Glib::ustring keyword = tplKeywords.substr(0, pos).data(); + data.keywords.insert(keyword.lowercase()); + _keywords.insert(keyword.lowercase()); + + if (pos == tplKeywords.size()) + break; + tplKeywords.erase(0, pos+1); + } + } +} + +} +} diff --git a/src/ui/dialog/template-load-tab.h b/src/ui/dialog/template-load-tab.h new file mode 100644 index 0000000..ad87aac --- /dev/null +++ b/src/ui/dialog/template-load-tab.h @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template abstract tab class + */ +/* Authors: + * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_LOAD_TAB_H +#define INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_LOAD_TAB_H + +#include <gtkmm/box.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/frame.h> +#include <gtkmm/liststore.h> +#include <gtkmm/treeview.h> +#include <map> +#include <set> +#include <string> + +#include "xml/node.h" +#include "io/resource.h" +#include "extension/effect.h" + + +namespace Inkscape { + +namespace Extension { +class Extension; +} + +namespace UI { + +class TemplateWidget; +class NewFromTemplate; + +class TemplateLoadTab : public Gtk::Box +{ + +public: + struct TemplateData + { + bool is_procedural; + std::string path; + Glib::ustring display_name; + Glib::ustring author; + Glib::ustring short_description; + Glib::ustring long_description; // unused + Glib::ustring preview_name; + Glib::ustring creation_date; + std::set<Glib::ustring> keywords; + Inkscape::Extension::Effect *tpl_effect; + }; + + TemplateLoadTab(NewFromTemplate* parent); + ~TemplateLoadTab() override; + virtual void createTemplate(); + +protected: + class StringModelColumns : public Gtk::TreeModelColumnRecord + { + public: + StringModelColumns() + { + add(textValue); + } + + Gtk::TreeModelColumn<Glib::ustring> textValue; + }; + + Glib::ustring _current_keyword; + Glib::ustring _current_template; + std::map<Glib::ustring, TemplateData> _tdata; + std::set<Glib::ustring> _keywords; + + + virtual void _displayTemplateInfo(); + virtual void _initKeywordsList(); + virtual void _refreshTemplatesList(); + void _loadTemplates(); + void _initLists(); + + Gtk::Box _tlist_box; + Gtk::Box _search_box; + TemplateWidget *_info_widget; + + Gtk::ComboBoxText _keywords_combo; + + Gtk::TreeView _tlist_view; + Glib::RefPtr<Gtk::ListStore> _tlist_store; + StringModelColumns _columns; + +private: + enum SearchType + { + LIST_KEYWORD, + USER_SPECIFIED, + ALL + }; + + SearchType _current_search_type; + NewFromTemplate* _parent_widget; + + void _getDataFromNode(Inkscape::XML::Node *, TemplateData &, Extension::Extension *extension=nullptr); + void _getProceduralTemplates(); + void _getTemplatesFromDomain(Inkscape::IO::Resource::Domain domain); + void _keywordSelected(); + TemplateData _processTemplateFile(const std::string &); + + void _onRowActivated(const Gtk::TreeModel::Path &, Gtk::TreeViewColumn*); +}; + +} +} + +#endif diff --git a/src/ui/dialog/template-widget.cpp b/src/ui/dialog/template-widget.cpp new file mode 100644 index 0000000..595e74a --- /dev/null +++ b/src/ui/dialog/template-widget.cpp @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template - templates widget - implementation + */ +/* Authors: + * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "template-widget.h" + +#include <glibmm/miscutils.h> +#include <gtkmm/messagedialog.h> + +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "file.h" +#include "inkscape.h" + +#include "extension/implementation/implementation.h" + +#include "object/sp-namedview.h" + +namespace Inkscape { +namespace UI { + + +TemplateWidget::TemplateWidget() + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) + , _more_info_button(_("More info")) + , _short_description_label(" ") + , _template_name_label(_("no template selected")) + , _effect_prefs(nullptr) + , _preview_box(Gtk::ORIENTATION_HORIZONTAL) +{ + pack_start(_template_name_label, Gtk::PACK_SHRINK, 10); + pack_start(_preview_box, Gtk::PACK_SHRINK, 0); + + _preview_box.pack_start(_preview_image, Gtk::PACK_EXPAND_PADDING, 15); + _preview_box.pack_start(_preview_render, Gtk::PACK_EXPAND_PADDING, 10); + + _short_description_label.set_line_wrap(true); + + _more_info_button.set_halign(Gtk::ALIGN_END); + _more_info_button.set_valign(Gtk::ALIGN_CENTER); + pack_end(_more_info_button, Gtk::PACK_SHRINK); + + pack_end(_short_description_label, Gtk::PACK_SHRINK, 5); + + _more_info_button.signal_clicked().connect( + sigc::mem_fun(*this, &TemplateWidget::_displayTemplateDetails)); + _more_info_button.set_sensitive(false); +} + + +void TemplateWidget::create() +{ + if (_current_template.display_name == "") + return; + + if (_current_template.is_procedural){ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPDesktop *desc = sp_file_new_default(); + _current_template.tpl_effect->effect(desc); + DocumentUndo::clearUndo(desc->getDocument()); + desc->getDocument()->setModifiedSinceSave(false); + + // Apply cx,cy etc. from document + sp_namedview_window_from_document( desc ); + + if (desktop) + desktop->clearWaitingCursor(); + } + else { + sp_file_new(_current_template.path); + } +} + + +void TemplateWidget::display(TemplateLoadTab::TemplateData data) +{ + clear(); + _current_template = data; + + _template_name_label.set_text(_current_template.display_name); + _short_description_label.set_text(_current_template.short_description); + + if (data.preview_name != ""){ + std::string imagePath = Glib::build_filename(Glib::path_get_dirname(_current_template.path), + Glib::filename_from_utf8(_current_template.preview_name)); + _preview_image.set(imagePath); + _preview_image.show(); + } + else if (!data.is_procedural){ + Glib::ustring gPath = data.path.c_str(); + _preview_render.showImage(gPath); + _preview_render.show(); + } + + if (data.is_procedural){ + _effect_prefs = data.tpl_effect->get_imp()->prefs_effect(data.tpl_effect, SP_ACTIVE_DESKTOP, nullptr, nullptr); + pack_start(*_effect_prefs); + } + _more_info_button.set_sensitive(true); +} + +void TemplateWidget::clear() +{ + _template_name_label.set_text(""); + _short_description_label.set_text(""); + _preview_render.hide(); + _preview_image.hide(); + if (_effect_prefs != nullptr){ + remove (*_effect_prefs); + _effect_prefs = nullptr; + } + _more_info_button.set_sensitive(false); +} + +void TemplateWidget::_displayTemplateDetails() +{ + Glib::ustring message = _current_template.display_name + "\n\n"; + + if (!_current_template.author.empty()) { + message += _("Author"); + message += ": "; + message += _current_template.author + " " + _current_template.creation_date + "\n\n"; + } + + if (!_current_template.keywords.empty()){ + message += _("Keywords"); + message += ":"; + for (const auto & keyword : _current_template.keywords) { + message += " "; + message += keyword; + } + message += "\n\n"; + } + + if (!_current_template.path.empty()) { + message += _("Path"); + message += ": "; + message += _current_template.path; + message += "\n\n"; + } + + Gtk::MessageDialog dl(message, false, Gtk::MESSAGE_OTHER); + dl.run(); +} + +} +} diff --git a/src/ui/dialog/template-widget.h b/src/ui/dialog/template-widget.h new file mode 100644 index 0000000..b13a3b9 --- /dev/null +++ b/src/ui/dialog/template-widget.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template - template widget + */ +/* Authors: + * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_WIDGET_H +#define INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_WIDGET_H + +#include "svg-preview.h" + +#include <gtkmm/box.h> + +#include "template-load-tab.h" + + +namespace Inkscape { +namespace UI { + + +class TemplateWidget : public Gtk::Box +{ +public: + TemplateWidget (); + void create(); + void display(TemplateLoadTab::TemplateData); + void clear(); + +private: + TemplateLoadTab::TemplateData _current_template; + + Gtk::Button _more_info_button; + Gtk::Box _preview_box; + Gtk::Image _preview_image; + Dialog::SVGPreview _preview_render; + Gtk::Label _short_description_label; + Gtk::Label _template_name_label; + Gtk::Widget *_effect_prefs; + + void _displayTemplateDetails(); +}; + +} +} + +#endif diff --git a/src/ui/dialog/text-edit.cpp b/src/ui/dialog/text-edit.cpp new file mode 100644 index 0000000..b8d258b --- /dev/null +++ b/src/ui/dialog/text-edit.cpp @@ -0,0 +1,512 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Text editing dialog. + */ +/* Authors: + * Lauris Kaplinski <lauris@ximian.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <goejendaagh@zonnet.nl> + * Abhishek Sharma + * John Smith + * Tavmjong Bah + * + * Copyright (C) 1999-2013 Authors + * 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 "text-edit.h" + +#include <glibmm/i18n.h> +#include <glibmm/markup.h> + +#include <gtkmm/box.h> +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/buttonbox.h> +#include <gtkmm/label.h> +#include <gtkmm/notebook.h> +#include <gtkmm/textbuffer.h> +#include <gtkmm/textview.h> + +#ifdef WITH_GSPELL +# include <gspell/gspell.h> +#endif + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "style.h" +#include "text-editing.h" + +#include <libnrtype/FontFactory.h> +#include <libnrtype/font-instance.h> +#include <libnrtype/font-lister.h> + +#include "object/sp-flowtext.h" +#include "object/sp-text.h" +#include "object/sp-textpath.h" + +#include "io/resource.h" +#include "svg/css-ostringstream.h" +#include "ui/icon-names.h" +#include "ui/toolbar/text-toolbar.h" +#include "ui/widget/font-selector.h" + +#include "util/units.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +TextEdit::TextEdit() + : DialogBase("/dialogs/textandfont", "Text"), + selectChangedConn(), + subselChangedConn(), + selectModifiedConn(), + blocked(false), + /* + TRANSLATORS: Test string used in text and font dialog (when no + * text has been entered) to get a preview of the font. Choose + * some representative characters that users of your locale will be + * interested in.*/ + samplephrase(_("AaBbCcIiPpQq12369$\342\202\254\302\242?.;/()")) +{ + + std::string gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-text-edit.glade"); + Glib::RefPtr<Gtk::Builder> builder; + try { + builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("GtkBuilder file loading failed for save template dialog"); + return; + } + + Gtk::Box *contents; + Gtk::Notebook *notebook; + Gtk::Box *font_box; + Gtk::Box *feat_box; + + builder->get_widget("contents", contents); + builder->get_widget("notebook", notebook); + builder->get_widget("font_box", font_box); + builder->get_widget("feat_box", feat_box); + builder->get_widget("preview_label", preview_label); + builder->get_widget("preview_label2", preview_label2); + builder->get_widget("text_view", text_view); + builder->get_widget("setasdefault_button", setasdefault_button); + builder->get_widget("apply_button", apply_button); + + text_buffer = Glib::RefPtr<Gtk::TextBuffer>::cast_static(builder->get_object("text_buffer")); + + font_box->pack_start(font_selector, true, true); + font_box->reorder_child(font_selector, 0); + feat_box->pack_start(font_features, true, true); + feat_box->reorder_child(font_features, 0); + +#ifdef WITH_GSPELL + /* + TODO: Use computed xml:lang attribute of relevant element, if present, to specify the + language (either as 2nd arg of gtkspell_new_attach, or with explicit + gtkspell_set_language call in; see advanced.c example in gtkspell docs). + onReadSelection looks like a suitable place. + */ + GspellTextView *gspell_view = gspell_text_view_get_from_gtk_text_view(text_view->gobj()); + gspell_text_view_basic_setup(gspell_view); +#endif + + add(*contents); + + /* Signal handlers */ + text_buffer->signal_changed().connect(sigc::mem_fun(*this, &TextEdit::onChange)); + setasdefault_button->signal_clicked().connect(sigc::mem_fun(*this, &TextEdit::onSetDefault)); + apply_button->signal_clicked().connect(sigc::mem_fun(*this, &TextEdit::onApply)); + fontChangedConn = font_selector.connectChanged(sigc::mem_fun(*this, &TextEdit::onFontChange)); + fontFeaturesChangedConn = font_features.connectChanged(sigc::mem_fun(*this, &TextEdit::onChange)); + notebook->signal_switch_page().connect(sigc::mem_fun(*this, &TextEdit::onFontFeatures)); + + font_selector.set_name("TextEdit"); + + show_all_children(); +} + +TextEdit::~TextEdit() +{ + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + fontChangedConn.disconnect(); + fontFeaturesChangedConn.disconnect(); +} + +void TextEdit::onReadSelection ( gboolean dostyle, gboolean /*docontent*/ ) +{ + if (blocked) + return; + + blocked = true; + + SPItem *text = getSelectedTextItem (); + + Glib::ustring phrase = samplephrase; + + if (text) + { + guint items = getSelectedTextCount (); + bool has_one_item = items == 1; + text_view->set_sensitive(has_one_item); + apply_button->set_sensitive(false); + setasdefault_button->set_sensitive(true); + + Glib::ustring str = sp_te_get_string_multiline(text); + if (!str.empty()) { + if (has_one_item) { + text_buffer->set_text(str); + text_buffer->set_modified(false); + } + phrase = str; + + } else { + text_buffer->set_text(""); + } + + text->getRepr(); // was being called but result ignored. Check this. + } else { + text_view->set_sensitive(false); + apply_button->set_sensitive(false); + setasdefault_button->set_sensitive(false); + } + + if (dostyle && text) { + auto *desktop = getDesktop(); + + // create temporary style + SPStyle query(desktop->getDocument()); + + // Query style from desktop into it. This returns a result flag and fills query with the + // style of subselection, if any, or selection + + int result_numbers = sp_desktop_query_style (desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + + // If querying returned nothing, read the style from the text tool prefs (default style for new texts). + if (result_numbers == QUERY_STYLE_NOTHING) { + query.readFromPrefs("/tools/text"); + } + + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + + // Update family/style based on selection. + font_lister->selection_update(); + Glib::ustring fontspec = font_lister->get_fontspec(); + + // Update Font Face. + font_selector.update_font (); + + // Update Size. + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + double size = sp_style_css_size_px_to_units(query.font_size.computed, unit); + font_selector.update_size (size); + selected_fontsize = size; + // Update font features (variant) widget + //int result_features = + sp_desktop_query_style (desktop, &query, QUERY_STYLE_PROPERTY_FONTVARIANTS); + int result_features = + sp_desktop_query_style (desktop, &query, QUERY_STYLE_PROPERTY_FONTFEATURESETTINGS); + font_features.update( &query, result_features == QUERY_STYLE_MULTIPLE_DIFFERENT, fontspec ); + Glib::ustring features = font_features.get_markup(); + + // Update Preview + setPreviewText (fontspec, features, phrase); + } + + blocked = false; +} + + +void TextEdit::setPreviewText (Glib::ustring font_spec, Glib::ustring font_features, Glib::ustring phrase) +{ + if (font_spec.empty()) { + preview_label->set_markup(""); + preview_label2->set_markup(""); + return; + } + + // Limit number of lines in preview to arbitrary amount to prevent Text and Font dialog + // from growing taller than a desktop + const int max_lines = 4; + // Ignore starting empty lines; they would show up as nothing + auto start_pos = phrase.find_first_not_of(" \n\r\t"); + if (start_pos == Glib::ustring::npos) { + start_pos = 0; + } + // Now take up to max_lines + auto end_pos = Glib::ustring::npos; + auto from = start_pos; + for (int i = 0; i < max_lines; ++i) { + end_pos = phrase.find("\n", from); + if (end_pos == Glib::ustring::npos) { break; } + from = end_pos + 1; + } + Glib::ustring phrase_trimmed = phrase.substr(start_pos, end_pos != Glib::ustring::npos ? end_pos - start_pos : end_pos); + + Glib::ustring font_spec_escaped = Glib::Markup::escape_text( font_spec ); + Glib::ustring phrase_escaped = Glib::Markup::escape_text(phrase_trimmed); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + double pt_size = + Inkscape::Util::Quantity::convert( + sp_style_css_size_units_to_px(font_selector.get_fontsize(), unit), "px", "pt"); + pt_size = std::min(pt_size, 100.0); + + // Pango font size is in 1024ths of a point + Glib::ustring size = std::to_string( int(pt_size * PANGO_SCALE) ); + Glib::ustring markup = "<span font=\'" + font_spec_escaped + + "\' size=\'" + size + "\'"; + if (!font_features.empty()) { + markup += " font_features=\'" + font_features + "\'"; + } + markup += ">" + phrase_escaped + "</span>"; + + preview_label->set_markup (markup); + preview_label2->set_markup (markup); +} + + +SPItem *TextEdit::getSelectedTextItem () +{ + if (!getDesktop()) + return nullptr; + + auto tmp= getDesktop()->getSelection()->items(); + for(auto i=tmp.begin();i!=tmp.end();++i) + { + if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) + return *i; + } + + return nullptr; +} + + +unsigned TextEdit::getSelectedTextCount () +{ + if (!getDesktop()) + return 0; + + unsigned int items = 0; + + auto tmp= getDesktop()->getSelection()->items(); + for(auto i=tmp.begin();i!=tmp.end();++i) + { + if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) + ++items; + } + + return items; +} + +void TextEdit::documentReplaced() +{ + onReadSelection(true, true); +} + +void TextEdit::selectionChanged(Selection *selection) +{ + onReadSelection(true, true); +} + +void TextEdit::selectionModified(Selection *selection, guint flags) +{ + bool style = ((flags & (SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG )) != 0 ); + bool content = ((flags & (SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_TEXT_CONTENT_MODIFIED_FLAG )) != 0 ); + onReadSelection (style, content); +} + + +void TextEdit::updateObjectText ( SPItem *text ) +{ + Gtk::TextIter start, end; + + // write text + if (text_buffer->get_modified()) { + text_buffer->get_bounds(start, end); + Glib::ustring str = text_buffer->get_text(start, end); + sp_te_set_repr_text_multiline (text, str.c_str()); + text_buffer->set_modified(false); + } +} + +SPCSSAttr *TextEdit::fillTextStyle () +{ + SPCSSAttr *css = sp_repr_css_attr_new (); + + Glib::ustring fontspec = font_selector.get_fontspec(); + + if( !fontspec.empty() ) { + + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + fontlister->fill_css( css, fontspec ); + + // TODO, possibly move this to FontLister::set_css to be shared. + Inkscape::CSSOStringStream os; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + if (prefs->getBool("/options/font/textOutputPx", true)) { + os << sp_style_css_size_units_to_px(font_selector.get_fontsize(), unit) + << sp_style_get_css_unit_string(SP_CSS_UNIT_PX); + } else { + os << font_selector.get_fontsize() << sp_style_get_css_unit_string(unit); + } + sp_repr_css_set_property (css, "font-size", os.str().c_str()); + } + + // Font features + font_features.fill_css( css ); + + return css; +} + +void TextEdit::onSetDefault() +{ + SPCSSAttr *css = fillTextStyle (); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + blocked = true; + prefs->mergeStyle("/tools/text/style", css); + blocked = false; + + sp_repr_css_attr_unref (css); + + setasdefault_button->set_sensitive ( false ); +} + +void TextEdit::onApply() +{ + blocked = true; + + SPDesktop *desktop = getDesktop(); + + unsigned items = 0; + auto item_list = desktop->getSelection()->items(); + SPCSSAttr *css = fillTextStyle (); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + for(auto i=item_list.begin();i!=item_list.end();++i){ + // apply style to the reprs of all text objects in the selection + if (SP_IS_TEXT (*i) || (SP_IS_FLOWTEXT (*i)) ) { + ++items; + } + } + if (items == 1) { + double factor = font_selector.get_fontsize() / selected_fontsize; + prefs->setDouble("/options/font/scaleLineHeightFromFontSIze", factor); + } + sp_desktop_set_style(desktop, css, true); + + if (items == 0) { + // no text objects; apply style to prefs for new objects + prefs->mergeStyle("/tools/text/style", css); + setasdefault_button->set_sensitive ( false ); + + } else if (items == 1) { + // exactly one text object; now set its text, too + SPItem *item = desktop->getSelection()->singleItem(); + if (SP_IS_TEXT (item) || SP_IS_FLOWTEXT(item)) { + updateObjectText (item); + SPStyle *item_style = item->style; + if (SP_IS_TEXT(item) && item_style->inline_size.value == 0) { + css = sp_css_attr_from_style(item_style, SP_STYLE_FLAG_IFSET); + sp_repr_css_unset_property(css, "inline-size"); + item->changeCSS(css, "style"); + } + } + } + + // Update FontLister + Glib::ustring fontspec = font_selector.get_fontspec(); + if( !fontspec.empty() ) { + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + fontlister->set_fontspec( fontspec, false ); + } + + // complete the transaction + DocumentUndo::done(desktop->getDocument(), _("Set text style"), INKSCAPE_ICON("draw-text")); + apply_button->set_sensitive ( false ); + + sp_repr_css_attr_unref (css); + + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + font_lister->update_font_list(desktop->getDocument()); + + blocked = false; +} + +void TextEdit::onFontFeatures(Gtk::Widget * widgt, int pos) +{ + if (pos == 1) { + Glib::ustring fontspec = font_selector.get_fontspec(); + if (!fontspec.empty()) { + font_instance *res = font_factory::Default()->FaceFromFontSpecification(fontspec.c_str()); + if (res && !res->fulloaded) { + res->InitTheFace(true); + font_features.update_opentype(fontspec); + } + } + } +} + +void TextEdit::onChange() +{ + if (blocked) { + return; + } + + Gtk::TextIter start, end; + text_buffer->get_bounds(start, end); + Glib::ustring str = text_buffer->get_text(start, end); + + Glib::ustring fontspec = font_selector.get_fontspec(); + Glib::ustring features = font_features.get_markup(); + const Glib::ustring& phrase = str.empty() ? samplephrase : str; + setPreviewText(fontspec, features, phrase); + + SPItem *text = getSelectedTextItem(); + if (text) { + apply_button->set_sensitive ( true ); + } + + setasdefault_button->set_sensitive ( true); +} + +void TextEdit::onFontChange(Glib::ustring fontspec) +{ + // Is not necessary update open type features this done when user click on font features tab + onChange(); +} + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/text-edit.h b/src/ui/dialog/text-edit.h new file mode 100644 index 0000000..72b8589 --- /dev/null +++ b/src/ui/dialog/text-edit.h @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Text-edit + */ +/* Authors: + * Lauris Kaplinski <lauris@ximian.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <goejendaagh@zonnet.nl> + * John Smith + * Kris De Gussem <Kris.DeGussem@gmail.com> + * Tavmjong Bah + * + * Copyright (C) 1999-2013 Authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_TEXT_EDIT_H +#define INKSCAPE_UI_DIALOG_TEXT_EDIT_H + +#include <glibmm/refptr.h> + +#include "ui/dialog/dialog-base.h" +#include "ui/widget/frame.h" + +#include "ui/widget/font-selector.h" +#include "ui/widget/font-variants.h" + +namespace Gtk { +class Box; +class Button; +class ButtonBox; +class Label; +class Notebook; +class TextBuffer; +class TextView; +} + +class SPItem; +class font_instance; +class SPCSSAttr; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +#define VB_MARGIN 4 +/** + * The TextEdit class defines the Text and font dialog. + * + * The Text and font dialog allows you to set the font family, style and size + * and shows a preview of the result. The dialogs layout settings include + * horizontal and vertical alignment and inter line distance. + */ +class TextEdit : public DialogBase +{ +public: + TextEdit(); + ~TextEdit() override; + + void documentReplaced() override; + void selectionChanged(Selection *selection) override; + void selectionModified(Selection *selection, guint flags) override; + + /** + * Helper function which returns a new instance of the dialog. + * getInstance is needed by the dialog manager (Inkscape::UI::Dialog::DialogManager). + */ + static TextEdit &getInstance() { return *new TextEdit(); } + +protected: + + /** + * Callback for pressing the default button. + */ + void onSetDefault (); + + /** + * Callback for pressing the apply button. + */ + void onApply (); + + /** + * Called whenever something 'changes' on canvas. + * + * onReadSelection gets the currently selected item from the canvas and sets all the controls in this dialog to the correct state. + * + * @param dostyle Indicates whether the modification of the user includes a style change. + * @param content Indicates whether the modification of the user includes a style change. Actually refers to the question if we do want to show the content? (Parameter currently not used) + */ + void onReadSelection (gboolean style, gboolean content); + + /** + * Callback invoked when the user modifies the text of the selected text object. + * + * onTextChange is responsible for initiating the commands after the user + * modified the text in the selected object. The UI of the dialog is + * updated. The subfunction setPreviewText updates the preview label. + * + * @param self pointer to the current instance of the dialog. + */ + void onChange (); + void onFontFeatures (Gtk::Widget * widgt, int pos); + + /** + * Callback invoked when the user modifies the font through the dialog or the tools control bar. + * + * onFontChange updates the dialog UI. The subfunction setPreviewText updates the preview label. + * + * @param fontspec for the text to be previewed. + */ + void onFontChange (Glib::ustring fontspec); + + /** + * Get the selected text off the main canvas. + * + * @return SPItem pointer to the selected text object + */ + SPItem *getSelectedTextItem (); + + /** + * Count the number of text objects in the selection on the canvas. + */ + unsigned getSelectedTextCount (); + + /** + * Helper function to create markup from a fontspec and display in the preview label. + * + * @param fontspec for the text to be previewed. + * @param font_features for text to be previewed (in CSS format). + * @param phrase text to be shown. + */ + void setPreviewText (Glib::ustring font_spec, Glib::ustring font_features, Glib::ustring phrase); + + void updateObjectText ( SPItem *text ); + SPCSSAttr *fillTextStyle (); + +private: + + /* + * All the dialogs widgets + */ + + // Tab 1: Font ---------------------- // + Inkscape::UI::Widget::FontSelector font_selector; + Inkscape::UI::Widget::FontVariations font_variations; + Gtk::Label *preview_label; // Share with variants tab? + + // Tab 2: Text ---------------------- // + Gtk::TextView *text_view; + Glib::RefPtr<Gtk::TextBuffer> text_buffer; + + // Tab 3: Features ----------------- // + Inkscape::UI::Widget::FontVariants font_features; + Gtk::Label *preview_label2; // Could reparent preview_label but having a second label is probably easier. + + // Shared ------- ------------------ // + Gtk::Button *setasdefault_button; + Gtk::Button *apply_button; + + // Signals + sigc::connection selectChangedConn; + sigc::connection subselChangedConn; + sigc::connection selectModifiedConn; + sigc::connection fontChangedConn; + sigc::connection fontFeaturesChangedConn; + + // Other + double selected_fontsize; + bool blocked; + const Glib::ustring samplephrase; + + + TextEdit(TextEdit const &d) = delete; + TextEdit operator=(TextEdit const &d) = delete; +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_TEXT_EDIT_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/ui/dialog/tile.cpp b/src/ui/dialog/tile.cpp new file mode 100644 index 0000000..661c4f6 --- /dev/null +++ b/src/ui/dialog/tile.cpp @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple dialog for creating grid type arrangements of selected objects + * + * Authors: + * Bob Jamison ( based off trace dialog) + * John Cliff + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * Declara Denis + * + * Copyright (C) 2004 Bob Jamison + * Copyright (C) 2004 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tile.h" + +#include <glibmm/i18n.h> + +#include "ui/dialog/grid-arrange-tab.h" +#include "ui/dialog/polar-arrange-tab.h" +#include "ui/dialog/align-and-distribute.h" +#include "ui/icon-names.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +Gtk::Box& create_tab_label(const char* label_text, const char* icon_name) { + auto box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 4); + auto image = Gtk::make_managed<Gtk::Image>(); + image->set_from_icon_name(icon_name, Gtk::ICON_SIZE_MENU); + auto label = Gtk::make_managed<Gtk::Label>(label_text, true); + box->pack_start(*image, false, true); + box->pack_start(*label, false, true); + box->show_all(); + return *box; +} + +ArrangeDialog::ArrangeDialog() + : DialogBase("/dialogs/gridtiler", "AlignDistribute") +{ + _align_tab = Gtk::manage(new AlignAndDistribute(this)); + _arrangeBox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + _arrangeBox->set_valign(Gtk::ALIGN_START); + _notebook = Gtk::manage(new Gtk::Notebook()); + _gridArrangeTab = Gtk::manage(new GridArrangeTab(this)); + _polarArrangeTab = Gtk::manage(new PolarArrangeTab(this)); + + set_valign(Gtk::ALIGN_START); + + _notebook->set_valign(Gtk::ALIGN_START); + _notebook->append_page(*_align_tab, create_tab_label(C_("Arrange dialog", "Align"), INKSCAPE_ICON("dialog-align-and-distribute"))); + // TRANSLATORS: "Grid" refers to grid (columns/rows) arrangement + _notebook->append_page(*_gridArrangeTab, create_tab_label(C_("Arrange dialog", "Grid"), INKSCAPE_ICON("arrange-grid"))); + // TRANSLATORS: "Circular" refers to circular/radial arrangement + _notebook->append_page(*_polarArrangeTab, create_tab_label(C_("Arrange dialog", "Circular"), INKSCAPE_ICON("arrange-circular"))); + _arrangeBox->pack_start(*_notebook); + _notebook->signal_switch_page().connect([=](Widget*, guint page){ + update_arrange_btn(); + }); + pack_start(*_arrangeBox); + + // Add button + _arrangeButton = Gtk::manage(new Gtk::Button(C_("Arrange dialog", "_Arrange"))); + _arrangeButton->signal_clicked().connect(sigc::mem_fun(*this, &ArrangeDialog::_apply)); + _arrangeButton->set_use_underline(true); + _arrangeButton->set_tooltip_text(_("Arrange selected objects")); + _arrangeButton->get_style_context()->add_class("wide-apply-button"); + _arrangeButton->set_no_show_all(); + + Gtk::ButtonBox *button_box = Gtk::manage(new Gtk::ButtonBox()); + button_box->set_layout(Gtk::BUTTONBOX_CENTER); + button_box->set_spacing(6); + button_box->set_border_width(4); + button_box->set_valign(Gtk::ALIGN_FILL); + + button_box->pack_end(*_arrangeButton); + pack_start(*button_box); + + show(); + show_all_children(); + update_arrange_btn(); +} + +void ArrangeDialog::update_arrange_btn() { + // "align" page doesn't use "Arrange" button + if (_notebook->get_current_page() == 0) { + _arrangeButton->hide(); + } + else { + _arrangeButton->show(); + } +} + +ArrangeDialog::~ArrangeDialog() +{ } + +void ArrangeDialog::_apply() +{ + switch(_notebook->get_current_page()) + { + case 0: + // not applicable to align panel + break; + case 1: + _gridArrangeTab->arrange(); + break; + case 2: + _polarArrangeTab->arrange(); + break; + } +} + +void ArrangeDialog::desktopReplaced() +{ + _gridArrangeTab->setDesktop(getDesktop()); + _align_tab->desktop_changed(getDesktop()); +} + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/tile.h b/src/ui/dialog/tile.h new file mode 100644 index 0000000..484c3a0 --- /dev/null +++ b/src/ui/dialog/tile.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for creating grid type arrangements of selected objects + */ +/* Authors: + * Bob Jamison ( based off trace dialog) + * John Cliff + * Other dudes from The Inkscape Organization + * Declara Denis + * + * Copyright (C) 2004 Bob Jamison + * Copyright (C) 2004 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_DIALOG_TILE_H +#define SEEN_UI_DIALOG_TILE_H + +#include <gtkmm/box.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/notebook.h> +#include <gtkmm/radiobutton.h> + +#include "ui/dialog/dialog-base.h" + +namespace Gtk { +class Button; +class Grid; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class AlignAndDistribute; +class ArrangeTab; +class GridArrangeTab; +class PolarArrangeTab; + +class ArrangeDialog : public DialogBase +{ +private: + Gtk::Box *_arrangeBox; + Gtk::Notebook *_notebook; + AlignAndDistribute* _align_tab; + GridArrangeTab *_gridArrangeTab; + PolarArrangeTab *_polarArrangeTab; + Gtk::Button *_arrangeButton; + +public: + ArrangeDialog(); + ~ArrangeDialog() override; + + void desktopReplaced() override; + + void update_arrange_btn(); + + /** + * Callback from Apply + */ + void _apply(); + + static ArrangeDialog& getInstance() { return *new ArrangeDialog(); } +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + + +#endif /* __TILEDIALOG_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/ui/dialog/tracedialog.cpp b/src/ui/dialog/tracedialog.cpp new file mode 100644 index 0000000..114e341 --- /dev/null +++ b/src/ui/dialog/tracedialog.cpp @@ -0,0 +1,485 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Bitmap tracing settings dialog - second implementation. + */ +/* Authors: + * Marc Jeanmougin <marc.jeanmougin@telecom-paristech.fr> + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tracedialog.h" + +#include <glibmm/i18n.h> +#include <gtkmm.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/notebook.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/spinbutton.h> +#include <gtkmm/stack.h> + +#include "desktop.h" +#include "display/cairo-utils.h" +#include "inkscape.h" +#include "io/resource.h" +#include "io/sys.h" +#include "selection.h" +#include "trace/autotrace/inkscape-autotrace.h" +#include "trace/depixelize/inkscape-depixelize.h" +#include "trace/potrace/inkscape-potrace.h" +#include "ui/util.h" + +// This maps the column ids in the glade file to useful enums +static const std::map<std::string, Inkscape::Trace::Potrace::TraceType> trace_types = { + {"SS_BC", Inkscape::Trace::Potrace::TRACE_BRIGHTNESS}, + {"SS_ED", Inkscape::Trace::Potrace::TRACE_CANNY}, + {"SS_CQ", Inkscape::Trace::Potrace::TRACE_QUANT}, + {"SS_AT", Inkscape::Trace::Potrace::AUTOTRACE_SINGLE}, + {"SS_CT", Inkscape::Trace::Potrace::AUTOTRACE_CENTERLINE}, + + {"MS_BS", Inkscape::Trace::Potrace::TRACE_BRIGHTNESS_MULTI}, + {"MS_C", Inkscape::Trace::Potrace::TRACE_QUANT_COLOR}, + {"MS_BW", Inkscape::Trace::Potrace::TRACE_QUANT_MONO}, + {"MS_AT", Inkscape::Trace::Potrace::AUTOTRACE_MULTI}, +}; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class TraceDialogImpl2 : public TraceDialog { + public: + TraceDialogImpl2(); + ~TraceDialogImpl2() override; + + void selectionModified(Selection *selection, guint flags) override; + void selectionChanged(Inkscape::Selection *selection) override; +private: + Inkscape::Trace::Tracer tracer; + void traceProcess(bool do_i_trace); + void abort(); + + void previewCallback(bool force); + bool previewResize(const Cairo::RefPtr<Cairo::Context>&); + void traceCallback(); + void onSetDefaults(); + void show_hide_params(); + void schedule_preview_update(); + static gboolean update_cb(gpointer user_data); + + Glib::RefPtr<Gtk::Builder> builder; + + Glib::RefPtr<Gtk::Adjustment> MS_scans, PA_curves, PA_islands, PA_sparse1, PA_sparse2, SS_AT_ET_T, SS_AT_FI_T, SS_BC_T, SS_CQ_T, + SS_ED_T, optimize, smooth, speckles; + Gtk::ComboBoxText *CBT_SS, *CBT_MS; + Gtk::CheckButton *CB_invert, *CB_MS_smooth, *CB_MS_stack, *CB_MS_rb, *CB_speckles, *CB_smooth, *CB_optimize, *CB_PA_optimize, + /* *CB_live,*/ *CB_SIOX; + Gtk::CheckButton* CB_SIOX1; + Gtk::CheckButton* CB_speckles1; + Gtk::CheckButton* CB_smooth1; + Gtk::CheckButton* CB_optimize1; + Gtk::RadioButton *RB_PA_voronoi; + Gtk::Button *B_RESET, *B_STOP, *B_OK, *B_Update; + Gtk::Box *mainBox; + Gtk::Notebook *choice_tab; + Glib::RefPtr<Gdk::Pixbuf> scaledPreview; + Gtk::DrawingArea *previewArea; + Gtk::Box* orient_box; + Gtk::Frame* _preview_frame; + Gtk::Grid* _param_grid; + Gtk::CheckButton* _live_preview; + guint _source = 0; +}; + +enum Page { + SingleScan, MultiScan, PixelArt +}; + +void TraceDialogImpl2::traceProcess(bool do_i_trace) +{ + SPDesktop* desktop = getDesktop(); + if (desktop) + desktop->setWaitingCursor(); + + auto current_page = choice_tab->get_current_page(); + + auto cb_siox = current_page == SingleScan ? CB_SIOX : CB_SIOX1; + if (cb_siox->get_active()) + tracer.enableSiox(true); + else + tracer.enableSiox(false); + + Glib::ustring type = current_page == SingleScan ? CBT_SS->get_active_id() : CBT_MS->get_active_id(); + + bool use_autotrace = false; + Inkscape::Trace::Autotrace::AutotraceTracingEngine ate; // TODO + + auto potraceType = trace_types.find(type); + assert(potraceType != trace_types.end()); + switch (potraceType->second) { + case Inkscape::Trace::Potrace::AUTOTRACE_SINGLE: + use_autotrace = true; + ate.opts->color_count = 2; + break; + case Inkscape::Trace::Potrace::AUTOTRACE_CENTERLINE: + use_autotrace = true; + ate.opts->color_count = 2; + ate.opts->centerline = true; + ate.opts->preserve_width = true; + break; + case Inkscape::Trace::Potrace::AUTOTRACE_MULTI: + use_autotrace = true; + ate.opts->color_count = (int)MS_scans->get_value() + 1; + break; + default: + break; + } + + ate.opts->filter_iterations = (int) SS_AT_FI_T->get_value(); + ate.opts->error_threshold = SS_AT_ET_T->get_value(); + + Inkscape::Trace::Potrace::PotraceTracingEngine pte( + potraceType->second, CB_invert->get_active(), (int)SS_CQ_T->get_value(), SS_BC_T->get_value(), + 0., // Brightness floor + SS_ED_T->get_value(), (int)MS_scans->get_value(), CB_MS_stack->get_active(), CB_MS_smooth->get_active(), + CB_MS_rb->get_active()); + + auto cb_optimize = current_page == SingleScan ? CB_optimize : CB_optimize1; + pte.potraceParams->opticurve = cb_optimize->get_active(); + pte.potraceParams->opttolerance = optimize->get_value(); + + auto cb_smooth = current_page == SingleScan ? CB_smooth : CB_smooth1; + pte.potraceParams->alphamax = cb_smooth->get_active() ? smooth->get_value() : 0; + + auto cb_speckles = current_page == SingleScan ? CB_speckles : CB_speckles1; + pte.potraceParams->turdsize = cb_speckles->get_active() ? (int)speckles->get_value() : 0; + + //Inkscape::Trace::Autotrace::AutotraceTracingEngine ate; // TODO + Inkscape::Trace::Depixelize::DepixelizeTracingEngine dte( + RB_PA_voronoi->get_active() ? Inkscape::Trace::Depixelize::TraceType::TRACE_VORONOI : Inkscape::Trace::Depixelize::TraceType::TRACE_BSPLINES, + PA_curves->get_value(), (int) PA_islands->get_value(), + (int) PA_sparse1->get_value(), PA_sparse2->get_value(), + CB_PA_optimize->get_active()); + + //TODO: preview for multiscan: grayscale bitmap with matching number of gray levels? + // Currently there's one created using brightness threshold, which is wrong and misleading + Glib::RefPtr<Gdk::Pixbuf> pixbuf = tracer.getSelectedImage(); + if (pixbuf) { + scaledPreview = use_autotrace ? ate.preview(pixbuf) : pte.preview(pixbuf); + } + else { + scaledPreview.reset(); + } + + previewArea->queue_draw(); + + if (do_i_trace){ + if (current_page == PixelArt){ + tracer.trace(&dte); + printf("dt\n"); + } else if (use_autotrace) { + tracer.trace(&ate); + printf("at\n"); + } else if (current_page == SingleScan || current_page == MultiScan) { + tracer.trace(&pte); + printf("pt\n"); + } + } + + if (desktop) + desktop->clearWaitingCursor(); +} + +bool TraceDialogImpl2::previewResize(const Cairo::RefPtr<Cairo::Context>& cr) +{ + /* Checkerboard - is not applicable here; left for reference + if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_toplevel())) { + auto color = wnd->get_style_context()->get_background_color(); + auto background = + gint32(0xff * color.get_red()) << 24 | + gint32(0xff * color.get_green()) << 16 | + gint32(0xff * color.get_blue()) << 8 | + 0xff; + + auto device_scale = get_scale_factor(); + Cairo::RefPtr<Cairo::Pattern> pattern(new Cairo::Pattern(ink_cairo_pattern_create_checkerboard(background))); + cr->save(); + cr->scale(device_scale, device_scale); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source(pattern); + cr->paint(); + cr->restore(); + } */ + + if (scaledPreview) { + int width = scaledPreview->get_width(); + int height = scaledPreview->get_height(); + const Gtk::Allocation &vboxAlloc = previewArea->get_allocation(); + double scaleFX = vboxAlloc.get_width() / (double)width; + double scaleFY = vboxAlloc.get_height() / (double)height; + double scaleFactor = scaleFX > scaleFY ? scaleFY : scaleFX; + int newWidth = (int)(((double)width) * scaleFactor); + int newHeight = (int)(((double)height) * scaleFactor); + int offsetX = (vboxAlloc.get_width() - newWidth)/2; + int offsetY = (vboxAlloc.get_height() - newHeight)/2; + cr->scale(scaleFactor, scaleFactor); + Gdk::Cairo::set_source_pixbuf(cr, scaledPreview, offsetX / scaleFactor, offsetY / scaleFactor); + cr->paint(); + } + else { + cr->set_source_rgba(0, 0, 0, 0); + cr->paint(); + } + + return false; +} + +void TraceDialogImpl2::abort() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) + desktop->clearWaitingCursor(); + tracer.abort(); +} + +void TraceDialogImpl2::selectionChanged(Inkscape::Selection *selection) { + // refresh preview when selecting or deselecting images (in general: when selection changes) + previewCallback(false); +} + +void TraceDialogImpl2::selectionModified(Selection *selection, guint flags) { + // Note: original refresh condition commended out, as selection modified fires when moving images around slowing on-canvas operations. + // Note: is there a use-case where preview needs to be refreshed when images are manipulated? I haven't found one. + +// if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG)) { + + // this condition is satisfied when imported image gets places on canvas; this is actually wrong + // and there should just be "selection changed", but there isn't + auto mask = SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG; + if ((flags & mask) == mask) { + previewCallback(false); + } +} + +void TraceDialogImpl2::onSetDefaults() +{ + MS_scans->set_value(8); + PA_curves->set_value(1); + PA_islands->set_value(5); + PA_sparse1->set_value(4); + PA_sparse2->set_value(1); + SS_AT_FI_T->set_value(4); + SS_AT_ET_T->set_value(2); + SS_BC_T->set_value(0.45); + SS_CQ_T->set_value(64); + SS_ED_T->set_value(.65); + optimize->set_value(0.2); + smooth->set_value(1); + speckles->set_value(2); + CB_invert->set_active(false); + CB_MS_smooth->set_active(true); + CB_MS_stack->set_active(true); + CB_MS_rb->set_active(false); + CB_speckles->set_active(true); + CB_smooth->set_active(true); + CB_optimize->set_active(true); + CB_speckles1->set_active(true); + CB_smooth1->set_active(true); + CB_optimize1->set_active(true); + CB_PA_optimize->set_active(false); + CB_SIOX->set_active(false); + CB_SIOX1->set_active(false); +} + +void TraceDialogImpl2::previewCallback(bool force) { + if (force || (_live_preview->get_active() && is_widget_effectively_visible(this))) { + traceProcess(false); + } +} + +void TraceDialogImpl2::traceCallback() { traceProcess(true); } + + +TraceDialogImpl2::TraceDialogImpl2() + : TraceDialog() +{ + const std::string req_widgets[] = { "MS_scans", "PA_curves", "PA_islands", "PA_sparse1", "PA_sparse2", + "SS_AT_FI_T", "SS_AT_ET_T", "SS_BC_T", "SS_CQ_T", "SS_ED_T", + "optimize", "smooth", "speckles", "CB_invert", "CB_MS_smooth", + "CB_MS_stack", "CB_MS_rb", "CB_speckles", "CB_smooth", "CB_optimize", + "CB_speckles1", "CB_smooth1", "CB_optimize1", "CB_SIOX1", + "CB_PA_optimize", /*"CB_live",*/ "CB_SIOX", "CBT_SS", "CBT_MS", + "B_RESET", "B_STOP", "B_OK", "mainBox", "choice_tab", + /*"choice_scan",*/ "previewArea", "_live_preview" }; + auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-trace.glade"); + try { + builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + + Glib::RefPtr<Glib::Object> test; + for (std::string w : req_widgets) { + test = builder->get_object(w); + if (!test) { + g_warning("Required widget %s does not exist", w.c_str()); + return; + } + } + +#define GET_O(name) \ + tmp = builder->get_object(#name); \ + name = Glib::RefPtr<Gtk::Adjustment>::cast_dynamic(tmp); + + Glib::RefPtr<Glib::Object> tmp; + +#define GET_W(name) builder->get_widget(#name, name); + GET_O(MS_scans) + GET_O(PA_curves) + GET_O(PA_islands) + GET_O(PA_sparse1) + GET_O(PA_sparse2) + GET_O(SS_AT_FI_T) + GET_O(SS_AT_ET_T) + GET_O(SS_BC_T) + GET_O(SS_CQ_T) + GET_O(SS_ED_T) + GET_O(optimize) + GET_O(smooth) + GET_O(speckles) + + GET_W(CB_invert) + GET_W(CB_MS_smooth) + GET_W(CB_MS_stack) + GET_W(CB_MS_rb) + GET_W(CB_speckles) + GET_W(CB_smooth) + GET_W(CB_optimize) + GET_W(CB_speckles1) + GET_W(CB_smooth1) + GET_W(CB_optimize1) + GET_W(CB_PA_optimize) + GET_W(CB_SIOX) + GET_W(CB_SIOX1) + GET_W(RB_PA_voronoi) + GET_W(CBT_SS) + GET_W(CBT_MS) + GET_W(B_RESET) + GET_W(B_STOP) + GET_W(B_OK) + GET_W(B_Update) + GET_W(mainBox) + GET_W(choice_tab) + GET_W(previewArea) + GET_W(orient_box) + GET_W(_preview_frame) + GET_W(_param_grid) + GET_W(_live_preview) +#undef GET_W +#undef GET_O + add(*mainBox); + + Inkscape::Preferences* prefs = Inkscape::Preferences::get(); + + _live_preview->set_active(prefs->getBool(getPrefsPath() + "liveUpdate", true)); + + B_Update->signal_clicked().connect([=](){ previewCallback(true); }); + B_OK->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::traceCallback)); + B_STOP->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::abort)); + B_RESET->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::onSetDefaults)); + previewArea->signal_draw().connect(sigc::mem_fun(*this, &TraceDialogImpl2::previewResize)); + + // attempt at making UI responsive: relocate preview to the right or bottom of dialog depending on dialog size + this->signal_size_allocate().connect([=](const Gtk::Allocation& alloc){ + // skip bogus sizes + if (alloc.get_width() < 10 || alloc.get_height() < 10) return; + // ratio: is dialog wide or is it tall? + double ratio = alloc.get_width() / static_cast<double>(alloc.get_height()); + // g_warning("size alloc: %d x %d - %f", alloc.get_width(), alloc.get_height(), ratio); + const auto hysteresis = 0.01; + if (ratio < 1 - hysteresis) { + // narrow/tall + choice_tab->set_valign(Gtk::ALIGN_START); + orient_box->set_orientation(Gtk::ORIENTATION_VERTICAL); + } + else if (ratio > 1 + hysteresis) { + // wide/short + orient_box->set_orientation(Gtk::ORIENTATION_HORIZONTAL); + choice_tab->set_valign(Gtk::ALIGN_FILL); + } + }); + + CBT_SS->signal_changed().connect([=](){ show_hide_params(); }); + show_hide_params(); + + // watch for changes, but only in params that can impact preview bitmap + for (auto adj : {SS_BC_T, SS_ED_T, SS_CQ_T, SS_AT_FI_T, SS_AT_ET_T, /* optimize, smooth, speckles,*/ MS_scans, PA_curves, PA_islands, PA_sparse1, PA_sparse2 }) { + adj->signal_value_changed().connect([=](){ schedule_preview_update(); }); + } + for (auto checkbtn : {CB_invert, CB_MS_rb, /* CB_MS_smooth, CB_MS_stack, CB_optimize1, CB_optimize, */ CB_PA_optimize, CB_SIOX1, CB_SIOX, /* CB_smooth1, CB_smooth, CB_speckles1, CB_speckles, */ _live_preview}) { + checkbtn->signal_toggled().connect([=](){ schedule_preview_update(); }); + } + for (auto combo : {CBT_SS, CBT_MS}) { + combo->signal_changed().connect([=](){ schedule_preview_update(); }); + } + choice_tab->signal_switch_page().connect([=](Gtk::Widget*, guint){ schedule_preview_update(); }); + + signal_set_focus_child().connect([=](Gtk::Widget* w){ + if (w) schedule_preview_update(); + }); +} + +TraceDialogImpl2::~TraceDialogImpl2() { + Inkscape::Preferences* prefs = Inkscape::Preferences::get(); + prefs->setBool(getPrefsPath() + "liveUpdate", _live_preview->get_active()); + + if (_source) { + g_source_destroy(g_main_context_find_source_by_id(nullptr, _source)); + } +} + +gboolean TraceDialogImpl2::update_cb(gpointer user_data) { + auto self = static_cast<TraceDialogImpl2*>(user_data); + self->previewCallback(false); + self->_source = 0; + return FALSE; +} + +void TraceDialogImpl2::schedule_preview_update() { + if (!_live_preview->get_active() || _source) return; + + _source = g_idle_add(&TraceDialogImpl2::update_cb, this); +} + +void TraceDialogImpl2::show_hide_params() { + int start_row = 2; + int option = CBT_SS->get_active_row_number(); + if (option >= 3) option = 3; + int show1 = start_row + option; + int show2 = start_row + option; + if (option >= 3) ++show2; + + for (int row = start_row; row < start_row + 5; ++row) { + for (int col = 0; col < 4; ++col) { + if (auto widget = _param_grid->get_child_at(col, row)) { + if (row == show1 || row == show2) widget->show(); else widget->hide(); + } + } + } +} + +TraceDialog &TraceDialog::getInstance() +{ + TraceDialog *dialog = new TraceDialogImpl2(); + return *dialog; +} + + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape diff --git a/src/ui/dialog/tracedialog.h b/src/ui/dialog/tracedialog.h new file mode 100644 index 0000000..92c9e59 --- /dev/null +++ b/src/ui/dialog/tracedialog.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Bitmap tracing settings dialog + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004, 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __TRACEDIALOG_H__ +#define __TRACEDIALOG_H__ + +#include "ui/dialog/dialog-base.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/** + * A dialog that displays log messages + */ +class TraceDialog : public DialogBase +{ + +public: + + /** + * Constructor + */ + TraceDialog() : DialogBase("/dialogs/trace", "Trace") {} + + + /** + * Factory method + */ + static TraceDialog &getInstance(); + + /** + * Destructor + */ + ~TraceDialog() override = default;; + + +}; + + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif /* __TRACEDIALOG_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/ui/dialog/transformation.cpp b/src/ui/dialog/transformation.cpp new file mode 100644 index 0000000..51ca959 --- /dev/null +++ b/src/ui/dialog/transformation.cpp @@ -0,0 +1,1216 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Transform dialog - implementation. + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * buliabyak@gmail.com + * Abhishek Sharma + * + * Copyright (C) 2004, 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "transformation.h" + +#include <gtkmm/dialog.h> + +#include <2geom/transforms.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-stack.h" +#include "selection-chemistry.h" + +#include "object/algorithms/bboxsort.h" +#include "object/sp-item-transform.h" +#include "object/sp-namedview.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" + + + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/*######################################################################## +# C O N S T R U C T O R +########################################################################*/ + +Transformation::Transformation() + : DialogBase("/dialogs/transformation", "Transform"), + _page_move (4, 2), + _page_scale (4, 2), + _page_rotate (4, 2), + _page_skew (4, 2), + _page_transform (3, 3), + _scalar_move_horizontal (_("_Horizontal:"), _("Horizontal displacement (relative) or position (absolute)"), UNIT_TYPE_LINEAR, + "", "transform-move-horizontal", &_units_move), + _scalar_move_vertical (_("_Vertical:"), _("Vertical displacement (relative) or position (absolute)"), UNIT_TYPE_LINEAR, + "", "transform-move-vertical", &_units_move), + _scalar_scale_horizontal(_("_Width:"), _("Horizontal size (absolute or percentage of current)"), UNIT_TYPE_DIMENSIONLESS, + "", "transform-scale-horizontal", &_units_scale), + _scalar_scale_vertical (_("_Height:"), _("Vertical size (absolute or percentage of current)"), UNIT_TYPE_DIMENSIONLESS, + "", "transform-scale-vertical", &_units_scale), + _scalar_rotate (_("A_ngle:"), _("Rotation angle (positive = counterclockwise)"), UNIT_TYPE_RADIAL, + "", "transform-rotate", &_units_rotate), + _scalar_skew_horizontal (_("_Horizontal:"), _("Horizontal skew angle (positive = counterclockwise), or absolute displacement, or percentage displacement"), UNIT_TYPE_LINEAR, + "", "transform-skew-horizontal", &_units_skew), + _scalar_skew_vertical (_("_Vertical:"), _("Vertical skew angle (positive = clockwise), or absolute displacement, or percentage displacement"), UNIT_TYPE_LINEAR, + "", "transform-skew-vertical", &_units_skew), + + _scalar_transform_a ("", _("Transformation matrix element A")), + _scalar_transform_b ("", _("Transformation matrix element B")), + _scalar_transform_c ("", _("Transformation matrix element C")), + _scalar_transform_d ("", _("Transformation matrix element D")), + _scalar_transform_e ("", _("Transformation matrix element E"), UNIT_TYPE_LINEAR, "", "", &_units_transform), + _scalar_transform_f ("", _("Transformation matrix element F"), UNIT_TYPE_LINEAR, "", "", &_units_transform), + + _counterclockwise_rotate (), + _clockwise_rotate (), + + _check_move_relative (_("Rela_tive move")), + _check_scale_proportional (_("_Scale proportionally")), + _check_apply_separately (_("Apply to each _object separately")), + _check_replace_matrix (_("Edit c_urrent matrix")) + +{ + _check_move_relative.set_use_underline(); + _check_move_relative.set_tooltip_text(_("Add the specified relative displacement to the current position; otherwise, edit the current absolute position directly")); + _check_scale_proportional.set_use_underline(); + _check_scale_proportional.set_tooltip_text(_("Preserve the width/height ratio of the scaled objects")); + _check_apply_separately.set_use_underline(); + _check_apply_separately.set_tooltip_text(_("Apply the scale/rotate/skew to each selected object separately; otherwise, transform the selection as a whole")); + _check_replace_matrix.set_use_underline(); + _check_replace_matrix.set_tooltip_text(_("Edit the current transform= matrix; otherwise, post-multiply transform= by this matrix")); + + set_spacing(0); + + // Notebook for individual transformations + pack_start(_notebook, false, false); + + _page_move.set_halign(Gtk::ALIGN_START); + _notebook.append_page(_page_move, _("_Move"), true); + layoutPageMove(); + + _page_scale.set_halign(Gtk::ALIGN_START); + _notebook.append_page(_page_scale, _("_Scale"), true); + layoutPageScale(); + + _page_rotate.set_halign(Gtk::ALIGN_START); + _notebook.append_page(_page_rotate, _("_Rotate"), true); + layoutPageRotate(); + + _page_skew.set_halign(Gtk::ALIGN_START); + _notebook.append_page(_page_skew, _("Ske_w"), true); + layoutPageSkew(); + + _page_transform.set_halign(Gtk::ALIGN_START); + _notebook.append_page(_page_transform, _("Matri_x"), true); + layoutPageTransform(); + + _tabSwitchConn = _notebook.signal_switch_page().connect(sigc::mem_fun(*this, &Transformation::onSwitchPage)); + + // Apply separately + pack_start(_check_apply_separately, false, false); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _check_apply_separately.set_active(prefs->getBool("/dialogs/transformation/applyseparately")); + _check_apply_separately.signal_toggled().connect(sigc::mem_fun(*this, &Transformation::onApplySeparatelyToggled)); + + // make sure all spinbuttons activate Apply on pressing Enter + ((Gtk::Entry *) (_scalar_move_horizontal.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply)); + ((Gtk::Entry *) (_scalar_move_vertical.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply)); + ((Gtk::Entry *) (_scalar_scale_horizontal.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply)); + ((Gtk::Entry *) (_scalar_scale_vertical.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply)); + ((Gtk::Entry *) (_scalar_rotate.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply)); + ((Gtk::Entry *) (_scalar_skew_horizontal.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply)); + ((Gtk::Entry *) (_scalar_skew_vertical.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply)); + + resetButton = Gtk::manage(new Gtk::Button()); + resetButton->set_image_from_icon_name("reset-settings-symbolic"); + resetButton->set_size_request(30, -1); + resetButton->set_halign(Gtk::ALIGN_CENTER); + resetButton->set_use_underline(); + resetButton->set_tooltip_text(_("Reset the values on the current tab to defaults")); + resetButton->set_sensitive(true); + resetButton->signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onClear)); + + applyButton = Gtk::manage(new Gtk::Button(_("_Apply"))); + applyButton->set_use_underline(); + applyButton->set_halign(Gtk::ALIGN_CENTER); + applyButton->set_tooltip_text(_("Apply transformation to selection")); + applyButton->set_sensitive(false); + applyButton->signal_clicked().connect(sigc::mem_fun(*this, &Transformation::_apply)); + applyButton->get_style_context()->add_class("wide-apply-button"); + + auto button_box = Gtk::manage(new Gtk::Box()); + button_box->set_margin_top(4); + button_box->set_spacing(8); + button_box->set_halign(Gtk::ALIGN_CENTER); + button_box->pack_start(*applyButton); + button_box->pack_start(*resetButton); + pack_start(*button_box, Gtk::PACK_SHRINK, 0); + + show_all_children(); +} + +Transformation::~Transformation() +{ + _tabSwitchConn.disconnect(); +} + +void Transformation::selectionChanged(Inkscape::Selection *selection) +{ + updateSelection((Inkscape::UI::Dialog::Transformation::PageType)getCurrentPage(), selection); +} +void Transformation::selectionModified(Inkscape::Selection *selection, guint flags) +{ + selectionChanged(selection); +} + +/*######################################################################## +# U T I L I T Y +########################################################################*/ + +void Transformation::presentPage(Transformation::PageType page) +{ + _notebook.set_current_page(page); + show(); +} + + + + +/*######################################################################## +# S E T U P L A Y O U T +########################################################################*/ + + +void Transformation::layoutPageMove() +{ + _units_move.setUnitType(UNIT_TYPE_LINEAR); + + _scalar_move_horizontal.initScalar(-1e6, 1e6); + _scalar_move_horizontal.setDigits(3); + _scalar_move_horizontal.setIncrements(0.1, 1.0); + _scalar_move_horizontal.set_hexpand(); + _scalar_move_horizontal.setWidthChars(7); + + _scalar_move_vertical.initScalar(-1e6, 1e6); + _scalar_move_vertical.setDigits(3); + _scalar_move_vertical.setIncrements(0.1, 1.0); + _scalar_move_vertical.set_hexpand(); + _scalar_move_vertical.setWidthChars(7); + + //_scalar_move_vertical.set_label_image( INKSCAPE_STOCK_ARROWS_HOR ); + + _page_move.table().attach(_scalar_move_horizontal, 0, 0, 2, 1); + _page_move.table().attach(_units_move, 2, 0, 1, 1); + + _scalar_move_horizontal.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onMoveValueChanged)); + + //_scalar_move_vertical.set_label_image( INKSCAPE_STOCK_ARROWS_VER ); + _page_move.table().attach(_scalar_move_vertical, 0, 1, 2, 1); + + _scalar_move_vertical.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onMoveValueChanged)); + + // Relative moves + _page_move.table().attach(_check_move_relative, 0, 2, 2, 1); + + _check_move_relative.set_active(true); + _check_move_relative.signal_toggled() + .connect(sigc::mem_fun(*this, &Transformation::onMoveRelativeToggled)); +} + +void Transformation::layoutPageScale() +{ + _units_scale.setUnitType(UNIT_TYPE_DIMENSIONLESS); + _units_scale.setUnitType(UNIT_TYPE_LINEAR); + + _scalar_scale_horizontal.initScalar(-1e6, 1e6); + _scalar_scale_horizontal.setValue(100.0, "%"); + _scalar_scale_horizontal.setDigits(3); + _scalar_scale_horizontal.setIncrements(0.1, 1.0); + _scalar_scale_horizontal.setAbsoluteIsIncrement(true); + _scalar_scale_horizontal.setPercentageIsIncrement(true); + _scalar_scale_horizontal.set_hexpand(); + _scalar_scale_horizontal.setWidthChars(7); + + _scalar_scale_vertical.initScalar(-1e6, 1e6); + _scalar_scale_vertical.setValue(100.0, "%"); + _scalar_scale_vertical.setDigits(3); + _scalar_scale_vertical.setIncrements(0.1, 1.0); + _scalar_scale_vertical.setAbsoluteIsIncrement(true); + _scalar_scale_vertical.setPercentageIsIncrement(true); + _scalar_scale_vertical.set_hexpand(); + _scalar_scale_vertical.setWidthChars(7); + + _page_scale.table().attach(_scalar_scale_horizontal, 0, 0, 2, 1); + + _scalar_scale_horizontal.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onScaleXValueChanged)); + + _page_scale.table().attach(_units_scale, 2, 0, 1, 1); + _page_scale.table().attach(_scalar_scale_vertical, 0, 1, 2, 1); + + _scalar_scale_vertical.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onScaleYValueChanged)); + + _page_scale.table().attach(_check_scale_proportional, 0, 2, 2, 1); + + _check_scale_proportional.set_active(false); + _check_scale_proportional.signal_toggled() + .connect(sigc::mem_fun(*this, &Transformation::onScaleProportionalToggled)); + + //TODO: add a widget for selecting the fixed point in scaling, or honour rotation center? +} + +void Transformation::layoutPageRotate() +{ + _units_rotate.setUnitType(UNIT_TYPE_RADIAL); + + _scalar_rotate.initScalar(-360.0, 360.0); + _scalar_rotate.setDigits(3); + _scalar_rotate.setIncrements(0.1, 1.0); + _scalar_rotate.set_hexpand(); + + auto object_rotate_left_icon = Gtk::manage(sp_get_icon_image("object-rotate-left", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + _counterclockwise_rotate.add(*object_rotate_left_icon); + _counterclockwise_rotate.set_mode(false); + _counterclockwise_rotate.set_relief(Gtk::RELIEF_NONE); + _counterclockwise_rotate.set_tooltip_text(_("Rotate in a counterclockwise direction")); + + auto object_rotate_right_icon = Gtk::manage(sp_get_icon_image("object-rotate-right", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + _clockwise_rotate.add(*object_rotate_right_icon); + _clockwise_rotate.set_mode(false); + _clockwise_rotate.set_relief(Gtk::RELIEF_NONE); + _clockwise_rotate.set_tooltip_text(_("Rotate in a clockwise direction")); + + Gtk::RadioButton::Group group = _counterclockwise_rotate.get_group(); + _clockwise_rotate.set_group(group); + + auto box = Gtk::make_managed<Gtk::Box>(); + _counterclockwise_rotate.set_halign(Gtk::ALIGN_START); + _clockwise_rotate.set_halign(Gtk::ALIGN_START); + box->pack_start(_counterclockwise_rotate); + box->pack_start(_clockwise_rotate); + + _page_rotate.table().attach(_scalar_rotate, 0, 0, 1, 1); + _page_rotate.table().attach(_units_rotate, 1, 0, 1, 1); + _page_rotate.table().attach(*box, 1, 1, 1, 1); + + _scalar_rotate.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onRotateValueChanged)); + + _counterclockwise_rotate.signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onRotateCounterclockwiseClicked)); + _clockwise_rotate.signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onRotateClockwiseClicked)); + + //TODO: honour rotation center? +} + +void Transformation::layoutPageSkew() +{ + _units_skew.setUnitType(UNIT_TYPE_LINEAR); + _units_skew.setUnitType(UNIT_TYPE_DIMENSIONLESS); + _units_skew.setUnitType(UNIT_TYPE_RADIAL); + + _scalar_skew_horizontal.initScalar(-1e6, 1e6); + _scalar_skew_horizontal.setDigits(3); + _scalar_skew_horizontal.setIncrements(0.1, 1.0); + _scalar_skew_horizontal.set_hexpand(); + _scalar_skew_horizontal.setWidthChars(7); + + _scalar_skew_vertical.initScalar(-1e6, 1e6); + _scalar_skew_vertical.setDigits(3); + _scalar_skew_vertical.setIncrements(0.1, 1.0); + _scalar_skew_vertical.set_hexpand(); + _scalar_skew_vertical.setWidthChars(7); + + _page_skew.table().attach(_scalar_skew_horizontal, 0, 0, 2, 1); + _page_skew.table().attach(_units_skew, 2, 0, 1, 1); + _page_skew.table().attach(_scalar_skew_vertical, 0, 1, 2, 1); + + _scalar_skew_horizontal.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onSkewValueChanged)); + _scalar_skew_vertical.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onSkewValueChanged)); + + //TODO: honour rotation center? +} + + +void Transformation::layoutPageTransform() +{ + _units_transform.setUnitType(UNIT_TYPE_LINEAR); + _units_transform.set_tooltip_text(_("E and F units")); + _units_transform.set_halign(Gtk::ALIGN_END); + _units_transform.set_margin_top(3); + _units_transform.set_margin_bottom(3); + + UI::Widget::Scalar* labels[] = {&_scalar_transform_a, &_scalar_transform_b, &_scalar_transform_c, &_scalar_transform_d, &_scalar_transform_e, &_scalar_transform_f}; + for (auto label : labels) { + label->hide_label(); + label->set_margin_start(2); + label->set_margin_end(2); + } + _page_transform.table().set_column_spacing(0); + _page_transform.table().set_row_spacing(1); + _page_transform.table().set_column_homogeneous(true); + + _scalar_transform_a.setWidgetSizeRequest(65, -1); + _scalar_transform_a.setRange(-1e10, 1e10); + _scalar_transform_a.setDigits(3); + _scalar_transform_a.setIncrements(0.1, 1.0); + _scalar_transform_a.setValue(1.0); + _scalar_transform_a.setWidthChars(6); + _scalar_transform_a.set_hexpand(); + + _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("A:"), 0, 0, 1, 1); + _page_transform.table().attach(_scalar_transform_a, 0, 1, 1, 1); + + _scalar_transform_a.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged)); + + _scalar_transform_b.setWidgetSizeRequest(65, -1); + _scalar_transform_b.setRange(-1e10, 1e10); + _scalar_transform_b.setDigits(3); + _scalar_transform_b.setIncrements(0.1, 1.0); + _scalar_transform_b.setValue(0.0); + _scalar_transform_b.setWidthChars(6); + _scalar_transform_b.set_hexpand(); + + _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("B:"), 0, 2, 1, 1); + _page_transform.table().attach(_scalar_transform_b, 0, 3, 1, 1); + + _scalar_transform_b.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged)); + + _scalar_transform_c.setWidgetSizeRequest(65, -1); + _scalar_transform_c.setRange(-1e10, 1e10); + _scalar_transform_c.setDigits(3); + _scalar_transform_c.setIncrements(0.1, 1.0); + _scalar_transform_c.setValue(0.0); + _scalar_transform_c.setWidthChars(6); + _scalar_transform_c.set_hexpand(); + + _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("C:"), 1, 0, 1, 1); + _page_transform.table().attach(_scalar_transform_c, 1, 1, 1, 1); + + _scalar_transform_c.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged)); + + + _scalar_transform_d.setWidgetSizeRequest(65, -1); + _scalar_transform_d.setRange(-1e10, 1e10); + _scalar_transform_d.setDigits(3); + _scalar_transform_d.setIncrements(0.1, 1.0); + _scalar_transform_d.setValue(1.0); + _scalar_transform_d.setWidthChars(6); + _scalar_transform_d.set_hexpand(); + + _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("D:"), 1, 2, 1, 1); + _page_transform.table().attach(_scalar_transform_d, 1, 3, 1, 1); + + _scalar_transform_d.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged)); + + + _scalar_transform_e.setWidgetSizeRequest(65, -1); + _scalar_transform_e.setRange(-1e10, 1e10); + _scalar_transform_e.setDigits(3); + _scalar_transform_e.setIncrements(0.1, 1.0); + _scalar_transform_e.setValue(0.0); + _scalar_transform_e.setWidthChars(6); + _scalar_transform_e.set_hexpand(); + + _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("E:"), 2, 0, 1, 1); + _page_transform.table().attach(_scalar_transform_e, 2, 1, 1, 1); + + _scalar_transform_e.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged)); + + + _scalar_transform_f.setWidgetSizeRequest(65, -1); + _scalar_transform_f.setRange(-1e10, 1e10); + _scalar_transform_f.setDigits(3); + _scalar_transform_f.setIncrements(0.1, 1.0); + _scalar_transform_f.setValue(0.0); + _scalar_transform_f.setWidthChars(6); + _scalar_transform_f.set_hexpand(); + + _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("F:"), 2, 2, 1, 1); + _page_transform.table().attach(_scalar_transform_f, 2, 3, 1, 1); + + auto img = Gtk::make_managed<Gtk::Image>(); + img->set_from_icon_name("matrix-2d", Gtk::ICON_SIZE_BUTTON); + img->set_pixel_size(52); + img->set_margin_top(4); + img->set_margin_bottom(4); + _page_transform.table().attach(*img, 0, 5, 1, 1); + + auto descr = Gtk::make_managed<Gtk::Label>(); + descr->set_line_wrap(); + descr->set_line_wrap_mode(Pango::WRAP_WORD); + descr->set_text( + "<small>" + "<a href=\"https://www.w3.org/TR/SVG11/coords.html#TransformMatrixDefined\">" + "2D transformation matrix</a> that combines translation (E,F), scaling (A,D)," + " rotation (A-D) and shearing (B,C)." + "</small>" + ); + descr->set_use_markup(); + _page_transform.table().attach(*descr, 1, 5, 2, 1); + + _page_transform.table().attach(_units_transform, 2, 4, 1, 1); + + _scalar_transform_f.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged)); + + // Edit existing matrix + _page_transform.table().attach(_check_replace_matrix, 0, 4, 2, 1); + + _check_replace_matrix.set_active(false); + _check_replace_matrix.signal_toggled() + .connect(sigc::mem_fun(*this, &Transformation::onReplaceMatrixToggled)); +} + + +/*######################################################################## +# U P D A T E +########################################################################*/ + +void Transformation::updateSelection(PageType page, Inkscape::Selection *selection) +{ + applyButton->set_sensitive(selection && !selection->isEmpty()); + + if (!selection || selection->isEmpty()) + return; + + switch (page) { + case PAGE_MOVE: { + updatePageMove(selection); + break; + } + case PAGE_SCALE: { + updatePageScale(selection); + break; + } + case PAGE_ROTATE: { + updatePageRotate(selection); + break; + } + case PAGE_SKEW: { + updatePageSkew(selection); + break; + } + case PAGE_TRANSFORM: { + updatePageTransform(selection); + break; + } + case PAGE_QTY: { + break; + } + } +} + +void Transformation::onSwitchPage(Gtk::Widget * /*page*/, guint pagenum) +{ + if (!getDesktop()) { + return; + } + + updateSelection((PageType)pagenum, getDesktop()->getSelection()); +} + + +void Transformation::updatePageMove(Inkscape::Selection *selection) +{ + if (selection && !selection->isEmpty()) { + if (!_check_move_relative.get_active()) { + Geom::OptRect bbox = selection->preferredBounds(); + if (bbox) { + double x = bbox->min()[Geom::X]; + double y = bbox->min()[Geom::Y]; + + double conversion = _units_move.getConversion("px"); + _scalar_move_horizontal.setValue(x / conversion); + _scalar_move_vertical.setValue(y / conversion); + } + } else { + // do nothing, so you can apply the same relative move to many objects in turn + } + _page_move.set_sensitive(true); + } else { + _page_move.set_sensitive(false); + } +} + +void Transformation::updatePageScale(Inkscape::Selection *selection) +{ + if (selection && !selection->isEmpty()) { + Geom::OptRect bbox = selection->preferredBounds(); + if (bbox) { + double w = bbox->dimensions()[Geom::X]; + double h = bbox->dimensions()[Geom::Y]; + _scalar_scale_horizontal.setHundredPercent(w); + _scalar_scale_vertical.setHundredPercent(h); + onScaleXValueChanged(); // to update x/y proportionality if switch is on + _page_scale.set_sensitive(true); + } else { + _page_scale.set_sensitive(false); + } + } else { + _page_scale.set_sensitive(false); + } +} + +void Transformation::updatePageRotate(Inkscape::Selection *selection) +{ + if (selection && !selection->isEmpty()) { + _page_rotate.set_sensitive(true); + } else { + _page_rotate.set_sensitive(false); + } +} + +void Transformation::updatePageSkew(Inkscape::Selection *selection) +{ + if (selection && !selection->isEmpty()) { + Geom::OptRect bbox = selection->preferredBounds(); + if (bbox) { + double w = bbox->dimensions()[Geom::X]; + double h = bbox->dimensions()[Geom::Y]; + _scalar_skew_vertical.setHundredPercent(w); + _scalar_skew_horizontal.setHundredPercent(h); + _page_skew.set_sensitive(true); + } else { + _page_skew.set_sensitive(false); + } + } else { + _page_skew.set_sensitive(false); + } +} + +void Transformation::updatePageTransform(Inkscape::Selection *selection) +{ + if (selection && !selection->isEmpty()) { + if (_check_replace_matrix.get_active()) { + Geom::Affine current (selection->items().front()->transform); // take from the first item in selection + + Geom::Affine new_displayed = current; + + _scalar_transform_a.setValue(new_displayed[0]); + _scalar_transform_b.setValue(new_displayed[1]); + _scalar_transform_c.setValue(new_displayed[2]); + _scalar_transform_d.setValue(new_displayed[3]); + _scalar_transform_e.setValue(new_displayed[4], "px"); + _scalar_transform_f.setValue(new_displayed[5], "px"); + } else { + // do nothing, so you can apply the same matrix to many objects in turn + } + _page_transform.set_sensitive(true); + } else { + _page_transform.set_sensitive(false); + } +} + + + + + +/*######################################################################## +# A P P L Y +########################################################################*/ + + + +void Transformation::_apply() +{ + auto selection = getSelection(); + if (!selection || selection->isEmpty()) + return; + + int const page = _notebook.get_current_page(); + + switch (page) { + case PAGE_MOVE: { + applyPageMove(selection); + break; + } + case PAGE_ROTATE: { + applyPageRotate(selection); + break; + } + case PAGE_SCALE: { + applyPageScale(selection); + break; + } + case PAGE_SKEW: { + applyPageSkew(selection); + break; + } + case PAGE_TRANSFORM: { + applyPageTransform(selection); + break; + } + } + + // Let's play with never turning this off + applyButton->set_sensitive(false); +} + +void Transformation::applyPageMove(Inkscape::Selection *selection) +{ + double x = _scalar_move_horizontal.getValue("px"); + double y = _scalar_move_vertical.getValue("px"); + if (_check_move_relative.get_active()) { + y *= getDesktop()->yaxisdir(); + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!prefs->getBool("/dialogs/transformation/applyseparately")) { + // move selection as a whole + if (_check_move_relative.get_active()) { + selection->moveRelative(x, y); + } else { + Geom::OptRect bbox = selection->preferredBounds(); + if (bbox) { + selection->moveRelative(x - bbox->min()[Geom::X], y - bbox->min()[Geom::Y]); + } + } + } else { + + if (_check_move_relative.get_active()) { + // shift each object relatively to the previous one + std::vector<SPItem*> selected(selection->items().begin(), selection->items().end()); + if (selected.empty()) return; + + if (fabs(x) > 1e-6) { + std::vector< BBoxSort > sorted; + for (auto item : selected) + { + Geom::OptRect bbox = item->desktopPreferredBounds(); + if (bbox) { + sorted.emplace_back(item, *bbox, Geom::X, x > 0? 1. : 0., x > 0? 0. : 1.); + } + } + //sort bbox by anchors + std::stable_sort(sorted.begin(), sorted.end()); + + double move = x; + for ( std::vector<BBoxSort> ::iterator it (sorted.begin()); + it < sorted.end(); + ++it ) + { + it->item->move_rel(Geom::Translate(move, 0)); + // move each next object by x relative to previous + move += x; + } + } + if (fabs(y) > 1e-6) { + std::vector< BBoxSort > sorted; + for (auto item : selected) + { + Geom::OptRect bbox = item->desktopPreferredBounds(); + if (bbox) { + sorted.emplace_back(item, *bbox, Geom::Y, y > 0? 1. : 0., y > 0? 0. : 1.); + } + } + //sort bbox by anchors + std::stable_sort(sorted.begin(), sorted.end()); + + double move = y; + for ( std::vector<BBoxSort> ::iterator it (sorted.begin()); + it < sorted.end(); + ++it ) + { + it->item->move_rel(Geom::Translate(0, move)); + // move each next object by x relative to previous + move += y; + } + } + } else { + Geom::OptRect bbox = selection->preferredBounds(); + if (bbox) { + selection->moveRelative(x - bbox->min()[Geom::X], y - bbox->min()[Geom::Y]); + } + } + } + + DocumentUndo::done( selection->desktop()->getDocument(), _("Move"), INKSCAPE_ICON("dialog-transform")); +} + +void Transformation::applyPageScale(Inkscape::Selection *selection) +{ + double scaleX = _scalar_scale_horizontal.getValue("px"); + double scaleY = _scalar_scale_vertical.getValue("px"); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs->getBool("/options/transform/stroke", true); + bool preserve = prefs->getBool("/options/preservetransform/value", false); + if (prefs->getBool("/dialogs/transformation/applyseparately")) { + auto tmp= selection->items(); + for(auto i=tmp.begin();i!=tmp.end();++i){ + SPItem *item = *i; + Geom::OptRect bbox_pref = item->desktopPreferredBounds(); + Geom::OptRect bbox_geom = item->desktopGeometricBounds(); + if (bbox_pref && bbox_geom) { + double new_width = scaleX; + double new_height = scaleY; + // the values are increments! + if (!_units_scale.isAbsolute()) { // Relative scaling, i.e in percent + new_width = scaleX/100 * bbox_pref->width(); + new_height = scaleY/100 * bbox_pref->height(); + } + if (fabs(new_width) < 1e-6) new_width = 1e-6; // not 0, as this would result in a nasty no-bbox object + if (fabs(new_height) < 1e-6) new_height = 1e-6; + + double x0 = bbox_pref->midpoint()[Geom::X] - new_width/2; + double y0 = bbox_pref->midpoint()[Geom::Y] - new_height/2; + double x1 = bbox_pref->midpoint()[Geom::X] + new_width/2; + double y1 = bbox_pref->midpoint()[Geom::Y] + new_height/2; + + Geom::Affine scaler = get_scale_transform_for_variable_stroke (*bbox_pref, *bbox_geom, transform_stroke, preserve, x0, y0, x1, y1); + item->set_i2d_affine(item->i2dt_affine() * scaler); + item->doWriteTransform(item->transform); + } + } + } else { + Geom::OptRect bbox_pref = selection->preferredBounds(); + Geom::OptRect bbox_geom = selection->geometricBounds(); + if (bbox_pref && bbox_geom) { + // the values are increments! + double new_width = scaleX; + double new_height = scaleY; + if (!_units_scale.isAbsolute()) { // Relative scaling, i.e in percent + new_width = scaleX/100 * bbox_pref->width(); + new_height = scaleY/100 * bbox_pref->height(); + } + if (fabs(new_width) < 1e-6) new_width = 1e-6; + if (fabs(new_height) < 1e-6) new_height = 1e-6; + + double x0 = bbox_pref->midpoint()[Geom::X] - new_width/2; + double y0 = bbox_pref->midpoint()[Geom::Y] - new_height/2; + double x1 = bbox_pref->midpoint()[Geom::X] + new_width/2; + double y1 = bbox_pref->midpoint()[Geom::Y] + new_height/2; + Geom::Affine scaler = get_scale_transform_for_variable_stroke (*bbox_pref, *bbox_geom, transform_stroke, preserve, x0, y0, x1, y1); + + selection->applyAffine(scaler); + } + } + + DocumentUndo::done(selection->desktop()->getDocument(), _("Scale"), INKSCAPE_ICON("dialog-transform")); +} + +void Transformation::applyPageRotate(Inkscape::Selection *selection) +{ + double angle = _scalar_rotate.getValue(DEG); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!prefs->getBool("/dialogs/transformation/rotateCounterClockwise", TRUE)) { + angle *= -1; + } + + if (prefs->getBool("/dialogs/transformation/applyseparately")) { + auto tmp= selection->items(); + for(auto i=tmp.begin();i!=tmp.end();++i){ + SPItem *item = *i; + item->rotate_rel(Geom::Rotate (angle*M_PI/180.0)); + } + } else { + std::optional<Geom::Point> center = selection->center(); + if (center) { + selection->rotateRelative(*center, angle); + } + } + + DocumentUndo::done(selection->desktop()->getDocument(), _("Rotate"), INKSCAPE_ICON("dialog-transform")); +} + +void Transformation::applyPageSkew(Inkscape::Selection *selection) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/dialogs/transformation/applyseparately")) { + auto items = selection->items(); + for(auto i = items.begin();i!=items.end();++i){ + SPItem *item = *i; + + if (!_units_skew.isAbsolute()) { // percentage + double skewX = _scalar_skew_horizontal.getValue("%"); + double skewY = _scalar_skew_vertical.getValue("%"); + skewY *= getDesktop()->yaxisdir(); + if (fabs(0.01*skewX*0.01*skewY - 1.0) < Geom::EPSILON) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>.")); + return; + } + item->skew_rel(0.01*skewX, 0.01*skewY); + } else if (_units_skew.isRadial()) { //deg or rad + double angleX = _scalar_skew_horizontal.getValue("rad"); + double angleY = _scalar_skew_vertical.getValue("rad"); + if ((fabs(angleX - angleY + M_PI/2) < Geom::EPSILON) + || (fabs(angleX - angleY - M_PI/2) < Geom::EPSILON) + || (fabs((angleX - angleY)/3 + M_PI/2) < Geom::EPSILON) + || (fabs((angleX - angleY)/3 - M_PI/2) < Geom::EPSILON)) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>.")); + return; + } + double skewX = tan(angleX); + double skewY = tan(angleY); + skewX *= getDesktop()->yaxisdir(); + skewY *= getDesktop()->yaxisdir(); + item->skew_rel(skewX, skewY); + } else { // absolute displacement + double skewX = _scalar_skew_horizontal.getValue("px"); + double skewY = _scalar_skew_vertical.getValue("px"); + skewY *= getDesktop()->yaxisdir(); + Geom::OptRect bbox = item->desktopPreferredBounds(); + if (bbox) { + double width = bbox->dimensions()[Geom::X]; + double height = bbox->dimensions()[Geom::Y]; + if (fabs(skewX*skewY - width*height) < Geom::EPSILON) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>.")); + return; + } + item->skew_rel(skewX/height, skewY/width); + } + } + } + } else { // transform whole selection + Geom::OptRect bbox = selection->preferredBounds(); + std::optional<Geom::Point> center = selection->center(); + + if ( bbox && center ) { + double width = bbox->dimensions()[Geom::X]; + double height = bbox->dimensions()[Geom::Y]; + + if (!_units_skew.isAbsolute()) { // percentage + double skewX = _scalar_skew_horizontal.getValue("%"); + double skewY = _scalar_skew_vertical.getValue("%"); + skewY *= getDesktop()->yaxisdir(); + if (fabs(0.01*skewX*0.01*skewY - 1.0) < Geom::EPSILON) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>.")); + return; + } + selection->skewRelative(*center, 0.01 * skewX, 0.01 * skewY); + } else if (_units_skew.isRadial()) { //deg or rad + double angleX = _scalar_skew_horizontal.getValue("rad"); + double angleY = _scalar_skew_vertical.getValue("rad"); + if ((fabs(angleX - angleY + M_PI/2) < Geom::EPSILON) + || (fabs(angleX - angleY - M_PI/2) < Geom::EPSILON) + || (fabs((angleX - angleY)/3 + M_PI/2) < Geom::EPSILON) + || (fabs((angleX - angleY)/3 - M_PI/2) < Geom::EPSILON)) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>.")); + return; + } + double skewX = tan(angleX); + double skewY = tan(angleY); + skewX *= getDesktop()->yaxisdir(); + skewY *= getDesktop()->yaxisdir(); + selection->skewRelative(*center, skewX, skewY); + } else { // absolute displacement + double skewX = _scalar_skew_horizontal.getValue("px"); + double skewY = _scalar_skew_vertical.getValue("px"); + skewY *= getDesktop()->yaxisdir(); + if (fabs(skewX*skewY - width*height) < Geom::EPSILON) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>.")); + return; + } + selection->skewRelative(*center, skewX / height, skewY / width); + } + } + } + + DocumentUndo::done(selection->desktop()->getDocument(), _("Skew"), INKSCAPE_ICON("dialog-transform")); +} + + +void Transformation::applyPageTransform(Inkscape::Selection *selection) +{ + double a = _scalar_transform_a.getValue(); + double b = _scalar_transform_b.getValue(); + double c = _scalar_transform_c.getValue(); + double d = _scalar_transform_d.getValue(); + double e = _scalar_transform_e.getValue("px"); + double f = _scalar_transform_f.getValue("px"); + + Geom::Affine displayed(a, b, c, d, e, f); + if (displayed.isSingular()) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>.")); + return; + } + + if (_check_replace_matrix.get_active()) { + auto tmp = selection->items(); + for(auto i=tmp.begin();i!=tmp.end();++i){ + SPItem *item = *i; + item->set_item_transform(displayed); + item->updateRepr(); + } + } else { + selection->applyAffine(displayed); // post-multiply each object's transform + } + + DocumentUndo::done(selection->desktop()->getDocument(), _("Edit transformation matrix"), INKSCAPE_ICON("dialog-transform")); +} + + + + + +/*######################################################################## +# V A L U E - C H A N G E D C A L L B A C K S +########################################################################*/ + +void Transformation::onMoveValueChanged() +{ + applyButton->set_sensitive(true); +} + +void Transformation::onMoveRelativeToggled() +{ + auto selection = getSelection(); + if (!selection || selection->isEmpty()) + return; + + double x = _scalar_move_horizontal.getValue("px"); + double y = _scalar_move_vertical.getValue("px"); + + double conversion = _units_move.getConversion("px"); + + //g_message("onMoveRelativeToggled: %f, %f px\n", x, y); + + Geom::OptRect bbox = selection->preferredBounds(); + + if (bbox) { + if (_check_move_relative.get_active()) { + // From absolute to relative + _scalar_move_horizontal.setValue((x - bbox->min()[Geom::X]) / conversion); + _scalar_move_vertical.setValue(( y - bbox->min()[Geom::Y]) / conversion); + } else { + // From relative to absolute + _scalar_move_horizontal.setValue((bbox->min()[Geom::X] + x) / conversion); + _scalar_move_vertical.setValue(( bbox->min()[Geom::Y] + y) / conversion); + } + } + + applyButton->set_sensitive(true); +} + +void Transformation::onScaleXValueChanged() +{ + if (_scalar_scale_horizontal.setProgrammatically) { + _scalar_scale_horizontal.setProgrammatically = false; + return; + } + + applyButton->set_sensitive(true); + + if (_check_scale_proportional.get_active()) { + if (!_units_scale.isAbsolute()) { // percentage, just copy over + _scalar_scale_vertical.setValue(_scalar_scale_horizontal.getValue("%")); + } else { + double scaleXPercentage = _scalar_scale_horizontal.getAsPercentage(); + _scalar_scale_vertical.setFromPercentage (scaleXPercentage); + } + } +} + +void Transformation::onScaleYValueChanged() +{ + if (_scalar_scale_vertical.setProgrammatically) { + _scalar_scale_vertical.setProgrammatically = false; + return; + } + + applyButton->set_sensitive(true); + + if (_check_scale_proportional.get_active()) { + if (!_units_scale.isAbsolute()) { // percentage, just copy over + _scalar_scale_horizontal.setValue(_scalar_scale_vertical.getValue("%")); + } else { + double scaleYPercentage = _scalar_scale_vertical.getAsPercentage(); + _scalar_scale_horizontal.setFromPercentage (scaleYPercentage); + } + } +} + +void Transformation::onRotateValueChanged() +{ + applyButton->set_sensitive(true); +} + +void Transformation::onRotateCounterclockwiseClicked() +{ + _scalar_rotate.setTooltipText(_("Rotation angle (positive = counterclockwise)")); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/transformation/rotateCounterClockwise", !getDesktop()->is_yaxisdown()); +} + +void Transformation::onRotateClockwiseClicked() +{ + _scalar_rotate.setTooltipText(_("Rotation angle (positive = clockwise)")); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/transformation/rotateCounterClockwise", getDesktop()->is_yaxisdown()); +} + +void Transformation::onSkewValueChanged() +{ + applyButton->set_sensitive(true); +} + +void Transformation::onTransformValueChanged() +{ + + /* + double a = _scalar_transform_a.getValue(); + double b = _scalar_transform_b.getValue(); + double c = _scalar_transform_c.getValue(); + double d = _scalar_transform_d.getValue(); + double e = _scalar_transform_e.getValue(); + double f = _scalar_transform_f.getValue(); + + //g_message("onTransformValueChanged: (%f, %f, %f, %f, %f, %f)\n", + // a, b, c, d, e ,f); + */ + + applyButton->set_sensitive(true); +} + +void Transformation::onReplaceMatrixToggled() +{ + auto selection = getSelection(); + if (!selection || selection->isEmpty()) + return; + + double a = _scalar_transform_a.getValue(); + double b = _scalar_transform_b.getValue(); + double c = _scalar_transform_c.getValue(); + double d = _scalar_transform_d.getValue(); + double e = _scalar_transform_e.getValue("px"); + double f = _scalar_transform_f.getValue("px"); + + Geom::Affine displayed (a, b, c, d, e, f); + Geom::Affine current = selection->items().front()->transform; // take from the first item in selection + + Geom::Affine new_displayed; + if (_check_replace_matrix.get_active()) { + new_displayed = current; + } else { + new_displayed = current.inverse() * displayed; + } + + _scalar_transform_a.setValue(new_displayed[0]); + _scalar_transform_b.setValue(new_displayed[1]); + _scalar_transform_c.setValue(new_displayed[2]); + _scalar_transform_d.setValue(new_displayed[3]); + _scalar_transform_e.setValue(new_displayed[4], "px"); + _scalar_transform_f.setValue(new_displayed[5], "px"); +} + +void Transformation::onScaleProportionalToggled() +{ + onScaleXValueChanged(); + if (_scalar_scale_vertical.setProgrammatically) { + _scalar_scale_vertical.setProgrammatically = false; + } +} + + +void Transformation::onClear() +{ + int const page = _notebook.get_current_page(); + + switch (page) { + case PAGE_MOVE: { + auto selection = getSelection(); + if (!selection || selection->isEmpty() || _check_move_relative.get_active()) { + _scalar_move_horizontal.setValue(0); + _scalar_move_vertical.setValue(0); + } else { + Geom::OptRect bbox = selection->preferredBounds(); + if (bbox) { + _scalar_move_horizontal.setValue(bbox->min()[Geom::X], "px"); + _scalar_move_vertical.setValue(bbox->min()[Geom::Y], "px"); + } + } + break; + } + case PAGE_ROTATE: { + _scalar_rotate.setValue(0); + break; + } + case PAGE_SCALE: { + _scalar_scale_horizontal.setValue(100, "%"); + _scalar_scale_vertical.setValue(100, "%"); + break; + } + case PAGE_SKEW: { + _scalar_skew_horizontal.setValue(0); + _scalar_skew_vertical.setValue(0); + break; + } + case PAGE_TRANSFORM: { + _scalar_transform_a.setValue(1); + _scalar_transform_b.setValue(0); + _scalar_transform_c.setValue(0); + _scalar_transform_d.setValue(1); + _scalar_transform_e.setValue(0, "px"); + _scalar_transform_f.setValue(0, "px"); + break; + } + } +} + +void Transformation::onApplySeparatelyToggled() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/transformation/applyseparately", _check_apply_separately.get_active()); +} + +void Transformation::desktopReplaced() +{ + // Setting default unit to document unit + if (auto desktop = getDesktop()) { + SPNamedView *nv = desktop->getNamedView(); + if (nv->display_units) { + _units_move.setUnit(nv->display_units->abbr); + _units_transform.setUnit(nv->display_units->abbr); + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/dialogs/transformation/rotateCounterClockwise", true) != desktop->is_yaxisdown()) { + _counterclockwise_rotate.set_active(); + onRotateCounterclockwiseClicked(); + } else { + _clockwise_rotate.set_active(); + onRotateClockwiseClicked(); + } + + updateSelection(PAGE_MOVE, getSelection()); + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/transformation.h b/src/ui/dialog/transformation.h new file mode 100644 index 0000000..77210d9 --- /dev/null +++ b/src/ui/dialog/transformation.h @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Transform dialog + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004, 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_TRANSFORMATION_H +#define INKSCAPE_UI_DIALOG_TRANSFORMATION_H + +#include <glibmm/i18n.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/notebook.h> +#include <gtkmm/radiobutton.h> + +#include "ui/dialog/dialog-base.h" +#include "ui/widget/notebook-page.h" +#include "ui/widget/scalar-unit.h" + +namespace Gtk { +class Button; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/** + * Transformation dialog. + * + * The transformation dialog allows to modify Inkscape objects. + * 5 transformation operations are currently possible: move, scale, + * rotate, skew and matrix. + */ +class Transformation : public DialogBase +{ + +public: + + /** + * Constructor for Transformation. + * + * This does the initialization + * and layout of the dialog used for transforming SVG objects. It + * consists of 5 pages for the 5 operations it handles: + * 'Move' allows x,y translation of SVG objects + * 'Scale' allows linear resizing of SVG objects + * 'Rotate' allows rotating SVG objects by a degree + * 'Skew' allows skewing SVG objects + * 'Matrix' allows applying a generic affine transform on SVG objects, + * with the user specifying the 6 degrees of freedom manually. + * + * The dialog is implemented as a Gtk::Notebook with five pages. + * The pages are implemented using Inkscape's NotebookPage which + * is used to help make sure all of Inkscape's notebooks follow + * the same style. We then populate the pages with our widgets, + * we use the ScalarUnit class for this. + */ + Transformation(); + + /** + * Cleanup + */ + ~Transformation() override; + + /** + * Factory method. Create an instance of this class/interface + */ + static Transformation &getInstance() + { return *new Transformation(); } + + + /** + * Show the Move panel + */ + void setPageMove() + { presentPage(PAGE_MOVE); } + + + /** + * Show the Scale panel + */ + void setPageScale() + { presentPage(PAGE_SCALE); } + + + /** + * Show the Rotate panel + */ + void setPageRotate() + { presentPage(PAGE_ROTATE); } + + /** + * Show the Skew panel + */ + void setPageSkew() + { presentPage(PAGE_SKEW); } + + /** + * Show the Transform panel + */ + void setPageTransform() + { presentPage(PAGE_TRANSFORM); } + + + int getCurrentPage() + { return _notebook.get_current_page(); } + + enum PageType { + PAGE_MOVE, PAGE_SCALE, PAGE_ROTATE, PAGE_SKEW, PAGE_TRANSFORM, PAGE_QTY + }; + + void desktopReplaced() override; + void selectionChanged(Inkscape::Selection *selection) override; + void selectionModified(Inkscape::Selection *selection, guint flags) override; + void updateSelection(PageType page, Inkscape::Selection *selection); + +protected: + + Gtk::Notebook _notebook; + + UI::Widget::NotebookPage _page_move; + UI::Widget::NotebookPage _page_scale; + UI::Widget::NotebookPage _page_rotate; + UI::Widget::NotebookPage _page_skew; + UI::Widget::NotebookPage _page_transform; + + UI::Widget::UnitMenu _units_move; + UI::Widget::UnitMenu _units_scale; + UI::Widget::UnitMenu _units_rotate; + UI::Widget::UnitMenu _units_skew; + UI::Widget::UnitMenu _units_transform; + + UI::Widget::ScalarUnit _scalar_move_horizontal; + UI::Widget::ScalarUnit _scalar_move_vertical; + UI::Widget::ScalarUnit _scalar_scale_horizontal; + UI::Widget::ScalarUnit _scalar_scale_vertical; + UI::Widget::ScalarUnit _scalar_rotate; + UI::Widget::ScalarUnit _scalar_skew_horizontal; + UI::Widget::ScalarUnit _scalar_skew_vertical; + + UI::Widget::Scalar _scalar_transform_a; + UI::Widget::Scalar _scalar_transform_b; + UI::Widget::Scalar _scalar_transform_c; + UI::Widget::Scalar _scalar_transform_d; + UI::Widget::ScalarUnit _scalar_transform_e; + UI::Widget::ScalarUnit _scalar_transform_f; + + Gtk::RadioButton _counterclockwise_rotate; + Gtk::RadioButton _clockwise_rotate; + + Gtk::CheckButton _check_move_relative; + Gtk::CheckButton _check_scale_proportional; + Gtk::CheckButton _check_apply_separately; + Gtk::CheckButton _check_replace_matrix; + + /** + * Layout the GUI components, and prepare for use + */ + void layoutPageMove(); + void layoutPageScale(); + void layoutPageRotate(); + void layoutPageSkew(); + void layoutPageTransform(); + + void _apply(); + void presentPage(PageType page); + + void onSwitchPage(Gtk::Widget *page, guint pagenum); + + /** + * Callbacks for when a user changes values on the panels + */ + void onMoveValueChanged(); + void onMoveRelativeToggled(); + void onScaleXValueChanged(); + void onScaleYValueChanged(); + void onRotateValueChanged(); + void onRotateCounterclockwiseClicked(); + void onRotateClockwiseClicked(); + void onSkewValueChanged(); + void onTransformValueChanged(); + void onReplaceMatrixToggled(); + void onScaleProportionalToggled(); + + void onClear(); + + void onApplySeparatelyToggled(); + + /** + * Called when the selection is updated, to make + * the panel(s) show the new values. + * Editor---->dialog + */ + void updatePageMove(Inkscape::Selection *); + void updatePageScale(Inkscape::Selection *); + void updatePageRotate(Inkscape::Selection *); + void updatePageSkew(Inkscape::Selection *); + void updatePageTransform(Inkscape::Selection *); + + /** + * Called when the Apply button is pushed + * Dialog---->editor + */ + void applyPageMove(Inkscape::Selection *); + void applyPageScale(Inkscape::Selection *); + void applyPageRotate(Inkscape::Selection *); + void applyPageSkew(Inkscape::Selection *); + void applyPageTransform(Inkscape::Selection *); + +private: + + /** + * Copy constructor + */ + Transformation(Transformation const &d) = delete; + + /** + * Assignment operator + */ + Transformation operator=(Transformation const &d) = delete; + + Gtk::Button *applyButton; + Gtk::Button *resetButton; + + sigc::connection _selChangeConn; + sigc::connection _selModifyConn; + sigc::connection _tabSwitchConn; +}; + + + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + + +#endif //INKSCAPE_UI_DIALOG_TRANSFORMATION_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/ui/dialog/undo-history.cpp b/src/ui/dialog/undo-history.cpp new file mode 100644 index 0000000..0e617a3 --- /dev/null +++ b/src/ui/dialog/undo-history.cpp @@ -0,0 +1,386 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Undo History dialog - implementation. + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "undo-history.h" + +#include "actions/actions-tools.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "ui/icon-loader.h" +#include "util/signal-blocker.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/* Rendering functions for custom cell renderers */ +void CellRendererSPIcon::render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags) +{ + // if there is no icon name. + if ( _property_icon_name == "") return; + + // if the icon isn't cached, render it to a pixbuf + if ( !_icon_cache[_property_icon_name] ) { + + Gtk::Image* icon = Gtk::manage(new Gtk::Image()); + icon = sp_get_icon_image(_property_icon_name, Gtk::ICON_SIZE_MENU); + + if (icon) { + + // check icon type (inkscape, gtk, none) + if ( GTK_IS_IMAGE(icon->gobj()) ) { + _property_icon = sp_get_icon_pixbuf(_property_icon_name, 16); + } else { + delete icon; + return; + } + + delete icon; + property_pixbuf() = _icon_cache[_property_icon_name] = _property_icon.get_value(); + } + + } else { + property_pixbuf() = _icon_cache[_property_icon_name]; + } + + Gtk::CellRendererPixbuf::render_vfunc(cr, widget, background_area, + cell_area, flags); +} + + +void CellRendererInt::render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags) +{ + if( _filter(_property_number) ) { + std::ostringstream s; + s << _property_number << std::flush; + property_text() = s.str(); + Gtk::CellRendererText::render_vfunc(cr, widget, background_area, + cell_area, flags); + } +} + +const CellRendererInt::Filter& CellRendererInt::no_filter = CellRendererInt::NoFilter(); + +UndoHistory& UndoHistory::getInstance() +{ + return *new UndoHistory(); +} + +UndoHistory::UndoHistory() + : DialogBase("/dialogs/undo-history", "UndoHistory"), + _event_log(nullptr), + _scrolled_window(), + _event_list_store(), + _event_list_selection(_event_list_view.get_selection()), + _callback_connections() +{ + auto *_columns = &EventLog::getColumns(); + + set_size_request(-1, -1); + + pack_start(_scrolled_window); + _scrolled_window.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + + _event_list_view.set_enable_search(false); + _event_list_view.set_headers_visible(false); + + CellRendererSPIcon* icon_renderer = Gtk::manage(new CellRendererSPIcon()); + icon_renderer->property_xpad() = 2; + icon_renderer->property_width() = 24; + int cols_count = _event_list_view.append_column("Icon", *icon_renderer); + + Gtk::TreeView::Column* icon_column = _event_list_view.get_column(cols_count-1); + icon_column->add_attribute(icon_renderer->property_icon_name(), _columns->icon_name); + + CellRendererInt* children_renderer = Gtk::manage(new CellRendererInt(greater_than_1)); + children_renderer->property_weight() = 600; // =Pango::WEIGHT_SEMIBOLD (not defined in old versions of pangomm) + children_renderer->property_xalign() = 1.0; + children_renderer->property_xpad() = 2; + children_renderer->property_width() = 24; + + cols_count = _event_list_view.append_column("Children", *children_renderer); + Gtk::TreeView::Column* children_column = _event_list_view.get_column(cols_count-1); + children_column->add_attribute(children_renderer->property_number(), _columns->child_count); + + Gtk::CellRendererText* description_renderer = Gtk::manage(new Gtk::CellRendererText()); + description_renderer->property_ellipsize() = Pango::ELLIPSIZE_END; + + cols_count = _event_list_view.append_column("Description", *description_renderer); + Gtk::TreeView::Column* description_column = _event_list_view.get_column(cols_count-1); + description_column->add_attribute(description_renderer->property_text(), _columns->description); + description_column->set_resizable(); + description_column->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE); + description_column->set_min_width (150); + + _event_list_view.set_expander_column( *_event_list_view.get_column(cols_count-1) ); + + _scrolled_window.add(_event_list_view); + _scrolled_window.set_overlay_scrolling(false); + // connect EventLog callbacks + _callback_connections[EventLog::CALLB_SELECTION_CHANGE] = + _event_list_selection->signal_changed().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onListSelectionChange)); + + _callback_connections[EventLog::CALLB_EXPAND] = + _event_list_view.signal_row_expanded().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onExpandEvent)); + + _callback_connections[EventLog::CALLB_COLLAPSE] = + _event_list_view.signal_row_collapsed().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onCollapseEvent)); + + show_all_children(); +} + +UndoHistory::~UndoHistory() +{ + disconnectEventLog(); +} + +void UndoHistory::documentReplaced() +{ + disconnectEventLog(); + if (auto document = getDocument()) { + g_assert (document->get_event_log() != nullptr); + SignalBlocker blocker(&_callback_connections[EventLog::CALLB_SELECTION_CHANGE]); + _event_list_view.unset_model(); + connectEventLog(); + } +} + +void UndoHistory::disconnectEventLog() +{ + if (_event_log) { + _event_log->removeDialogConnection(&_event_list_view, &_callback_connections); + _event_log->remove_destroy_notify_callback(this); + } +} + +void UndoHistory::connectEventLog() +{ + if (auto document = getDocument()) { + _event_log = document->get_event_log(); + _event_log->add_destroy_notify_callback(this, &_handleEventLogDestroyCB); + _event_list_store = _event_log->getEventListStore(); + _event_list_view.set_model(_event_list_store); + _event_log->addDialogConnection(&_event_list_view, &_callback_connections); + _event_list_view.scroll_to_row(_event_list_store->get_path(_event_list_selection->get_selected())); + } +} + +void *UndoHistory::_handleEventLogDestroyCB(void *data) +{ + void *result = nullptr; + if (data) { + UndoHistory *self = reinterpret_cast<UndoHistory*>(data); + result = self->_handleEventLogDestroy(); + } + return result; +} + +// called *after* _event_log has been destroyed. +void *UndoHistory::_handleEventLogDestroy() +{ + if (_event_log) { + SignalBlocker blocker(&_callback_connections[EventLog::CALLB_SELECTION_CHANGE]); + + _event_list_view.unset_model(); + _event_list_store.reset(); + _event_log = nullptr; + } + + return nullptr; +} + +void +UndoHistory::_onListSelectionChange() +{ + + EventLog::const_iterator selected = _event_list_selection->get_selected(); + + /* If no event is selected in the view, find the right one and select it. This happens whenever + * a branch we're currently in is collapsed. + */ + // this fix crashes on redo with knots forcing regenerate knots on undo + SPDesktop *dt = getDesktop(); + Glib::ustring switch_selector_to = ""; + if (dt) { + switch_selector_to = get_active_tool(dt); + if (switch_selector_to != "Select") { + set_active_tool(dt, "Select"); + } + } + if (!selected) { + + EventLog::iterator curr_event = _event_log->getCurrEvent(); + + if (curr_event->parent()) { + + EventLog::iterator curr_event_parent = curr_event->parent(); + EventLog::iterator last = curr_event_parent->children().end(); + + _event_log->blockNotifications(); + for ( --last ; curr_event != last ; ++curr_event ) { + DocumentUndo::redo(getDocument()); + } + _event_log->blockNotifications(false); + + _event_log->setCurrEvent(curr_event); + _event_list_selection->select(curr_event_parent); + + } else { // this should not happen + _event_list_selection->select(curr_event); + } + + } else { + + EventLog::const_iterator last_selected = _event_log->getCurrEvent(); + + /* Selecting a collapsed parent event is equal to selecting the last child + * of that parent's branch. + */ + + if ( !selected->children().empty() && + !_event_list_view.row_expanded(_event_list_store->get_path(selected)) ) + { + selected = selected->children().end(); + --selected; + } + + // An event before the current one has been selected. Undo to the selected event. + if ( _event_list_store->get_path(selected) < + _event_list_store->get_path(last_selected) ) + { + _event_log->blockNotifications(); + + while ( selected != last_selected ) { + + DocumentUndo::undo(getDocument()); + + if ( last_selected->parent() && + last_selected == last_selected->parent()->children().begin() ) + { + last_selected = last_selected->parent(); + _event_log->setCurrEventParent((EventLog::iterator)nullptr); + } else { + --last_selected; + if ( !last_selected->children().empty() ) { + _event_log->setCurrEventParent(last_selected); + last_selected = last_selected->children().end(); + --last_selected; + } + } + } + _event_log->blockNotifications(false); + _event_log->updateUndoVerbs(); + + } else { // An event after the current one has been selected. Redo to the selected event. + + _event_log->blockNotifications(); + + while ( selected != last_selected ) { + + DocumentUndo::redo(getDocument()); + + if ( !last_selected->children().empty() ) { + _event_log->setCurrEventParent(last_selected); + last_selected = last_selected->children().begin(); + } else { + ++last_selected; + if ( last_selected->parent() && + last_selected == last_selected->parent()->children().end() ) + { + last_selected = last_selected->parent(); + ++last_selected; + _event_log->setCurrEventParent((EventLog::iterator)nullptr); + } + } + } + _event_log->blockNotifications(false); + + } + + _event_log->setCurrEvent(selected); + _event_log->updateUndoVerbs(); + } + if (dt && switch_selector_to != "Select") { + set_active_tool(dt, switch_selector_to); + } +} + +void +UndoHistory::_onExpandEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &/*path*/) +{ + if ( iter == _event_list_selection->get_selected() ) { + _event_list_selection->select(_event_log->getCurrEvent()); + } +} + +void +UndoHistory::_onCollapseEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &/*path*/) +{ + // Collapsing a branch we're currently in is equal to stepping to the last event in that branch + if ( iter == _event_log->getCurrEvent() ) { + // this fix crashes on redo with knots forcing regenerate knots on undo + SPDesktop *dt = getDesktop(); + Glib::ustring switch_selector_to = ""; + if (dt) { + switch_selector_to = get_active_tool(dt); + if (switch_selector_to != "Select") { + set_active_tool(dt, "Select"); + } + } + EventLog::const_iterator curr_event_parent = _event_log->getCurrEvent(); + EventLog::const_iterator curr_event = curr_event_parent->children().begin(); + EventLog::const_iterator last = curr_event_parent->children().end(); + + _event_log->blockNotifications(); + DocumentUndo::redo(getDocument()); + + for ( --last ; curr_event != last ; ++curr_event ) { + DocumentUndo::redo(getDocument()); + } + _event_log->blockNotifications(false); + + _event_log->setCurrEvent(curr_event); + _event_log->setCurrEventParent(curr_event_parent); + _event_list_selection->select(curr_event_parent); + if (dt && switch_selector_to != "Select") { + set_active_tool(dt, switch_selector_to); + } + } +} + +const CellRendererInt::Filter& UndoHistory::greater_than_1 = UndoHistory::GreaterThan(1); + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/undo-history.h b/src/ui/dialog/undo-history.h new file mode 100644 index 0000000..0b69693 --- /dev/null +++ b/src/ui/dialog/undo-history.h @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Undo History dialog + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_UNDO_HISTORY_H +#define INKSCAPE_UI_DIALOG_UNDO_HISTORY_H + +#include <functional> +#include <glibmm/property.h> +#include <gtkmm/cellrendererpixbuf.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/treemodel.h> +#include <gtkmm/treeselection.h> +#include <sstream> + +#include "event-log.h" +#include "ui/dialog/dialog-base.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/* Custom cell renderers */ + +class CellRendererSPIcon : public Gtk::CellRendererPixbuf { +public: + + CellRendererSPIcon() + : Glib::ObjectBase(typeid(CellRendererPixbuf)) + , Gtk::CellRendererPixbuf() + , _property_icon(*this, "icon", Glib::RefPtr<Gdk::Pixbuf>(nullptr)) + , _property_icon_name(*this, "our-icon-name", "inkscape-logo") // icon-name/icon_name used by Gtk + { } + + Glib::PropertyProxy<Glib::ustring> + property_icon_name() { return _property_icon_name.get_proxy(); } + +protected: + void render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags) override; +private: + + Glib::Property<Glib::RefPtr<Gdk::Pixbuf> > _property_icon; + Glib::Property<Glib::ustring> _property_icon_name; + std::map<Glib::ustring, Glib::RefPtr<Gdk::Pixbuf> > _icon_cache; + +}; + + +class CellRendererInt : public Gtk::CellRendererText { +public: + + struct Filter : std::unary_function<int, bool> { + virtual ~Filter() = default; + virtual bool operator() (const int&) const =0; + }; + + CellRendererInt(const Filter& filter=no_filter) : + Glib::ObjectBase(typeid(CellRendererText)), + Gtk::CellRendererText(), + _property_number(*this, "number", 0), + _filter (filter) + { } + + + Glib::PropertyProxy<int> + property_number() { return _property_number.get_proxy(); } + + static const Filter& no_filter; + +protected: + void render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags) override; + +private: + + Glib::Property<int> _property_number; + const Filter& _filter; + + struct NoFilter : Filter { bool operator() (const int& /*x*/) const override { return true; } }; +}; + +/** + * \brief Dialog for presenting document change history + * + * This dialog allows the user to undo and redo multiple events in a more convenient way + * than repateaded ctrl-z, ctrl-shift-z. + */ +class UndoHistory : public DialogBase +{ +public: + ~UndoHistory() override; + + static UndoHistory &getInstance(); + void documentReplaced() override; + +protected: + EventLog *_event_log; + + Gtk::ScrolledWindow _scrolled_window; + + Glib::RefPtr<Gtk::TreeModel> _event_list_store; + Gtk::TreeView _event_list_view; + Glib::RefPtr<Gtk::TreeSelection> _event_list_selection; + + EventLog::CallbackMap _callback_connections; + + static void *_handleEventLogDestroyCB(void *data); + + void disconnectEventLog(); + void connectEventLog(); + + void *_handleEventLogDestroy(); + void _onListSelectionChange(); + void _onExpandEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path); + void _onCollapseEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path); + +private: + UndoHistory(); + + // no default constructor, noncopyable, nonassignable + UndoHistory(UndoHistory const &d) = delete; + UndoHistory operator=(UndoHistory const &d) = delete; + + struct GreaterThan : CellRendererInt::Filter { + GreaterThan(int _i) : i (_i) {} + bool operator() (const int& x) const override { return x > i; } + int i; + }; + + static const CellRendererInt::Filter& greater_than_1; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_UI_DIALOG_UNDO_HISTORY_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/ui/dialog/xml-tree.cpp b/src/ui/dialog/xml-tree.cpp new file mode 100644 index 0000000..1c82a11 --- /dev/null +++ b/src/ui/dialog/xml-tree.cpp @@ -0,0 +1,952 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * XML editor. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * MenTaLguY <mental@rydia.net> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <goejendaagh@zonnet.nl> + * David Turner + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2006 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include "xml-tree.h" + +#include <glibmm/i18n.h> +#include <memory> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "layer-manager.h" +#include "message-context.h" +#include "message-stack.h" + +#include "object/sp-root.h" +#include "object/sp-string.h" + +#include "ui/dialog-events.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/tools/tool-base.h" + +#include "widgets/sp-xmlview-tree.h" + +namespace { +/** + * Set the orientation of `paned` to vertical or horizontal, and make the first child resizable + * if vertical, and the second child resizable if horizontal. + * @pre `paned` has two children + */ +void paned_set_vertical(Gtk::Paned &paned, bool vertical) +{ + paned.child_property_resize(*paned.get_child1()) = vertical; + assert(paned.child_property_resize(*paned.get_child2())); + paned.set_orientation(vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL); +} +} // namespace + +namespace Inkscape { +namespace UI { +namespace Dialog { + +XmlTree::XmlTree() + : DialogBase("/dialogs/xml/", "XMLEditor") + , blocked(0) + , _message_stack(nullptr) + , _message_context(nullptr) + , selected_attr(0) + , selected_repr(nullptr) + , tree(nullptr) + , status("") + , new_window(nullptr) + , _updating(false) + , node_box(Gtk::ORIENTATION_VERTICAL) + , status_box(Gtk::ORIENTATION_HORIZONTAL) +{ + Gtk::Box *contents = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + status.set_halign(Gtk::ALIGN_START); + status.set_valign(Gtk::ALIGN_CENTER); + status.set_size_request(1, -1); + status.set_markup(""); + status.set_line_wrap(true); + status.get_style_context()->add_class("inksmall"); + status_box.pack_start( status, TRUE, TRUE, 0); + contents->pack_start(_paned, true, true, 0); + contents->set_valign(Gtk::ALIGN_FILL); + contents->child_property_fill(_paned); + + _paned.set_vexpand(true); + _message_stack = std::make_shared<Inkscape::MessageStack>(); + _message_context = std::unique_ptr<Inkscape::MessageContext>(new Inkscape::MessageContext(_message_stack)); + _message_changed_connection = _message_stack->connectChanged( + sigc::bind(sigc::ptr_fun(_set_status_message), GTK_WIDGET(status.gobj()))); + + /* tree view */ + tree = SP_XMLVIEW_TREE(sp_xmlview_tree_new(nullptr, nullptr, nullptr)); + gtk_widget_set_tooltip_text( GTK_WIDGET(tree), _("Drag to reorder nodes") ); + + tree_toolbar.set_toolbar_style(Gtk::TOOLBAR_ICONS); + + auto xml_element_new_icon = Gtk::manage(sp_get_icon_image("xml-element-new", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + xml_element_new_button.set_icon_widget(*xml_element_new_icon); + xml_element_new_button.set_label(_("New element node")); + xml_element_new_button.set_tooltip_text(_("New element node")); + xml_element_new_button.set_sensitive(false); + tree_toolbar.add(xml_element_new_button); + + auto xml_text_new_icon = Gtk::manage(sp_get_icon_image("xml-text-new", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + xml_text_new_button.set_icon_widget(*xml_text_new_icon); + xml_text_new_button.set_label(_("New text node")); + xml_text_new_button.set_tooltip_text(_("New text node")); + xml_text_new_button.set_sensitive(false); + tree_toolbar.add(xml_text_new_button); + + auto xml_node_duplicate_icon = Gtk::manage(sp_get_icon_image("xml-node-duplicate", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + xml_node_duplicate_button.set_icon_widget(*xml_node_duplicate_icon); + xml_node_duplicate_button.set_label(_("Duplicate node")); + xml_node_duplicate_button.set_tooltip_text(_("Duplicate node")); + xml_node_duplicate_button.set_sensitive(false); + tree_toolbar.add(xml_node_duplicate_button); + + tree_toolbar.add(separator); + + auto xml_node_delete_icon = Gtk::manage(sp_get_icon_image("xml-node-delete", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + xml_node_delete_button.set_icon_widget(*xml_node_delete_icon); + xml_node_delete_button.set_label(_("Delete node")); + xml_node_delete_button.set_tooltip_text(_("Delete node")); + xml_node_delete_button.set_sensitive(false); + tree_toolbar.add(xml_node_delete_button); + + tree_toolbar.add(separator2); + + auto format_indent_less_icon = Gtk::manage(sp_get_icon_image("format-indent-less", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + unindent_node_button.set_icon_widget(*format_indent_less_icon); + unindent_node_button.set_label(_("Unindent node")); + unindent_node_button.set_tooltip_text(_("Unindent node")); + unindent_node_button.set_sensitive(false); + tree_toolbar.add(unindent_node_button); + + auto format_indent_more_icon = Gtk::manage(sp_get_icon_image("format-indent-more", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + indent_node_button.set_icon_widget(*format_indent_more_icon); + indent_node_button.set_label(_("Indent node")); + indent_node_button.set_tooltip_text(_("Indent node")); + indent_node_button.set_sensitive(false); + tree_toolbar.add(indent_node_button); + + auto go_up_icon = Gtk::manage(sp_get_icon_image("go-up", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + raise_node_button.set_icon_widget(*go_up_icon); + raise_node_button.set_label(_("Raise node")); + raise_node_button.set_tooltip_text(_("Raise node")); + raise_node_button.set_sensitive(false); + tree_toolbar.add(raise_node_button); + + auto go_down_icon = Gtk::manage(sp_get_icon_image("go-down", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + lower_node_button.set_icon_widget(*go_down_icon); + lower_node_button.set_label(_("Lower node")); + lower_node_button.set_tooltip_text(_("Lower node")); + lower_node_button.set_sensitive(false); + tree_toolbar.add(lower_node_button); + + node_box.pack_start(tree_toolbar, FALSE, TRUE, 0); + + Gtk::ScrolledWindow *tree_scroller = new Gtk::ScrolledWindow(); + tree_scroller->set_overlay_scrolling(false); + tree_scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + tree_scroller->set_shadow_type(Gtk::SHADOW_IN); + tree_scroller->add(*Gtk::manage(Glib::wrap(GTK_WIDGET(tree)))); + fix_inner_scroll(tree_scroller); + + node_box.pack_start(*Gtk::manage(tree_scroller)); + + node_box.pack_end(status_box, false, false, 2); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool attrtoggler = prefs->getBool("/dialogs/xml/attrtoggler", true); + bool dir = prefs->getBool("/dialogs/xml/vertical", true); + attributes = new AttrDialog(); + _paned.set_wide_handle(true); + _paned.pack1(node_box, false, false); + /* attributes */ + Gtk::Box *actionsbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + actionsbox->set_valign(Gtk::ALIGN_START); + Gtk::Label *attrtogglerlabel = Gtk::manage(new Gtk::Label(_("Show attributes"))); + attrtogglerlabel->set_margin_end(5); + _attrswitch.get_style_context()->add_class("inkswitch"); + _attrswitch.get_style_context()->add_class("rawstyle"); + _attrswitch.property_active() = attrtoggler; + _attrswitch.property_active().signal_changed().connect(sigc::mem_fun(*this, &XmlTree::_attrtoggler)); + attrtogglerlabel->get_style_context()->add_class("inksmall"); + actionsbox->pack_start(*attrtogglerlabel, Gtk::PACK_SHRINK); + actionsbox->pack_start(_attrswitch, Gtk::PACK_SHRINK); + Gtk::RadioButton::Group group; + Gtk::RadioButton *_horizontal = Gtk::manage(new Gtk::RadioButton()); + Gtk::RadioButton *_vertical = Gtk::manage(new Gtk::RadioButton()); + _horizontal->set_image_from_icon_name(INKSCAPE_ICON("horizontal")); + _vertical->set_image_from_icon_name(INKSCAPE_ICON("vertical")); + _horizontal->set_group(group); + _vertical->set_group(group); + _vertical->set_active(dir); + _vertical->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &XmlTree::_toggleDirection), _vertical)); + _horizontal->property_draw_indicator() = false; + _vertical->property_draw_indicator() = false; + actionsbox->pack_end(*_horizontal, false, false, 0); + actionsbox->pack_end(*_vertical, false, false, 0); + _paned.pack2(*attributes, true, false); + paned_set_vertical(_paned, dir); + contents->pack_start(*actionsbox, false, false, 0); + /* Signal handlers */ + GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(tree)); + _selection_changed = g_signal_connect (G_OBJECT(selection), "changed", G_CALLBACK (on_tree_select_row), this); + _tree_move = g_signal_connect_after( G_OBJECT(tree), "tree_move", G_CALLBACK(after_tree_move), this); + + xml_element_new_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_new_element_node)); + xml_text_new_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_new_text_node)); + xml_node_duplicate_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_duplicate_node)); + xml_node_delete_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_delete_node)); + unindent_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_unindent_node)); + indent_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_indent_node)); + raise_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_raise_node)); + lower_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_lower_node)); + + set_name("XMLAndAttributesDialog"); + set_spacing(0); + set_size_request(320, -1); + show_all(); + + int panedpos = prefs->getInt("/dialogs/xml/panedpos", 200); + _paned.property_position() = panedpos; + _paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &XmlTree::_resized)); + + tree_reset_context(); + pack_start(*Gtk::manage(contents), true, true); +} + +void XmlTree::_resized() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + prefs->setInt("/dialogs/xml/panedpos", _paned.property_position()); +} + +void XmlTree::_toggleDirection(Gtk::RadioButton *vertical) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool dir = vertical->get_active(); + prefs->setBool("/dialogs/xml/vertical", dir); + paned_set_vertical(_paned, dir); + prefs->setInt("/dialogs/xml/panedpos", _paned.property_position()); +} + +void XmlTree::_attrtoggler() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool attrtoggler = !prefs->getBool("/dialogs/xml/attrtoggler", true); + prefs->setBool("/dialogs/xml/attrtoggler", attrtoggler); + if (attrtoggler) { + attributes->show(); + } else { + attributes->hide(); + } +} + +XmlTree::~XmlTree () +{ + // disconnect signals, they can fire after we leave destructor when 'tree' gets deleted + GtkTreeSelection* selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(tree)); + g_signal_handler_disconnect(G_OBJECT(selection), _selection_changed); + g_signal_handler_disconnect(G_OBJECT(tree), _tree_move); + + unsetDocument(); + _message_changed_connection.disconnect(); +} + +/** + * Sets the XML status bar when the tree is selected. + */ +void XmlTree::tree_reset_context() +{ + _message_context->set(Inkscape::NORMAL_MESSAGE, + _("<b>Click</b> to select nodes, <b>drag</b> to rearrange.")); +} + +void XmlTree::unsetDocument() +{ + document_uri_set_connection.disconnect(); + if (deferred_on_tree_select_row_id != 0) { + g_source_destroy(g_main_context_find_source_by_id(nullptr, deferred_on_tree_select_row_id)); + deferred_on_tree_select_row_id = 0; + } +} + +void XmlTree::documentReplaced() +{ + unsetDocument(); + if (auto document = getDocument()) { + // TODO: Why is this a document property? + document->setXMLDialogSelectedObject(nullptr); + + document_uri_set_connection = + document->connectFilenameSet(sigc::bind(sigc::ptr_fun(&on_document_uri_set), document)); + on_document_uri_set(document->getDocumentFilename(), document); + set_tree_repr(document->getReprRoot()); + } else { + set_tree_repr(nullptr); + } +} + +void XmlTree::selectionChanged(Selection *selection) +{ + if (!blocked++) { + Inkscape::XML::Node *node = get_dt_select(); + set_tree_select(node); + } + blocked--; +} + +void XmlTree::set_tree_repr(Inkscape::XML::Node *repr) +{ + if (repr == selected_repr) { + return; + } + + sp_xmlview_tree_set_repr(tree, repr); + if (repr) { + set_tree_select(get_dt_select()); + } else { + set_tree_select(nullptr); + } + + propagate_tree_select(selected_repr); + +} + +/** + * Expand all parent nodes of `repr` + */ +static void expand_parents(SPXMLViewTree *tree, Inkscape::XML::Node *repr) +{ + auto parentrepr = repr->parent(); + if (!parentrepr) { + return; + } + + expand_parents(tree, parentrepr); + + GtkTreeIter node; + if (sp_xmlview_tree_get_repr_node(tree, parentrepr, &node)) { + GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), &node); + if (path) { + gtk_tree_view_expand_row(GTK_TREE_VIEW(tree), path, false); + } + } +} + +void XmlTree::set_tree_select(Inkscape::XML::Node *repr) +{ + if (selected_repr) { + Inkscape::GC::release(selected_repr); + } + selected_repr = repr; + if (selected_repr) { + Inkscape::GC::anchor(selected_repr); + } + if (auto document = getDocument()) { + document->setXMLDialogSelectedObject(nullptr); + } + if (repr) { + GtkTreeIter node; + + Inkscape::GC::anchor(selected_repr); + + expand_parents(tree, repr); + + if (sp_xmlview_tree_get_repr_node(SP_XMLVIEW_TREE(tree), repr, &node)) { + + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + gtk_tree_selection_unselect_all (selection); + + GtkTreePath* path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), &node); + gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(tree), path, nullptr, TRUE, 0.66, 0.0); + gtk_tree_selection_select_iter(selection, &node); + gtk_tree_view_set_cursor(GTK_TREE_VIEW(tree), path, NULL, false); + gtk_tree_path_free(path); + + } else { + g_message("XmlTree::set_tree_select : Couldn't find repr node"); + } + } else { + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + gtk_tree_selection_unselect_all (selection); + + on_tree_unselect_row_disable(); + } + propagate_tree_select(repr); +} + + + +void XmlTree::propagate_tree_select(Inkscape::XML::Node *repr) +{ + if (repr && + (repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE || + repr->type() == Inkscape::XML::NodeType::TEXT_NODE || + repr->type() == Inkscape::XML::NodeType::COMMENT_NODE)) + { + attributes->setRepr(repr); + } else { + attributes->setRepr(nullptr); + } +} + + +Inkscape::XML::Node *XmlTree::get_dt_select() +{ + if (auto selection = getSelection()) { + return selection->singleRepr(); + } + return nullptr; +} + + +/** + * Like SPDesktop::isLayer(), but ignores SPGroup::effectiveLayerMode(). + */ +static bool isRealLayer(SPObject const *object) +{ + auto group = dynamic_cast<SPGroup const *>(object); + return group && group->layerMode() == SPGroup::LAYER; +} + +void XmlTree::set_dt_select(Inkscape::XML::Node *repr) +{ + auto document = getDocument(); + if (!document) + return; + + SPObject *object; + if (repr) { + while ( ( repr->type() != Inkscape::XML::NodeType::ELEMENT_NODE ) + && repr->parent() ) + { + repr = repr->parent(); + } // end of while loop + + object = document->getObjectByRepr(repr); + } else { + object = nullptr; + } + + blocked++; + + if (!object || !in_dt_coordsys(*object)) { + // object not on canvas + } else if (isRealLayer(object)) { + getDesktop()->layerManager().setCurrentLayer(object); + } else { + if (SP_IS_GROUP(object->parent)) { + getDesktop()->layerManager().setCurrentLayer(object->parent); + } + + getSelection()->set(SP_ITEM(object)); + } + + document->setXMLDialogSelectedObject(object); + blocked--; +} + + +void XmlTree::on_tree_select_row(GtkTreeSelection *selection, gpointer data) +{ + XmlTree *self = static_cast<XmlTree *>(data); + + if (self->blocked || !self->getDesktop()) { + return; + } + + // Defer the update after all events have been processed. Allows skipping + // of invalid intermediate selection states, like the automatic next row + // selection after `gtk_tree_store_remove`. + if (self->deferred_on_tree_select_row_id == 0) { + self->deferred_on_tree_select_row_id = // + g_idle_add(XmlTree::deferred_on_tree_select_row, data); + } +} + +gboolean XmlTree::deferred_on_tree_select_row(gpointer data) +{ + XmlTree *self = static_cast<XmlTree *>(data); + + self->deferred_on_tree_select_row_id = 0; + + GtkTreeIter iter; + GtkTreeModel *model; + + if (self->selected_repr) { + Inkscape::GC::release(self->selected_repr); + self->selected_repr = nullptr; + } + + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self->tree)); + + if (!gtk_tree_selection_get_selected (selection, &model, &iter)) { + // Nothing selected, update widgets + self->propagate_tree_select(nullptr); + self->set_dt_select(nullptr); + self->on_tree_unselect_row_disable(); + return FALSE; + } + + Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(model, &iter); + g_assert(repr != nullptr); + + + self->selected_repr = repr; + Inkscape::GC::anchor(self->selected_repr); + + self->propagate_tree_select(self->selected_repr); + + self->set_dt_select(self->selected_repr); + + self->tree_reset_context(); + + self->on_tree_select_row_enable(&iter); + + return FALSE; +} + + +void XmlTree::after_tree_move(SPXMLViewTree * /*tree*/, gpointer value, gpointer data) +{ + XmlTree *self = static_cast<XmlTree *>(data); + guint val = GPOINTER_TO_UINT(value); + + if (val) { + DocumentUndo::done(self->getDocument(), Q_("Undo History / XML dialog|Drag XML subtree"), INKSCAPE_ICON("dialog-xml-editor")); + } else { + DocumentUndo::cancel(self->getDocument()); + } +} + +void XmlTree::_set_status_message(Inkscape::MessageType /*type*/, const gchar *message, GtkWidget *widget) +{ + if (widget) { + gtk_label_set_markup(GTK_LABEL(widget), message ? message : ""); + } +} + +void XmlTree::on_tree_select_row_enable(GtkTreeIter *node) +{ + if (!node) { + return; + } + + Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(GTK_TREE_MODEL(tree->store), node); + Inkscape::XML::Node *parent=repr->parent(); + + //on_tree_select_row_enable_if_mutable + xml_node_duplicate_button.set_sensitive(xml_tree_node_mutable(node)); + xml_node_delete_button.set_sensitive(xml_tree_node_mutable(node)); + + //on_tree_select_row_enable_if_element + if (repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE) { + xml_element_new_button.set_sensitive(true); + xml_text_new_button.set_sensitive(true); + + } else { + xml_element_new_button.set_sensitive(false); + xml_text_new_button.set_sensitive(false); + } + + //on_tree_select_row_enable_if_has_grandparent + { + GtkTreeIter parent; + if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &parent, node)) { + GtkTreeIter grandparent; + if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &grandparent, &parent)) { + unindent_node_button.set_sensitive(true); + } else { + unindent_node_button.set_sensitive(false); + } + } else { + unindent_node_button.set_sensitive(false); + } + } + // on_tree_select_row_enable_if_indentable + gboolean indentable = FALSE; + + if (xml_tree_node_mutable(node)) { + Inkscape::XML::Node *prev; + + if ( parent && repr != parent->firstChild() ) { + g_assert(parent->firstChild()); + + // skip to the child just before the current repr + for ( prev = parent->firstChild() ; + prev && prev->next() != repr ; + prev = prev->next() ){}; + + if (prev && (prev->type() == Inkscape::XML::NodeType::ELEMENT_NODE)) { + indentable = TRUE; + } + } + } + + indent_node_button.set_sensitive(indentable); + + //on_tree_select_row_enable_if_not_first_child + { + if ( parent && repr != parent->firstChild() ) { + raise_node_button.set_sensitive(true); + } else { + raise_node_button.set_sensitive(false); + } + } + + //on_tree_select_row_enable_if_not_last_child + { + if ( parent && (parent->parent() && repr->next())) { + lower_node_button.set_sensitive(true); + } else { + lower_node_button.set_sensitive(false); + } + } +} + + +gboolean XmlTree::xml_tree_node_mutable(GtkTreeIter *node) +{ + // top-level is immutable, obviously + GtkTreeIter parent; + if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &parent, node)) { + return false; + } + + + // if not in base level (where namedview, defs, etc go), we're mutable + GtkTreeIter child; + if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &child, &parent)) { + return true; + } + + Inkscape::XML::Node *repr; + repr = sp_xmlview_tree_node_get_repr(GTK_TREE_MODEL(tree->store), node); + g_assert(repr); + + // don't let "defs" or "namedview" disappear + if ( !strcmp(repr->name(),"svg:defs") || + !strcmp(repr->name(),"sodipodi:namedview") ) { + return false; + } + + // everyone else is okay, I guess. :) + return true; +} + + + +void XmlTree::on_tree_unselect_row_disable() +{ + xml_text_new_button.set_sensitive(false); + xml_element_new_button.set_sensitive(false); + xml_node_delete_button.set_sensitive(false); + xml_node_duplicate_button.set_sensitive(false); + unindent_node_button.set_sensitive(false); + indent_node_button.set_sensitive(false); + raise_node_button.set_sensitive(false); + lower_node_button.set_sensitive(false); +} + +void XmlTree::onCreateNameChanged() +{ + Glib::ustring text = name_entry->get_text(); + /* TODO: need to do checking a little more rigorous than this */ + create_button->set_sensitive(!text.empty()); +} + +void XmlTree::on_document_uri_set(gchar const * /*uri*/, SPDocument * /*document*/) +{ +/* + * Seems to be no way to set the title on a docked dialog +*/ +} + +gboolean XmlTree::quit_on_esc (GtkWidget *w, GdkEventKey *event, GObject */*tbl*/) +{ + switch (Inkscape::UI::Tools::get_latin_keyval (event)) { + case GDK_KEY_Escape: // defocus + gtk_widget_destroy(w); + return TRUE; + case GDK_KEY_Return: // create + case GDK_KEY_KP_Enter: + gtk_widget_destroy(w); + return TRUE; + } + return FALSE; +} + +void XmlTree::cmd_new_element_node() +{ + auto document = getDocument(); + if (!document) + return; + + Gtk::Dialog dialog; + Gtk::Entry entry; + + dialog.get_content_area()->pack_start(entry); + dialog.add_button("Cancel", Gtk::RESPONSE_CANCEL); + dialog.add_button("Create", Gtk::RESPONSE_OK); + dialog.show_all(); + + int result = dialog.run(); + if (result == Gtk::RESPONSE_OK) { + Glib::ustring new_name = entry.get_text(); + if (!new_name.empty()) { + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *new_repr; + new_repr = xml_doc->createElement(new_name.c_str()); + Inkscape::GC::release(new_repr); + selected_repr->appendChild(new_repr); + set_tree_select(new_repr); + set_dt_select(new_repr); + + DocumentUndo::done(document, Q_("Undo History / XML dialog|Create new element node"), INKSCAPE_ICON("dialog-xml-editor")); + } + } +} // end of cmd_new_element_node() + + +void XmlTree::cmd_new_text_node() +{ + auto document = getDocument(); + if (!document) + return; + + g_assert(selected_repr != nullptr); + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *text = xml_doc->createTextNode(""); + selected_repr->appendChild(text); + + DocumentUndo::done(document, Q_("Undo History / XML dialog|Create new text node"), INKSCAPE_ICON("dialog-xml-editor")); + + set_tree_select(text); + set_dt_select(text); +} + +void XmlTree::cmd_duplicate_node() +{ + auto document = getDocument(); + if (!document) + return; + + g_assert(selected_repr != nullptr); + + Inkscape::XML::Node *parent = selected_repr->parent(); + Inkscape::XML::Node *dup = selected_repr->duplicate(parent->document()); + parent->addChild(dup, selected_repr); + + DocumentUndo::done(document, Q_("Undo History / XML dialog|Duplicate node"), INKSCAPE_ICON("dialog-xml-editor")); + + GtkTreeIter node; + + if (sp_xmlview_tree_get_repr_node(SP_XMLVIEW_TREE(tree), dup, &node)) { + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + gtk_tree_selection_select_iter(selection, &node); + } +} + +void XmlTree::cmd_delete_node() +{ + auto document = getDocument(); + if (!document) + return; + + g_assert(selected_repr != nullptr); + + document->setXMLDialogSelectedObject(nullptr); + + Inkscape::XML::Node *parent = selected_repr->parent(); + + sp_repr_unparent(selected_repr); + + if (parent) { + auto parentobject = document->getObjectByRepr(parent); + if (parentobject) { + parentobject->requestDisplayUpdate(SP_OBJECT_CHILD_MODIFIED_FLAG); + } + } + + DocumentUndo::done(document, Q_("Undo History / XML dialog|Delete node"), INKSCAPE_ICON("dialog-xml-editor")); +} + +void XmlTree::cmd_raise_node() +{ + auto document = getDocument(); + if (!document) + return; + + g_assert(selected_repr != nullptr); + + Inkscape::XML::Node *parent = selected_repr->parent(); + g_return_if_fail(parent != nullptr); + g_return_if_fail(parent->firstChild() != selected_repr); + + Inkscape::XML::Node *ref = nullptr; + Inkscape::XML::Node *before = parent->firstChild(); + while (before && (before->next() != selected_repr)) { + ref = before; + before = before->next(); + } + + parent->changeOrder(selected_repr, ref); + + DocumentUndo::done(document, Q_("Undo History / XML dialog|Raise node"), INKSCAPE_ICON("dialog-xml-editor")); + + set_tree_select(selected_repr); + set_dt_select(selected_repr); +} + + + +void XmlTree::cmd_lower_node() +{ + auto document = getDocument(); + if (!document) + return; + + g_assert(selected_repr != nullptr); + + g_return_if_fail(selected_repr->next() != nullptr); + Inkscape::XML::Node *parent = selected_repr->parent(); + + parent->changeOrder(selected_repr, selected_repr->next()); + + DocumentUndo::done(document, Q_("Undo History / XML dialog|Lower node"), INKSCAPE_ICON("dialog-xml-editor")); + + set_tree_select(selected_repr); + set_dt_select(selected_repr); +} + +void XmlTree::cmd_indent_node() +{ + auto document = getDocument(); + if (!document) + return; + + Inkscape::XML::Node *repr = selected_repr; + g_assert(repr != nullptr); + + Inkscape::XML::Node *parent = repr->parent(); + g_return_if_fail(parent != nullptr); + g_return_if_fail(parent->firstChild() != repr); + + Inkscape::XML::Node* prev = parent->firstChild(); + while (prev && (prev->next() != repr)) { + prev = prev->next(); + } + g_return_if_fail(prev != nullptr); + g_return_if_fail(prev->type() == Inkscape::XML::NodeType::ELEMENT_NODE); + + Inkscape::XML::Node* ref = nullptr; + if (prev->firstChild()) { + for( ref = prev->firstChild() ; ref->next() ; ref = ref->next() ){}; + } + + parent->removeChild(repr); + prev->addChild(repr, ref); + + DocumentUndo::done(document, Q_("Undo History / XML dialog|Indent node"), INKSCAPE_ICON("dialog-xml-editor")); + set_tree_select(repr); + set_dt_select(repr); + +} // end of cmd_indent_node() + + + +void XmlTree::cmd_unindent_node() +{ + auto document = getDocument(); + if (!document) + return; + + Inkscape::XML::Node *repr = selected_repr; + g_assert(repr != nullptr); + + Inkscape::XML::Node *parent = repr->parent(); + g_return_if_fail(parent); + Inkscape::XML::Node *grandparent = parent->parent(); + g_return_if_fail(grandparent); + + parent->removeChild(repr); + grandparent->addChild(repr, parent); + + DocumentUndo::done(document, Q_("Undo History / XML dialog|Unindent node"), INKSCAPE_ICON("dialog-xml-editor")); + + set_tree_select(repr); + set_dt_select(repr); + +} // end of cmd_unindent_node() + +/** Returns true iff \a item is suitable to be included in the selection, in particular + whether it has a bounding box in the desktop coordinate system for rendering resize handles. + + Descendents of <defs> nodes (markers etc.) return false, for example. +*/ +bool XmlTree::in_dt_coordsys(SPObject const &item) +{ + /* Definition based on sp_item_i2doc_affine. */ + SPObject const *child = &item; + while (SP_IS_ITEM(child)) { + SPObject const * const parent = child->parent; + if (parent == nullptr) { + g_assert(SP_IS_ROOT(child)); + if (child == &item) { + // item is root + return false; + } + return true; + } + child = parent; + } + g_assert(!SP_IS_ROOT(child)); + return false; +} + +void XmlTree::desktopReplaced() { + // subdialog does not receive desktopReplace calls, we need to propagate desktop change + if (attributes) { + attributes->setDesktop(getDesktop()); + } +} + +} +} +} + +/* + 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/ui/dialog/xml-tree.h b/src/ui/dialog/xml-tree.h new file mode 100644 index 0000000..3586666 --- /dev/null +++ b/src/ui/dialog/xml-tree.h @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * This is XML tree editor, which allows direct modifying of all elements + * of Inkscape document, including foreign ones. + *//* + * Authors: see git history + * Lauris Kaplinski, 2000 + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_DIALOGS_XML_TREE_H +#define SEEN_UI_DIALOGS_XML_TREE_H + +#include <gtkmm/button.h> +#include <gtkmm/entry.h> +#include <gtkmm/paned.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/separatortoolitem.h> +#include <gtkmm/switch.h> +#include <gtkmm/textview.h> +#include <gtkmm/toolbar.h> +#include <memory> + +#include "message.h" +#include "ui/dialog/attrdialog.h" +#include "ui/dialog/dialog-base.h" + +class SPObject; +struct SPXMLViewAttrList; +struct SPXMLViewContent; +struct SPXMLViewTree; + +namespace Inkscape { +class MessageStack; +class MessageContext; + +namespace XML { +class Node; +} + +namespace UI { +namespace Dialog { + +/** + * A dialog widget to view and edit the document xml + * + */ + +class XmlTree : public DialogBase +{ +public: + XmlTree (); + ~XmlTree () override; + + static XmlTree &getInstance() { return *new XmlTree(); } + +private: + void unsetDocument(); + void documentReplaced() override; + void selectionChanged(Selection *selection) override; + void desktopReplaced() override; + + /** + * Select a node in the xml tree + */ + void set_tree_repr(Inkscape::XML::Node *repr); + + /** + * Sets the XML status bar when the tree is selected. + */ + void tree_reset_context(); + + /** + * Is the selected tree node editable + */ + gboolean xml_tree_node_mutable(GtkTreeIter *node); + + /** + * Callback to close the add dialog on Escape key + */ + static gboolean quit_on_esc (GtkWidget *w, GdkEventKey *event, GObject */*tbl*/); + + /** + * Select a node in the xml tree + */ + void set_tree_select(Inkscape::XML::Node *repr); + + /** + * Set the attribute list to match the selected node in the tree + */ + void propagate_tree_select(Inkscape::XML::Node *repr); + + /** + * Find the current desktop selection + */ + Inkscape::XML::Node *get_dt_select(); + + /** + * Select the current desktop selection + */ + void set_dt_select(Inkscape::XML::Node *repr); + + /** + * Callback for a node in the tree being selected + */ + static void on_tree_select_row(GtkTreeSelection *selection, gpointer data); + /** + * Callback for deferring the `on_tree_select_row` response in order to + * skip invalid intermediate selection states. In particular, + * `gtk_tree_store_remove` makes an undesired selection that we will + * immediately revert and don't want to an early response for. + */ + static gboolean deferred_on_tree_select_row(gpointer); + /// Event source ID for the last scheduled `deferred_on_tree_select_row` event. + guint deferred_on_tree_select_row_id = 0; + + /** + * Callback when a node is moved in the tree + */ + static void after_tree_move(SPXMLViewTree *tree, gpointer value, gpointer data); + + /** + * Callback for when an attribute is edited. + */ + //static void on_attr_edited(SPXMLViewAttrList *attributes, const gchar * name, const gchar * value, gpointer /*data*/); + + /** + * Callback for when attribute list values change + */ + //static void on_attr_row_changed(SPXMLViewAttrList *attributes, const gchar * name, gpointer data); + + /** + * Enable widgets based on current selections + */ + void on_tree_select_row_enable(GtkTreeIter *node); + void on_tree_unselect_row_disable(); + void on_tree_unselect_row_hide(); + void on_attr_unselect_row_disable(); + + void onNameChanged(); + void onCreateNameChanged(); + + /** + * Callbacks for changes in desktop selection and current document + */ + static void on_document_uri_set(gchar const *uri, SPDocument *document); + static void _set_status_message(Inkscape::MessageType type, const gchar *message, GtkWidget *dialog); + + /** + * Callbacks for toolbar buttons being pressed + */ + void cmd_new_element_node(); + void cmd_new_text_node(); + void cmd_duplicate_node(); + void cmd_delete_node(); + void cmd_raise_node(); + void cmd_lower_node(); + void cmd_indent_node(); + void cmd_unindent_node(); + + void _attrtoggler(); + void _toggleDirection(Gtk::RadioButton *vertical); + void _resized(); + bool in_dt_coordsys(SPObject const &item); + + /** + * Flag to ensure only one operation is performed at once + */ + gint blocked; + + bool _updating; + /** + * Status bar + */ + std::shared_ptr<Inkscape::MessageStack> _message_stack; + std::unique_ptr<Inkscape::MessageContext> _message_context; + + /** + * Signal handlers + */ + sigc::connection _message_changed_connection; + sigc::connection document_uri_set_connection; + + gint selected_attr; + Inkscape::XML::Node *selected_repr; + + /* XmlTree Widgets */ + SPXMLViewTree *tree; + //SPXMLViewAttrList *attributes; + AttrDialog *attributes; + Gtk::Box *_attrbox; + + /* XML Node Creation pop-up window */ + Gtk::Entry *name_entry; + Gtk::Button *create_button; + Gtk::Paned _paned; + + Gtk::Box node_box; + Gtk::Box status_box; + Gtk::Switch _attrswitch; + Gtk::Label status; + Gtk::Toolbar tree_toolbar; + Gtk::ToolButton xml_element_new_button; + Gtk::ToolButton xml_text_new_button; + Gtk::ToolButton xml_node_delete_button; + Gtk::SeparatorToolItem separator; + Gtk::ToolButton xml_node_duplicate_button; + Gtk::SeparatorToolItem separator2; + Gtk::ToolButton unindent_node_button; + Gtk::ToolButton indent_node_button; + Gtk::ToolButton raise_node_button; + Gtk::ToolButton lower_node_button; + + GtkWidget *new_window; + + gulong _selection_changed = 0; + gulong _tree_move = 0; +}; + +} +} +} + +#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 : |