diff options
Diffstat (limited to 'src/ui/dialog')
133 files changed, 59679 insertions, 0 deletions
diff --git a/src/ui/dialog/aboutbox.cpp b/src/ui/dialog/aboutbox.cpp new file mode 100644 index 0000000..72d31cc --- /dev/null +++ b/src/ui/dialog/aboutbox.cpp @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkscape About box - implementation. + */ +/* Authors: + * Derek P. Moore <derekm@hackunix.org> + * MenTaLguY <mental@rydia.net> + * Kees Cook <kees@outflux.net> + * Jon Phillips <jon@rejon.org> + * Abhishek Sharma + * + * Copyright (C) 2004 Derek P. Moore + * Copyright 2004 Kees Cook + * Copyright 2004 Jon Phillips + * Copyright 2005 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "aboutbox.h" + +#include <fstream> + +#include <glibmm/fileutils.h> +#include <glibmm/i18n.h> +#include <glibmm/miscutils.h> + +#include <gtkmm/aspectframe.h> +#include <gtkmm/textview.h> + +#include "document.h" +#include "inkscape-version.h" +#include "path-prefix.h" +#include "text-editing.h" + +#include "object/sp-text.h" + +#include "ui/icon-names.h" +#include "ui/view/svg-view-widget.h" + +#include "util/units.h" + + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static AboutBox *window=nullptr; + +void AboutBox::show_about() { + if (!window) + window = new AboutBox(); + window->show(); +} + +void AboutBox::hide_about() { + if (window) + window->hide(); +} + +/** + * Constructor + */ +AboutBox::AboutBox() + : _splash_widget(nullptr) +{ + // call this first + initStrings(); + + // Insert the Splash widget. This is placed directly into the + // content area of the dialog, whereas everything else is placed + // automatically by the Gtk::AboutDialog parent class + build_splash_widget(); + if (_splash_widget) { + get_content_area()->pack_end(*manage(_splash_widget), true, true); + _splash_widget->show_all(); + } + + // Set Application metadata, which will be automatically + // inserted into text widgets by the Gtk::AboutDialog parent class + // clang-format off + set_program_name ( "Inkscape"); + set_version ( Inkscape::version_string); + set_logo_icon_name( INKSCAPE_ICON("org.inkscape.Inkscape")); + set_website ( "https://www.inkscape.org"); + set_website_label (_("Inkscape website")); + set_license_type (Gtk::LICENSE_GPL_3_0); + set_copyright (_("© 2020 Inkscape Developers")); + set_comments (_("Open Source Scalable Vector Graphics Editor\n" + "Draw Freely.")); + // clang-format on + + get_content_area()->set_border_width(3); + get_action_area()->set_border_width(3); +} + +/** + * @brief Create a Gtk::AspectFrame containing the splash image + */ +void AboutBox::build_splash_widget() { + /* TRANSLATORS: This is the filename of the `About Inkscape' picture in + the `screens' directory. Thus the translation of "about.svg" should be + the filename of its translated version, e.g. about.zh.svg for Chinese. + + Please don't translate the filename unless the translated picture exists. */ + + // Try to get the translated version of the 'About Inkscape' file first. If the + // translation fails, or if the file does not exist, then fall-back to the + // default untranslated "about.svg" file + // + // FIXME? INKSCAPE_SCREENSDIR and "about.svg" are in UTF-8, not the + // native filename encoding... and the filename passed to sp_document_new + // should be in UTF-*8.. + auto about = Glib::build_filename(INKSCAPE_SCREENSDIR, _("about.svg")); + if (!Glib::file_test (about, Glib::FILE_TEST_EXISTS)) { + about = Glib::build_filename(INKSCAPE_SCREENSDIR, "about.svg"); + } + + // Create an Inkscape document from the 'About Inkscape' picture + SPDocument *doc=SPDocument::createNewDoc (about.c_str(), TRUE); + + // Leave _splash_widget as a nullptr if there is no document + if(doc) { + SPObject *version = doc->getObjectById("version"); + if ( version && SP_IS_TEXT(version) ) { + sp_te_set_repr_text_multiline (SP_TEXT (version), Inkscape::version_string); + } + doc->ensureUpToDate(); + + auto viewer = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc)); + + // temporary hack: halve the dimensions so the dialog will fit + double width=doc->getWidth().value("px") / 2.0; + double height=doc->getHeight().value("px") / 2.0; + viewer->setResize(width, height); + + _splash_widget = new Gtk::AspectFrame(); + _splash_widget->unset_label(); + _splash_widget->set_shadow_type(Gtk::SHADOW_NONE); + _splash_widget->property_ratio() = width / height; + _splash_widget->add(*viewer); + } +} + +/** + * @brief Read the author and translator credits from file + */ +void AboutBox::initStrings() { + //############################## + //# A U T H O R S + //############################## + + // Create an empty vector to store the list of authors + std::vector<Glib::ustring> authors; + + // Try to copy the list of authors from the "AUTHORS" file, which + // should have been installed into the share/doc directory + auto authors_filename = Glib::build_filename(INKSCAPE_DOCDIR, "AUTHORS"); + std::ifstream authors_filestream(authors_filename); + if(authors_filestream) { + std::string author_line; + + while (std::getline(authors_filestream, author_line)) { + authors.emplace_back(author_line); + } + } + + // Set the author credits in this dialog, using the author list + set_authors(authors); + + //############################## + //# T R A N S L A T O R S + //############################## + + Glib::ustring translators_text; + + // TRANSLATORS: Put here your name (and other national contributors') + // one per line in the form of: name surname (email). Use \n for newline. + Glib::ustring thisTranslation = _("translator-credits"); + + /** + * See if the translators for the current language + * made an entry for "translator-credits". If it exists, + * put it at the top of the window, add some space between + * it and the list of all translators. + * + * NOTE: Do we need 2 more .po entries for titles: + * "translators for this language" + * "all translators" ?? + */ + if (thisTranslation != "translator-credits") { + translators_text.append(thisTranslation); + translators_text.append("\n\n\n"); + } + + auto translators_filename = Glib::build_filename(INKSCAPE_DOCDIR, "TRANSLATORS"); + + if (Glib::file_test (translators_filename, Glib::FILE_TEST_EXISTS)) { + auto all_translators = Glib::file_get_contents(translators_filename); + translators_text.append(all_translators); + } + + set_translator_credits(translators_text); +} + +void AboutBox::on_response(int response_id) { + 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/aboutbox.h b/src/ui/dialog/aboutbox.h new file mode 100644 index 0000000..e5b9f27 --- /dev/null +++ b/src/ui/dialog/aboutbox.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Inkscape About box + * + * The standard Gnome::UI::About class doesn't include a place to stuff + * a renderable View that holds the classic Inkscape "about.svg". + */ +/* Author: + * Kees Cook <kees@outflux.net> + * + * Copyright (C) 2005 Kees Cook + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_ABOUTBOX_H +#define INKSCAPE_UI_DIALOG_ABOUTBOX_H + +#include <gtkmm/aboutdialog.h> + +namespace Gtk { +class AspectFrame; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class AboutBox : public Gtk::AboutDialog { + +public: + + static void show_about(); + static void hide_about(); + +private: + + AboutBox(); + + /** A widget containing an SVG "splash screen" + * image to display in the content area of the dialo + */ + Gtk::AspectFrame *_splash_widget; + + void initStrings(); + void build_splash_widget(); + + void on_response(int response_id) override; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_ABOUTBOX_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..31605a0 --- /dev/null +++ b/src/ui/dialog/align-and-distribute.cpp @@ -0,0 +1,1339 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Align and Distribute dialog - implementation. + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Aubanel MONNIER <aubi@libertysurf.fr> + * Frank Felfe <innerspace@iname.com> + * Lauris Kaplinski <lauris@kaplinski.com> + * Tim Dwyer <tgdwyer@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> + +#include <2geom/transforms.h> + +#include <utility> + +#include "align-and-distribute.h" + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "graphlayout.h" +#include "inkscape.h" +#include "preferences.h" +#include "removeoverlap.h" +#include "text-editing.h" +#include "unclump.h" +#include "verbs.h" + +#include "object/sp-flowtext.h" +#include "object/sp-item-transform.h" +#include "object/sp-root.h" +#include "object/sp-text.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tools-switch.h" +#include "ui/tools/node-tool.h" +#include "ui/widget/spinbutton.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/////////helper classes////////////////////////////////// + +Action::Action(Glib::ustring id, + const Glib::ustring &tiptext, + guint row, guint column, + Gtk::Grid &parent, + AlignAndDistribute &dialog): + _dialog(dialog), + _id(std::move(id)), + _parent(parent) +{ + Gtk::Image* pIcon = Gtk::manage(new Gtk::Image()); + pIcon = sp_get_icon_image(_id, Gtk::ICON_SIZE_LARGE_TOOLBAR); + Gtk::Button * pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + + pButton->signal_clicked() + .connect(sigc::mem_fun(*this, &Action::on_button_click)); + pButton->set_tooltip_text(tiptext); + parent.attach(*pButton, column, row, 1, 1); +} + + +void ActionAlign::do_node_action(Inkscape::UI::Tools::NodeTool *nt, int verb) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int prev_pref = prefs->getInt("/dialogs/align/align-nodes-to"); + switch(verb){ + case SP_VERB_ALIGN_HORIZONTAL_LEFT: + prefs->setInt("/dialogs/align/align-nodes-to", MIN_NODE ); + nt->_multipath->alignNodes(Geom::Y); + break; + case SP_VERB_ALIGN_HORIZONTAL_CENTER: + nt->_multipath->alignNodes(Geom::Y); + break; + case SP_VERB_ALIGN_HORIZONTAL_RIGHT: + prefs->setInt("/dialogs/align/align-nodes-to", MAX_NODE ); + nt->_multipath->alignNodes(Geom::Y); + break; + case SP_VERB_ALIGN_VERTICAL_TOP: + prefs->setInt("/dialogs/align/align-nodes-to", MAX_NODE ); + nt->_multipath->alignNodes(Geom::X); + break; + case SP_VERB_ALIGN_VERTICAL_CENTER: + nt->_multipath->alignNodes(Geom::X); + break; + case SP_VERB_ALIGN_VERTICAL_BOTTOM: + prefs->setInt("/dialogs/align/align-nodes-to", MIN_NODE ); + nt->_multipath->alignNodes(Geom::X); + break; + case SP_VERB_ALIGN_BOTH_CENTER: + nt->_multipath->alignNodes(Geom::X); + nt->_multipath->alignNodes(Geom::Y); + break; + default:return; + } + prefs->setInt("/dialogs/align/align-nodes-to", prev_pref ); +} + +void ActionAlign::do_action(SPDesktop *desktop, int index) +{ + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool sel_as_group = prefs->getBool("/dialogs/align/sel-as-groups"); + + std::vector<SPItem*> selected(selection->items().begin(), selection->items().end()); + if (selected.empty()) return; + + Coeffs a = _allCoeffs[index]; // copy + SPItem *focus = nullptr; + Geom::OptRect b = Geom::OptRect(); + Selection::CompareSize horiz = (a.mx0 != 0.0) || (a.mx1 != 0.0) + ? Selection::VERTICAL : Selection::HORIZONTAL; + + switch (AlignTarget(prefs->getInt("/dialogs/align/align-to", 6))) + { + case LAST: + focus = SP_ITEM(selected.back()); + break; + case FIRST: + focus = SP_ITEM(selected.front()); + break; + case BIGGEST: + focus = selection->largestItem(horiz); + break; + case SMALLEST: + focus = selection->smallestItem(horiz); + break; + case PAGE: + b = desktop->getDocument()->preferredBounds(); + break; + case DRAWING: + b = desktop->getDocument()->getRoot()->desktopPreferredBounds(); + break; + case SELECTION: + b = selection->preferredBounds(); + break; + default: + g_assert_not_reached (); + break; + }; + + if(focus) + b = focus->desktopPreferredBounds(); + + g_return_if_fail(b); + + if (desktop->is_yaxisdown()) { + std::swap(a.my0, a.my1); + std::swap(a.sy0, a.sy1); + } + + // Generate the move point from the selected bounding box + Geom::Point mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X], + a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]); + + if (sel_as_group) { + if (focus) { + // use bounding box of all selected elements except the "focused" element + Inkscape::ObjectSet copy; + copy.add(selection->objects().begin(), selection->objects().end()); + copy.remove(focus); + b = copy.preferredBounds(); + } else { + // use bounding box of all selected elements + b = selection->preferredBounds(); + } + } + + //Move each item in the selected list separately + bool changed = false; + for (auto item : selected) + { + desktop->getDocument()->ensureUpToDate(); + if (!sel_as_group) + b = (item)->desktopPreferredBounds(); + if (b && (!focus || (item) != focus)) { + Geom::Point const sp(a.sx0 * b->min()[Geom::X] + a.sx1 * b->max()[Geom::X], + a.sy0 * b->min()[Geom::Y] + a.sy1 * b->max()[Geom::Y]); + Geom::Point const mp_rel( mp - sp ); + if (LInfty(mp_rel) > 1e-9) { + item->move_rel(Geom::Translate(mp_rel)); + changed = true; + } + } + } + + if (changed) { + DocumentUndo::done( desktop->getDocument() , SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Align")); + } +} + + +ActionAlign::Coeffs const ActionAlign::_allCoeffs[19] = { + {1., 0., 0., 0., 0., 1., 0., 0., SP_VERB_ALIGN_HORIZONTAL_RIGHT_TO_ANCHOR}, + {1., 0., 0., 0., 1., 0., 0., 0., SP_VERB_ALIGN_HORIZONTAL_LEFT}, + {.5, .5, 0., 0., .5, .5, 0., 0., SP_VERB_ALIGN_HORIZONTAL_CENTER}, + {0., 1., 0., 0., 0., 1., 0., 0., SP_VERB_ALIGN_HORIZONTAL_RIGHT}, + {0., 1., 0., 0., 1., 0., 0., 0., SP_VERB_ALIGN_HORIZONTAL_LEFT_TO_ANCHOR}, + {0., 0., 0., 1., 0., 0., 1., 0., SP_VERB_ALIGN_VERTICAL_BOTTOM_TO_ANCHOR}, + {0., 0., 0., 1., 0., 0., 0., 1., SP_VERB_ALIGN_VERTICAL_TOP}, + {0., 0., .5, .5, 0., 0., .5, .5, SP_VERB_ALIGN_VERTICAL_CENTER}, + {0., 0., 1., 0., 0., 0., 1., 0., SP_VERB_ALIGN_VERTICAL_BOTTOM}, + {0., 0., 1., 0., 0., 0., 0., 1., SP_VERB_ALIGN_VERTICAL_TOP_TO_ANCHOR}, + {1., 0., 0., 1., 1., 0., 0., 1., SP_VERB_ALIGN_BOTH_TOP_LEFT}, + {0., 1., 0., 1., 0., 1., 0., 1., SP_VERB_ALIGN_BOTH_TOP_RIGHT}, + {0., 1., 1., 0., 0., 1., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT}, + {1., 0., 1., 0., 1., 0., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_LEFT}, + {0., 1., 1., 0., 1., 0., 0., 1., SP_VERB_ALIGN_BOTH_TOP_LEFT_TO_ANCHOR}, + {1., 0., 1., 0., 0., 1., 0., 1., SP_VERB_ALIGN_BOTH_TOP_RIGHT_TO_ANCHOR}, + {1., 0., 0., 1., 0., 1., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT_TO_ANCHOR}, + {0., 1., 0., 1., 1., 0., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_LEFT_TO_ANCHOR}, + {.5, .5, .5, .5, .5, .5, .5, .5, SP_VERB_ALIGN_BOTH_CENTER} +}; + +void ActionAlign::do_verb_action(SPDesktop *desktop, int verb) +{ + Inkscape::UI::Tools::ToolBase *event_context = desktop->getEventContext(); + if (INK_IS_NODE_TOOL(event_context)) { + Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(event_context); + if(!nt->_selected_nodes->empty()){ + do_node_action(nt, verb); + return; + } + } + do_action(desktop, verb_to_coeff(verb)); +} + +int ActionAlign::verb_to_coeff(int verb) { + + for(guint i = 0; i < G_N_ELEMENTS(_allCoeffs); i++) { + if (_allCoeffs[i].verb_id == verb) { + return i; + } + } + + return -1; +} + +BBoxSort::BBoxSort(SPItem *pItem, Geom::Rect const &bounds, Geom::Dim2 orientation, double kBegin, double kEnd) : + item(pItem), + bbox (bounds) +{ + anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation]; +} +BBoxSort::BBoxSort(const BBoxSort &rhs) + //NOTE : this copy ctor is called O(sort) when sorting the vector + //this is bad. The vector should be a vector of pointers. + //But I'll wait the bohem GC before doing that += default; + +bool operator< (const BBoxSort &a, const BBoxSort &b) +{ + return (a.anchor < b.anchor); +} + +class ActionDistribute : public Action { +public : + ActionDistribute(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, guint column, + AlignAndDistribute &dialog, + bool onInterSpace, + Geom::Dim2 orientation, + double kBegin, double kEnd + ): + Action(id, tiptext, row, column, + dialog.distribute_table(), dialog), + _dialog(dialog), + _onInterSpace(onInterSpace), + _orientation(orientation), + _kBegin(kBegin), + _kEnd( kEnd) + {} + +private : + void on_button_click() override { + //Retrieve selected objects + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + std::vector<SPItem*> selected(selection->items().begin(), selection->items().end()); + if (selected.empty()) return; + + //Check 2 or more selected objects + std::vector<SPItem*>::iterator second(selected.begin()); + ++second; + if (second == selected.end()) return; + + double kBegin = _kBegin; + double kEnd = _kEnd; + if (_orientation == Geom::Y && desktop->is_yaxisdown()) { + kBegin = 1. - kBegin; + kEnd = 1. - kEnd; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int prefs_bbox = prefs->getBool("/tools/bounding_box"); + std::vector< BBoxSort > sorted; + for (auto item : selected){ + Geom::OptRect bbox = !prefs_bbox ? (item)->desktopVisualBounds() : (item)->desktopGeometricBounds(); + if (bbox) { + sorted.emplace_back(item, *bbox, _orientation, kBegin, kEnd); + } + } + //sort bbox by anchors + std::stable_sort(sorted.begin(), sorted.end()); + + // see comment in ActionAlign above + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + unsigned int len = sorted.size(); + bool changed = false; + if (_onInterSpace) + { + //overall bboxes span + float dist = (sorted.back().bbox.max()[_orientation] - + sorted.front().bbox.min()[_orientation]); + //space eaten by bboxes + float span = 0; + for (unsigned int i = 0; i < len; i++) + { + span += sorted[i].bbox[_orientation].extent(); + } + //new distance between each bbox + float step = (dist - span) / (len - 1); + float pos = sorted.front().bbox.min()[_orientation]; + for ( std::vector<BBoxSort> ::iterator it (sorted.begin()); + it < sorted.end(); + ++it ) + { + if (!Geom::are_near(pos, it->bbox.min()[_orientation], 1e-6)) { + Geom::Point t(0.0, 0.0); + t[_orientation] = pos - it->bbox.min()[_orientation]; + it->item->move_rel(Geom::Translate(t)); + changed = true; + } + pos += it->bbox[_orientation].extent(); + pos += step; + } + } + else + { + //overall anchor span + float dist = sorted.back().anchor - sorted.front().anchor; + //distance between anchors + float step = dist / (len - 1); + + for ( unsigned int i = 0; i < len ; i ++ ) + { + BBoxSort & it(sorted[i]); + //new anchor position + float pos = sorted.front().anchor + i * step; + //Don't move if we are really close + if (!Geom::are_near(pos, it.anchor, 1e-6)) { + //Compute translation + Geom::Point t(0.0, 0.0); + t[_orientation] = pos - it.anchor; + //translate + it.item->move_rel(Geom::Translate(t)); + changed = true; + } + } + } + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + if (changed) { + DocumentUndo::done( desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Distribute")); + } + } + guint _index; + AlignAndDistribute &_dialog; + bool _onInterSpace; + Geom::Dim2 _orientation; + + double _kBegin; + double _kEnd; + +}; + + +class ActionNode : public Action { +public : + ActionNode(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint column, + AlignAndDistribute &dialog, + Geom::Dim2 orientation, bool distribute): + Action(id, tiptext, 0, column, + dialog.nodes_table(), dialog), + _orientation(orientation), + _distribute(distribute) + {} + +private : + Geom::Dim2 _orientation; + bool _distribute; + + void on_button_click() override { + if (!_dialog.getDesktop()) { + return; + } + + Inkscape::UI::Tools::ToolBase *event_context = _dialog.getDesktop()->getEventContext(); + + if (!INK_IS_NODE_TOOL(event_context)) { + return; + } + + Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(event_context); + + if (_distribute) { + nt->_multipath->distributeNodes(_orientation); + } else { + nt->_multipath->alignNodes(_orientation); + } + } +}; + +class ActionRemoveOverlaps : public Action { +private: + Gtk::Label removeOverlapXGapLabel; + Gtk::Label removeOverlapYGapLabel; + Inkscape::UI::Widget::SpinButton removeOverlapXGap; + Inkscape::UI::Widget::SpinButton removeOverlapYGap; + +public: + ActionRemoveOverlaps(Glib::ustring const &id, + Glib::ustring const &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog) : + Action(id, tiptext, row, column + 4, + dialog.removeOverlap_table(), dialog) + { + dialog.removeOverlap_table().set_column_spacing(3); + + removeOverlapXGap.set_digits(1); + removeOverlapXGap.set_size_request(60, -1); + removeOverlapXGap.set_increments(1.0, 0); + removeOverlapXGap.set_range(-1000.0, 1000.0); + removeOverlapXGap.set_value(0); + removeOverlapXGap.set_tooltip_text(_("Minimum horizontal gap (in px units) between bounding boxes")); + //TRANSLATORS: "H:" stands for horizontal gap + removeOverlapXGapLabel.set_text_with_mnemonic(C_("Gap", "_H:")); + removeOverlapXGapLabel.set_mnemonic_widget(removeOverlapXGap); + + removeOverlapYGap.set_digits(1); + removeOverlapYGap.set_size_request(60, -1); + removeOverlapYGap.set_increments(1.0, 0); + removeOverlapYGap.set_range(-1000.0, 1000.0); + removeOverlapYGap.set_value(0); + removeOverlapYGap.set_tooltip_text(_("Minimum vertical gap (in px units) between bounding boxes")); + /* TRANSLATORS: Vertical gap */ + removeOverlapYGapLabel.set_text_with_mnemonic(C_("Gap", "_V:")); + removeOverlapYGapLabel.set_mnemonic_widget(removeOverlapYGap); + + dialog.removeOverlap_table().attach(removeOverlapXGapLabel, column, row, 1, 1); + dialog.removeOverlap_table().attach(removeOverlapXGap, column+1, row, 1, 1); + dialog.removeOverlap_table().attach(removeOverlapYGapLabel, column+2, row, 1, 1); + dialog.removeOverlap_table().attach(removeOverlapYGap, column+3, row, 1, 1); + } + +private : + void on_button_click() override + { + if (!_dialog.getDesktop()) return; + + // see comment in ActionAlign above + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + // xGap and yGap are the minimum space required between bounding rectangles. + double const xGap = removeOverlapXGap.get_value(); + double const yGap = removeOverlapYGap.get_value(); + auto tmp = _dialog.getDesktop()->getSelection()->items(); + std::vector<SPItem *> vec(tmp.begin(), tmp.end()); + removeoverlap(vec, xGap, yGap); + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Remove overlaps")); + } +}; + +class ActionGraphLayout : public Action { +public: + ActionGraphLayout(Glib::ustring const &id, + Glib::ustring const &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog) : + Action(id, tiptext, row, column, + dialog.rearrange_table(), dialog) + {} + +private : + void on_button_click() override + { + if (!_dialog.getDesktop()) return; + + // see comment in ActionAlign above + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + auto tmp = _dialog.getDesktop()->getSelection()->items(); + std::vector<SPItem *> vec(tmp.begin(), tmp.end()); + graphlayout(vec); + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Arrange connector network")); + } +}; + +class ActionExchangePositions : public Action { +public: + enum SortOrder { + None, + ZOrder, + Clockwise + }; + + ActionExchangePositions(Glib::ustring const &id, + Glib::ustring const &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog, SortOrder order = None) : + Action(id, tiptext, row, column, + dialog.rearrange_table(), dialog), + sortOrder(order) + {}; + + +private : + const SortOrder sortOrder; + static boost::optional<Geom::Point> center; + + static bool sort_compare(const SPItem * a,const SPItem * b) { + if (a == nullptr) return false; + if (b == nullptr) return true; + if (center) { + Geom::Point point_a = a->getCenter() - (*center); + Geom::Point point_b = b->getCenter() - (*center); + // First criteria: Sort according to the angle to the center point + double angle_a = atan2(double(point_a[Geom::Y]), double(point_a[Geom::X])); + double angle_b = atan2(double(point_b[Geom::Y]), double(point_b[Geom::X])); + double dt_yaxisdir = SP_ACTIVE_DESKTOP ? SP_ACTIVE_DESKTOP->yaxisdir() : 1; + angle_a *= -dt_yaxisdir; + angle_b *= -dt_yaxisdir; + if (angle_a != angle_b) return (angle_a < angle_b); + // Second criteria: Sort according to the distance the center point + Geom::Coord length_a = point_a.length(); + Geom::Coord length_b = point_b.length(); + if (length_a != length_b) return (length_a > length_b); + } + // Last criteria: Sort according to the z-coordinate + return sp_item_repr_compare_position(a,b)<0; + } + + void on_button_click() override + { + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + std::vector<SPItem*> selected(selection->items().begin(), selection->items().end()); + + //Check 2 or more selected objects + if (selected.size() < 2) return; + + // see comment in ActionAlign above + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + // sort the list + if (sortOrder != None) { + if (sortOrder == Clockwise) { + center = selection->center(); + } else { // sorting by ZOrder is outomatically done by not setting the center + center.reset(); + } + sort(selected.begin(),selected.end(),sort_compare); + } + + Geom::Point p1 = selected.back()->getCenter(); + for (SPItem *item : selected) + { + Geom::Point p2 = item->getCenter(); + Geom::Point delta = p1 - p2; + item->move_rel(Geom::Translate(delta[Geom::X],delta[Geom::Y] )); + p1 = p2; + } + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Exchange Positions")); + } +}; + +// instantiate the private static member +boost::optional<Geom::Point> ActionExchangePositions::center; + +class ActionUnclump : public Action { +public : + ActionUnclump(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog): + Action(id, tiptext, row, column, + dialog.rearrange_table(), dialog) + {} + +private : + void on_button_click() override + { + if (!_dialog.getDesktop()) return; + + // see comment in ActionAlign above + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + auto tmp = _dialog.getDesktop()->getSelection()->items(); + std::vector<SPItem*> x(tmp.begin(), tmp.end()); + unclump (x); + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Unclump")); + } +}; + +class ActionRandomize : public Action { +public : + ActionRandomize(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog): + Action(id, tiptext, row, column, + dialog.rearrange_table(), dialog) + {} + +private : + void on_button_click() override + { + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + std::vector<SPItem*> selected(selection->items().begin(), selection->items().end()); + if (selected.empty()) return; + + //Check 2 or more selected objects + if (selected.size() < 2) return; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int prefs_bbox = prefs->getBool("/tools/bounding_box"); + Geom::OptRect sel_bbox = !prefs_bbox ? selection->visualBounds() : selection->geometricBounds(); + if (!sel_bbox) { + return; + } + + // This bbox is cached between calls to randomize, so that there's no growth nor shrink + // nor drift on sequential randomizations. Discard cache on global (or better active + // desktop's) selection_change signal. + if (!_dialog.randomize_bbox) { + _dialog.randomize_bbox = *sel_bbox; + } + + // see comment in ActionAlign above + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + for (auto item : selected) + { + desktop->getDocument()->ensureUpToDate(); + Geom::OptRect item_box = !prefs_bbox ? (item)->desktopVisualBounds() : (item)->desktopGeometricBounds(); + if (item_box) { + // find new center, staying within bbox + double x = _dialog.randomize_bbox->min()[Geom::X] + (*item_box)[Geom::X].extent() /2 + + g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::X].extent() - (*item_box)[Geom::X].extent()); + double y = _dialog.randomize_bbox->min()[Geom::Y] + (*item_box)[Geom::Y].extent()/2 + + g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::Y].extent() - (*item_box)[Geom::Y].extent()); + // displacement is the new center minus old: + Geom::Point t = Geom::Point (x, y) - 0.5*(item_box->max() + item_box->min()); + item->move_rel(Geom::Translate(t)); + } + } + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Randomize positions")); + } +}; + +struct Baselines +{ + SPItem *_item; + Geom::Point _base; + Geom::Dim2 _orientation; + Baselines(SPItem *item, Geom::Point base, Geom::Dim2 orientation) : + _item (item), + _base (base), + _orientation (orientation) + {} +}; + +static bool operator< (const Baselines &a, const Baselines &b) +{ + return (a._base[a._orientation] < b._base[b._orientation]); +} + +class ActionBaseline : public Action { +public : + ActionBaseline(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog, + Gtk::Grid &table, + Geom::Dim2 orientation, bool distribute): + Action(id, tiptext, row, column, + table, dialog), + _orientation(orientation), + _distribute(distribute) + {} + +private : + Geom::Dim2 _orientation; + bool _distribute; + void on_button_click() override + { + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + std::vector<SPItem*> selected(selection->items().begin(), selection->items().end()); + + //Check 2 or more selected objects + if (selected.size() < 2) return; + + Geom::Point b_min = Geom::Point (HUGE_VAL, HUGE_VAL); + Geom::Point b_max = Geom::Point (-HUGE_VAL, -HUGE_VAL); + + std::vector<Baselines> sorted; + + for (auto item : selected) + { + if (SP_IS_TEXT (item) || SP_IS_FLOWTEXT (item)) { + Inkscape::Text::Layout const *layout = te_get_layout(item); + boost::optional<Geom::Point> pt = layout->baselineAnchorPoint(); + if (pt) { + Geom::Point base = *pt * (item)->i2dt_affine(); + if (base[Geom::X] < b_min[Geom::X]) b_min[Geom::X] = base[Geom::X]; + if (base[Geom::Y] < b_min[Geom::Y]) b_min[Geom::Y] = base[Geom::Y]; + if (base[Geom::X] > b_max[Geom::X]) b_max[Geom::X] = base[Geom::X]; + if (base[Geom::Y] > b_max[Geom::Y]) b_max[Geom::Y] = base[Geom::Y]; + Baselines b (item, base, _orientation); + sorted.push_back(b); + } + } + } + + if (sorted.size() <= 1) return; + + //sort baselines + std::stable_sort(sorted.begin(), sorted.end()); + + bool changed = false; + + if (_distribute) { + double step = (b_max[_orientation] - b_min[_orientation])/(sorted.size() - 1); + for (unsigned int i = 0; i < sorted.size(); i++) { + SPItem *item = sorted[i]._item; + Geom::Point base = sorted[i]._base; + Geom::Point t(0.0, 0.0); + t[_orientation] = b_min[_orientation] + step * i - base[_orientation]; + item->move_rel(Geom::Translate(t)); + changed = true; + } + + if (changed) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Distribute text baselines")); + } + + } else { //align + Geom::Point ref_point; + SPItem *focus = nullptr; + Geom::OptRect b = Geom::OptRect(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + switch (AlignTarget(prefs->getInt("/dialogs/align/align-to", 6))) + { + case LAST: + focus = SP_ITEM(selected.back()); + break; + case FIRST: + focus = SP_ITEM(selected.front()); + break; + case BIGGEST: + focus = selection->largestItem(Selection::AREA); + break; + case SMALLEST: + focus = selection->smallestItem(Selection::AREA); + break; + case PAGE: + b = desktop->getDocument()->preferredBounds(); + break; + case DRAWING: + b = desktop->getDocument()->getRoot()->desktopPreferredBounds(); + break; + case SELECTION: + b = selection->preferredBounds(); + break; + default: + g_assert_not_reached (); + break; + }; + + if(focus) { + if (SP_IS_TEXT (focus) || SP_IS_FLOWTEXT (focus)) { + ref_point = *(te_get_layout(focus)->baselineAnchorPoint())*(focus->i2dt_affine()); + } else { + ref_point = focus->desktopPreferredBounds()->min(); + } + } else { + ref_point = b->min(); + } + + for (auto item : selected) + { + if (SP_IS_TEXT (item) || SP_IS_FLOWTEXT (item)) { + Inkscape::Text::Layout const *layout = te_get_layout(item); + boost::optional<Geom::Point> pt = layout->baselineAnchorPoint(); + if (pt) { + Geom::Point base = *pt * (item)->i2dt_affine(); + Geom::Point t(0.0, 0.0); + t[_orientation] = ref_point[_orientation] - base[_orientation]; + item->move_rel(Geom::Translate(t)); + changed = true; + } + } + } + + if (changed) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Align text baselines")); + } + } + } +}; + + + +static void on_tool_changed(AlignAndDistribute *daad) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop && desktop->getEventContext()) + daad->setMode(tools_active(desktop) == TOOLS_NODES); + else + daad->setMode(false); + +} + +static void on_selection_changed(AlignAndDistribute *daad) +{ + daad->randomize_bbox = Geom::OptRect(); +} + +///////////////////////////////////////////////////////// + + + + +AlignAndDistribute::AlignAndDistribute() + : UI::Widget::Panel("/dialogs/align", SP_VERB_DIALOG_ALIGN_DISTRIBUTE), + randomize_bbox(), + _alignFrame(_("Align")), + _distributeFrame(_("Distribute")), + _rearrangeFrame(_("Rearrange")), + _removeOverlapFrame(_("Remove overlaps")), + _nodesFrame(_("Nodes")), + _alignTable(), + _distributeTable(), + _rearrangeTable(), + _removeOverlapTable(), + _nodesTable(), + _anchorLabel(_("Relative to: ")), + _anchorLabelNode(_("Relative to: ")) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + //Instantiate the align buttons + addAlignButton(INKSCAPE_ICON("align-horizontal-right-to-anchor"), + _("Align right edges of objects to the left edge of the anchor"), + 0, 0); + addAlignButton(INKSCAPE_ICON("align-horizontal-left"), + _("Align left edges"), + 0, 1); + addAlignButton(INKSCAPE_ICON("align-horizontal-center"), + _("Center on vertical axis"), + 0, 2); + addAlignButton(INKSCAPE_ICON("align-horizontal-right"), + _("Align right sides"), + 0, 3); + addAlignButton(INKSCAPE_ICON("align-horizontal-left-to-anchor"), + _("Align left edges of objects to the right edge of the anchor"), + 0, 4); + addAlignButton(INKSCAPE_ICON("align-vertical-bottom-to-anchor"), + _("Align bottom edges of objects to the top edge of the anchor"), + 1, 0); + addAlignButton(INKSCAPE_ICON("align-vertical-top"), + _("Align top edges"), + 1, 1); + addAlignButton(INKSCAPE_ICON("align-vertical-center"), + _("Center on horizontal axis"), + 1, 2); + addAlignButton(INKSCAPE_ICON("align-vertical-bottom"), + _("Align bottom edges"), + 1, 3); + addAlignButton(INKSCAPE_ICON("align-vertical-top-to-anchor"), + _("Align top edges of objects to the bottom edge of the anchor"), + 1, 4); + + //Baseline aligns + addBaselineButton(INKSCAPE_ICON("align-horizontal-baseline"), + _("Align baseline anchors of texts horizontally"), + 0, 5, this->align_table(), Geom::X, false); + addBaselineButton(INKSCAPE_ICON("align-vertical-baseline"), + _("Align baselines of texts"), + 1, 5, this->align_table(), Geom::Y, false); + + //The distribute buttons + addDistributeButton(INKSCAPE_ICON("distribute-horizontal-gaps"), + _("Make horizontal gaps between objects equal"), + 0, 4, true, Geom::X, .5, .5); + + addDistributeButton(INKSCAPE_ICON("distribute-horizontal-left"), + _("Distribute left edges equidistantly"), + 0, 1, false, Geom::X, 1., 0.); + addDistributeButton(INKSCAPE_ICON("distribute-horizontal-center"), + _("Distribute centers equidistantly horizontally"), + 0, 2, false, Geom::X, .5, .5); + addDistributeButton(INKSCAPE_ICON("distribute-horizontal-right"), + _("Distribute right edges equidistantly"), + 0, 3, false, Geom::X, 0., 1.); + + addDistributeButton(INKSCAPE_ICON("distribute-vertical-gaps"), + _("Make vertical gaps between objects equal"), + 1, 4, true, Geom::Y, .5, .5); + + addDistributeButton(INKSCAPE_ICON("distribute-vertical-top"), + _("Distribute top edges equidistantly"), + 1, 1, false, Geom::Y, 0, 1); + addDistributeButton(INKSCAPE_ICON("distribute-vertical-center"), + _("Distribute centers equidistantly vertically"), + 1, 2, false, Geom::Y, .5, .5); + addDistributeButton(INKSCAPE_ICON("distribute-vertical-bottom"), + _("Distribute bottom edges equidistantly"), + 1, 3, false, Geom::Y, 1., 0.); + + //Baseline distribs + addBaselineButton(INKSCAPE_ICON("distribute-horizontal-baseline"), + _("Distribute baseline anchors of texts horizontally"), + 0, 5, this->distribute_table(), Geom::X, true); + addBaselineButton(INKSCAPE_ICON("distribute-vertical-baseline"), + _("Distribute baselines of texts vertically"), + 1, 5, this->distribute_table(), Geom::Y, true); + + // Rearrange + //Graph Layout + addGraphLayoutButton(INKSCAPE_ICON("distribute-graph"), + _("Nicely arrange selected connector network"), + 0, 0); + addExchangePositionsButton(INKSCAPE_ICON("exchange-positions"), + _("Exchange positions of selected objects - selection order"), + 0, 1); + addExchangePositionsByZOrderButton(INKSCAPE_ICON("exchange-positions-zorder"), + _("Exchange positions of selected objects - stacking order"), + 0, 2); + addExchangePositionsClockwiseButton(INKSCAPE_ICON("exchange-positions-clockwise"), + _("Exchange positions of selected objects - clockwise rotate"), + 0, 3); + + //Randomize & Unclump + addRandomizeButton(INKSCAPE_ICON("distribute-randomize"), + _("Randomize centers in both dimensions"), + 0, 4); + addUnclumpButton(INKSCAPE_ICON("distribute-unclump"), + _("Unclump objects: try to equalize edge-to-edge distances"), + 0, 5); + + //Remove overlaps + addRemoveOverlapsButton(INKSCAPE_ICON("distribute-remove-overlaps"), + _("Move objects as little as possible so that their bounding boxes do not overlap"), + 0, 0); + + //Node Mode buttons + // NOTE: "align nodes vertically" means "move nodes vertically until they align on a common + // _horizontal_ line". This is analogous to what the "align-vertical-center" icon means. + // There is no doubt some ambiguity. For this reason the descriptions are different. + addNodeButton(INKSCAPE_ICON("align-vertical-node"), + _("Align selected nodes to a common horizontal line"), + 0, Geom::X, false); + addNodeButton(INKSCAPE_ICON("align-horizontal-node"), + _("Align selected nodes to a common vertical line"), + 1, Geom::Y, false); + addNodeButton(INKSCAPE_ICON("distribute-horizontal-node"), + _("Distribute selected nodes horizontally"), + 2, Geom::X, true); + addNodeButton(INKSCAPE_ICON("distribute-vertical-node"), + _("Distribute selected nodes vertically"), + 3, Geom::Y, true); + + //Rest of the widgetry + + _combo.append(_("Last selected")); + _combo.append(_("First selected")); + _combo.append(_("Biggest object")); + _combo.append(_("Smallest object")); + _combo.append(_("Page")); + _combo.append(_("Drawing")); + _combo.append(_("Selection Area")); + _combo.set_active(prefs->getInt("/dialogs/align/align-to", 6)); + _combo.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_ref_change)); + + _comboNode.append(_("Last selected")); + _comboNode.append(_("First selected")); + _comboNode.append(_("Middle of selection")); + _comboNode.append(_("Min value")); + _comboNode.append(_("Max value")); + _comboNode.set_active(prefs->getInt("/dialogs/align/align-nodes-to", 2)); + _comboNode.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_node_ref_change)); + + Gtk::Image* selgrp_icon = Gtk::manage(new Gtk::Image()); + selgrp_icon = sp_get_icon_image("align-sel-as-group", Gtk::ICON_SIZE_LARGE_TOOLBAR); + _selgrp.add(*selgrp_icon); + + _selgrp.set_active(prefs->getBool("/dialogs/align/sel-as-groups")); + _selgrp.set_relief(Gtk::RELIEF_NONE); + _selgrp.set_tooltip_text(_("Treat selection as group")); + _selgrp.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_selgrp_toggled)); + _anchorBox.pack_end(_selgrp, false, false); + _anchorBox.pack_end(_combo, false, false); + _anchorBox.pack_end(_anchorLabel, false, false); + + _anchorBoxNode.pack_end(_comboNode, false, false); + _anchorBoxNode.pack_end(_anchorLabelNode, false, false); + + Gtk::Image* oncanvas_icon = Gtk::manage(new Gtk::Image()); + oncanvas_icon = sp_get_icon_image("align-on-canvas", Gtk::ICON_SIZE_LARGE_TOOLBAR); + _oncanvas.add(*oncanvas_icon); + + _oncanvas.set_relief(Gtk::RELIEF_NONE); + _oncanvas.set_tooltip_text(_("Enable on-canvas alignment handles.")); + _anchorBox.pack_start(_oncanvas, false, false); + _oncanvas.set_active(prefs->getBool("/dialogs/align/oncanvas")); + _oncanvas.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_oncanvas_toggled)); + + // Right align the buttons + _alignTableBox.pack_end(_alignTable, false, false); + _distributeTableBox.pack_end(_distributeTable, false, false); + _rearrangeTableBox.pack_end(_rearrangeTable, false, false); + _removeOverlapTableBox.pack_end(_removeOverlapTable, false, false); + _nodesTableBox.pack_end(_nodesTable, false, false); + + _alignBox.pack_start(_anchorBox); + _alignBox.pack_start(_selgrpBox); + _alignBox.pack_start(_alignTableBox); + + _alignBoxNode.pack_start(_anchorBoxNode, false, false); + _alignBoxNode.pack_start(_nodesTableBox); + + + _alignFrame.add(_alignBox); + _distributeFrame.add(_distributeTableBox); + _rearrangeFrame.add(_rearrangeTableBox); + _removeOverlapFrame.add(_removeOverlapTableBox); + _nodesFrame.add(_alignBoxNode); + + Gtk::Box *contents = _getContents(); + contents->set_spacing(4); + + // Notebook for individual transformations + + contents->pack_start(_alignFrame, true, true); + contents->pack_start(_distributeFrame, true, true); + contents->pack_start(_rearrangeFrame, true, true); + contents->pack_start(_removeOverlapFrame, true, true); + contents->pack_start(_nodesFrame, true, false); + + //Connect to the global tool change signal + _toolChangeConn = INKSCAPE.signal_eventcontext_set.connect(sigc::hide<0>(sigc::bind(sigc::ptr_fun(&on_tool_changed), this))); + + // Connect to the global selection change, to invalidate cached randomize_bbox + _selChangeConn = INKSCAPE.signal_selection_changed.connect(sigc::hide<0>(sigc::bind(sigc::ptr_fun(&on_selection_changed), this))); + randomize_bbox = Geom::OptRect(); + + _desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &AlignAndDistribute::setDesktop) ); + _deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); + + on_tool_changed (this); // set current mode +} + +AlignAndDistribute::~AlignAndDistribute() +{ + for (auto & it : _actionList) { + delete it; + } + + _toolChangeConn.disconnect(); + _selChangeConn.disconnect(); + _desktopChangeConn.disconnect(); + _deskTrack.disconnect(); +} + +void AlignAndDistribute::setTargetDesktop(SPDesktop *desktop) +{ + if (_desktop != desktop) { + _desktop = desktop; + on_tool_changed (this); + } +} + + +void AlignAndDistribute::on_ref_change(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/align/align-to", _combo.get_active_row_number()); + + //Make blink the master +} + +void AlignAndDistribute::on_node_ref_change(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/align/align-nodes-to", _comboNode.get_active_row_number()); + + //Make blink the master +} + +void AlignAndDistribute::on_selgrp_toggled(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/align/sel-as-groups", _selgrp.get_active()); + + //Make blink the master +} + +void AlignAndDistribute::on_oncanvas_toggled(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/align/oncanvas", _oncanvas.get_active()); + + //Make blink the master +} + +void AlignAndDistribute::setMode(bool nodeEdit) +{ + //Act on widgets used in node mode + void ( Gtk::Widget::*mNode) () = nodeEdit ? + &Gtk::Widget::show_all : &Gtk::Widget::hide; + + //Act on widgets used in selection mode + void ( Gtk::Widget::*mSel) () = nodeEdit ? + &Gtk::Widget::hide : &Gtk::Widget::show_all; + + ((_alignFrame).*(mSel))(); + ((_distributeFrame).*(mSel))(); + ((_rearrangeFrame).*(mSel))(); + ((_removeOverlapFrame).*(mSel))(); + ((_nodesFrame).*(mNode))(); + _getContents()->queue_resize(); + +} +void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionAlign( + id, tiptext, row, col, + *this , col + row * 5)); +} +void AlignAndDistribute::addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col, bool onInterSpace, + Geom::Dim2 orientation, float kBegin, float kEnd) +{ + _actionList.push_back( + new ActionDistribute( + id, tiptext, row, col, *this , + onInterSpace, orientation, + kBegin, kEnd + ) + ); +} + +void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint col, Geom::Dim2 orientation, bool distribute) +{ + _actionList.push_back( + new ActionNode( + id, tiptext, col, + *this, orientation, distribute)); +} + +void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionRemoveOverlaps( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionGraphLayout( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addExchangePositionsButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionExchangePositions( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addExchangePositionsByZOrderButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionExchangePositions( + id, tiptext, row, col, *this, ActionExchangePositions::ZOrder) + ); +} + +void AlignAndDistribute::addExchangePositionsClockwiseButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionExchangePositions( + id, tiptext, row, col, *this, ActionExchangePositions::Clockwise) + ); +} + +void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionUnclump( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionRandomize( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col, Gtk::Grid &table, Geom::Dim2 orientation, bool distribute) +{ + _actionList.push_back( + new ActionBaseline( + id, tiptext, row, col, + *this, table, orientation, distribute)); +} + +} // 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..175ebdc --- /dev/null +++ b/src/ui/dialog/align-and-distribute.h @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Align and Distribute dialog + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Aubanel MONNIER <aubi@libertysurf.fr> + * Frank Felfe <innerspace@iname.com> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_ALIGN_AND_DISTRIBUTE_H +#define INKSCAPE_UI_DIALOG_ALIGN_AND_DISTRIBUTE_H + +#include <list> +#include "ui/widget/panel.h" +#include "ui/widget/frame.h" + +#include <gtkmm/frame.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/label.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/grid.h> + +#include "2geom/rect.h" +#include "ui/dialog/desktop-tracker.h" + +class SPItem; + +namespace Inkscape { +namespace UI { +namespace Tools{ +class NodeTool; +} +namespace Dialog { + +class Action; + + +class AlignAndDistribute : public Widget::Panel { +public: + AlignAndDistribute(); + ~AlignAndDistribute() override; + + static AlignAndDistribute &getInstance() { return *new AlignAndDistribute(); } + + Gtk::Grid &align_table(){return _alignTable;} + Gtk::Grid &distribute_table(){return _distributeTable;} + Gtk::Grid &rearrange_table(){return _rearrangeTable;} + Gtk::Grid &removeOverlap_table(){return _removeOverlapTable;} + Gtk::Grid &nodes_table(){return _nodesTable;} + + void setMode(bool nodeEdit); + + Geom::OptRect randomize_bbox; + +protected: + + void on_ref_change(); + void on_node_ref_change(); + void on_selgrp_toggled(); + void on_oncanvas_toggled(); + void addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col, bool onInterSpace, + Geom::Dim2 orientation, float kBegin, float kEnd); + void addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col); + void addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint col, Geom::Dim2 orientation, bool distribute); + void addRemoveOverlapsButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addGraphLayoutButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addExchangePositionsButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addExchangePositionsByZOrderButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addExchangePositionsClockwiseButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col); + void addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col); + void addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col, Gtk::Grid &table, Geom::Dim2 orientation, bool distribute); + void setTargetDesktop(SPDesktop *desktop); + + std::list<Action *> _actionList; + UI::Widget::Frame _alignFrame, _distributeFrame, _rearrangeFrame, _removeOverlapFrame, _nodesFrame; + Gtk::Grid _alignTable, _distributeTable, _rearrangeTable, _removeOverlapTable, _nodesTable; + Gtk::HBox _anchorBox; + Gtk::HBox _selgrpBox; + Gtk::VBox _alignBox; + Gtk::VBox _alignBoxNode; + Gtk::HBox _alignTableBox; + Gtk::HBox _distributeTableBox; + Gtk::HBox _rearrangeTableBox; + Gtk::HBox _removeOverlapTableBox; + Gtk::HBox _nodesTableBox; + Gtk::Label _anchorLabel; + Gtk::Label _anchorLabelNode; + Gtk::ToggleButton _selgrp; + Gtk::ToggleButton _oncanvas; + Gtk::ComboBoxText _combo; + Gtk::HBox _anchorBoxNode; + Gtk::ComboBoxText _comboNode; + + SPDesktop *_desktop; + DesktopTracker _deskTrack; + sigc::connection _desktopChangeConn; + sigc::connection _toolChangeConn; + sigc::connection _selChangeConn; +private: + AlignAndDistribute(AlignAndDistribute const &d) = delete; + AlignAndDistribute& operator=(AlignAndDistribute const &d) = delete; +}; + + +struct BBoxSort +{ + SPItem *item; + float anchor; + Geom::Rect bbox; + BBoxSort(SPItem *pItem, Geom::Rect const &bounds, Geom::Dim2 orientation, double kBegin, double kEnd); + BBoxSort(const BBoxSort &rhs); +}; +bool operator< (const BBoxSort &a, const BBoxSort &b); + + +class Action { +public : + + enum AlignTarget { LAST=0, FIRST, BIGGEST, SMALLEST, PAGE, DRAWING, SELECTION }; + enum AlignTargetNode { LAST_NODE=0, FIRST_NODE, MID_NODE, MIN_NODE, MAX_NODE }; + Action(Glib::ustring id, + const Glib::ustring &tiptext, + guint row, guint column, + Gtk::Grid &parent, + AlignAndDistribute &dialog); + + virtual ~Action()= default; + + AlignAndDistribute &_dialog; + +private : + virtual void on_button_click(){} + + Glib::ustring _id; + Gtk::Grid &_parent; +}; + + +class ActionAlign : public Action { +public : + struct Coeffs { + double mx0, mx1, my0, my1; + double sx0, sx1, sy0, sy1; + int verb_id; + }; + ActionAlign(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, guint column, + AlignAndDistribute &dialog, + guint coeffIndex): + Action(id, tiptext, row, column, + dialog.align_table(), dialog), + _index(coeffIndex), + _dialog(dialog) + {} + + /* + * Static function called to align from a keyboard shortcut + */ + static void do_verb_action(SPDesktop *desktop, int verb); + static int verb_to_coeff(int verb); + +private : + + + void on_button_click() override { + //Retrieve selected objects + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + do_action(desktop, _index); + } + + static void do_action(SPDesktop *desktop, int index); + static void do_node_action(Inkscape::UI::Tools::NodeTool *nt, int index); + + guint _index; + AlignAndDistribute &_dialog; + + static const Coeffs _allCoeffs[19]; + +}; + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_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..42e141e --- /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::VBox +{ +public: + ArrangeTab() = default;; + ~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..175c2ad --- /dev/null +++ b/src/ui/dialog/attrdialog.cpp @@ -0,0 +1,682 @@ +// 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 "verbs.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/widget/iconrenderer.h" + +#include "xml/node-event-vector.h" +#include "xml/attribute-record.h" + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +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) +{ + ATTR_DIALOG(data)->onAttrChanged(repr, "content", repr->content()); +} + +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() + : UI::Widget::Panel("/dialogs/attr", SP_VERB_DIALOG_ATTR) + , _desktop(nullptr) + , _repr(nullptr) +{ + set_size_request(20, 15); + _mainBox.pack_start(_scrolledWindow, Gtk::PACK_EXPAND_WIDGET); + _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); + _getContents()->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 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); + _getContents()->pack_start(_mainBox, Gtk::PACK_EXPAND_WIDGET); + setDesktop(getDesktop()); + // I couldent 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() +{ + setDesktop(nullptr); + _message_changed_connection.disconnect(); + _message_context = nullptr; + _message_stack = nullptr; + _message_changed_connection.~connection(); +} + +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 || + name == "content") { + 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::setDesktop + * @param desktop + * This function sets the 'desktop' for the CSS pane. + */ +void AttrDialog::setDesktop(SPDesktop* desktop) +{ + _desktop = desktop; +} + +/** + * @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); + } +} + +void AttrDialog::setUndo(Glib::ustring const &event_description) +{ + SPDocument *document = this->_desktop->doc(); + DocumentUndo::done(document, SP_VERB_DIALOG_XML_EDITOR, event_description); +} + +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]; + if (name == "content") { + return; + } else { + 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]; + if (name != "content") { + 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; + } + if (old_name == "content") { + 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) +{ + 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; + if (name == "content") { + _repr->setContent(value.c_str()); + } else { + _repr->setAttributeOrRemoveIfEmpty(name, value); + } + if(!value.empty()) { + row[_attrColumns._attributeValue] = value; + Glib::ustring renderval = prepare_rendervalue(value.c_str()); + row[_attrColumns._attributeValueRender] = renderval; + } + Inkscape::Selection *selection = _desktop->getSelection(); + 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..23437f0 --- /dev/null +++ b/src/ui/dialog/attrdialog.h @@ -0,0 +1,128 @@ +// 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 "desktop.h" +#include "message.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 <ui/widget/panel.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 UI::Widget::Panel +{ +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; + + /** + * Status bar + */ + std::shared_ptr<Inkscape::MessageStack> _message_stack; + std::unique_ptr<Inkscape::MessageContext> _message_context; + + // Widgets + Gtk::VBox _mainBox; + Gtk::ScrolledWindow _scrolledWindow; + Gtk::ScrolledWindow _scrolled_text_view; + Gtk::HBox _buttonBox; + Gtk::Button _buttonAddAttribute; + // Variables - Inkscape + SPDesktop* _desktop; + Inkscape::XML::Node* _repr; + Gtk::HBox status_box; + Gtk::Label status; + bool _updating; + + // Helper functions + void setDesktop(SPDesktop* desktop) override; + 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/behavior.h b/src/ui/dialog/behavior.h new file mode 100644 index 0000000..1b6c7c7 --- /dev/null +++ b/src/ui/dialog/behavior.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog behavior interface + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_BEHAVIOR_H +#define INKSCAPE_UI_DIALOG_BEHAVIOR_H + +#include <gtkmm/button.h> +#include <gtkmm/box.h> + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class Dialog; + +namespace Behavior { + +class Behavior; + +typedef Behavior *(*BehaviorFactory)(Dialog &dialog); + +template <typename T> +Behavior *create(Dialog &dialog) +{ + return T::create(dialog); +} + + +class Behavior { + +public: + virtual ~Behavior() = default; + + /** Gtk::Dialog methods */ + virtual operator Gtk::Widget&() =0; + virtual GtkWidget *gobj() =0; + virtual void present() =0; + virtual Gtk::Box *get_vbox() =0; + virtual void show() =0; + virtual void hide() =0; + virtual void show_all_children() =0; + virtual void resize(int width, int height) =0; + virtual void move(int x, int y) =0; + virtual void set_position(Gtk::WindowPosition) =0; + virtual void set_size_request(int width, int height) =0; + virtual void size_request(Gtk::Requisition &requisition) =0; + virtual void get_position(int &x, int &y) =0; + virtual void get_size(int &width, int &height) =0; + virtual void set_title(Glib::ustring title) =0; + virtual void set_sensitive(bool sensitive) =0; + + /** Gtk::Dialog signal proxies */ + virtual Glib::SignalProxy0<void> signal_show() =0; + virtual Glib::SignalProxy0<void> signal_hide() =0; + virtual Glib::SignalProxy1<bool, GdkEventAny *> signal_delete_event() =0; + + /** Custom signal handlers */ + virtual void onHideF12() =0; + virtual void onShowF12() =0; + virtual void onShutdown() =0; + virtual void onDesktopActivated(SPDesktop *desktop) =0; + +protected: + Behavior(Dialog &dialog) + : _dialog (dialog) + { } + + Dialog& _dialog; //< reference to the owner + +private: + Behavior() = delete; // no constructor without params + Behavior(const Behavior &) = delete; // no copy + Behavior &operator=(const Behavior &) = delete; // no assign +}; + +} // namespace Behavior +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + +#endif //INKSCAPE_UI_DIALOG_BEHAVIOR_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/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..edc88ba --- /dev/null +++ b/src/ui/dialog/clonetiler.cpp @@ -0,0 +1,2834 @@ +// 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/liststore.h> +#include <gtkmm/radiobutton.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 "unclump.h" +#include "verbs.h" + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing.h" + +#include "ui/icon-loader.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 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 () : + UI::Widget::Panel("/dialogs/clonetiler/", SP_VERB_DIALOG_CLONETILER), + desktop(nullptr), + deskTrack(), + table_row_labels(nullptr) +{ + Gtk::Box *contents = _getContents(); + contents->set_spacing(0); + + { + auto prefs = Inkscape::Preferences::get(); + + auto mainbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_set_homogeneous(GTK_BOX(mainbox), FALSE); + gtk_container_set_border_width (GTK_CONTAINER (mainbox), 6); + + contents->pack_start (*Gtk::manage(Glib::wrap(mainbox)), true, true, 0); + + nb = gtk_notebook_new (); + gtk_box_pack_start (GTK_BOX (mainbox), nb, FALSE, FALSE, 0); + + + // Symmetry + { + GtkWidget *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, NULL); + + for (const auto & sg : sym_groups) { + // Add the description of the symgroup to a new row + combo->append(sg.label); + } + + gtk_box_pack_start (GTK_BOX (vb), GTK_WIDGET(combo->gobj()), 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_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + + // Shift + { + GtkWidget *vb = new_tab (nb, _("S_hift")); + + GtkWidget *table = table_x_y_rand (3); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + // X + { + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "shift" means: the tiles will be shifted (offset) horizontally by this amount + // xgettext:no-c-format + gtk_label_set_markup (GTK_LABEL(l), _("<b>Shift X:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + { + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "shift" means: the tiles will be shifted (offset) vertically by this amount + // xgettext:no-c-format + gtk_label_set_markup (GTK_LABEL(l), _("<b>Shift Y:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Exponent:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Alternate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Cumulate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Cumulate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Cumulate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Exclude tile:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + { + GtkWidget *vb = new_tab (nb, _("Sc_ale")); + + GtkWidget *table = table_x_y_rand (2); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + // X + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Scale X:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Scale Y:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Exponent:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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) + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Base:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Alternate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Cumulate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Cumulate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + { + GtkWidget *vb = new_tab (nb, _("_Rotation")); + + GtkWidget *table = table_x_y_rand (1); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + // Angle + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Angle:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Alternate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Cumulate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Cumulate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + { + GtkWidget *vb = new_tab (nb, _("_Blur & opacity")); + + GtkWidget *table = table_x_y_rand (1); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + + // Blur + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Blur:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Alternate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Opacity:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Alternate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + { + GtkWidget *vb = new_tab (nb, _("Co_lor")); + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + GtkWidget *l = gtk_label_new (_("Initial color: ")); + gtk_box_pack_start (GTK_BOX (hb), 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)); + + gtk_box_pack_start (GTK_BOX (hb), reinterpret_cast<GtkWidget*>(color_picker->gobj()), FALSE, FALSE, 0); + + gtk_box_pack_start (GTK_BOX (vb), hb, FALSE, FALSE, 0); + } + + + GtkWidget *table = table_x_y_rand (3); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + // Hue + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>H:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>S:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>L:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<small>Alternate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, 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 + { + GtkWidget *vb = new_tab (nb, _("_Trace")); + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, VB_MARGIN); + gtk_container_set_border_width(GTK_CONTAINER(hb), 4); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_start (GTK_BOX (vb), hb, FALSE, FALSE, 0); + + _b = Gtk::manage(new Gtk::CheckButton(_("Trace the drawing under the clones/sprayed items"))); + _b->set_data("uncheckable", GINT_TO_POINTER(TRUE)); + 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")); + gtk_box_pack_start (GTK_BOX (hb), GTK_WIDGET(_b->gobj()), FALSE, FALSE, 0); + _b->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::do_pick_toggled)); + } + + { + auto vvb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_box_set_homogeneous(GTK_BOX(vvb), FALSE); + gtk_box_pack_start (GTK_BOX (vb), vvb, FALSE, FALSE, 0); + _dotrace = vvb; + + { + GtkWidget *frame = gtk_frame_new (_("1. Pick from the drawing:")); + gtk_box_pack_start (GTK_BOX (vvb), frame, FALSE, FALSE, 0); + + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 4); + gtk_grid_set_column_spacing(GTK_GRID(table), 6); + gtk_container_set_border_width(GTK_CONTAINER(table), 4); + gtk_container_add(GTK_CONTAINER(frame), 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); + } + + } + + { + GtkWidget *frame = gtk_frame_new (_("2. Tweak the picked value:")); + gtk_box_pack_start (GTK_BOX (vvb), frame, FALSE, FALSE, VB_MARGIN); + + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 4); + gtk_grid_set_column_spacing(GTK_GRID(table), 6); + gtk_container_set_border_width(GTK_CONTAINER(table), 4); + gtk_container_add(GTK_CONTAINER(frame), table); + + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("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); + } + + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("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); + } + + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("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); + } + } + + { + GtkWidget *frame = gtk_frame_new (_("3. Apply the value to the clones':")); + gtk_box_pack_start (GTK_BOX (vvb), frame, FALSE, FALSE, 0); + + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 4); + gtk_grid_set_column_spacing(GTK_GRID(table), 6); + gtk_container_set_border_width(GTK_CONTAINER(table), 4); + gtk_container_add(GTK_CONTAINER(frame), 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")); + } + } + gtk_widget_set_sensitive (vvb, prefs->getBool(prefs_path + "dotrace")); + } + } + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, VB_MARGIN); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_start (GTK_BOX (mainbox), hb, FALSE, FALSE, 0); + GtkWidget *l = gtk_label_new(""); + gtk_label_set_markup (GTK_LABEL(l), _("Apply to tiled clones:")); + gtk_box_pack_start (GTK_BOX (hb), l, FALSE, FALSE, 0); + } + // Rows/columns, width/height + { + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 4); + gtk_grid_set_column_spacing(GTK_GRID(table), 6); + + gtk_container_set_border_width (GTK_CONTAINER (table), VB_MARGIN); + gtk_box_pack_start (GTK_BOX (mainbox), table, FALSE, FALSE, 0); + + { + _rowscols = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + + { + 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); + _rowscols->pack_start(*sb, true, true, 0); + + 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("×"); + _rowscols->pack_start(*l, true, true, 0); + } + + { + 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); + _rowscols->pack_start(*sb, true, true, 0); + + a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::xy_changed), a, "imax")); + } + + table_attach (table, GTK_WIDGET(_rowscols->gobj()), 0.0, 1, 2); + } + + { + _widthheight = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + + // 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); + _widthheight->pack_start(*e, true, true, 0); + fill_width->signal_value_changed().connect(sigc::mem_fun(*this, &CloneTiler::fill_width_changed)); + } + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup("×"); + _widthheight->pack_start(*l, true, true, 0); + } + + { + // 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); + _widthheight->pack_start(*e, true, true, 0); + fill_height->signal_value_changed().connect(sigc::mem_fun(*this, &CloneTiler::fill_height_changed)); + } + + _widthheight->pack_start(*unit_menu, true, true, 0); + table_attach (table, GTK_WIDGET(_widthheight->gobj()), 0.0, 2, 2); + + } + + // 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, GTK_WIDGET(radio->gobj()), 0.0, 1, 1); + radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_create)); + + if (!prefs->getBool(prefs_path + "fillrect")) { + radio->set_active(true); + } + } + { + 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, GTK_WIDGET(radio->gobj()), 0.0, 2, 1); + radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_fill)); + + if (prefs->getBool(prefs_path + "fillrect")) { + radio->set_active(true); + } + } + } + + + // Use saved pos + { + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + gtk_box_pack_start (GTK_BOX (mainbox), GTK_WIDGET(hb->gobj()), FALSE, FALSE, 0); + + _cb_keep_bbox = Gtk::manage(new Gtk::CheckButton(_("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_box_new(GTK_ORIENTATION_HORIZONTAL, VB_MARGIN); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_end (GTK_BOX (mainbox), hb, FALSE, FALSE, 0); + GtkWidget *l = gtk_label_new(""); + _status = l; + gtk_box_pack_start (GTK_BOX (hb), l, FALSE, FALSE, 0); + } + + // Buttons + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, VB_MARGIN); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_start (GTK_BOX (mainbox), 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)); + gtk_box_pack_end (GTK_BOX (hb), GTK_WIDGET(b->gobj()), FALSE, FALSE, 0); + } + + { // buttons which are enabled only when there are tiled clones + auto sb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); + gtk_box_set_homogeneous(GTK_BOX(sb), FALSE); + gtk_box_pack_end (GTK_BOX (hb), 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)); + gtk_box_pack_end (GTK_BOX (sb), GTK_WIDGET(b->gobj()), 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)); + gtk_box_pack_end (GTK_BOX (sb), GTK_WIDGET(b->gobj()), 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)); + gtk_box_pack_start (GTK_BOX (hb), GTK_WIDGET(b->gobj()), FALSE, FALSE, 0); + } + } + + gtk_widget_show_all (mainbox); + } + + show_all(); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &CloneTiler::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + +} + +CloneTiler::~CloneTiler () +{ + //subselChangedConn.disconnect(); + //selectModifiedConn.disconnect(); + selectChangedConn.disconnect(); + externChangedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.disconnect(); + color_changed_connection.disconnect(); +} + +void CloneTiler::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void CloneTiler::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + this->desktop = desktop; + } +} + +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()) { + gtk_widget_set_sensitive (_buttons_on_tiles, FALSE); + gtk_label_set_markup (GTK_LABEL(_status), _("<small>Nothing selected.</small>")); + return; + } + + if (boost::distance(selection->items()) > 1) { + gtk_widget_set_sensitive (_buttons_on_tiles, FALSE); + gtk_label_set_markup (GTK_LABEL(_status), _("<small>More than one object selected.</small>")); + return; + } + + guint n = number_of_clones(selection->singleItem()); + if (n > 0) { + gtk_widget_set_sensitive (_buttons_on_tiles, TRUE); + gchar *sta = g_strdup_printf (_("<small>Object has <b>%d</b> tiled clones.</small>"), n); + gtk_label_set_markup (GTK_LABEL(_status), sta); + g_free (sta); + } else { + gtk_widget_set_sensitive (_buttons_on_tiles, FALSE); + gtk_label_set_markup (GTK_LABEL(_status), _("<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 desktop = SP_ACTIVE_DESKTOP; + if (desktop == nullptr) { + return; + } + + auto selection = desktop->getSelection(); + + // check if something is selected + if (selection->isEmpty() || boost::distance(selection->items()) > 1) { + desktop->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); + } + } + + desktop->getDocument()->ensureUpToDate(); + reverse(to_unclump.begin(),to_unclump.end()); + ::unclump (to_unclump); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_CLONETILER, + _("Unclump tiled 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*/) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop == nullptr) { + return; + } + + Inkscape::Selection *selection = desktop->getSelection(); + + // check if something is selected + if (selection->isEmpty() || boost::distance(selection->items()) > 1) { + desktop->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(desktop->getDocument(), SP_VERB_DIALOG_CLONETILER, + _("Delete tiled 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() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop == nullptr) { + return; + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Inkscape::Selection *selection = desktop->getSelection(); + + // 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 + gtk_label_set_markup (GTK_LABEL(_status), _("<small>Creating tiled clones...</small>")); + gtk_widget_queue_draw(GTK_WIDGET(_status)); + + 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 = desktop->getDocument()->getDocumentScale().inverse(); + double scale_units = scale[Geom::X]; // Use just x direction.... + + 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(desktop->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 = 0; + double cy = 0; + sp_repr_get_double (obj_repr, "inkscape:tile-cx", &cx); + sp_repr_get_double (obj_repr, "inkscape:tile-cy", &cy); + center = Geom::Point (cx, cy); + + sp_repr_get_double (obj_repr, "inkscape:tile-w", &w); + sp_repr_get_double (obj_repr, "inkscape:tile-h", &h); + sp_repr_get_double (obj_repr, "inkscape:tile-x0", &x0); + sp_repr_get_double (obj_repr, "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()); + + sp_repr_set_svg_double(obj_repr, "inkscape:tile-cx", center[Geom::X]); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-cy", center[Geom::Y]); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-w", w); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-h", h); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-x0", x0); + sp_repr_set_svg_double(obj_repr, "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; + } + + gchar *affinestr=sp_svg_transform_write(t); + clone->setAttribute("transform", affinestr); + g_free(affinestr); + + if (opacity < 1.0) { + sp_repr_set_css_double(clone, "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); + double perimeter = perimeter_original * t.descrim(); + double radius = blur * perimeter; + // this is necessary for all newly added clones to have correct bboxes, + // otherwise filters won't work: + desktop->getDocument()->ensureUpToDate(); + // it's hard to figure out exact width/height of the tile without having an object + // that we can take bbox of; however here we only need a lower bound so that blur + // margins are not too small, and the perimeter should work + SPFilter *constructed = new_filter_gaussian_blur(desktop->getDocument(), radius, t.descrim(), t.expansionX(), t.expansionY(), perimeter, perimeter); + 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(desktop->getDocument(), SP_VERB_DIALOG_CLONETILER, + _("Create tiled clones")); +} + +GtkWidget * CloneTiler::new_tab(GtkWidget *nb, const gchar *label) +{ + GtkWidget *l = gtk_label_new_with_mnemonic (label); + auto vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, VB_MARGIN); + gtk_box_set_homogeneous(GTK_BOX(vb), FALSE); + gtk_container_set_border_width (GTK_CONTAINER (vb), VB_MARGIN); + gtk_notebook_append_page (GTK_NOTEBOOK (nb), 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 Gtk::CheckButton()); + 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_data("uncheckable", GINT_TO_POINTER(true)); + + 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_data ("oneable", GINT_TO_POINTER(TRUE)); + } else { + sb->set_data ("zeroable", GINT_TO_POINTER(TRUE)); + } + } + + { + 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(GtkWidget *w) +{ + if (w && G_IS_OBJECT(w)) { + { + int r = GPOINTER_TO_INT (g_object_get_data(G_OBJECT(w), "zeroable")); + if (r && GTK_IS_SPIN_BUTTON(w)) { // spinbutton + GtkAdjustment *a = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON(w)); + gtk_adjustment_set_value (a, 0); + } + } + { + int r = GPOINTER_TO_INT (g_object_get_data(G_OBJECT(w), "oneable")); + if (r && GTK_IS_SPIN_BUTTON(w)) { // spinbutton + GtkAdjustment *a = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON(w)); + gtk_adjustment_set_value (a, 1); + } + } + { + int r = GPOINTER_TO_INT (g_object_get_data(G_OBJECT(w), "uncheckable")); + if (r && GTK_IS_TOGGLE_BUTTON(w)) { // checkbox + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(w), FALSE); + } + } + } + + if (GTK_IS_CONTAINER(w)) { + std::vector<Gtk::Widget*> c = Glib::wrap(GTK_CONTAINER(w))->get_children(); + for ( auto i : c ) { + reset_recursive(i->gobj()); + } + } +} + +void CloneTiler::reset() +{ + reset_recursive(GTK_WIDGET(this->gobj())); +} + +void CloneTiler::table_attach(GtkWidget *table, Gtk::Widget *widget, float align, int row, int col) +{ + table_attach(table, GTK_WIDGET(widget->gobj()), align, row, col); +} + +void CloneTiler::table_attach(GtkWidget *table, GtkWidget *widget, float align, int row, int col) +{ + gtk_widget_set_halign(widget, GTK_ALIGN_FILL); + gtk_widget_set_valign(widget, GTK_ALIGN_CENTER); + gtk_grid_attach(GTK_GRID(table), widget, col, row, 1, 1); +} + +GtkWidget * CloneTiler::table_x_y_rand(int values) +{ + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 6); + gtk_grid_set_column_spacing(GTK_GRID(table), 8); + + gtk_container_set_border_width (GTK_CONTAINER (table), VB_MARGIN); + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + GtkWidget *i = sp_get_icon_image("object-rows", GTK_ICON_SIZE_MENU); + gtk_box_pack_start(GTK_BOX(hb), i, FALSE, FALSE, 2); + + GtkWidget *l = gtk_label_new(""); + gtk_label_set_markup(GTK_LABEL(l), _("<small>Per row:</small>")); + gtk_box_pack_start(GTK_BOX(hb), l, FALSE, FALSE, 2); + + table_attach(table, hb, 0, 1, 2); + } + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + GtkWidget *i = sp_get_icon_image("object-columns", GTK_ICON_SIZE_MENU); + gtk_box_pack_start(GTK_BOX(hb), i, FALSE, FALSE, 2); + + GtkWidget *l = gtk_label_new(""); + gtk_label_set_markup(GTK_LABEL(l), _("<small>Per column:</small>")); + gtk_box_pack_start(GTK_BOX(hb), l, FALSE, FALSE, 2); + + table_attach(table, hb, 0, 1, 3); + } + + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<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() +{ + if (_rowscols) { + _rowscols->set_sensitive(true); + } + if (_widthheight) { + _widthheight->set_sensitive(false); + } + + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + "fillrect", false); +} + + +void CloneTiler::switch_to_fill() +{ + if (_rowscols) { + _rowscols->set_sensitive(false); + } + if (_widthheight) { + _widthheight->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); + gtk_adjustment_set_value(fill_width->gobj(), width_value); + gtk_adjustment_set_value(fill_height->gobj(), 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) { + gtk_widget_set_sensitive (_dotrace, active); + } +} + +void CloneTiler::show_page_trace() +{ + gtk_notebook_set_current_page(GTK_NOTEBOOK(nb),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..47c20ad --- /dev/null +++ b/src/ui/dialog/clonetiler.h @@ -0,0 +1,226 @@ +// 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/widget/panel.h" + +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/color-picker.h" + +namespace Gtk { + class CheckButton; + class ComboBox; + class ToggleButton; +} + +class SPItem; +class SPObject; + +namespace Geom { + class Rect; + class Affine; +} + +namespace Inkscape { +namespace UI { + +namespace Widget { + class UnitMenu; +} + +namespace Dialog { + +class CloneTiler : public Widget::Panel { +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 + }; + + GtkWidget * new_tab(GtkWidget *nb, const gchar *label); + GtkWidget * 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(GtkWidget *table, GtkWidget *widget, float align, int row, int col); + void table_attach(GtkWidget *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(GtkWidget *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; + + Gtk::CheckButton *_b; + Gtk::CheckButton *_cb_keep_bbox; + GtkWidget *nb; + SPDesktop *desktop; + DesktopTracker deskTrack; + Inkscape::UI::Widget::ColorPicker *color_picker; + GtkSizeGroup* table_row_labels; + Inkscape::UI::Widget::UnitMenu *unit_menu; + + Glib::RefPtr<Gtk::Adjustment> fill_width; + Glib::RefPtr<Gtk::Adjustment> fill_height; + + sigc::connection desktopChangeConn; + sigc::connection selectChangedConn; + sigc::connection externChangedConn; + sigc::connection subselChangedConn; + sigc::connection selectModifiedConn; + sigc::connection color_changed_connection; + sigc::connection unitChangedConn; + + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + + // Variables that used to be set using GObject + GtkWidget *_buttons_on_tiles; + GtkWidget *_dotrace; + GtkWidget *_status; + Gtk::Box *_rowscols; + Gtk::Box *_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..acec954 --- /dev/null +++ b/src/ui/dialog/color-item.cpp @@ -0,0 +1,751 @@ +// 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 "io/resource.h" +#include "io/sys.h" +#include "message-context.h" +#include "svg/svg-color.h" +#include "verbs.h" +#include "widgets/gradient-vector.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static std::vector<std::string> mimeStrings; +static std::map<std::string, guint> mimeToInt; + + +#if ENABLE_MAGIC_COLORS +// TODO remove this soon: +extern std::vector<SwatchPage*> possible; +#endif // ENABLE_MAGIC_COLORS + + +#if ENABLE_MAGIC_COLORS +static bool bruteForce( SPDocument* document, Inkscape::XML::Node* node, Glib::ustring const& match, int r, int g, int b ) +{ + bool changed = false; + + if ( node ) { + gchar const * val = node->attribute("inkscape:x-fill-tag"); + if ( val && (match == val) ) { + SPObject *obj = document->getObjectByRepr( node ); + + gchar c[64] = {0}; + sp_svg_write_color( c, sizeof(c), SP_RGBA32_U_COMPOSE( r, g, b, 0xff ) ); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property( css, "fill", c ); + + sp_desktop_apply_css_recursive( obj, css, true ); + static_cast<SPItem*>(obj)->updateRepr(); + + changed = true; + } + + val = node->attribute("inkscape:x-stroke-tag"); + if ( val && (match == val) ) { + SPObject *obj = document->getObjectByRepr( node ); + + gchar c[64] = {0}; + sp_svg_write_color( c, sizeof(c), SP_RGBA32_U_COMPOSE( r, g, b, 0xff ) ); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property( css, "stroke", c ); + + sp_desktop_apply_css_recursive( (SPItem*)obj, css, true ); + ((SPItem*)obj)->updateRepr(); + + changed = true; + } + + Inkscape::XML::Node* first = node->firstChild(); + changed |= bruteForce( document, first, match, r, g, b ); + + changed |= bruteForce( document, node->next(), match, r, g, b ); + } + + return changed; +} +#endif // ENABLE_MAGIC_COLORS + +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 void dieDieDie( GObject *obj, gpointer user_data ) +{ + g_message("die die die %p %p", obj, user_data ); +} + +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 ), + _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 ); + } + + +#if ENABLE_MAGIC_COLORS + // Look for objects using this color + { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if ( desktop ) { + SPDocument* document = desktop->getDocument(); + Inkscape::XML::Node *rroot = document->getReprRoot(); + if ( rroot ) { + + // Find where this thing came from + Glib::ustring paletteName; + bool found = false; + int index = 0; + for ( std::vector<SwatchPage*>::iterator it2 = possible.begin(); it2 != possible.end() && !found; ++it2 ) { + SwatchPage* curr = *it2; + index = 0; + for ( boost::ptr_vector<ColorItem>::iterator zz = curr->_colors.begin(); zz != curr->_colors.end(); ++zz ) { + if ( this == &*zz ) { + found = true; + paletteName = curr->_name; + break; + } else { + index++; + } + } + } + + if ( !paletteName.empty() ) { + gchar* str = g_strdup_printf("%d|", index); + paletteName.insert( 0, str ); + g_free(str); + str = 0; + + if ( bruteForce( document, rroot, paletteName, def.getR(), def.getG(), def.getB() ) ) { + SPDocumentUndo::done( document , SP_VERB_DIALOG_SWATCHES, + _("Change color definition")); + } + } + } + } + } +#endif // ENABLE_MAGIC_COLORS + +} + +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::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; + } + + _previews.push_back( widget ); + + 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(), SP_VERB_DIALOG_SWATCHES, descr.c_str() ); + } +} + +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..7210ed2 --- /dev/null +++ b/src/ui/dialog/color-item.h @@ -0,0 +1,130 @@ +// 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; + +private: + + 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/debug.cpp b/src/ui/dialog/debug.cpp new file mode 100644 index 0000000..af2f08b --- /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, 400); + 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/desktop-tracker.cpp b/src/ui/dialog/desktop-tracker.cpp new file mode 100644 index 0000000..67f2cef --- /dev/null +++ b/src/ui/dialog/desktop-tracker.cpp @@ -0,0 +1,154 @@ +// 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. + */ + +#include "widgets/desktop-widget.h" + +#include "desktop-tracker.h" + +#include "inkscape.h" +#include "desktop.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +DesktopTracker::DesktopTracker() : + base(nullptr), + desktop(nullptr), + widget(nullptr), + hierID(0), + trackActive(false), + desktopChangedSig() +{ +} + +DesktopTracker::~DesktopTracker() +{ + disconnect(); +} + +void DesktopTracker::connect(GtkWidget *widget) +{ + disconnect(); + + this->widget = widget; + + // Use C/gobject callbacks to avoid gtkmm rewrap-during-destruct issues: + hierID = g_signal_connect( G_OBJECT(widget), "hierarchy-changed", G_CALLBACK(hierarchyChangeCB), this ); + inkID = INKSCAPE.signal_activate_desktop.connect( + sigc::bind( + sigc::ptr_fun(&DesktopTracker::activateDesktopCB), this) + ); + + GtkWidget *wdgt = gtk_widget_get_ancestor(widget, SP_TYPE_DESKTOP_WIDGET); + if (wdgt && !base) { + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(wdgt); + if (dtw && dtw->desktop) { + setBase(dtw->desktop); // may also set desktop + } + } +} + +void DesktopTracker::disconnect() +{ + if (hierID) { + if (widget) { + g_signal_handler_disconnect(G_OBJECT(widget), hierID); + } + hierID = 0; + } + if (inkID.connected()) { + inkID.disconnect(); + } +} + +void DesktopTracker::setBase(SPDesktop *desktop) +{ + if (this->base != desktop) { + base = desktop; + // Do not override an existing target desktop + if (!this->desktop) { + setDesktop(desktop); + } + } +} + +SPDesktop *DesktopTracker::getBase() const +{ + return base; +} + +SPDesktop *DesktopTracker::getDesktop() const +{ + return desktop; +} + +sigc::connection DesktopTracker::connectDesktopChanged( const sigc::slot<void, SPDesktop*> & slot ) +{ + return desktopChangedSig.connect(slot); +} + +void DesktopTracker::activateDesktopCB(SPDesktop *desktop, DesktopTracker *self ) +{ + if (self && self->trackActive) { + self->setDesktop(desktop); + } + //return FALSE; +} + +bool DesktopTracker::hierarchyChangeCB(GtkWidget * /*widget*/, GtkWidget* /*prev*/, DesktopTracker *self) +{ + if (self) { + self->handleHierarchyChange(); + } + return false; +} + +void DesktopTracker::handleHierarchyChange() +{ + GtkWidget *wdgt = gtk_widget_get_ancestor(widget, SP_TYPE_DESKTOP_WIDGET); + bool newFlag = (wdgt == nullptr); // true means not in an SPDesktopWidget, thus floating. + if (wdgt && !base) { + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(wdgt); + if (dtw && dtw->desktop) { + setBase(dtw->desktop); // may also set desktop + } + } + if (newFlag != trackActive) { + trackActive = newFlag; + if (trackActive) { + setDesktop(SP_ACTIVE_DESKTOP); + } else if (desktop != base) { + setDesktop(getBase()); + } + } +} + +void DesktopTracker::setDesktop(SPDesktop *desktop) +{ + if (desktop != this->desktop) { + this->desktop = desktop; + desktopChangedSig.emit(desktop); + } +} + + +} // 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/desktop-tracker.h b/src/ui/dialog/desktop-tracker.h new file mode 100644 index 0000000..7b94390 --- /dev/null +++ b/src/ui/dialog/desktop-tracker.h @@ -0,0 +1,69 @@ +// 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_DIALOG_DESKTOP_TRACKER +#define SEEN_DIALOG_DESKTOP_TRACKER + +#include <cstddef> +#include <sigc++/connection.h> + +typedef struct _GtkWidget GtkWidget; +class SPDesktop; +struct InkscapeApplication; + +namespace Inkscape { + +namespace UI { +namespace Dialog { + +class DesktopTracker +{ +public: + DesktopTracker(); + virtual ~DesktopTracker(); + + void connect(GtkWidget *widget); + void disconnect(); + + SPDesktop *getDesktop() const; + + void setBase(SPDesktop *desktop); + SPDesktop *getBase() const; + + sigc::connection connectDesktopChanged( const sigc::slot<void, SPDesktop*> & slot ); + +private: + static void activateDesktopCB(SPDesktop *desktop, DesktopTracker *self ); + static bool hierarchyChangeCB(GtkWidget *widget, GtkWidget* prev, DesktopTracker *self); + + void handleHierarchyChange(); + void setDesktop(SPDesktop *desktop); + + SPDesktop *base; + SPDesktop *desktop; + GtkWidget *widget; + unsigned long hierID; + sigc::connection inkID; + bool trackActive; + sigc::signal<void, SPDesktop*> desktopChangedSig; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_DIALOG_DESKTOP_TRACKER +/* + 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..02bb270 --- /dev/null +++ b/src/ui/dialog/dialog-manager.cpp @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Object for managing a set of dialogs, including their signals and + * construction/caching/destruction of them. + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Jon Phillips <jon@rejon.org> + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2004-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/dialog/dialog-manager.h" + +#include "style.h" +#include "ui/dialog/align-and-distribute.h" +#include "ui/dialog/document-metadata.h" +#include "ui/dialog/document-properties.h" +#include "ui/dialog/extension-editor.h" +#include "ui/dialog/fill-and-stroke.h" +#include "ui/dialog/filter-editor.h" +#include "ui/dialog/filter-effects-dialog.h" +#include "ui/dialog/find.h" +#include "ui/dialog/glyphs.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/paint-servers.h" +#include "ui/dialog/symbols.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/panel-dialog.h" +#include "ui/dialog/layers.h" +#include "ui/dialog/icon-preview.h" +//#include "ui/dialog/print-colors-preview-dialog.h" +#include "ui/dialog/clonetiler.h" +#include "ui/dialog/export.h" +#include "ui/dialog/object-attributes.h" +#include "ui/dialog/object-properties.h" +#include "ui/dialog/objects.h" +#include "ui/dialog/selectorsdialog.h" + +#if HAVE_ASPELL +# include "ui/dialog/spellcheck.h" +#endif + +#include "ui/dialog/tags.h" + +#include "ui/dialog/styledialog.h" +#include "ui/dialog/svg-fonts-dialog.h" +#include "ui/dialog/text-edit.h" +#include "ui/dialog/xml-tree.h" +#include "util/ege-appear-time-tracker.h" +namespace Inkscape { +namespace UI { +namespace Dialog { + +namespace { + +using namespace Behavior; + +template <typename T, typename B> +inline Dialog *create() { return PanelDialog<B>::template create<T>(); } + +} + +/** + * This class is provided as a container for Inkscape's various + * dialogs. This allows InkscapeApplication to treat the various + * dialogs it invokes, as abstractions. + * + * DialogManager is essentially a cache of dialogs. It lets us + * initialize dialogs lazily - instead of constructing them during + * application startup, they're constructed the first time they're + * actually invoked by InkscapeApplication. The constructed + * dialog is held here after that, so future invocations of the + * dialog don't need to get re-constructed each time. The memory for + * the dialogs are then reclaimed when the DialogManager is destroyed. + * + * In addition, DialogManager also serves as a signal manager for + * dialogs. It provides a set of signals that can be sent to all + * dialogs for doing things such as hiding/unhiding them, etc. + * DialogManager ensures that every dialog it handles will listen + * to these signals. + * + */ +DialogManager::DialogManager() { + + using namespace Behavior; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int dialogs_type = prefs->getIntLimited("/options/dialogtype/value", DOCK, 0, 1); + + // The preferences dialog is broken, the DockBehavior code resizes it's floating window to the smallest size + registerFactory("InkscapePreferences", &create<InkscapePreferences, FloatingBehavior>); + + if (dialogs_type == FLOATING) { + registerFactory("AlignAndDistribute", &create<AlignAndDistribute, FloatingBehavior>); + registerFactory("DocumentMetadata", &create<DocumentMetadata, FloatingBehavior>); + registerFactory("DocumentProperties", &create<DocumentProperties, FloatingBehavior>); + registerFactory("ExtensionEditor", &create<ExtensionEditor, FloatingBehavior>); + registerFactory("FillAndStroke", &create<FillAndStroke, FloatingBehavior>); + registerFactory("FilterEffectsDialog", &create<FilterEffectsDialog, FloatingBehavior>); + registerFactory("FilterEditorDialog", &create<FilterEditorDialog, FloatingBehavior>); + registerFactory("Find", &create<Find, FloatingBehavior>); + registerFactory("Glyphs", &create<GlyphsPanel, FloatingBehavior>); + registerFactory("IconPreviewPanel", &create<IconPreviewPanel, FloatingBehavior>); + registerFactory("LayersPanel", &create<LayersPanel, FloatingBehavior>); + registerFactory("ObjectsPanel", &create<ObjectsPanel, FloatingBehavior>); + registerFactory("TagsPanel", &create<TagsPanel, FloatingBehavior>); + registerFactory("LivePathEffect", &create<LivePathEffectEditor, FloatingBehavior>); + registerFactory("Memory", &create<Memory, FloatingBehavior>); + registerFactory("Messages", &create<Messages, FloatingBehavior>); + registerFactory("ObjectAttributes", &create<ObjectAttributes, FloatingBehavior>); + registerFactory("ObjectProperties", &create<ObjectProperties, FloatingBehavior>); +// registerFactory("PrintColorsPreviewDialog", &create<PrintColorsPreviewDialog, FloatingBehavior>); + registerFactory("SvgFontsDialog", &create<SvgFontsDialog, FloatingBehavior>); + registerFactory("Swatches", &create<SwatchesPanel, FloatingBehavior>); + registerFactory("TileDialog", &create<ArrangeDialog, FloatingBehavior>); + registerFactory("Symbols", &create<SymbolsDialog, FloatingBehavior>); + registerFactory("PaintServers", &create<PaintServersDialog, FloatingBehavior>); + registerFactory("StyleDialog", &create<StyleDialog, FloatingBehavior>); + registerFactory("Trace", &create<TraceDialog, FloatingBehavior>); + + registerFactory("Transformation", &create<Transformation, FloatingBehavior>); + registerFactory("UndoHistory", &create<UndoHistory, FloatingBehavior>); + registerFactory("InputDevices", &create<InputDialog, FloatingBehavior>); + registerFactory("TextFont", &create<TextEdit, FloatingBehavior>); + +#if HAVE_ASPELL + registerFactory("SpellCheck", &create<SpellCheck, FloatingBehavior>); +#endif + + registerFactory("Export", &create<Export, FloatingBehavior>); + registerFactory("CloneTiler", &create<CloneTiler, FloatingBehavior>); + registerFactory("XmlTree", &create<XmlTree, FloatingBehavior>); + registerFactory("Selectors", &create<SelectorsDialog, FloatingBehavior>); + + } else { + + registerFactory("AlignAndDistribute", &create<AlignAndDistribute, DockBehavior>); + registerFactory("DocumentMetadata", &create<DocumentMetadata, DockBehavior>); + registerFactory("DocumentProperties", &create<DocumentProperties, DockBehavior>); + registerFactory("ExtensionEditor", &create<ExtensionEditor, DockBehavior>); + registerFactory("FillAndStroke", &create<FillAndStroke, DockBehavior>); + registerFactory("FilterEffectsDialog", &create<FilterEffectsDialog, DockBehavior>); + registerFactory("FilterEditorDialog", &create<FilterEditorDialog, DockBehavior>); + registerFactory("Find", &create<Find, DockBehavior>); + registerFactory("Glyphs", &create<GlyphsPanel, DockBehavior>); + registerFactory("IconPreviewPanel", &create<IconPreviewPanel, DockBehavior>); + registerFactory("LayersPanel", &create<LayersPanel, DockBehavior>); + registerFactory("ObjectsPanel", &create<ObjectsPanel, DockBehavior>); + registerFactory("TagsPanel", &create<TagsPanel, DockBehavior>); + registerFactory("LivePathEffect", &create<LivePathEffectEditor, DockBehavior>); + registerFactory("Memory", &create<Memory, DockBehavior>); + registerFactory("Messages", &create<Messages, DockBehavior>); + registerFactory("ObjectAttributes", &create<ObjectAttributes, DockBehavior>); + registerFactory("ObjectProperties", &create<ObjectProperties, DockBehavior>); +// registerFactory("PrintColorsPreviewDialog", &create<PrintColorsPreviewDialog, DockBehavior>); + registerFactory("SvgFontsDialog", &create<SvgFontsDialog, DockBehavior>); + registerFactory("Swatches", &create<SwatchesPanel, DockBehavior>); + registerFactory("TileDialog", &create<ArrangeDialog, DockBehavior>); + registerFactory("Symbols", &create<SymbolsDialog, DockBehavior>); + registerFactory("PaintServers", &create<PaintServersDialog, DockBehavior>); + registerFactory("Trace", &create<TraceDialog, DockBehavior>); + + registerFactory("Transformation", &create<Transformation, DockBehavior>); + registerFactory("UndoHistory", &create<UndoHistory, DockBehavior>); + registerFactory("InputDevices", &create<InputDialog, DockBehavior>); + registerFactory("TextFont", &create<TextEdit, DockBehavior>); + +#if HAVE_ASPELL + registerFactory("SpellCheck", &create<SpellCheck, DockBehavior>); +#endif + + registerFactory("Export", &create<Export, DockBehavior>); + registerFactory("CloneTiler", &create<CloneTiler, DockBehavior>); + registerFactory("XmlTree", &create<XmlTree, DockBehavior>); + registerFactory("Selectors", &create<SelectorsDialog, DockBehavior>); + } +} + +DialogManager::~DialogManager() { + // TODO: Disconnect the signals + // TODO: Do we need to explicitly delete the dialogs? + // Appears to cause a segfault if we do +} + + +DialogManager &DialogManager::getInstance() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int dialogs_type = prefs->getIntLimited("/options/dialogtype/value", DOCK, 0, 1); + + /* Use singleton behavior for floating dialogs */ + if (dialogs_type == FLOATING) { + static DialogManager *instance = nullptr; + + if (!instance) + instance = new DialogManager(); + return *instance; + } + + return *new DialogManager(); +} + +/** + * Registers a dialog factory function used to create the named dialog. + */ +void DialogManager::registerFactory(gchar const *name, + DialogManager::DialogFactory factory) +{ + registerFactory(g_quark_from_string(name), factory); +} + +/** + * Registers a dialog factory function used to create the named dialog. + */ +void DialogManager::registerFactory(GQuark name, + DialogManager::DialogFactory factory) +{ + _factory_map[name] = factory; +} + +/** + * Fetches the named dialog, creating it if it has not already been + * created (assuming a factory has been registered for it). + */ +Dialog *DialogManager::getDialog(gchar const *name) { + return getDialog(g_quark_from_string(name)); +} + +/** + * Fetches the named dialog, creating it if it has not already been + * created (assuming a factory has been registered for it). + */ +Dialog *DialogManager::getDialog(GQuark name) { + DialogMap::iterator dialog_found; + dialog_found = _dialog_map.find(name); + + Dialog *dialog=nullptr; + if ( dialog_found != _dialog_map.end() ) { + dialog = dialog_found->second; + } else { + FactoryMap::iterator factory_found; + factory_found = _factory_map.find(name); + + if ( factory_found != _factory_map.end() ) { + dialog = factory_found->second(); + _dialog_map[name] = dialog; + } + } + + return dialog; +} + +/** + * Shows the named dialog, creating it if necessary. + */ +void DialogManager::showDialog(gchar const *name, bool grabfocus) { + showDialog(g_quark_from_string(name), grabfocus); +} + +/** + * Shows the named dialog, creating it if necessary. + */ +void DialogManager::showDialog(GQuark name, bool /*grabfocus*/) { + bool wantTiming = Inkscape::Preferences::get()->getBool("/dialogs/debug/trackAppear", false); + GTimer *timer = (wantTiming) ? g_timer_new() : nullptr; // if needed, must be created/started before getDialog() + Dialog *dialog = getDialog(name); + if ( dialog ) { + if ( wantTiming ) { + gchar const * nameStr = g_quark_to_string(name); + ege::AppearTimeTracker *tracker = new ege::AppearTimeTracker(timer, dialog->gobj(), nameStr); + tracker->setAutodelete(true); + timer = nullptr; + } + // should check for grabfocus, but lp:1348927 prevents it + dialog->present(); + } + + if ( timer ) { + g_timer_destroy(timer); + timer = 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/dialog-manager.h b/src/ui/dialog/dialog-manager.h new file mode 100644 index 0000000..5b22189 --- /dev/null +++ b/src/ui/dialog/dialog-manager.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Object for managing a set of dialogs, including their signals and + * construction/caching/destruction of them. + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Jon Phillips <jon@rejon.org> + * + * Copyright (C) 2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_MANAGER_H +#define INKSCAPE_UI_DIALOG_MANAGER_H + +#include "dialog.h" +#include <map> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class DialogManager { +public: + typedef Dialog *(*DialogFactory)(); + + DialogManager(); + virtual ~DialogManager(); + + static DialogManager &getInstance(); + + // sigc::signal<void> show_dialogs; + // sigc::signal<void> show_f12; + // sigc::signal<void> hide_dialogs; + // sigc::signal<void> hide_f12; + // sigc::signal<void> transientize; + + /* generic dialog management start */ + typedef std::map<GQuark, DialogFactory> FactoryMap; + typedef std::map<GQuark, Dialog*> DialogMap; + + void registerFactory(gchar const *name, DialogFactory factory); + void registerFactory(GQuark name, DialogFactory factory); + Dialog *getDialog(gchar const* dlgName); + Dialog *getDialog(GQuark dlgName); + void showDialog(gchar const *name, bool grabfocus=true); + void showDialog(GQuark name, bool grabfocus=true); + +protected: + DialogManager(DialogManager const &d); // no copy + DialogManager& operator=(DialogManager const &d); // no assign + + FactoryMap _factory_map; //< factories to create dialogs + DialogMap _dialog_map; //< map of already created dialogs +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_UI_DIALOG_MANAGER_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.cpp b/src/ui/dialog/dialog.cpp new file mode 100644 index 0000000..cf94097 --- /dev/null +++ b/src/ui/dialog/dialog.cpp @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Base class for dialogs in Inkscape - implementation. + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * buliabyak@gmail.com + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2004--2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "dialog-manager.h" +#include <gtkmm/dialog.h> + +#include <gdk/gdkkeysyms.h> + +#include <utility> + +#include "inkscape.h" +#include "ui/monitor.h" +#include "ui/tools/tool-base.h" +#include "desktop.h" + +#include "shortcuts.h" +#include "ui/interface.h" +#include "verbs.h" +#include "ui/tool/event-utils.h" + +#define MIN_ONSCREEN_DISTANCE 50 + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +gboolean sp_retransientize_again(gpointer dlgPtr) +{ + Dialog *dlg = static_cast<Dialog *>(dlgPtr); + dlg->retransientize_suppress = false; + return FALSE; // so that it is only called once +} + +//===================================================================== + +Dialog::Dialog(Behavior::BehaviorFactory behavior_factory, const char *prefs_path, int verb_num, + Glib::ustring apply_label) + : _user_hidden(false), + _hiddenF12(false), + retransientize_suppress(false), + _prefs_path(prefs_path), + _verb_num(verb_num), + _title(), + _apply_label(std::move(apply_label)), + _desktop(nullptr), + _is_active_desktop(true), + _behavior(nullptr) +{ + gchar title[500]; + + if (verb_num) { + sp_ui_dialog_title_string (Inkscape::Verb::get(verb_num), title); + } + + _title = title; + _behavior = behavior_factory(*this); + _desktop = SP_ACTIVE_DESKTOP; + + Gtk::Widget *widg = dynamic_cast<Gtk::Widget *>(Glib::wrap(_behavior->gobj())); + INKSCAPE.signal_activate_desktop.connect(sigc::mem_fun(*this, &Dialog::onDesktopActivated)); + INKSCAPE.signal_dialogs_hide.connect(sigc::mem_fun(*this, &Dialog::onHideF12)); + INKSCAPE.signal_dialogs_unhide.connect(sigc::mem_fun(*this, &Dialog::onShowF12)); + INKSCAPE.signal_shut_down.connect(sigc::mem_fun(*this, &Dialog::onShutdown)); + INKSCAPE.signal_change_theme.connect(sigc::bind(sigc::ptr_fun(&sp_add_top_window_classes), widg)); + + Glib::wrap(gobj())->signal_event().connect(sigc::mem_fun(*this, &Dialog::_onEvent)); + Glib::wrap(gobj())->signal_key_press_event().connect(sigc::mem_fun(*this, &Dialog::_onKeyPress)); + + read_geometry(); + sp_add_top_window_classes(widg); +} + +Dialog::~Dialog() +{ + save_geometry(); + delete _behavior; + _behavior = nullptr; +} + + +//--------------------------------------------------------------------- + + +void Dialog::onDesktopActivated(SPDesktop *desktop) +{ + _is_active_desktop = (desktop == _desktop); + _behavior->onDesktopActivated(desktop); +} + +void Dialog::onShutdown() +{ + save_geometry(); + //_user_hidden = true; + _behavior->onShutdown(); +} + +void Dialog::onHideF12() +{ + _hiddenF12 = true; + _behavior->onHideF12(); +} + +void Dialog::onShowF12() +{ + if (_user_hidden) + return; + + if (_hiddenF12) { + _behavior->onShowF12(); + } + + _hiddenF12 = false; +} + + +inline Dialog::operator Gtk::Widget &() { return *_behavior; } +inline GtkWidget *Dialog::gobj() { return _behavior->gobj(); } +inline void Dialog::present() { _behavior->present(); } +inline Gtk::Box *Dialog::get_vbox() { return _behavior->get_vbox(); } +inline void Dialog::hide() { _behavior->hide(); } +inline void Dialog::show() { _behavior->show(); } +inline void Dialog::show_all_children() { _behavior->show_all_children(); } +inline void Dialog::set_size_request(int width, int height) { _behavior->set_size_request(width, height); } +inline void Dialog::size_request(Gtk::Requisition &requisition) { _behavior->size_request(requisition); } +inline void Dialog::get_position(int &x, int &y) { _behavior->get_position(x, y); } +inline void Dialog::get_size(int &width, int &height) { _behavior->get_size(width, height); } +inline void Dialog::resize(int width, int height) { _behavior->resize(width, height); } +inline void Dialog::move(int x, int y) { _behavior->move(x, y); } +inline void Dialog::set_position(Gtk::WindowPosition position) { _behavior->set_position(position); } +inline void Dialog::set_title(Glib::ustring title) { _behavior->set_title(title); } +inline void Dialog::set_sensitive(bool sensitive) { _behavior->set_sensitive(sensitive); } + +Glib::SignalProxy0<void> Dialog::signal_show() { return _behavior->signal_show(); } +Glib::SignalProxy0<void> Dialog::signal_hide() { return _behavior->signal_hide(); } + +void Dialog::read_geometry() +{ + _user_hidden = false; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int x = prefs->getInt(_prefs_path + "/x", -1000); + int y = prefs->getInt(_prefs_path + "/y", -1000); + int w = prefs->getInt(_prefs_path + "/w", 0); + int h = prefs->getInt(_prefs_path + "/h", 0); + + // g_print ("read %d %d %d %d\n", x, y, w, h); + + // If there are stored height and width values for the dialog, + // resize the window to match; otherwise we leave it at its default + if (w != 0 && h != 0) { + resize(w, h); + } + + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_primary(); + auto const screen_width = monitor_geometry.get_width(); + auto const screen_height = monitor_geometry.get_height(); + + // If there are stored values for where the dialog should be + // located, then restore the dialog to that position. + // also check if (x,y) is actually onscreen with the current screen dimensions + if ( (x >= 0) && (y >= 0) && (x < (screen_width-MIN_ONSCREEN_DISTANCE)) && (y < (screen_height-MIN_ONSCREEN_DISTANCE)) ) { + move(x, y); + } else { + // ...otherwise just put it in the middle of the screen + set_position(Gtk::WIN_POS_CENTER); + } + +} + + +void Dialog::save_geometry() +{ + int y, x, w, h; + + get_position(x, y); + get_size(w, h); + + // g_print ("write %d %d %d %d\n", x, y, w, h); + + if (x<0) x=0; + if (y<0) y=0; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt(_prefs_path + "/x", x); + prefs->setInt(_prefs_path + "/y", y); + prefs->setInt(_prefs_path + "/w", w); + prefs->setInt(_prefs_path + "/h", h); + +} + +void +Dialog::save_status(int visible, int state, int placement) +{ + // Only save dialog status for dialogs on the "last document" + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop != nullptr || !_is_active_desktop ) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs) { + prefs->setInt(_prefs_path + "/visible", visible); + prefs->setInt(_prefs_path + "/state", state); + prefs->setInt(_prefs_path + "/placement", placement); + } +} + + +void Dialog::_handleResponse(int response_id) +{ + switch (response_id) { + case Gtk::RESPONSE_CLOSE: { + _close(); + break; + } + } +} + +bool Dialog::_onDeleteEvent(GdkEventAny */*event*/) +{ + save_geometry(); + _user_hidden = true; + + return false; +} + +bool Dialog::_onEvent(GdkEvent *event) +{ + bool ret = false; + + switch (event->type) { + case GDK_KEY_PRESS: { + switch (Inkscape::UI::Tools::get_latin_keyval (&event->key)) { + case GDK_KEY_Escape: { + _defocus(); + ret = true; + break; + } + case GDK_KEY_F4: + case GDK_KEY_w: + case GDK_KEY_W: { + if (Inkscape::UI::held_only_control(event->key)) { + _close(); + ret = true; + } + break; + } + default: { // pass keypress to the canvas + break; + } + } + } + default: + ; + } + + return ret; +} + +bool Dialog::_onKeyPress(GdkEventKey *event) +{ + unsigned int shortcut; + shortcut = sp_shortcut_get_for_event((GdkEventKey*)event); + return sp_shortcut_invoke(shortcut, SP_ACTIVE_DESKTOP); +} + +void Dialog::_apply() +{ + g_warning("Apply button clicked for dialog [Dialog::_apply()]"); +} + +void Dialog::_close() +{ + _behavior->hide(); + _onDeleteEvent(nullptr); +} + +void Dialog::_defocus() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + if (desktop) { + Gtk::Widget *canvas = Glib::wrap(GTK_WIDGET(desktop->canvas)); + + // make sure the canvas window is present before giving it focus + Gtk::Window *toplevel_window = dynamic_cast<Gtk::Window *>(canvas->get_toplevel()); + if (toplevel_window) + toplevel_window->present(); + + canvas->grab_focus(); + } +} + +Inkscape::Selection* +Dialog::_getSelection() +{ + return SP_ACTIVE_DESKTOP->getSelection(); +} + +void sp_add_top_window_classes_callback(Gtk::Widget *widg) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + Gtk::Widget *canvas = Glib::wrap(GTK_WIDGET(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); + } +} + +} // 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.h b/src/ui/dialog/dialog.h new file mode 100644 index 0000000..012bbc6 --- /dev/null +++ b/src/ui/dialog/dialog.h @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Base class for dialogs in Inkscape + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Gustav Broberg <broberg@kth.se> + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004--2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_H +#define INKSCAPE_DIALOG_H + +#include "dock-behavior.h" +#include "floating-behavior.h" + +class SPDesktop; +struct InkscapeApplication; + +namespace Inkscape { +class Selection; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +enum BehaviorType { FLOATING, DOCK }; + +gboolean sp_retransientize_again(gpointer dlgPtr); +void sp_dialog_shutdown(GObject *object, gpointer dlgPtr); + +/** + * Base class for Inkscape dialogs. + * + * UI::Dialog::Dialog is a base class for all dialogs in Inkscape. The + * purpose of this class is to provide a unified place for ensuring + * style and behavior. Specifically, this class provides functionality + * for saving and restoring the size and position of dialogs (through + * the user's preferences file). + * + * It also provides some general purpose signal handlers for things like + * showing and hiding all dialogs. + * + * Fundamental parts of the dialog's behavior are controlled by + * a UI::Dialog::Behavior subclass instance connected to the dialog. + * + * @see UI::Widget::Panel panel class from which the dialogs are actually derived from. + * @see UI::Dialog::DialogManager manages the dialogs within inkscape. + * @see UI::Dialog::PanelDialog which links Panel and Dialog together in a dockable and floatable dialog. + */ +class Dialog { + +public: + + /** + * Constructor. + * + * @param behavior_factory floating or docked. + * @param prefs_path characteristic path for loading/saving dialog position. + * @param verb_num the dialog verb. + */ + Dialog(Behavior::BehaviorFactory behavior_factory, const char *prefs_path = nullptr, + int verb_num = 0, Glib::ustring apply_label = ""); + + virtual ~Dialog(); + + virtual void onDesktopActivated(SPDesktop*); + virtual void onShutdown(); + + /* Hide and show dialogs */ + virtual void onHideF12(); + virtual void onShowF12(); + + virtual operator Gtk::Widget &(); + virtual GtkWidget *gobj(); + virtual void present(); + virtual Gtk::Box *get_vbox(); + virtual void show(); + virtual void hide(); + virtual void show_all_children(); + virtual void set_size_request(int, int); + virtual void size_request(Gtk::Requisition &); + virtual void get_position(int &x, int &y); + virtual void get_size(int &width, int &height); + virtual void resize(int width, int height); + virtual void move(int x, int y); + virtual void set_position(Gtk::WindowPosition position); + virtual void set_title(Glib::ustring title); + virtual void set_sensitive(bool sensitive=true); + + virtual Glib::SignalProxy0<void> signal_show(); + virtual Glib::SignalProxy0<void> signal_hide(); + + bool _user_hidden; // when it is closed by the user, to prevent repopping on f12 + bool _hiddenF12; + + /** + * Read window position from preferences. + */ + void read_geometry(); + + /** + * Save window position to preferences. + */ + void save_geometry(); + void save_status(int visible, int state, int placement); + + bool retransientize_suppress; // when true, do not retransientize (prevents races when switching new windows too fast) + +protected: + Glib::ustring const _prefs_path; + int _verb_num; + Glib::ustring _title; + Glib::ustring _apply_label; + SPDesktop * _desktop; + bool _is_active_desktop; + + virtual void _handleResponse(int response_id); + + virtual bool _onDeleteEvent (GdkEventAny*); + virtual bool _onEvent(GdkEvent *event); + virtual bool _onKeyPress(GdkEventKey *event); + + virtual void _apply(); + + /* Closes the dialog window. + * + * This code sends a delete_event to the dialog, + * instead of just destroying it, so that the + * dialog can do some housekeeping, such as remember + * its position. + */ + virtual void _close(); + virtual void _defocus(); + + Inkscape::Selection* _getSelection(); + + sigc::connection _desktop_activated_connection; + sigc::connection _dialogs_hidden_connection; + sigc::connection _dialogs_unhidden_connection; + sigc::connection _shutdown_connection; + sigc::connection _change_theme_connection; + + private: + Behavior::Behavior* _behavior; + + Dialog() = delete; // no constructor without params + + Dialog(Dialog const &d) = delete; // no copy + Dialog& operator=(Dialog const &d) = delete; // no assign + + friend class Behavior::FloatingBehavior; + friend class Behavior::DockBehavior; +}; + +void sp_add_top_window_classes(Gtk::Widget *widg); +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + + +#endif //INKSCAPE_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/dock-behavior.cpp b/src/ui/dialog/dock-behavior.cpp new file mode 100644 index 0000000..9ea9d07 --- /dev/null +++ b/src/ui/dialog/dock-behavior.cpp @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A dockable dialog implementation. + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "dock-behavior.h" +#include "inkscape.h" +#include "desktop.h" +#include "ui/widget/dock.h" +#include "verbs.h" +#include "dialog.h" +#include "ui/dialog-events.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { +namespace Behavior { + + +DockBehavior::DockBehavior(Dialog &dialog) : + Behavior(dialog), + _dock_item(*SP_ACTIVE_DESKTOP->getDock(), + Inkscape::Verb::get(dialog._verb_num)->get_id(), dialog._title.c_str(), + (Inkscape::Verb::get(dialog._verb_num)->get_image() ? + Inkscape::Verb::get(dialog._verb_num)->get_image() : ""), + static_cast<Widget::DockItem::State>( + Inkscape::Preferences::get()->getInt(_dialog._prefs_path + "/state", + UI::Widget::DockItem::DOCKED_STATE)), + static_cast<GdlDockPlacement>( + Inkscape::Preferences::get()->getInt(_dialog._prefs_path + "/placement", + GDL_DOCK_TOP))) + +{ + // Connect signals + _signal_hide_connection = signal_hide().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::Behavior::DockBehavior::_onHide)); + signal_show().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::Behavior::DockBehavior::_onShow)); + _dock_item.signal_state_changed().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::Behavior::DockBehavior::_onStateChanged)); + if (_dock_item.getState() == Widget::DockItem::FLOATING_STATE) { + if (Gtk::Window *floating_win = _dock_item.getWindow()) { + sp_transientize(GTK_WIDGET(floating_win->gobj())); + } + } +} + +DockBehavior::~DockBehavior() += default; + + +Behavior * +DockBehavior::create(Dialog &dialog) +{ + return new DockBehavior(dialog); +} + + +DockBehavior::operator Gtk::Widget &() +{ + return _dock_item.getWidget(); +} + +GtkWidget * +DockBehavior::gobj() +{ + return _dock_item.gobj(); +} + +Gtk::VBox * +DockBehavior::get_vbox() +{ + return _dock_item.get_vbox(); +} + +void +DockBehavior::present() +{ + bool was_attached = _dock_item.isAttached(); + + _dock_item.present(); + + if (!was_attached) + _dialog.read_geometry(); +} + +void +DockBehavior::hide() +{ + _signal_hide_connection.block(); + _dock_item.hide(); + _signal_hide_connection.unblock(); +} + +void +DockBehavior::show() +{ + _dock_item.show(); +} + +void +DockBehavior::show_all_children() +{ + get_vbox()->show_all_children(); +} + +void +DockBehavior::get_position(int &x, int &y) +{ + _dock_item.get_position(x, y); +} + +void +DockBehavior::get_size(int &width, int &height) +{ + _dock_item.get_size(width, height); +} + +void +DockBehavior::resize(int width, int height) +{ + _dock_item.resize(width, height); +} + +void +DockBehavior::move(int x, int y) +{ + _dock_item.move(x, y); +} + +void +DockBehavior::set_position(Gtk::WindowPosition position) +{ + _dock_item.set_position(position); +} + +void +DockBehavior::set_size_request(int width, int height) +{ + _dock_item.set_size_request(width, height); +} + +void +DockBehavior::size_request(Gtk::Requisition &requisition) +{ + _dock_item.size_request(requisition); +} + +void +DockBehavior::set_title(Glib::ustring title) +{ + _dock_item.set_title(title); +} + +void DockBehavior::set_sensitive(bool sensitive) +{ + // TODO check this. Seems to be bad that we ignore the parameter + get_vbox()->set_sensitive(); +} + + +void +DockBehavior::_onHide() +{ + _dialog.save_geometry(); + _dialog._user_hidden = true; +} + +void +DockBehavior::_onShow() +{ + _dialog._user_hidden = false; +} + +void +DockBehavior::_onStateChanged(Widget::DockItem::State /*prev_state*/, + Widget::DockItem::State new_state) +{ +// TODO probably need to avoid window calls unless the state is different. Check. + + if (new_state == Widget::DockItem::FLOATING_STATE) { + if (Gtk::Window *floating_win = _dock_item.getWindow()) + sp_transientize(GTK_WIDGET(floating_win->gobj())); + } +} + +void +DockBehavior::onHideF12() +{ + _dialog.save_geometry(); + hide(); +} + +void +DockBehavior::onShowF12() +{ + present(); +} + +void +DockBehavior::onShutdown() +{ + int visible = _dock_item.isIconified() || !_dialog._user_hidden; + int status = (_dock_item.getState() == Inkscape::UI::Widget::DockItem::UNATTACHED) ? _dock_item.getPrevState() : _dock_item.getState(); + _dialog.save_status( visible, status, _dock_item.getPlacement() ); +} + +void +DockBehavior::onDesktopActivated(SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint transient_policy = prefs->getIntLimited( "/options/transientpolicy/value", 1, 0, 2); + +#ifdef _WIN32 // Win32 special code to enable transient dialogs + transient_policy = 2; +#endif + + if (!transient_policy) + return; + + Gtk::Window *floating_win = _dock_item.getWindow(); + + if (floating_win) { + + if (_dialog.retransientize_suppress) { + /* if retransientizing of this dialog is still forbidden after + * previous call warning turned off because it was confusingly fired + * when loading many files from command line + */ + + // g_warning("Retranzientize aborted! You're switching windows too fast!"); + return; + } + + if (GtkWindow *dialog_win = floating_win->gobj()) { + + _dialog.retransientize_suppress = true; // disallow other attempts to retranzientize this dialog + + desktop->setWindowTransient (dialog_win); + + /* + * This enables "aggressive" transientization, + * i.e. dialogs always emerging on top when you switch documents. Note + * however that this breaks "click to raise" policy of a window + * manager because the switched-to document will be raised at once + * (so that its transients also could raise) + */ + if (transient_policy == 2 && ! _dialog._hiddenF12 && !_dialog._user_hidden) { + // without this, a transient window not always emerges on top + gtk_window_present (dialog_win); + } + } + + // we're done, allow next retransientizing not sooner than after 120 msec + g_timeout_add (120, (GSourceFunc) sp_retransientize_again, (gpointer) &_dialog); + } +} + + +/* Signal wrappers */ + +Glib::SignalProxy0<void> +DockBehavior::signal_show() { return _dock_item.signal_show(); } + +Glib::SignalProxy0<void> +DockBehavior::signal_hide() { return _dock_item.signal_hide(); } + +Glib::SignalProxy1<bool, GdkEventAny *> +DockBehavior::signal_delete_event() { return _dock_item.signal_delete_event(); } + +Glib::SignalProxy0<void> +DockBehavior::signal_drag_begin() { return _dock_item.signal_drag_begin(); } + +Glib::SignalProxy1<void, bool> +DockBehavior::signal_drag_end() { return _dock_item.signal_drag_end(); } + + +} // namespace Behavior +} // 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/dock-behavior.h b/src/ui/dialog/dock-behavior.h new file mode 100644 index 0000000..85b8244 --- /dev/null +++ b/src/ui/dialog/dock-behavior.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dockable dialog implementation. + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef INKSCAPE_UI_DIALOG_DOCK_BEHAVIOR_H +#define INKSCAPE_UI_DIALOG_DOCK_BEHAVIOR_H + +#include "ui/widget/dock-item.h" +#include "behavior.h" + +namespace Gtk { + class Paned; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { +namespace Behavior { + +class DockBehavior : public Behavior { + +public: + static Behavior *create(Dialog& dialog); + + ~DockBehavior() override; + + /** Gtk::Dialog methods */ + operator Gtk::Widget&() override; + GtkWidget *gobj() override; + void present() override; + Gtk::VBox *get_vbox() override; + void show() override; + void hide() override; + void show_all_children() override; + void resize(int width, int height) override; + void move(int x, int y) override; + void set_position(Gtk::WindowPosition) override; + void set_size_request(int width, int height) override; + void size_request(Gtk::Requisition& requisition) override; + void get_position(int& x, int& y) override; + void get_size(int& width, int& height) override; + void set_title(Glib::ustring title) override; + void set_sensitive(bool sensitive) override; + + /** Gtk::Dialog signal proxies */ + Glib::SignalProxy0<void> signal_show() override; + Glib::SignalProxy0<void> signal_hide() override; + Glib::SignalProxy1<bool, GdkEventAny *> signal_delete_event() override; + Glib::SignalProxy0<void> signal_drag_begin(); + Glib::SignalProxy1<void, bool> signal_drag_end(); + + /** Custom signal handlers */ + void onHideF12() override; + void onShowF12() override; + void onDesktopActivated(SPDesktop *desktop) override; + void onShutdown() override; + +private: + Widget::DockItem _dock_item; + + DockBehavior(Dialog& dialog); + + /** Internal helpers */ + Gtk::Paned *_getPaned(); //< gives the parent pane, if the dock item has one + void _requestHeight(int height); //< tries to resize the dock item to the requested height + + /** Internal signal handlers */ + void _onHide(); + void _onShow(); + bool _onDeleteEvent(GdkEventAny *event); + void _onStateChanged(Widget::DockItem::State prev_state, Widget::DockItem::State new_state); + bool _onKeyPress(GdkEventKey *event); + + sigc::connection _signal_hide_connection; + sigc::connection _signal_key_press_event_connection; + +}; + +} // namespace Behavior +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_DOCK_BEHAVIOR_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-metadata.cpp b/src/ui/dialog/document-metadata.cpp new file mode 100644 index 0000000..a4c2707 --- /dev/null +++ b/src/ui/dialog/document-metadata.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Document metadata 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) + * + * Copyright (C) 2000 - 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "document-metadata.h" +#include "desktop.h" +#include "rdf.h" +#include "verbs.h" + +#include "object/sp-namedview.h" + +#include "ui/widget/entity-entry.h" +#include "xml/node-event-vector.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +#define SPACE_SIZE_X 15 +#define SPACE_SIZE_Y 15 + +//=================================================== + +//--------------------------------------------------- + +static void on_repr_attr_changed (Inkscape::XML::Node *, gchar const *, gchar const *, gchar const *, bool, gpointer); + +static Inkscape::XML::NodeEventVector const _repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + on_repr_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + + +DocumentMetadata & +DocumentMetadata::getInstance() +{ + DocumentMetadata &instance = *new DocumentMetadata(); + instance.init(); + return instance; +} + + +DocumentMetadata::DocumentMetadata() + : UI::Widget::Panel("/dialogs/documentmetadata", SP_VERB_DIALOG_METADATA) +{ + hide(); + _getContents()->set_spacing (4); + _getContents()->pack_start(_notebook, true, true); + + _page_metadata1.set_border_width(4); + _page_metadata2.set_border_width(4); + + _page_metadata1.set_column_spacing(2); + _page_metadata2.set_column_spacing(2); + _page_metadata1.set_row_spacing(2); + _page_metadata2.set_row_spacing(2); + + _notebook.append_page(_page_metadata1, _("Metadata")); + _notebook.append_page(_page_metadata2, _("License")); + + signalDocumentReplaced().connect(sigc::mem_fun(*this, &DocumentMetadata::_handleDocumentReplaced)); + signalActivateDesktop().connect(sigc::mem_fun(*this, &DocumentMetadata::_handleActivateDesktop)); + signalDeactiveDesktop().connect(sigc::mem_fun(*this, &DocumentMetadata::_handleDeactivateDesktop)); + + build_metadata(); +} + +void +DocumentMetadata::init() +{ + update(); + + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + repr->addListener (&_repr_events, this); + + show_all_children(); +} + +DocumentMetadata::~DocumentMetadata() +{ + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + repr->removeListenerByData (this); + + for (auto & it : _rdflist) + delete it; +} + +// TODO: This duplicates code in document-properties.cpp +void +DocumentMetadata::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.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.attach(w->_label, 0, row, 1, 1); + + w->_packable->set_hexpand(); + w->_packable->set_valign(Gtk::ALIGN_CENTER); + _page_metadata1.attach(*w->_packable, 1, row, 1, 1); + } + } + + _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.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.attach(_licensor, 1, row, 1, 1); +} + +/** + * Update dialog widgets from desktop. + */ +void DocumentMetadata::update() +{ + if (_wr.isUpdating()) return; + + _wr.setUpdating (true); + set_sensitive (true); + + //-----------------------------------------------------------meta pages + /* update the RDF entities */ + for (auto & it : _rdflist) + it->update (SP_ACTIVE_DOCUMENT); + + _licensor.update (SP_ACTIVE_DOCUMENT); + + _wr.setUpdating (false); +} + +void +DocumentMetadata::_handleDocumentReplaced(SPDesktop* desktop, SPDocument *) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->addListener (&_repr_events, this); + update(); +} + +void +DocumentMetadata::_handleActivateDesktop(SPDesktop *desktop) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->addListener(&_repr_events, this); + update(); +} + +void +DocumentMetadata::_handleDeactivateDesktop(SPDesktop *desktop) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->removeListenerByData(this); +} + +//-------------------------------------------------------------------- + +/** + * 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 (DocumentMetadata *dialog = static_cast<DocumentMetadata *>(data)) + dialog->update(); +} + +} // 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-metadata.h b/src/ui/dialog/document-metadata.h new file mode 100644 index 0000000..7004dca --- /dev/null +++ b/src/ui/dialog/document-metadata.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * \brief Document Metadata dialog + */ +/* Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * Bryce W. Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004, 2005, 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_DOCUMENT_METADATA_H +#define INKSCAPE_UI_DIALOG_DOCUMENT_METADATA_H + +#include <list> +#include <cstddef> +#include "ui/widget/panel.h" +#include <gtkmm/notebook.h> +#include <gtkmm/grid.h> + +#include "inkscape.h" +#include "ui/widget/licensor.h" +#include "ui/widget/registry.h" + +namespace Inkscape { + namespace XML { + class Node; + } + namespace UI { + namespace Widget { + class EntityEntry; + } + namespace Dialog { + +typedef std::list<UI::Widget::EntityEntry*> RDElist; + +class DocumentMetadata : public Inkscape::UI::Widget::Panel { +public: + void update(); + + static DocumentMetadata &getInstance(); + + static void destroy(); + +protected: + void build_metadata(); + void init(); + + void _handleDocumentReplaced(SPDesktop* desktop, SPDocument *document); + void _handleActivateDesktop(SPDesktop *desktop); + void _handleDeactivateDesktop(SPDesktop *desktop); + + Gtk::Notebook _notebook; + + Gtk::Grid _page_metadata1; + Gtk::Grid _page_metadata2; + + //--------------------------------------------------------------- + RDElist _rdflist; + UI::Widget::Licensor _licensor; + + UI::Widget::Registry _wr; + +private: + ~DocumentMetadata() override; + DocumentMetadata(); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_DOCUMENT_METADATA_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..31f59ec --- /dev/null +++ b/src/ui/dialog/document-properties.cpp @@ -0,0 +1,1708 @@ +// 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 "verbs.h" + +#include "display/canvas-grid.h" +#include "document-properties.h" +#include "helper/action.h" +#include "include/gtkmm_version.h" +#include "io/sys.h" +#include "object/sp-root.h" +#include "object/sp-script.h" +#include "ui/dialog/filedialog.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/shape-editor.h" +#include "ui/tools-switch.h" +#include "ui/widget/entity-entry.h" +#include "ui/widget/notebook-page.h" +#include "xml/node-event-vector.h" + +#if defined(HAVE_LIBLCMS2) +#include "object/color-profile.h" +#endif // defined(HAVE_LIBLCMS2) + +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() + : UI::Widget::Panel("/dialogs/documentoptions", SP_VERB_DIALOG_NAMEDVIEW) + , _page_page(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true))) + , _page_guides(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_snap(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))) + //--------------------------------------------------------------- + , _rcb_antialias(_("Use antialiasing"), _("If unset, no antialiasing will be done on the drawing"), "shape-rendering", _wr, false, nullptr, nullptr, nullptr, "crispEdges") + , _rcb_checkerboard(_("Checkerboard background"), _("If set, use a colored checkerboard for the canvas background"), "inkscape:pagecheckerboard", _wr, false) + , _rcb_canb(_("Show page _border"), _("If set, rectangular page border is shown"), "showborder", _wr, false) + , _rcb_bord(_("Border on _top of drawing"), _("If set, border is always on top of the drawing"), "borderlayer", _wr, false) + , _rcb_shad(_("_Show border shadow"), _("If set, page border shows a shadow on its right and lower side"), "inkscape:showpageshadow", _wr, false) + , _rcp_bg(_("Back_ground color:"), _("Background color"), _("Color of the canvas background. Note: opacity is ignored except when exporting to bitmap."), "pagecolor", "inkscape:pageopacity", _wr) + , _rcp_bord(_("Border _color:"), _("Page border color"), _("Color of the page border"), "bordercolor", "borderopacity", _wr) + , _rum_deflt(_("Display _units:"), "inkscape:document-units", _wr) + , _page_sizer(_wr) + //--------------------------------------------------------------- + //General snap 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")) + //--------------------------------------------------------------- + , _rsu_sno(_("Snap _distance"), _("Snap only when _closer than:"), _("Always snap"), + _("Snapping distance, in screen pixels, for snapping to objects"), _("Always snap to objects, regardless of their distance"), + _("If set, objects only snap to another object when it's within the range specified below"), + "objecttolerance", _wr) + //Options for snapping to grids + , _rsu_sn(_("Snap d_istance"), _("Snap only when c_loser than:"), _("Always snap"), + _("Snapping distance, in screen pixels, for snapping to grid"), _("Always snap to grids, regardless of the distance"), + _("If set, objects only snap to a grid line when it's within the range specified below"), + "gridtolerance", _wr) + //Options for snapping to guides + , _rsu_gusn(_("Snap dist_ance"), _("Snap only when close_r than:"), _("Always snap"), + _("Snapping distance, in screen pixels, for snapping to guides"), _("Always snap to guides, regardless of the distance"), + _("If set, objects only snap to a guide when it's within the range specified below"), + "guidetolerance", _wr) + //--------------------------------------------------------------- + , _rcb_snclp(_("Snap to clip paths"), _("When snapping to paths, then also try snapping to clip paths"), "inkscape:snap-path-clip", _wr) + , _rcb_snmsk(_("Snap to mask paths"), _("When snapping to paths, then also try snapping to mask paths"), "inkscape:snap-path-mask", _wr) + , _rcb_perp(_("Snap perpendicularly"), _("When snapping to paths or guides, then also try snapping perpendicularly"), "inkscape:snap-perpendicular", _wr) + , _rcb_tang(_("Snap tangentially"), _("When snapping to paths or guides, then also try snapping tangentially"), "inkscape:snap-tangential", _wr) + //--------------------------------------------------------------- + , _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) +{ + _getContents()->set_spacing (4); + _getContents()->pack_start(_notebook, true, true); + + _notebook.append_page(*_page_page, _("Page")); + _notebook.append_page(*_page_guides, _("Guides")); + _notebook.append_page(_grids_vbox, _("Grids")); + _notebook.append_page(*_page_snap, _("Snap")); + _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_snap(); +#if defined(HAVE_LIBLCMS2) + build_cms(); +#endif // defined(HAVE_LIBLCMS2) + 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)); + + signalDocumentReplaced().connect(sigc::mem_fun(*this, &DocumentProperties::_handleDocumentReplaced)); + signalActivateDesktop().connect(sigc::mem_fun(*this, &DocumentProperties::_handleActivateDesktop)); + signalDeactiveDesktop().connect(sigc::mem_fun(*this, &DocumentProperties::_handleDeactivateDesktop)); + + _rum_deflt._changed_connection.block(); + _rum_deflt.getUnitMenu()->signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::onDocUnitChange)); +} + +void DocumentProperties::init() +{ + update(); + + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + repr->addListener (&_repr_events, this); + Inkscape::XML::Node *root = getDesktop()->getDocument()->getRoot()->getRepr(); + root->addListener (&_repr_events, this); + + show_all_children(); + _grids_button_remove.hide(); +} + +DocumentProperties::~DocumentProperties() +{ + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + repr->removeListenerByData (this); + Inkscape::XML::Node *root = getDesktop()->getDocument()->getRoot()->getRepr(); + root->removeListenerByData (this); + + 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; + if (dynamic_cast<Inkscape::UI::Widget::PageSizer*>(arr[i+1])) { + // only the PageSizer in Document Properties|Page should be stretched vertically + yoptions = Gtk::FILL|Gtk::EXPAND; + } + 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 DocumentProperties::build_page() +{ + _page_page->show(); + + Gtk::Label* label_gen = Gtk::manage (new Gtk::Label); + label_gen->set_markup (_("<b>General</b>")); + + Gtk::Label *label_for = Gtk::manage (new Gtk::Label); + label_for->set_markup (_("<b>Page Size</b>")); + + Gtk::Label* label_bkg = Gtk::manage (new Gtk::Label); + label_bkg->set_markup (_("<b>Background</b>")); + + Gtk::Label* label_bdr = Gtk::manage (new Gtk::Label); + label_bdr->set_markup (_("<b>Border</b>")); + + Gtk::Label* label_dsp = Gtk::manage (new Gtk::Label); + label_dsp->set_markup (_("<b>Display</b>")); + + _page_sizer.init(); + + _rcb_doc_props_left.set_border_width(4); + _rcb_doc_props_left.set_row_spacing(4); + _rcb_doc_props_left.set_column_spacing(4); + _rcb_doc_props_right.set_border_width(4); + _rcb_doc_props_right.set_row_spacing(4); + _rcb_doc_props_right.set_column_spacing(4); + + Gtk::Widget *const widget_array[] = + { + label_gen, nullptr, + nullptr, &_rum_deflt, + nullptr, nullptr, + label_for, nullptr, + nullptr, &_page_sizer, + nullptr, nullptr, + &_rcb_doc_props_left, &_rcb_doc_props_right, + }; + attach_all(_page_page->table(), widget_array, G_N_ELEMENTS(widget_array)); + + Gtk::Widget *const widget_array_left[] = + { + label_bkg, nullptr, + nullptr, &_rcb_checkerboard, + nullptr, &_rcp_bg, + label_dsp, nullptr, + nullptr, &_rcb_antialias, + }; + attach_all(_rcb_doc_props_left, widget_array_left, G_N_ELEMENTS(widget_array_left)); + + Gtk::Widget *const widget_array_right[] = + { + label_bdr, nullptr, + nullptr, &_rcb_canb, + nullptr, &_rcb_bord, + nullptr, &_rcb_shad, + nullptr, &_rcp_bord, + }; + attach_all(_rcb_doc_props_right, widget_array_right, G_N_ELEMENTS(widget_array_right)); + + std::list<Gtk::Widget*> _slaveList; + _slaveList.push_back(&_rcb_bord); + _slaveList.push_back(&_rcb_shad); + _slaveList.push_back(&_rcp_bord); + _rcb_canb.setSlaveWidgets(_slaveList); +} + +void DocumentProperties::build_guides() +{ + _page_guides->show(); + + Gtk::Label *label_gui = Gtk::manage (new Gtk::Label); + label_gui->set_markup (_("<b>Guides</b>")); + + _rum_deflt.set_margin_start(0); + _rcp_bg.set_margin_start(0); + _rcp_bord.set_margin_start(0); + _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); + + _create_guides_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::create_guides_around_page)); + _delete_guides_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::delete_all_guides)); +} + +void DocumentProperties::build_snap() +{ + _page_snap->show(); + + Gtk::Label *label_o = Gtk::manage (new Gtk::Label); + label_o->set_markup (_("<b>Snap to objects</b>")); + Gtk::Label *label_gr = Gtk::manage (new Gtk::Label); + label_gr->set_markup (_("<b>Snap to grids</b>")); + Gtk::Label *label_gu = Gtk::manage (new Gtk::Label); + label_gu->set_markup (_("<b>Snap to guides</b>")); + Gtk::Label *label_m = Gtk::manage (new Gtk::Label); + label_m->set_markup (_("<b>Miscellaneous</b>")); + + auto spacer = Gtk::manage(new Gtk::Label()); + + Gtk::Widget *const array[] = + { + label_o, nullptr, + nullptr, _rsu_sno._vbox, + &_rcb_snclp, spacer, + nullptr, &_rcb_snmsk, + nullptr, nullptr, + label_gr, nullptr, + nullptr, _rsu_sn._vbox, + nullptr, nullptr, + label_gu, nullptr, + nullptr, _rsu_gusn._vbox, + nullptr, nullptr, + label_m, nullptr, + nullptr, &_rcb_perp, + nullptr, &_rcb_tang + }; + attach_all(_page_snap->table(), array, G_N_ELEMENTS(array)); + } + +void DocumentProperties::create_guides_around_page() +{ + SPDesktop *dt = getDesktop(); + Verb *verb = Verb::get( SP_VERB_EDIT_GUIDES_AROUND_PAGE ); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(dt)); + if (action) { + sp_action_perform(action, nullptr); + } + } +} + +void DocumentProperties::delete_all_guides() +{ + SPDesktop *dt = getDesktop(); + Verb *verb = Verb::get( SP_VERB_EDIT_DELETE_ALL_GUIDES ); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(dt)); + if (action) { + sp_action_perform(action, nullptr); + } + } +} + +#if defined(HAVE_LIBLCMS2) +/// 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) + // TODO remove use of 'active' desktop + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop){ + g_warning("No active desktop"); + } else { + // Find the index of the currently-selected row in the color profiles combobox + Gtk::TreeModel::iterator iter = _AvailableProfilesList.get_active(); + + if (!iter) { + g_warning("No color profile available."); + 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 = SP_ACTIVE_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 = desktop->doc()->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(desktop->doc()->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(desktop->doc(), SP_VERB_EDIT_LINK_COLOR_PROFILE, _("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(); + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "iccprofile" ); + if (! current.empty()) { + _emb_profiles_observer.set((*(current.begin()))->parent); + } + + std::set<Inkscape::ColorProfile *, Inkscape::ColorProfile::pointerComparator> _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; + } + } + std::vector<SPObject *> current = SP_ACTIVE_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(SP_ACTIVE_DOCUMENT, SP_VERB_EDIT_REMOVE_COLOR_PROFILE, _("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>")); + + _link_btn.set_tooltip_text(_("Link Profile")); + docprops_style_button(_link_btn, INKSCAPE_ICON("list-add")); + + _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::HBox* spacer = Gtk::manage(new Gtk::HBox()); + 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); + + _link_btn.set_halign(Gtk::ALIGN_CENTER); + _link_btn.set_valign(Gtk::ALIGN_CENTER); + _link_btn.set_margin_start(2); + _link_btn.set_margin_end(2); + _page_cms->table().attach(_link_btn, 1, 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)); + + 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); + + _link_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::linkSelectedProfile)); + _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)); + + std::vector<SPObject *> current = SP_ACTIVE_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(); +} +#endif // defined(HAVE_LIBLCMS2) + +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::HBox* spacer_external = Gtk::manage(new Gtk::HBox()); + 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::HBox* spacer_embedded = Gtk::manage(new Gtk::HBox()); + 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)); + +#if defined(HAVE_LIBLCMS2) + + _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)); +#endif // defined(HAVE_LIBLCMS2) + +//TODO: review this observers code: + std::vector<SPObject *> current = SP_ACTIVE_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(); +} + +// TODO: This duplicates code in document-metadata.cpp +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(){ + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) { + g_warning("No active desktop"); + 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 = desktop->doc()->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(desktop->doc(), SP_VERB_EDIT_ADD_EXTERNAL_SCRIPT, _("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 = SP_ACTIVE_DESKTOP; + if (!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(){ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop){ + g_warning("No active desktop"); + } else { + Inkscape::XML::Document *xml_doc = desktop->doc()->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(desktop->doc(), SP_VERB_EDIT_ADD_EMBEDDED_SCRIPT, _("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; + } + } + + std::vector<SPObject *> current = SP_ACTIVE_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(SP_ACTIVE_DOCUMENT, SP_VERB_EDIT_REMOVE_EXTERNAL_SCRIPT, _("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; + } + } + + SPObject* obj = SP_ACTIVE_DOCUMENT->getObjectById(id); + if (obj) { + //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(SP_ACTIVE_DOCUMENT, SP_VERB_EDIT_REMOVE_EMBEDDED_SCRIPT, _("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; + } + } + + bool voidscript=true; + std::vector<SPObject *> current = SP_ACTIVE_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; + } + } + + Inkscape::XML::Document *xml_doc = SP_ACTIVE_DOCUMENT->getReprDoc(); + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "script" ); + for (auto obj : current) { + 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(xml_doc->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(SP_ACTIVE_DOCUMENT, SP_VERB_EDIT_EMBEDDED_SCRIPT, _("Edit embedded script")); + } + } + } +} + +void DocumentProperties::populate_script_lists(){ + _ExternalScriptsListStore->clear(); + _EmbeddedScriptsListStore->clear(); + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->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 (e.g. when a new grid was manually added in XML) +*/ +void DocumentProperties::update_gridspage() +{ + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->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. + + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->getNamedView(); + (void)nv; + + _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_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); + + update_gridspage(); +} + + + +/** + * Update dialog widgets from desktop. Also call updateWidget routines of the grids. + */ +void DocumentProperties::update() +{ + if (_wr.isUpdating()) return; + + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->getNamedView(); + + _wr.setUpdating (true); + set_sensitive (true); + + //-----------------------------------------------------------page page + _rcb_checkerboard.setActive (nv->pagecheckerboard); + _rcp_bg.setRgba32 (nv->pagecolor); + _rcb_canb.setActive (nv->showborder); + _rcb_bord.setActive (nv->borderlayer == SP_BORDER_LAYER_TOP); + _rcp_bord.setRgba32 (nv->bordercolor); + _rcb_shad.setActive (nv->showpageshadow); + + SPRoot *root = dt->getDocument()->getRoot(); + _rcb_antialias.set_xml_target(root->getRepr(), dt->getDocument()); + _rcb_antialias.setActive(root->style->shape_rendering.computed != SP_CSS_SHAPE_RENDERING_CRISPEDGES); + + if (nv->display_units) { + _rum_deflt.setUnit (nv->display_units->abbr); + } + + double doc_w = dt->getDocument()->getRoot()->width.value; + Glib::ustring doc_w_unit = unit_table.getUnit(dt->getDocument()->getRoot()->width.unit)->abbr; + if (doc_w_unit == "") { + doc_w_unit = "px"; + } else if (doc_w_unit == "%" && dt->getDocument()->getRoot()->viewBox_set) { + doc_w_unit = "px"; + doc_w = dt->getDocument()->getRoot()->viewBox.width(); + } + double doc_h = dt->getDocument()->getRoot()->height.value; + Glib::ustring doc_h_unit = unit_table.getUnit(dt->getDocument()->getRoot()->height.unit)->abbr; + if (doc_h_unit == "") { + doc_h_unit = "px"; + } else if (doc_h_unit == "%" && dt->getDocument()->getRoot()->viewBox_set) { + doc_h_unit = "px"; + doc_h = dt->getDocument()->getRoot()->viewBox.height(); + } + _page_sizer.setDim(Inkscape::Util::Quantity(doc_w, doc_w_unit), Inkscape::Util::Quantity(doc_h, doc_h_unit)); + _page_sizer.updateFitMarginsUI(nv->getRepr()); + _page_sizer.updateScaleUI(); + + //-----------------------------------------------------------guide page + + _rcb_sgui.setActive (nv->showguides); + _rcb_lgui.setActive (nv->lockguides); + _rcp_gui.setRgba32 (nv->guidecolor); + _rcp_hgui.setRgba32 (nv->guidehicolor); + + //-----------------------------------------------------------snap page + + _rsu_sno.setValue (nv->snap_manager.snapprefs.getObjectTolerance()); + _rsu_sn.setValue (nv->snap_manager.snapprefs.getGridTolerance()); + _rsu_gusn.setValue (nv->snap_manager.snapprefs.getGuideTolerance()); + _rcb_snclp.setActive (nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_CLIP)); + _rcb_snmsk.setActive (nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_MASK)); + _rcb_perp.setActive (nv->snap_manager.snapprefs.getSnapPerp()); + _rcb_tang.setActive (nv->snap_manager.snapprefs.getSnapTang()); + + //-----------------------------------------------------------grids page + + update_gridspage(); + + //------------------------------------------------Color Management page + +#if defined(HAVE_LIBLCMS2) + populate_linked_profiles_box(); + populate_available_profiles(); +#endif // defined(HAVE_LIBLCMS2) + + //-----------------------------------------------------------meta pages + /* update the RDF entities */ + for (auto & it : _rdflist) + it->update (SP_ACTIVE_DOCUMENT); + + _licensor.update (SP_ACTIVE_DOCUMENT); + + + _wr.setUpdating (false); +} + +// TODO: copied from fill-and-stroke.cpp factor out into new ui/widget file? +Gtk::HBox& +DocumentProperties::_createPageTabLabel(const Glib::ustring& label, const char *label_image) +{ + Gtk::HBox *_tab_label_box = Gtk::manage(new Gtk::HBox(false, 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_bg.closeWindow(); + _rcp_bord.closeWindow(); + _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*/ + for (auto & it : _rdflist) { + it->save_to_preferences (SP_ACTIVE_DOCUMENT); + } +} + + +void DocumentProperties::_handleDocumentReplaced(SPDesktop* desktop, SPDocument *document) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->addListener(&_repr_events, this); + Inkscape::XML::Node *root = document->getRoot()->getRepr(); + root->addListener(&_repr_events, this); + update(); +} + +void DocumentProperties::_handleActivateDesktop(SPDesktop *desktop) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->addListener(&_repr_events, this); + Inkscape::XML::Node *root = desktop->getDocument()->getRoot()->getRepr(); + root->addListener(&_repr_events, this); + update(); +} + +void DocumentProperties::_handleDeactivateDesktop(SPDesktop *desktop) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->removeListenerByData(this); + Inkscape::XML::Node *root = desktop->getDocument()->getRoot()->getRepr(); + root->removeListenerByData(this); +} + +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(); +} + + +/*######################################################################## +# BUTTON CLICK HANDLERS (callbacks) +########################################################################*/ + +void DocumentProperties::onNewGrid() +{ + SPDesktop *dt = getDesktop(); + Inkscape::XML::Node *repr = dt->getNamedView()->getRepr(); + SPDocument *doc = dt->getDocument(); + + Glib::ustring typestring = _grids_combo_gridtype.get_active_text(); + CanvasGrid::writeNewGridToRepr(repr, doc, CanvasGrid::getGridTypeFromName(typestring.c_str())); + + // toggle grid showing to ON: + dt->showGrids(true); +} + + +void DocumentProperties::onRemoveGrid() +{ + gint pagenum = _grids_notebook.get_current_page(); + if (pagenum == -1) // no pages + return; + + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->getNamedView(); + Inkscape::CanvasGrid * found_grid = nullptr; + if( pagenum < (gint)nv->grids.size()) + found_grid = nv->grids[pagenum]; + + 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(dt->getDocument(), SP_VERB_DIALOG_NAMEDVIEW, _("Remove grid")); + } +} + +/** Callback for document unit change. */ +/* 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::onDocUnitChange() +{ + SPDocument *doc = SP_ACTIVE_DOCUMENT; + // Don't execute when change is being undone + if (!DocumentUndo::getUndoSensitive(doc)) { + 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()); + + _page_sizer.updateScaleUI(); + + // 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 (tools_active(getDesktop()) == TOOLS_NODES) { + tools_switch(getDesktop(), TOOLS_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 + + doc->setModifiedSinceSave(); + + DocumentUndo::done(doc, SP_VERB_NONE, _("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..8b4e088 --- /dev/null +++ b/src/ui/dialog/document-properties.h @@ -0,0 +1,262 @@ +// 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 <sigc++/sigc++.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/liststore.h> +#include <gtkmm/notebook.h> +#include <gtkmm/buttonbox.h> +#include <gtkmm/textview.h> + +#include "ui/widget/page-sizer.h" +#include "ui/widget/registered-widget.h" +#include "ui/widget/registry.h" +#include "ui/widget/tolerance-slider.h" +#include "ui/widget/panel.h" +#include "ui/widget/licensor.h" + +#include "xml/helper-observer.h" + +namespace Inkscape { + namespace XML { + class Node; + } + namespace UI { + namespace Widget { + class EntityEntry; + class NotebookPage; + } + namespace Dialog { + +typedef std::list<UI::Widget::EntityEntry*> RDElist; + +class DocumentProperties : public UI::Widget::Panel { +public: + void update(); + static DocumentProperties &getInstance(); + static void destroy(); + + void update_gridspage(); + +protected: + void build_page(); + void build_grid(); + void build_guides(); + void build_snap(); + void build_gridspage(); + + void create_guides_around_page(); + void delete_all_guides(); +#if defined(HAVE_LIBLCMS2) + void build_cms(); +#endif // defined(HAVE_LIBLCMS2) + void build_scripting(); + void build_metadata(); + void init(); + + virtual void on_response (int); +#if defined(HAVE_LIBLCMS2) + 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); +#endif // defined(HAVE_LIBLCMS2) + + 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 _handleDocumentReplaced(SPDesktop* desktop, SPDocument *document); + void _handleActivateDesktop(SPDesktop *desktop); + void _handleDeactivateDesktop(SPDesktop *desktop); + + 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_snap; + 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::VBox _grids_vbox; + + UI::Widget::Registry _wr; + //--------------------------------------------------------------- + Gtk::Grid _rcb_doc_props_left; + Gtk::Grid _rcb_doc_props_right; + UI::Widget::RegisteredCheckButton _rcb_antialias; + UI::Widget::RegisteredCheckButton _rcb_checkerboard; + UI::Widget::RegisteredCheckButton _rcb_canb; + UI::Widget::RegisteredCheckButton _rcb_bord; + UI::Widget::RegisteredCheckButton _rcb_shad; + UI::Widget::RegisteredColorPicker _rcp_bg; + UI::Widget::RegisteredColorPicker _rcp_bord; + UI::Widget::RegisteredUnitMenu _rum_deflt; + UI::Widget::PageSizer _page_sizer; + //--------------------------------------------------------------- + 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::ToleranceSlider _rsu_sno; + UI::Widget::ToleranceSlider _rsu_sn; + UI::Widget::ToleranceSlider _rsu_gusn; + UI::Widget::RegisteredCheckButton _rcb_snclp; + UI::Widget::RegisteredCheckButton _rcb_snmsk; + UI::Widget::RegisteredCheckButton _rcb_perp; + UI::Widget::RegisteredCheckButton _rcb_tang; + //--------------------------------------------------------------- + Gtk::Button _link_btn; + 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::HBox _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::HBox _grids_space; + //--------------------------------------------------------------- + + RDElist _rdflist; + UI::Widget::Licensor _licensor; + + Gtk::HBox& _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 document unit change + void onDocUnitChange(); +}; + +} // 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.cpp b/src/ui/dialog/export.cpp new file mode 100644 index 0000000..7d68559 --- /dev/null +++ b/src/ui/dialog/export.cpp @@ -0,0 +1,1948 @@ +// 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> + * + * Copyright (C) 1999-2007, 2012 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 <png.h> + +#include <gtkmm/box.h> +#include <gtkmm/buttonbox.h> +#include <gtkmm/dialog.h> +#include <gtkmm/entry.h> +#include <gtkmm/grid.h> +#include <gtkmm/spinbutton.h> + +#include <glibmm/i18n.h> +#include <glibmm/miscutils.h> + +#include <gdl/gdl-dock-item.h> + +#include "document-undo.h" +#include "document.h" +#include "file.h" +#include "inkscape.h" +#include "preferences.h" +#include "selection-chemistry.h" +#include "verbs.h" + +// required to set status message after export +#include "desktop.h" +#include "message-stack.h" + +#include "helper/png-write.h" + +#include "io/resource.h" +#include "io/sys.h" + +#include "object/sp-namedview.h" +#include "object/sp-root.h" + +#include "ui/dialog-events.h" +#include "ui/interface.h" +#include "ui/widget/unit-menu.h" + +#include "extension/db.h" +#include "extension/output.h" + + +#ifdef _WIN32 +#include <windows.h> +#include <commdlg.h> +#include <gdk/gdkwin32.h> +#include <glibmm/fileutils.h> +#endif + +#define SP_EXPORT_MIN_SIZE 1.0 + +#define DPI_BASE Inkscape::Util::Quantity::convert(1, "in", "px") + +#define EXPORT_COORD_PRECISION 3 + +#include "export.h" + +using Inkscape::Util::unit_table; + +namespace { + +class MessageCleaner +{ +public: + MessageCleaner(Inkscape::MessageId messageId, SPDesktop *desktop) : + _desktop(desktop), + _messageId(messageId) + { + } + + ~MessageCleaner() + { + if (_messageId && _desktop) { + _desktop->messageStack()->cancel(_messageId); + } + } + +private: + MessageCleaner(MessageCleaner const &other) = delete; + MessageCleaner &operator=(MessageCleaner const &other) = delete; + + SPDesktop *_desktop; + Inkscape::MessageId _messageId; +}; + +} // namespace + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** A list of strings that is used both in the preferences, and in the + data fields to describe the various values of \c selection_type. */ +static const char * selection_names[SELECTION_NUMBER_OF] = { + "page", "drawing", "selection", "custom" +}; + +/** The names on the buttons for the various selection types. */ +static const char * selection_labels[SELECTION_NUMBER_OF] = { + N_("_Page"), N_("_Drawing"), N_("_Selection"), N_("_Custom") +}; + +Export::Export () : + UI::Widget::Panel("/dialogs/export/", SP_VERB_DIALOG_EXPORT), + current_key(SELECTION_PAGE), + original_name(), + doc_export_name(), + filename_modified(false), + was_empty(true), + update(false), + togglebox(true, 0), + area_box(false, 3), + singleexport_box(false, 0), + size_box(false, 3), + file_box(false, 3), + unitbox(false, 0), + unit_selector(), + units_label(_("Units:")), + filename_box(false, 5), + browse_label(_("_Export As..."), true), + browse_image(), + batch_box(false, 5), + batch_export(_("B_atch export all selected objects")), + interlacing(_("Use interlacing")), + bitdepth_label(_("Bit depth")), + bitdepth_cb(), + zlib_label(_("Compression")), + zlib_compression(), + pHYs_label(_("pHYs dpi")), + pHYs_sb(pHYs_adj, 1.0, 2), + antialiasing_label(_("Antialiasing")), + antialiasing_cb(), + hide_box(false, 3), + hide_export(_("Hide all except selected")), + closeWhenDone(_("Close when complete")), + button_box(false, 3), + _prog(), + prog_dlg(nullptr), + interrupted(false), + prefs(nullptr), + desktop(nullptr), + deskTrack(), + selectChangedConn(), + subselChangedConn(), + selectModifiedConn() +{ + batch_export.set_use_underline(); + batch_export.set_tooltip_text(_("Export each selected object into its own PNG file, using export hints if any (caution, overwrites without asking!)")); + hide_export.set_use_underline(); + hide_export.set_tooltip_text(_("In the exported image, hide all objects except those that are selected")); + interlacing.set_use_underline(); + interlacing.set_tooltip_text(_("Enables ADAM7 interlacing for PNG output. This results in slightly heavier images, but big images will look better sooner when loading the file")); + closeWhenDone.set_use_underline(); + closeWhenDone.set_tooltip_text(_("Once the export completes, close this dialog")); + prefs = Inkscape::Preferences::get(); + + singleexport_box.set_border_width(0); + + /* Export area frame */ + { + Gtk::Label* lbl = new Gtk::Label(_("<b>Export area</b>"), Gtk::ALIGN_START); + lbl->set_use_markup(true); + area_box.pack_start(*lbl); + + /* Units box */ + /* gets added to the vbox later, but the unit selector is needed + earlier than that */ + unit_selector.setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + unit_selector.setUnit(desktop->getNamedView()->display_units->abbr); + } + unitChangedConn = unit_selector.signal_changed().connect(sigc::mem_fun(*this, &Export::onUnitChanged)); + unitbox.pack_end(unit_selector, false, false, 0); + unitbox.pack_end(units_label, false, false, 3); + + for (int i = 0; i < SELECTION_NUMBER_OF; i++) { + selectiontype_buttons[i] = new Gtk::RadioButton(_(selection_labels[i]), true); + if (i > 0) { + Gtk::RadioButton::Group group = selectiontype_buttons[0]->get_group(); + selectiontype_buttons[i]->set_group(group); + } + selectiontype_buttons[i]->set_mode(false); + togglebox.pack_start(*selectiontype_buttons[i], false, true, 0); + selectiontype_buttons[i]->signal_clicked().connect(sigc::mem_fun(*this, &Export::onAreaToggled)); + } + + auto t = new Gtk::Grid(); + t->set_row_spacing(4); + t->set_column_spacing(4); + + x0_adj = createSpinbutton ( "x0", 0.0, -1000000.0, 1000000.0, 0.1, 1.0, + t, 0, 0, _("_x0:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaX0Change); + + x1_adj = createSpinbutton ( "x1", 0.0, -1000000.0, 1000000.0, 0.1, 1.0, + t, 0, 1, _("x_1:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaX1Change); + + width_adj = createSpinbutton ( "width", 0.0, 0.0, PNG_UINT_31_MAX, 0.1, 1.0, + t, 0, 2, _("Wid_th:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaWidthChange); + + y0_adj = createSpinbutton ( "y0", 0.0, -1000000.0, 1000000.0, 0.1, 1.0, + t, 2, 0, _("_y0:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaY0Change); + + y1_adj = createSpinbutton ( "y1", 0.0, -1000000.0, 1000000.0, 0.1, 1.0, + t, 2, 1, _("y_1:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaY1Change); + + height_adj = createSpinbutton ( "height", 0.0, 0.0, PNG_UINT_31_MAX, 0.1, 1.0, + t, 2, 2, _("Hei_ght:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaHeightChange); + + area_box.pack_start(togglebox, false, false, 3); + area_box.pack_start(*t, false, false, 0); + area_box.pack_start(unitbox, false, false, 0); + + area_box.set_border_width(3); + singleexport_box.pack_start(area_box, false, false, 0); + + } // end of area box + + /* Bitmap size frame */ + { + size_box.set_border_width(3); + bm_label = new Gtk::Label(_("<b>Image size</b>"), Gtk::ALIGN_START); + bm_label->set_use_markup(true); + size_box.pack_start(*bm_label, false, false, 0); + + auto t = new Gtk::Grid(); + t->set_row_spacing(4); + t->set_column_spacing(4); + + size_box.pack_start(*t); + + bmwidth_adj = createSpinbutton ( "bmwidth", 16.0, 1.0, 1000000.0, 1.0, 10.0, + t, 0, 0, + _("_Width:"), _("pixels at"), 0, 1, + &Export::onBitmapWidthChange); + + xdpi_adj = createSpinbutton ( "xdpi", + prefs->getDouble("/dialogs/export/defaultxdpi/value", DPI_BASE), + 0.01, 100000.0, 0.1, 1.0, t, 3, 0, + "", _("dp_i"), 2, 1, + &Export::onExportXdpiChange); + + bmheight_adj = createSpinbutton ( "bmheight", 16.0, 1.0, 1000000.0, 1.0, 10.0, + t, 0, 1, + _("_Height:"), _("pixels at"), 0, 1, + &Export::onBitmapHeightChange); + + /** TODO + * There's no way to set ydpi currently, so we use the defaultxdpi value here, too... + */ + ydpi_adj = createSpinbutton ( "ydpi", prefs->getDouble("/dialogs/export/defaultxdpi/value", DPI_BASE), + 0.01, 100000.0, 0.1, 1.0, t, 3, 1, + "", _("dpi"), 2, 0, nullptr ); + + singleexport_box.pack_start(size_box, Gtk::PACK_SHRINK); + } + + /* File entry */ + { + file_box.set_border_width(3); + flabel = new Gtk::Label(_("<b>_Filename</b>"), Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true); + flabel->set_use_markup(true); + file_box.pack_start(*flabel, false, false, 0); + + set_default_filename(); + + filename_box.pack_start (filename_entry, true, true, 0); + + Gtk::HBox* browser_im_label = new Gtk::HBox(false, 3); + browse_image.set_from_icon_name("folder", Gtk::ICON_SIZE_BUTTON); + browser_im_label->pack_start(browse_image); + browser_im_label->pack_start(browse_label); + browse_button.add(*browser_im_label); + filename_box.pack_end (browse_button, false, false); + + file_box.add(filename_box); + + original_name = filename_entry.get_text(); + + // focus is in the filename initially: + filename_entry.grab_focus(); + + // mnemonic in frame label moves focus to filename: + flabel->set_mnemonic_widget(filename_entry); + + singleexport_box.pack_start(file_box, Gtk::PACK_SHRINK); + } + + batch_export.set_sensitive(true); + batch_box.pack_start(batch_export, false, false, 3); + + hide_export.set_sensitive(true); + hide_export.set_active (prefs->getBool("/dialogs/export/hideexceptselected/value", false)); + hide_box.pack_start(hide_export, false, false, 3); + + + /* Export Button row */ + export_button.set_label(_("_Export")); + export_button.set_use_underline(); + export_button.set_tooltip_text (_("Export the bitmap file with these settings")); + + button_box.set_border_width(3); + button_box.pack_start(closeWhenDone, true, true, 0); + button_box.pack_end(export_button, false, false, 0); + + /*Advanced*/ + Gtk::Label *label_advanced = Gtk::manage(new Gtk::Label(_("Advanced"),true)); + expander.set_label_widget(*label_advanced); + expander.set_vexpand(false); + const char* const modes_list[]={"Gray_1", "Gray_2","Gray_4","Gray_8","Gray_16","RGB_8","RGB_16","GrayAlpha_8","GrayAlpha_16","RGBA_8","RGBA_16"}; + for(auto i : modes_list) + bitdepth_cb.append(i); + bitdepth_cb.set_active_text("RGBA_8"); + bitdepth_cb.set_hexpand(); + const char* const zlist[]={"Z_NO_COMPRESSION","Z_BEST_SPEED","2","3","4","5","Z_DEFAULT_COMPRESSION","7","8","Z_BEST_COMPRESSION"}; + for(auto i : zlist) + zlib_compression.append(i); + zlib_compression.set_active_text("Z_DEFAULT_COMPRESSION"); + pHYs_adj = Gtk::Adjustment::create(0, 0, 100000, 0.1, 1.0, 0); + pHYs_sb.set_adjustment(pHYs_adj); + pHYs_sb.set_width_chars(7); + pHYs_sb.set_tooltip_text( _("Will force-set the physical dpi for the png file. Set this to 72 if you're planning to work on your png with Photoshop") ); + zlib_compression.set_hexpand(); + const char* const antialising_list[] = {"CAIRO_ANTIALIAS_NONE","CAIRO_ANTIALIAS_FAST","CAIRO_ANTIALIAS_GOOD (default)","CAIRO_ANTIALIAS_BEST"}; + for(auto i : antialising_list) + antialiasing_cb.append(i); + antialiasing_cb.set_active_text(antialising_list[2]); + bitdepth_label.set_halign(Gtk::ALIGN_START); + zlib_label.set_halign(Gtk::ALIGN_START); + pHYs_label.set_halign(Gtk::ALIGN_START); + antialiasing_label.set_halign(Gtk::ALIGN_START); + auto table = new Gtk::Grid(); + expander.add(*table); + // gtk_container_add(GTK_CONTAINER(expander.gobj()), (GtkWidget*)(table->gobj())); + table->set_border_width(4); + table->attach(interlacing,0,0,1,1); + table->attach(bitdepth_label,0,1,1,1); + table->attach(bitdepth_cb,1,1,1,1); + table->attach(zlib_label,0,2,1,1); + table->attach(zlib_compression,1,2,1,1); + table->attach(pHYs_label,0,3,1,1); + table->attach(pHYs_sb,1,3,1,1); + table->attach(antialiasing_label,0,4,1,1); + table->attach(antialiasing_cb,1,4,1,1); + table->show(); + + /* Main dialog */ + Gtk::Box *contents = _getContents(); + contents->set_spacing(0); + contents->pack_start(singleexport_box, Gtk::PACK_SHRINK); + contents->pack_start(batch_box, Gtk::PACK_SHRINK); + contents->pack_start(hide_box, Gtk::PACK_SHRINK); + contents->pack_start(expander, Gtk::PACK_SHRINK); + contents->pack_end(button_box, Gtk::PACK_SHRINK); + contents->pack_end(_prog, Gtk::PACK_SHRINK); + + /* Signal handlers */ + filename_entry.signal_changed().connect( sigc::mem_fun(*this, &Export::onFilenameModified) ); + // pressing enter in the filename field is the same as clicking export: + filename_entry.signal_activate().connect(sigc::mem_fun(*this, &Export::onExport) ); + browse_button.signal_clicked().connect(sigc::mem_fun(*this, &Export::onBrowse)); + batch_export.signal_clicked().connect(sigc::mem_fun(*this, &Export::onBatchClicked)); + export_button.signal_clicked().connect(sigc::mem_fun(*this, &Export::onExport)); + hide_export.signal_clicked().connect(sigc::mem_fun(*this, &Export::onHideExceptSelected)); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &Export::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); + setExporting(false); + + findDefaultSelection(); + onAreaToggled(); +} + +Export::~Export () +{ + was_empty = TRUE; + + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +void Export::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void Export::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (this->desktop) { + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &Export::onSelectionChanged))); + subselChangedConn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &Export::onSelectionChanged))); + + //// Must check flags, so can't call widget_setup() directly. + selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &Export::onSelectionModified))); + } + } +} + +/* + * set the default filename to be that of the current path + document + * with .png extension + * + * One thing to notice here is that this filename may get + * overwritten, but it won't happen here. The filename gets + * written into the text field, but then the button to select + * the area gets set. In that code the filename can be changed + * if there are some with presidence in the document. So, while + * this code sets the name first, it may not be the one users + * really see. + */ +void Export::set_default_filename () { + + if ( SP_ACTIVE_DOCUMENT && SP_ACTIVE_DOCUMENT->getDocumentURI() ) + { + SPDocument * doc = SP_ACTIVE_DOCUMENT; + const gchar *uri = doc->getDocumentURI(); + auto &&text_extension = get_file_save_extension(Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS); + Inkscape::Extension::Output * oextension = nullptr; + + if (!text_extension.empty()) { + oextension = dynamic_cast<Inkscape::Extension::Output *>(Inkscape::Extension::db.get(text_extension.c_str())); + } + + if (oextension != nullptr) { + gchar * old_extension = oextension->get_extension(); + if (g_str_has_suffix(uri, old_extension)) { + gchar * uri_copy; + gchar * extension_point; + gchar * final_name; + + uri_copy = g_strdup(uri); + extension_point = g_strrstr(uri_copy, old_extension); + extension_point[0] = '\0'; + + final_name = g_strconcat(uri_copy, ".png", NULL); + filename_entry.set_text(final_name); + filename_entry.set_position(strlen(final_name)); + + g_free(final_name); + g_free(uri_copy); + } + } else { + gchar *name = g_strconcat(uri, ".png", NULL); + filename_entry.set_text(name); + filename_entry.set_position(strlen(name)); + + g_free(name); + } + + doc_export_name = filename_entry.get_text(); + } + else if ( SP_ACTIVE_DOCUMENT ) + { + Glib::ustring filename = create_filepath_from_id (_("bitmap"), filename_entry.get_text()); + filename_entry.set_text(filename); + filename_entry.set_position(filename.length()); + + doc_export_name = filename_entry.get_text(); + } +} + +Glib::RefPtr<Gtk::Adjustment> Export::createSpinbutton( gchar const * /*key*/, float val, float min, float max, + float step, float page, + Gtk::Grid *t, int x, int y, + const Glib::ustring& ll, const Glib::ustring& lr, + int digits, unsigned int sensitive, + void (Export::*cb)() ) +{ + auto adj = Gtk::Adjustment::create(val, min, max, step, page, 0); + + int pos = 0; + Gtk::Label *l = nullptr; + + if (!ll.empty()) { + l = new Gtk::Label(ll,true); + l->set_halign(Gtk::ALIGN_END); + l->set_valign(Gtk::ALIGN_CENTER); + l->set_hexpand(); + t->attach(*l, x + pos, y, 1, 1); + l->set_sensitive(sensitive); + pos++; + } + + auto sb = new Gtk::SpinButton(adj, 1.0, digits); + sb->set_hexpand(); + t->attach(*sb, x + pos, y, 1, 1); + + sb->set_width_chars(7); + sb->set_sensitive (sensitive); + pos++; + + if (l) { + l->set_mnemonic_widget(*sb); + } + + if (!lr.empty()) { + l = new Gtk::Label(lr,true); + l->set_halign(Gtk::ALIGN_START); + l->set_valign(Gtk::ALIGN_CENTER); + l->set_hexpand(); + t->attach(*l, x + pos, y, 1, 1); + l->set_sensitive (sensitive); + pos++; + l->set_mnemonic_widget (*sb); + } + + if (cb) { + adj->signal_value_changed().connect( sigc::mem_fun(*this, cb) ); + } + + return adj; +} // end of createSpinbutton() + + +Glib::ustring Export::create_filepath_from_id (Glib::ustring id, const Glib::ustring &file_entry_text) +{ + if (id.empty()) + { /* This should never happen */ + id = "bitmap"; + } + + Glib::ustring directory; + + if (!file_entry_text.empty()) { + directory = Glib::path_get_dirname(file_entry_text); + } + + if (directory.empty()) { + /* Grab document directory */ + const gchar* docURI = SP_ACTIVE_DOCUMENT->getDocumentURI(); + if (docURI) { + directory = Glib::path_get_dirname(docURI); + } + } + + if (directory.empty()) { + directory = Inkscape::IO::Resource::homedir_path(nullptr); + } + + Glib::ustring filename = Glib::build_filename(directory, id+".png"); + return filename; +} + +void Export::onBatchClicked () +{ + if (batch_export.get_active()) { + singleexport_box.set_sensitive(false); + } else { + singleexport_box.set_sensitive(true); + } +} + +void Export::updateCheckbuttons () +{ + gint num = (gint) boost::distance(SP_ACTIVE_DESKTOP->getSelection()->items()); + if (num >= 2) { + batch_export.set_sensitive(true); + batch_export.set_label(g_strdup_printf (ngettext("B_atch export %d selected object","B_atch export %d selected objects",num), num)); + } else { + batch_export.set_active (false); + batch_export.set_sensitive(false); + } + + //hide_export.set_sensitive (num > 0); +} + +inline void Export::findDefaultSelection() +{ + selection_type key = SELECTION_NUMBER_OF; + + if ((SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false) { + key = SELECTION_SELECTION; + } + + /* Try using the preferences */ + if (key == SELECTION_NUMBER_OF) { + + int i = SELECTION_NUMBER_OF; + + Glib::ustring what = prefs->getString("/dialogs/export/exportarea/value"); + + if (!what.empty()) { + for (i = 0; i < SELECTION_NUMBER_OF; i++) { + if (what == selection_names[i]) { + break; + } + } + } + + key = (selection_type)i; + } + + if (key == SELECTION_NUMBER_OF) { + key = SELECTION_SELECTION; + } + + current_key = key; + selectiontype_buttons[current_key]->set_active(true); + updateCheckbuttons (); +} + + +/** + * If selection changed or a different document activated, we must + * recalculate any chosen areas. + */ +void Export::onSelectionChanged() +{ + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + + if ((current_key == SELECTION_DRAWING || current_key == SELECTION_PAGE) && + (SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false && + was_empty) { + current_key = SELECTION_SELECTION; + selectiontype_buttons[current_key]->set_active(true); + } + was_empty = (SP_ACTIVE_DESKTOP->getSelection())->isEmpty(); + + if ( selection && + SELECTION_CUSTOM != current_key) { + onAreaToggled(); + } + + updateCheckbuttons (); +} + +void Export::onSelectionModified ( guint /*flags*/ ) +{ + Inkscape::Selection * Sel; + switch (current_key) { + case SELECTION_DRAWING: + if ( SP_ACTIVE_DESKTOP ) { + SPDocument *doc; + doc = SP_ACTIVE_DESKTOP->getDocument(); + Geom::OptRect bbox = doc->getRoot()->desktopVisualBounds(); + if (bbox) { + setArea ( bbox->left(), + bbox->top(), + bbox->right(), + bbox->bottom()); + } + } + break; + case SELECTION_SELECTION: + Sel = SP_ACTIVE_DESKTOP->getSelection(); + if (Sel->isEmpty() == false) { + Geom::OptRect bbox = Sel->visualBounds(); + if (bbox) + { + setArea ( bbox->left(), + bbox->top(), + bbox->right(), + bbox->bottom()); + } + } + break; + default: + /* Do nothing for page or for custom */ + break; + } + + return; +} + +/// Called when one of the selection buttons was toggled. +void Export::onAreaToggled () +{ + if (update) { + return; + } + + /* Find which button is active */ + selection_type key = current_key; + for (int i = 0; i < SELECTION_NUMBER_OF; i++) { + if (selectiontype_buttons[i]->get_active()) { + key = (selection_type)i; + } + } + + if ( SP_ACTIVE_DESKTOP ) + { + SPDocument *doc; + Geom::OptRect bbox; + bbox = Geom::Rect(Geom::Point(0.0, 0.0),Geom::Point(0.0, 0.0)); + doc = SP_ACTIVE_DESKTOP->getDocument(); + + /* Notice how the switch is used to 'fall through' here to get + various backups. If you modify this without noticing you'll + probably screw something up. */ + switch (key) { + case SELECTION_SELECTION: + if ((SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false) + { + bbox = SP_ACTIVE_DESKTOP->getSelection()->visualBounds(); + /* Only if there is a selection that we can set + do we break, otherwise we fall through to the + drawing */ + // std::cout << "Using selection: SELECTION" << std::endl; + key = SELECTION_SELECTION; + break; + } + case SELECTION_DRAWING: + /** \todo + * This returns wrong values if the document has a viewBox. + */ + bbox = doc->getRoot()->desktopVisualBounds(); + /* If the drawing is valid, then we'll use it and break + otherwise we drop through to the page settings */ + if (bbox) { + // std::cout << "Using selection: DRAWING" << std::endl; + key = SELECTION_DRAWING; + break; + } + case SELECTION_PAGE: + bbox = Geom::Rect(Geom::Point(0.0, 0.0), + Geom::Point(doc->getWidth().value("px"), doc->getHeight().value("px"))); + + // std::cout << "Using selection: PAGE" << std::endl; + key = SELECTION_PAGE; + break; + case SELECTION_CUSTOM: + default: + break; + } // switch + + current_key = key; + + // remember area setting + prefs->setString("/dialogs/export/exportarea/value", selection_names[current_key]); + + if ( key != SELECTION_CUSTOM && bbox ) { + setArea ( bbox->min()[Geom::X], + bbox->min()[Geom::Y], + bbox->max()[Geom::X], + bbox->max()[Geom::Y]); + } + + } // end of if ( SP_ACTIVE_DESKTOP ) + + if (SP_ACTIVE_DESKTOP && !filename_modified) { + + Glib::ustring filename; + float xdpi = 0.0, ydpi = 0.0; + + switch (key) { + case SELECTION_PAGE: + case SELECTION_DRAWING: { + SPDocument * doc = SP_ACTIVE_DOCUMENT; + sp_document_get_export_hints (doc, filename, &xdpi, &ydpi); + + if (filename.empty()) { + if (!doc_export_name.empty()) { + filename = doc_export_name; + } + } + break; + } + case SELECTION_SELECTION: + if ((SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false) { + + SP_ACTIVE_DESKTOP->getSelection()->getExportHints(filename, &xdpi, &ydpi); + + /* If we still don't have a filename -- let's build + one that's nice */ + if (filename.empty()) { + const gchar * id = "object"; + auto reprlst = SP_ACTIVE_DESKTOP->getSelection()->xmlNodes(); + for(auto i=reprlst.begin(); reprlst.end() != i; ++i) { + Inkscape::XML::Node * repr = *i; + if (repr->attribute("id")) { + id = repr->attribute("id"); + break; + } + } + + filename = create_filepath_from_id (id, filename_entry.get_text()); + } + } + break; + case SELECTION_CUSTOM: + default: + break; + } + + if (!filename.empty()) { + original_name = filename; + filename_entry.set_text(filename); + filename_entry.set_position(filename.length()); + } + + if (xdpi != 0.0) { + setValue(xdpi_adj, xdpi); + } + + /* These can't be separate, and setting x sets y, so for + now setting this is disabled. Hopefully it won't be in + the future */ + if (FALSE && ydpi != 0.0) { + setValue(ydpi_adj, ydpi); + } + } + + return; +} // end of sp_export_area_toggled() + +/// Called when dialog is deleted +bool Export::onProgressDelete (GdkEventAny * /*event*/) +{ + interrupted = true; + return TRUE; +} // end of sp_export_progress_delete() + + +/// Called when progress is cancelled +void Export::onProgressCancel () +{ + interrupted = true; +} // end of sp_export_progress_cancel() + + +/// Called for every progress iteration +unsigned int Export::onProgressCallback(float value, void *dlg) +{ + Gtk::Dialog *dlg2 = reinterpret_cast<Gtk::Dialog*>(dlg); + + Export *self = reinterpret_cast<Export *>(dlg2->get_data("exportPanel")); + if (self->interrupted) + return FALSE; + + gint current = GPOINTER_TO_INT(dlg2->get_data("current")); + gint total = GPOINTER_TO_INT(dlg2->get_data("total")); + if (total > 0) { + double completed = current; + completed /= static_cast<double>(total); + + value = completed + (value / static_cast<double>(total)); + } + + Gtk::ProgressBar *prg = reinterpret_cast<Gtk::ProgressBar *>(dlg2->get_data("progress")); + prg->set_fraction(value); + + if (self) { + self->_prog.set_fraction(value); + } + + int evtcount = 0; + while ((evtcount < 16) && gdk_events_pending()) { + gtk_main_iteration_do(FALSE); + evtcount += 1; + } + + gtk_main_iteration_do(FALSE); + return TRUE; +} // end of sp_export_progress_callback() + +void Export::setExporting(bool exporting, Glib::ustring const &text) +{ + if (exporting) { + _prog.set_text(text); + _prog.set_fraction(0.0); + _prog.set_sensitive(true); + + export_button.set_sensitive(false); + } else { + _prog.set_text(""); + _prog.set_fraction(0.0); + _prog.set_sensitive(false); + + export_button.set_sensitive(true); + } +} + +Gtk::Dialog * Export::create_progress_dialog (Glib::ustring progress_text) { + Gtk::Dialog *dlg = new Gtk::Dialog(_("Export in progress"), TRUE); + dlg->set_transient_for( *(INKSCAPE.active_desktop()->getToplevel()) ); + + Gtk::ProgressBar *prg = new Gtk::ProgressBar (); + prg->set_text(progress_text); + dlg->set_data ("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, &Export::onProgressCancel) ); + dlg->signal_delete_event().connect( sigc::mem_fun(*this, &Export::onProgressDelete) ); + + dlg->show_all (); + return dlg; +} + +// FIXME: Some lib function should be available to do this ... +Glib::ustring Export::filename_add_extension (Glib::ustring filename, Glib::ustring extension) +{ + auto pos = int(filename.size()) - int(extension.size()); + if (pos > 0 && filename[pos - 1] == '.' && filename.substr(pos).lowercase() == extension.lowercase()) { + return filename; + } + + return filename + "." + extension; +} + +Glib::ustring Export::absolutize_path_from_document_location (SPDocument *doc, const Glib::ustring &filename) +{ + Glib::ustring path; + //Make relative paths go from the document location, if possible: + if (!Glib::path_is_absolute(filename) && doc->getDocumentURI()) { + Glib::ustring dirname = Glib::path_get_dirname(doc->getDocumentURI()); + if (!dirname.empty()) { + path = Glib::build_filename(dirname, filename); + } + } + if (path.empty()) { + path = filename; + } + return path; +} + +// Called when unit is changed +void Export::onUnitChanged() +{ + onAreaToggled(); +} + +void Export::onHideExceptSelected () +{ + prefs->setBool("/dialogs/export/hideexceptselected/value", hide_export.get_active()); +} + +/// Called when export button is clicked +void Export::onExport () +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) return; + + SPNamedView *nv = desktop->getNamedView(); + SPDocument *doc = desktop->getDocument(); + + bool exportSuccessful = false; + + bool hide = hide_export.get_active (); + + // Advanced parameters + bool do_interlace = (interlacing.get_active()); + float pHYs = 0; + int zlib = zlib_compression.get_active_row_number() ; + const char* const modes_list[]={"Gray_1", "Gray_2","Gray_4","Gray_8","Gray_16","RGB_8","RGB_16","GrayAlpha_8","GrayAlpha_16","RGBA_8","RGBA_16"}; + int colortypes[] = {0,0,0,0,0,2,2,4,4,6,6}; //keep in sync with modes_list in Export constructor. values are from libpng doc. + int bitdepths[] = {1,2,4,8,16,8,16,8,16,8,16}; + int color_type = colortypes[bitdepth_cb.get_active_row_number()] ; + int bit_depth = bitdepths[bitdepth_cb.get_active_row_number()] ; + int antialiasing = antialiasing_cb.get_active_row_number(); + + + if (batch_export.get_active ()) { + // Batch export of selected objects + + gint num = (gint) boost::distance(desktop->getSelection()->items()); + gint n = 0; + + if (num < 1) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("No items selected.")); + return; + } + + prog_dlg = create_progress_dialog(Glib::ustring::compose(_("Exporting %1 files"), num)); + prog_dlg->set_data("exportPanel", this); + setExporting(true, Glib::ustring::compose(_("Exporting %1 files"), num)); + + gint export_count = 0; + + auto itemlist= desktop->getSelection()->items(); + for(auto i = itemlist.begin();i!=itemlist.end() && !interrupted ;++i){ + SPItem *item = *i; + + prog_dlg->set_data("current", GINT_TO_POINTER(n)); + prog_dlg->set_data("total", GINT_TO_POINTER(num)); + onProgressCallback(0.0, prog_dlg); + + // retrieve export filename hint + const gchar *filename = item->getRepr()->attribute("inkscape:export-filename"); + Glib::ustring path; + if (!filename) { + Glib::ustring tmp; + path = create_filepath_from_id(item->getId(), tmp); + } else { + path = absolutize_path_from_document_location(doc, filename); + } + + // retrieve export dpi hints + const gchar *dpi_hint = item->getRepr()->attribute("inkscape:export-xdpi"); // only xdpi, ydpi is always the same now + gdouble dpi = 0.0; + if (dpi_hint) { + dpi = atof(dpi_hint); + } + if (dpi == 0.0) { + dpi = getValue(xdpi_adj); + } + pHYs = (pHYs_adj->get_value() > 0.01) ? pHYs_adj->get_value() : dpi; + + Geom::OptRect area = item->documentVisualBounds(); + if (area) { + gint width = (gint) (area->width() * dpi / DPI_BASE + 0.5); + gint height = (gint) (area->height() * dpi / DPI_BASE + 0.5); + + if (width > 1 && height > 1) { + // Do export + gchar * safeFile = Inkscape::IO::sanitizeString(path.c_str()); + MessageCleaner msgCleanup(desktop->messageStack()->pushF(Inkscape::IMMEDIATE_MESSAGE, + _("Exporting file <b>%s</b>..."), safeFile), desktop); + MessageCleaner msgFlashCleanup(desktop->messageStack()->flashF(Inkscape::IMMEDIATE_MESSAGE, + _("Exporting file <b>%s</b>..."), safeFile), desktop); + std::vector<SPItem*> x; + std::vector<SPItem*> selected(desktop->getSelection()->items().begin(), desktop->getSelection()->items().end()); + if (!sp_export_png_file (doc, path.c_str(), + *area, width, height, pHYs, pHYs, + nv->pagecolor, + onProgressCallback, (void*)prog_dlg, + TRUE, // overwrite without asking + hide ? selected : x, + do_interlace, color_type, bit_depth, zlib, antialiasing + )) { + gchar * error = g_strdup_printf(_("Could not export to filename %s.\n"), safeFile); + + desktop->messageStack()->flashF(Inkscape::ERROR_MESSAGE, + _("Could not export to filename <b>%s</b>."), safeFile); + + sp_ui_error_dialog(error); + g_free(error); + } else { + ++export_count; // one more item exported successfully + } + g_free(safeFile); + } + } + + n++; + } + + desktop->messageStack()->flashF(Inkscape::INFORMATION_MESSAGE, + _("Successfully exported <b>%d</b> files from <b>%d</b> selected items."), export_count, num); + + setExporting(false); + delete prog_dlg; + prog_dlg = nullptr; + interrupted = false; + exportSuccessful = (export_count > 0); + } else { + Glib::ustring filename = filename_entry.get_text(); + + 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; + } + + float const x0 = getValuePx(x0_adj); + float const y0 = getValuePx(y0_adj); + float const x1 = getValuePx(x1_adj); + float const y1 = getValuePx(y1_adj); + float const xdpi = getValue(xdpi_adj); + float const ydpi = getValue(ydpi_adj); + pHYs = (pHYs_adj->get_value() > 0.01) ? pHYs_adj->get_value() : xdpi; + unsigned long int const width = int(getValue(bmwidth_adj) + 0.5); + unsigned long int const height = int(getValue(bmheight_adj) + 0.5); + + if (!((x1 > x0) && (y1 > y0) && (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; + } + + // make sure that .png is the extension of the file: + Glib::ustring const filename_ext = filename_add_extension(filename, "png"); + filename_entry.set_text(filename_ext); + filename_entry.set_position(filename_ext.length()); + Glib::ustring path = absolutize_path_from_document_location(doc, filename_ext); + + 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)) ) + { + gchar *safeDir = Inkscape::IO::sanitizeString(dirname.c_str()); + gchar *error = g_strdup_printf(_("Directory %s does not exist or is not a directory.\n"), + safeDir); + + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error); + sp_ui_error_dialog(error); + + g_free(safeDir); + g_free(error); + return; + } + + Glib::ustring fn = path_get_basename (path); + + /* 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)"), fn, width, height)); + prog_dlg->set_data("exportPanel", this); + setExporting(true, Glib::ustring::compose(_("Exporting %1 (%2 x %3)"), fn, width, height)); + + prog_dlg->set_data("current", GINT_TO_POINTER(0)); + prog_dlg->set_data("total", GINT_TO_POINTER(0)); + + auto area = Geom::Rect(Geom::Point(x0, y0), Geom::Point(x1, y1)) * desktop->dt2doc(); + + /* Do export */ + std::vector<SPItem*> x; + std::vector<SPItem*> selected(desktop->getSelection()->items().begin(), desktop->getSelection()->items().end()); + ExportResult status = sp_export_png_file(desktop->getDocument(), path.c_str(), + area, width, height, pHYs, pHYs, //previously xdpi, ydpi. + nv->pagecolor, + onProgressCallback, (void*)prog_dlg, + FALSE, + hide ? selected : x, + do_interlace, color_type, bit_depth, zlib, antialiasing + ); + if (status == EXPORT_ERROR) { + gchar * safeFile = Inkscape::IO::sanitizeString(path.c_str()); + gchar * error = g_strdup_printf(_("Could not export to filename %s.\n"), safeFile); + + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error); + sp_ui_error_dialog(error); + + g_free(safeFile); + g_free(error); + } else if (status == EXPORT_OK) { + exportSuccessful = true; + gchar *safeFile = Inkscape::IO::sanitizeString(path.c_str()); + + desktop->messageStack()->flashF(Inkscape::INFORMATION_MESSAGE, _("Drawing exported to <b>%s</b>."), safeFile); + + g_free(safeFile); + } else { + desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Export aborted.")); + } + + /* Reset the filename so that it can be changed again by changing + selections and all that */ + original_name = filename_ext; + filename_modified = false; + + setExporting(false); + delete prog_dlg; + prog_dlg = nullptr; + interrupted = false; + + /* Setup the values in the document */ + switch (current_key) { + case SELECTION_PAGE: + case SELECTION_DRAWING: { + SPDocument * doc = SP_ACTIVE_DOCUMENT; + Inkscape::XML::Node * repr = doc->getReprRoot(); + bool modified = false; + + bool saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); + + gchar const *temp_string = repr->attribute("inkscape:export-filename"); + if (temp_string == nullptr || (filename_ext != temp_string)) { + repr->setAttribute("inkscape:export-filename", filename_ext); + modified = true; + } + temp_string = repr->attribute("inkscape:export-xdpi"); + if (temp_string == nullptr || xdpi != atof(temp_string)) { + sp_repr_set_svg_double(repr, "inkscape:export-xdpi", xdpi); + modified = true; + } + temp_string = repr->attribute("inkscape:export-ydpi"); + if (temp_string == nullptr || ydpi != atof(temp_string)) { + sp_repr_set_svg_double(repr, "inkscape:export-ydpi", ydpi); + modified = true; + } + DocumentUndo::setUndoSensitive(doc, saved); + + if (modified) { + doc->setModifiedSinceSave(); + } + break; + } + case SELECTION_SELECTION: { + SPDocument * doc = SP_ACTIVE_DOCUMENT; + bool modified = false; + + bool saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); + auto reprlst = desktop->getSelection()->xmlNodes(); + + for(auto i=reprlst.begin(); reprlst.end() != i; ++i) { + Inkscape::XML::Node * repr = *i; + const gchar * temp_string; + Glib::ustring dir = Glib::path_get_dirname(filename.c_str()); + const gchar* docURI=SP_ACTIVE_DOCUMENT->getDocumentURI(); + Glib::ustring docdir; + if (docURI) + { + docdir = Glib::path_get_dirname(docURI); + } + if (repr->attribute("id") == nullptr || + !(filename_ext.find_last_of(repr->attribute("id")) && + ( !docURI || + (dir == docdir)))) { + temp_string = repr->attribute("inkscape:export-filename"); + if (temp_string == nullptr || (filename_ext != temp_string)) { + repr->setAttribute("inkscape:export-filename", filename_ext); + modified = true; + } + } + temp_string = repr->attribute("inkscape:export-xdpi"); + if (temp_string == nullptr || xdpi != atof(temp_string)) { + sp_repr_set_svg_double(repr, "inkscape:export-xdpi", xdpi); + modified = true; + } + temp_string = repr->attribute("inkscape:export-ydpi"); + if (temp_string == nullptr || ydpi != atof(temp_string)) { + sp_repr_set_svg_double(repr, "inkscape:export-ydpi", ydpi); + modified = true; + } + } + DocumentUndo::setUndoSensitive(doc, saved); + + if (modified) { + doc->setModifiedSinceSave(); + } + break; + } + default: + break; + } + } + + if (exportSuccessful && closeWhenDone.get_active()) { + for ( Gtk::Container *parent = get_parent(); parent; parent = parent->get_parent()) { + if ( GDL_IS_DOCK_ITEM(parent->gobj()) ) { + GdlDockItem *item = GDL_DOCK_ITEM(parent->gobj()); + if (item) { + gdl_dock_item_hide_item(item); + } + break; + } + } + } +} // end of sp_export_export_clicked() + +/// Called when Browse button is clicked +/// @todo refactor this code to use ui/dialogs/filedialog.cpp +void Export::onBrowse () +{ + GtkWidget *fs; + Glib::ustring filename; + + fs = gtk_file_chooser_dialog_new (_("Select a filename for exporting"), + (GtkWindow*)desktop->getToplevel(), + GTK_FILE_CHOOSER_ACTION_SAVE, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Save"), GTK_RESPONSE_ACCEPT, + NULL ); + + gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(fs), false); + + sp_transientize (fs); + + gtk_window_set_modal(GTK_WINDOW (fs), true); + + filename = filename_entry.get_text(); + + if (filename.empty()) { + Glib::ustring tmp; + filename = create_filepath_from_id(tmp, tmp); + } + + gtk_file_chooser_set_filename (GTK_FILE_CHOOSER (fs), filename.c_str()); + +#ifdef _WIN32 + // code in this section is borrowed from ui/dialogs/filedialogimpl-win32.cpp + OPENFILENAMEW opf; + WCHAR filter_string[20]; + wcsncpy(filter_string, L"PNG#*.png##", 11); + filter_string[3] = L'\0'; + filter_string[9] = L'\0'; + filter_string[10] = L'\0'; + WCHAR* title_string = (WCHAR*)g_utf8_to_utf16(_("Select a filename for exporting"), -1, NULL, NULL, NULL); + WCHAR* extension_string = (WCHAR*)g_utf8_to_utf16("*.png", -1, NULL, NULL, NULL); + // Copy the selected file name, converting from UTF-8 to UTF-16 + std::string dirname = Glib::path_get_dirname(filename.raw()); + if ( !Glib::file_test(dirname, Glib::FILE_TEST_EXISTS) || + Glib::file_test(filename, Glib::FILE_TEST_IS_DIR) || + dirname.empty() ) + { + Glib::ustring tmp; + filename = create_filepath_from_id(tmp, tmp); + } + WCHAR _filename[_MAX_PATH + 1]; + memset(_filename, 0, sizeof(_filename)); + gunichar2* utf16_path_string = g_utf8_to_utf16(filename.c_str(), -1, NULL, NULL, NULL); + wcsncpy(_filename, reinterpret_cast<wchar_t*>(utf16_path_string), _MAX_PATH); + g_free(utf16_path_string); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Glib::RefPtr<const Gdk::Window> parentWindow = desktop->getToplevel()->get_window(); + g_assert(parentWindow->gobj() != NULL); + + opf.hwndOwner = (HWND)gdk_win32_window_get_handle((GdkWindow*)parentWindow->gobj()); + opf.lpstrFilter = filter_string; + opf.lpstrCustomFilter = 0; + opf.nMaxCustFilter = 0L; + opf.nFilterIndex = 1L; + opf.lpstrFile = _filename; + opf.nMaxFile = _MAX_PATH; + opf.lpstrFileTitle = NULL; + opf.nMaxFileTitle=0; + opf.lpstrInitialDir = 0; + opf.lpstrTitle = title_string; + opf.nFileOffset = 0; + opf.nFileExtension = 2; + opf.lpstrDefExt = extension_string; + opf.lpfnHook = NULL; + opf.lCustData = 0; + opf.Flags = OFN_PATHMUSTEXIST; + opf.lStructSize = sizeof(OPENFILENAMEW); + if (GetSaveFileNameW(&opf) != 0) + { + // Copy the selected file name, converting from UTF-16 to UTF-8 + gchar *utf8string = g_utf16_to_utf8((const gunichar2*)opf.lpstrFile, _MAX_PATH, NULL, NULL, NULL); + filename_entry.set_text(utf8string); + filename_entry.set_position(strlen(utf8string)); + g_free(utf8string); + + } + g_free(extension_string); + g_free(title_string); + +#else + if (gtk_dialog_run (GTK_DIALOG (fs)) == GTK_RESPONSE_ACCEPT) + { + gchar *file; + + file = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (fs)); + + gchar * utf8file = g_filename_to_utf8( file, -1, nullptr, nullptr, nullptr ); + filename_entry.set_text (utf8file); + filename_entry.set_position(strlen(utf8file)); + + g_free(utf8file); + g_free(file); + } +#endif + + gtk_widget_destroy (fs); + + return; +} // end of sp_export_browse_clicked() + +// TODO: Move this to nr-rect-fns.h. +bool Export::bbox_equal(Geom::Rect const &one, Geom::Rect const &two) +{ + double const epsilon = pow(10.0, -EXPORT_COORD_PRECISION); + return ( + (fabs(one.min()[Geom::X] - two.min()[Geom::X]) < epsilon) && + (fabs(one.min()[Geom::Y] - two.min()[Geom::Y]) < epsilon) && + (fabs(one.max()[Geom::X] - two.max()[Geom::X]) < epsilon) && + (fabs(one.max()[Geom::Y] - two.max()[Geom::Y]) < epsilon) + ); +} + +/** + *This function is used to detect the current selection setting + * based on the values in the x0, y0, x1 and y0 fields. + * + * One of the most confusing parts of this function is why the array + * is built at the beginning. What needs to happen here is that we + * should always check the current selection to see if it is the valid + * one. While this is a performance improvement it is also a usability + * one during the cases where things like selections and drawings match + * size. This way buttons change less 'randomly' (at least in the eyes + * of the user). To do this an array is built where the current selection + * type is placed first, and then the others in an order from smallest + * to largest (this can be configured by reshuffling \c test_order). + * + * All of the values in this function are rounded to two decimal places + * because that is what is shown to the user. While everything is kept + * more accurate than that, the user can't control more accurate than + * that, so for this to work for them - it needs to check on that level + * of accuracy. + * + * @todo finish writing this up. + */ +void Export::detectSize() { + static const selection_type test_order[SELECTION_NUMBER_OF] = {SELECTION_SELECTION, SELECTION_DRAWING, SELECTION_PAGE, SELECTION_CUSTOM}; + selection_type this_test[SELECTION_NUMBER_OF + 1]; + selection_type key = SELECTION_NUMBER_OF; + + Geom::Point x(getValuePx(x0_adj), + getValuePx(y0_adj)); + Geom::Point y(getValuePx(x1_adj), + getValuePx(y1_adj)); + Geom::Rect current_bbox(x, y); + + this_test[0] = current_key; + for (int i = 0; i < SELECTION_NUMBER_OF; i++) { + this_test[i + 1] = test_order[i]; + } + + for (int i = 0; + i < SELECTION_NUMBER_OF + 1 && + key == SELECTION_NUMBER_OF && + SP_ACTIVE_DESKTOP != nullptr; + i++) { + switch (this_test[i]) { + case SELECTION_SELECTION: + if ((SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false) { + Geom::OptRect bbox = (SP_ACTIVE_DESKTOP->getSelection())->bounds(SPItem::VISUAL_BBOX); + + if ( bbox && bbox_equal(*bbox,current_bbox)) { + key = SELECTION_SELECTION; + } + } + break; + case SELECTION_DRAWING: { + SPDocument *doc = SP_ACTIVE_DESKTOP->getDocument(); + + Geom::OptRect bbox = doc->getRoot()->desktopVisualBounds(); + + if ( bbox && bbox_equal(*bbox,current_bbox) ) { + key = SELECTION_DRAWING; + } + break; + } + + case SELECTION_PAGE: { + SPDocument *doc; + + doc = SP_ACTIVE_DESKTOP->getDocument(); + + Geom::Point x(0.0, 0.0); + Geom::Point y(doc->getWidth().value("px"), + doc->getHeight().value("px")); + Geom::Rect bbox(x, y); + + if (bbox_equal(bbox,current_bbox)) { + key = SELECTION_PAGE; + } + + break; + } + default: + break; + } + } + // std::cout << std::endl; + + if (key == SELECTION_NUMBER_OF) { + key = SELECTION_CUSTOM; + } + + current_key = key; + selectiontype_buttons[current_key]->set_active(true); + + return; +} /* sp_export_detect_size */ + +/// Called when area x0 value is changed +void Export::areaXChange(Glib::RefPtr<Gtk::Adjustment>& adj) +{ + float x0, x1, xdpi, width; + + if (update) { + return; + } + + update = true; + + x0 = getValuePx(x0_adj); + x1 = getValuePx(x1_adj); + xdpi = getValue(xdpi_adj); + + width = floor ((x1 - x0) * xdpi / DPI_BASE + 0.5); + + if (width < SP_EXPORT_MIN_SIZE) { + width = SP_EXPORT_MIN_SIZE; + + if (adj == x1_adj) { + x1 = x0 + width * DPI_BASE / xdpi; + setValuePx(x1_adj, x1); + } else { + x0 = x1 - width * DPI_BASE / xdpi; + setValuePx(x0_adj, x0); + } + } + + setValuePx(width_adj, x1 - x0); + setValue(bmwidth_adj, width); + + detectSize(); + + update = false; + + return; +} // end of sp_export_area_x_value_changed() + +/// Called when area y0 value is changed. +void Export::areaYChange(Glib::RefPtr<Gtk::Adjustment>& adj) +{ + float y0, y1, ydpi, height; + + if (update) { + return; + } + + update = true; + + y0 = getValuePx(y0_adj); + y1 = getValuePx(y1_adj); + ydpi = getValue(ydpi_adj); + + height = floor ((y1 - y0) * ydpi / DPI_BASE + 0.5); + + if (height < SP_EXPORT_MIN_SIZE) { + //const gchar *key; + height = SP_EXPORT_MIN_SIZE; + //key = (const gchar *)g_object_get_data(G_OBJECT (adj), "key"); + if (adj == y1_adj) { + //if (!strcmp (key, "y0")) { + y1 = y0 + height * DPI_BASE / ydpi; + setValuePx(y1_adj, y1); + } else { + y0 = y1 - height * DPI_BASE / ydpi; + setValuePx(y0_adj, y0); + } + } + + setValuePx(height_adj, y1 - y0); + setValue(bmheight_adj, height); + + detectSize(); + + update = false; + + return; +} // end of sp_export_area_y_value_changed() + +/// Called when x1-x0 or area width is changed +void Export::onAreaWidthChange() +{ + if (update) { + return; + } + + update = true; + + float x0 = getValuePx(x0_adj); + float xdpi = getValue(xdpi_adj); + float width = getValuePx(width_adj); + float bmwidth = floor(width * xdpi / DPI_BASE + 0.5); + + if (bmwidth < SP_EXPORT_MIN_SIZE) { + + bmwidth = SP_EXPORT_MIN_SIZE; + width = bmwidth * DPI_BASE / xdpi; + setValuePx(width_adj, width); + } + + setValuePx(x1_adj, x0 + width); + setValue(bmwidth_adj, bmwidth); + + update = false; + + return; +} // end of sp_export_area_width_value_changed() + +/// Called when y1-y0 or area height is changed. +void Export::onAreaHeightChange() +{ + if (update) { + return; + } + + update = true; + + float y0 = getValuePx(y0_adj); + //float y1 = sp_export_value_get_px(y1_adj); + float ydpi = getValue(ydpi_adj); + float height = getValuePx(height_adj); + float bmheight = floor (height * ydpi / DPI_BASE + 0.5); + + if (bmheight < SP_EXPORT_MIN_SIZE) { + bmheight = SP_EXPORT_MIN_SIZE; + height = bmheight * DPI_BASE / ydpi; + setValuePx(height_adj, height); + } + + setValuePx(y1_adj, y0 + height); + setValue(bmheight_adj, bmheight); + + update = false; + + return; +} // end of sp_export_area_height_value_changed() + +/** + * A function to set the ydpi. + * @param base The export dialog. + * + * This function grabs all of the y values and then figures out the + * new bitmap size based on the changing dpi value. The dpi value is + * gotten from the xdpi setting as these can not currently be independent. + */ +void Export::setImageY() +{ + float y0, y1, xdpi; + + y0 = getValuePx(y0_adj); + y1 = getValuePx(y1_adj); + xdpi = getValue(xdpi_adj); + + setValue(ydpi_adj, xdpi); + setValue(bmheight_adj, (y1 - y0) * xdpi / DPI_BASE); + + return; +} // end of setImageY() + +/** + * A function to set the xdpi. + * + * This function grabs all of the x values and then figures out the + * new bitmap size based on the changing dpi value. The dpi value is + * gotten from the xdpi setting as these can not currently be independent. + * + */ +void Export::setImageX() +{ + float x0, x1, xdpi; + + x0 = getValuePx(x0_adj); + x1 = getValuePx(x1_adj); + xdpi = getValue(xdpi_adj); + + setValue(ydpi_adj, xdpi); + setValue(bmwidth_adj, (x1 - x0) * xdpi / DPI_BASE); + + return; +} // end of setImageX() + +/// Called when pixel width is changed +void Export::onBitmapWidthChange () +{ + float x0, x1, bmwidth, xdpi; + + if (update) { + return; + } + + update = true; + + x0 = getValuePx(x0_adj); + x1 = getValuePx(x1_adj); + bmwidth = getValue(bmwidth_adj); + + if (bmwidth < SP_EXPORT_MIN_SIZE) { + bmwidth = SP_EXPORT_MIN_SIZE; + setValue(bmwidth_adj, bmwidth); + } + + xdpi = bmwidth * DPI_BASE / (x1 - x0); + setValue(xdpi_adj, xdpi); + + setImageY (); + + update = false; + + return; +} // end of sp_export_bitmap_width_value_changed() + +/// Called when pixel height is changed +void Export::onBitmapHeightChange () +{ + float y0, y1, bmheight, xdpi; + + if (update) { + return; + } + + update = true; + + y0 = getValuePx(y0_adj); + y1 = getValuePx(y1_adj); + bmheight = getValue(bmheight_adj); + + if (bmheight < SP_EXPORT_MIN_SIZE) { + bmheight = SP_EXPORT_MIN_SIZE; + setValue(bmheight_adj, bmheight); + } + + xdpi = bmheight * DPI_BASE / (y1 - y0); + setValue(xdpi_adj, xdpi); + + setImageX (); + + update = false; + + return; +} // end of sp_export_bitmap_width_value_changed() + +/** + * A function to adjust the bitmap width when the xdpi value changes. + * + * The first thing this function checks is to see if we are doing an + * update. If we are, this function just returns because there is another + * instance of it that will handle everything for us. If there is a + * units change, we also assume that everyone is being updated appropriately + * and there is nothing for us to do. + * + * If we're the highest level function, we set the update flag, and + * continue on our way. + * + * All of the values are grabbed using the \c sp_export_value_get functions + * (call to the _pt ones for x0 and x1 but just standard for xdpi). The + * xdpi value is saved in the preferences for the next time the dialog + * is opened. (does the selection dpi need to be set here?) + * + * A check is done to to ensure that we aren't outputting an invalid width, + * this is set by SP_EXPORT_MIN_SIZE. If that is the case the dpi is + * changed to make it valid. + * + * After all of this the bitmap width is changed. + * + * We also change the ydpi. This is a temporary hack as these can not + * currently be independent. This is likely to change in the future. + * + */ +void Export::onExportXdpiChange() +{ + float x0, x1, xdpi, bmwidth; + + if (update) { + return; + } + + update = true; + + x0 = getValuePx(x0_adj); + x1 = getValuePx(x1_adj); + xdpi = getValue(xdpi_adj); + + // remember xdpi setting + prefs->setDouble("/dialogs/export/defaultxdpi/value", xdpi); + + bmwidth = (x1 - x0) * xdpi / DPI_BASE; + + if (bmwidth < SP_EXPORT_MIN_SIZE) { + bmwidth = SP_EXPORT_MIN_SIZE; + if (x1 != x0) + xdpi = bmwidth * DPI_BASE / (x1 - x0); + else + xdpi = DPI_BASE; + setValue(xdpi_adj, xdpi); + } + + setValue(bmwidth_adj, bmwidth); + + setImageY (); + + update = false; + + return; +} // end of sp_export_xdpi_value_changed() + + +/** + * A function to change the area that is used for the exported. + * bitmap. + * + * This function just calls \c sp_export_value_set_px for each of the + * parameters that is passed in. This allows for setting them all in + * one convenient area. + * + * Update is set to suspend all of the other test running while all the + * values are being set up. This allows for a performance increase, but + * it also means that the wrong type won't be detected with only some of + * the values set. After all the values are set everyone is told that + * there has been an update. + * + * @param x0 Horizontal upper left hand corner of the picture in points. + * @param y0 Vertical upper left hand corner of the picture in points. + * @param x1 Horizontal lower right hand corner of the picture in points. + * @param y1 Vertical lower right hand corner of the picture in points. + */ +void Export::setArea( double x0, double y0, double x1, double y1 ) +{ + update = true; + setValuePx(x1_adj, x1); + setValuePx(y1_adj, y1); + setValuePx(x0_adj, x0); + setValuePx(y0_adj, y0); + update = false; + + areaXChange (x1_adj); + areaYChange (y1_adj); + + return; +} + +/** + * Sets the value of an adjustment. + * + * @param adj The adjustment widget + * @param val What value to set it to. + */ +void Export::setValue(Glib::RefPtr<Gtk::Adjustment>& adj, double val ) +{ + if (adj) { + adj->set_value(val); + } +} + +/** + * A function to set a value using the units points. + * + * This function first gets the adjustment for the key that is passed + * in. It then figures out what units are currently being used in the + * dialog. After doing all of that, it then converts the incoming + *value and sets the adjustment. + * + * @param adj The adjustment widget + * @param val What the value should be in points. + */ +void Export::setValuePx(Glib::RefPtr<Gtk::Adjustment>& adj, double val) +{ + Unit const *unit = unit_selector.getUnit(); + + setValue(adj, Inkscape::Util::Quantity::convert(val, "px", unit)); + + return; +} + +/** + * Get the value of an adjustment in the export dialog. + * + * This function gets the adjustment from the data field in the export + * dialog. It then grabs the value from the adjustment. + * + * @param adj The adjustment widget + * + * @return The value in the specified adjustment. + */ +float Export::getValue(Glib::RefPtr<Gtk::Adjustment>& adj) +{ + if (!adj) { + g_message("sp_export_value_get : adj is NULL"); + return 0.0; + } + return adj->get_value(); +} + +/** + * Grabs a value in the export dialog and converts the unit + * to points. + * + * This function, at its most basic, is a call to \c sp_export_value_get + * to get the value of the adjustment. It then finds the units that + * are being used by looking at the "units" attribute of the export + * dialog. Using that it converts the returned value into points. + * + * @param adj The adjustment widget + * + * @return The value in the adjustment in points. + */ +float Export::getValuePx(Glib::RefPtr<Gtk::Adjustment>& adj) +{ + float value = getValue( adj); + Unit const *unit = unit_selector.getUnit(); + + return Inkscape::Util::Quantity::convert(value, unit, "px"); +} // end of sp_export_value_get_px() + +/** + * This function is called when the filename is changed by + * anyone. It resets the virgin bit. + * + * This function gets called when the text area is modified. It is + * looking for the case where the text area is modified from its + * original value. In that case it sets the "filename-modified" bit + * to TRUE. If the text dialog returns back to the original text, the + * bit gets reset. This should stop simple mistakes. + */ +void Export::onFilenameModified() +{ + if (original_name == filename_entry.get_text()) { + filename_modified = false; + } else { + filename_modified = true; + } + + return; +} // end sp_export_filename_modified + +} +} +} + +/* + 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.h b/src/ui/dialog/export.h new file mode 100644 index 0000000..9210ac5 --- /dev/null +++ b/src/ui/dialog/export.h @@ -0,0 +1,367 @@ +// 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> + * + * Copyright (C) 1999-2007 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/progressbar.h> +#include <gtkmm/expander.h> +#include <gtkmm/grid.h> +#include <gtkmm/comboboxtext.h> + +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/panel.h" + +namespace Gtk { +class Dialog; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** What type of button is being pressed */ +enum selection_type { + SELECTION_PAGE = 0, /**< Export the whole page */ + SELECTION_DRAWING, /**< Export everything drawn on the page */ + SELECTION_SELECTION, /**< Export everything that is selected */ + SELECTION_CUSTOM, /**< Allows the user to set the region exported */ + SELECTION_NUMBER_OF /**< A counter for the number of these guys */ +}; + +/** + * A dialog widget to export to various image formats such as bitmap and png. + * + * Creates a dialog window for exporting an image to a bitmap if one doesn't already exist and + * shows it to the user. If the dialog has already been created, it simply shows the window. + * + */ +class Export : public Widget::Panel { +public: + Export (); + ~Export () override; + + static Export &getInstance() { + return *new Export(); + } + +private: + + /** + * A function to set the xdpi. + * + * This function grabs all of the x values and then figures out the + * new bitmap size based on the changing dpi value. The dpi value is + * gotten from the xdpi setting as these can not currently be independent. + * + */ + void setImageX(); + + /** + * A function to set the ydpi. + * + * This function grabs all of the y values and then figures out the + * new bitmap size based on the changing dpi value. The dpi value is + * gotten from the xdpi setting as these can not currently be independent. + */ + void setImageY(); + bool bbox_equal(Geom::Rect const &one, Geom::Rect const &two); + void updateCheckbuttons (); + inline void findDefaultSelection(); + void detectSize(); + void setArea ( double x0, double y0, double x1, double y1); + /* + * Getter/setter style functions for the spinbuttons + */ + void setValue(Glib::RefPtr<Gtk::Adjustment>& adj, double val); + void setValuePx(Glib::RefPtr<Gtk::Adjustment>& adj, double val); + float getValue(Glib::RefPtr<Gtk::Adjustment>& adj); + float getValuePx(Glib::RefPtr<Gtk::Adjustment>& adj); + + /** + * Helper function to create, style and pack spinbuttons for the export dialog. + * + * Creates a new spin button for the export dialog. + * @param key The name of the spin button + * @param val A default value for the spin button + * @param min Minimum value for the spin button + * @param max Maximum value for the spin button + * @param step The step size for the spin button + * @param page Size of the page increment + * @param t Table to put the spin button in + * @param x X location in the table \c t to start with + * @param y Y location in the table \c t to start with + * @param ll Text to put on the left side of the spin button (optional) + * @param lr Text to put on the right side of the spin button (optional) + * @param digits Number of digits to display after the decimal + * @param sensitive Whether the spin button is sensitive or not + * @param cb Callback for when this spin button is changed (optional) + * + * No unit_selector is stored in the created spinbutton, relies on external unit management + */ + Glib::RefPtr<Gtk::Adjustment> createSpinbutton( gchar const *key, float val, float min, float max, + float step, float page, + Gtk::Grid *t, int x, int y, + const Glib::ustring& ll, const Glib::ustring& lr, + int digits, unsigned int sensitive, + void (Export::*cb)() ); + + /** + * One of the area select radio buttons was pressed + */ + void onAreaToggled(); + + /** + * Export button callback + */ + void onExport (); + + /** + * File Browse button callback + */ + void onBrowse (); + + /** + * Area X value changed callback + */ + void onAreaX0Change() { + areaXChange(x0_adj); + } ; + void onAreaX1Change() { + areaXChange(x1_adj); + } ; + void areaXChange(Glib::RefPtr<Gtk::Adjustment>& adj); + + /** + * Area Y value changed callback + */ + void onAreaY0Change() { + areaYChange(y0_adj); + } ; + void onAreaY1Change() { + areaYChange(y1_adj); + } ; + void areaYChange(Glib::RefPtr<Gtk::Adjustment>& adj); + + /** + * Unit changed callback + */ + void onUnitChanged(); + + /** + * Hide except selected callback + */ + void onHideExceptSelected (); + + /** + * Area width value changed callback + */ + void onAreaWidthChange (); + + /** + * Area height value changed callback + */ + void onAreaHeightChange (); + + /** + * Bitmap width value changed callback + */ + void onBitmapWidthChange (); + + /** + * Bitmap height value changed callback + */ + void onBitmapHeightChange (); + + /** + * Export xdpi value changed callback + */ + void onExportXdpiChange (); + + /** + * Batch export callback + */ + void onBatchClicked (); + + /** + * Inkscape selection change callback + */ + void onSelectionChanged (); + void onSelectionModified (guint flags); + + /** + * Filename modified callback + */ + void onFilenameModified (); + + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + + /** + * Creates progress dialog for batch exporting. + * + * @param progress_text Text to be shown in the progress bar + */ + Gtk::Dialog * 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); + + /** + * Handles state changes as exporting starts or stops. + */ + void setExporting(bool exporting, Glib::ustring const &text = ""); + + /* + * Utility filename and path functions + */ + void set_default_filename (); + Glib::ustring create_filepath_from_id (Glib::ustring id, const Glib::ustring &file_entry_text); + Glib::ustring filename_add_extension (Glib::ustring filename, Glib::ustring extension); + Glib::ustring absolutize_path_from_document_location (SPDocument *doc, const Glib::ustring &filename); + + /* + * Currently selected export area type + */ + selection_type current_key; + /* + * Original name for the export object + */ + Glib::ustring original_name; + Glib::ustring doc_export_name; + /* + * Was the Original name modified + */ + bool filename_modified; + bool was_empty; + /* + * Flag to stop simultaneous updates + */ + bool update; + + /* Area selection radio buttons */ + Gtk::HBox togglebox; + Gtk::RadioButton *selectiontype_buttons[SELECTION_NUMBER_OF]; + + Gtk::VBox area_box; + Gtk::VBox singleexport_box; + + /* Custom size widgets */ + Glib::RefPtr<Gtk::Adjustment> x0_adj; + Glib::RefPtr<Gtk::Adjustment> x1_adj; + Glib::RefPtr<Gtk::Adjustment> y0_adj; + Glib::RefPtr<Gtk::Adjustment> y1_adj; + Glib::RefPtr<Gtk::Adjustment> width_adj; + Glib::RefPtr<Gtk::Adjustment> height_adj; + + /* Bitmap size widgets */ + Glib::RefPtr<Gtk::Adjustment> bmwidth_adj; + Glib::RefPtr<Gtk::Adjustment> bmheight_adj; + Glib::RefPtr<Gtk::Adjustment> xdpi_adj; + Glib::RefPtr<Gtk::Adjustment> ydpi_adj; + + Gtk::VBox size_box; + Gtk::Label* bm_label; + + Gtk::VBox file_box; + Gtk::Label *flabel; + Gtk::Entry filename_entry; + + /* Unit selector widgets */ + Gtk::HBox unitbox; + Inkscape::UI::Widget::UnitMenu unit_selector; + Gtk::Label units_label; + + /* Filename widgets */ + Gtk::HBox filename_box; + Gtk::Button browse_button; + Gtk::Label browse_label; + Gtk::Image browse_image; + + Gtk::HBox batch_box; + Gtk::CheckButton batch_export; + + Gtk::HBox hide_box; + Gtk::CheckButton hide_export; + + Gtk::CheckButton closeWhenDone; + + /* Advanced */ + Gtk::Expander expander; + Gtk::CheckButton interlacing; + Gtk::Label bitdepth_label; + Gtk::ComboBoxText bitdepth_cb; + Gtk::Label zlib_label; + Gtk::ComboBoxText zlib_compression; + Gtk::Label pHYs_label; + Glib::RefPtr<Gtk::Adjustment> pHYs_adj; + Gtk::SpinButton pHYs_sb; + Gtk::Label antialiasing_label; + Gtk::ComboBoxText antialiasing_cb; + + /* Export Button widgets */ + Gtk::HBox button_box; + Gtk::Button export_button; + + Gtk::ProgressBar _prog; + + Gtk::Dialog *prog_dlg; + bool interrupted; // indicates whether export needs to be interrupted (read: user pressed cancel in the progress dialog) + + Inkscape::Preferences *prefs; + SPDesktop *desktop; + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; + sigc::connection selectChangedConn; + sigc::connection subselChangedConn; + sigc::connection selectModifiedConn; + sigc::connection unitChangedConn; + +}; + +} +} +} +#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/extension-editor.cpp b/src/ui/dialog/extension-editor.cpp new file mode 100644 index 0000000..1852010 --- /dev/null +++ b/src/ui/dialog/extension-editor.cpp @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Extension editor dialog. + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension-editor.h" +#include <glibmm/i18n.h> + +#include <gtkmm/frame.h> +#include <gtkmm/notebook.h> + +#include "verbs.h" +#include "preferences.h" +#include "ui/interface.h" + +#include "extension/db.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * Create a new ExtensionEditor dialog. + * + * This function creates a new extension editor dialog. The dialog + * consists of two basic areas. The left side is a tree widget, which + * is only used as a list. And the right side is a notebook of information + * about the selected extension. A handler is set up so that when + * a new extension is selected, the notebooks are changed appropriately. + */ +ExtensionEditor::ExtensionEditor() + : UI::Widget::Panel("/dialogs/extensioneditor", SP_VERB_DIALOG_EXTENSIONEDITOR) +{ + _notebook_info.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _notebook_params.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + + //Main HBox + Gtk::HBox* hbox_list_page = Gtk::manage(new Gtk::HBox()); + hbox_list_page->set_border_width(12); + hbox_list_page->set_spacing(12); + _getContents()->add(*hbox_list_page); + + + //Pagelist + Gtk::Frame* list_frame = Gtk::manage(new Gtk::Frame()); + Gtk::ScrolledWindow* scrolled_window = Gtk::manage(new Gtk::ScrolledWindow()); + hbox_list_page->pack_start(*list_frame, false, true, 0); + _page_list.set_headers_visible(false); + scrolled_window->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + scrolled_window->add(_page_list); + list_frame->set_shadow_type(Gtk::SHADOW_IN); + list_frame->add(*scrolled_window); + _page_list_model = Gtk::TreeStore::create(_page_list_columns); + _page_list.set_model(_page_list_model); + _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, &ExtensionEditor::on_pagelist_selection_changed)); + page_list_selection->set_mode(Gtk::SELECTION_BROWSE); + + + //Pages + Gtk::VBox* vbox_page = Gtk::manage(new Gtk::VBox()); + hbox_list_page->pack_start(*vbox_page, true, true, 0); + Gtk::Notebook * notebook = Gtk::manage(new Gtk::Notebook()); + notebook->append_page(_notebook_info, *Gtk::manage(new Gtk::Label(_("Information")))); + notebook->append_page(_notebook_params, *Gtk::manage(new Gtk::Label(_("Parameters")))); + vbox_page->pack_start(*notebook, true, true, 0); + + Inkscape::Extension::db.foreach(dbfunc, this); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring defaultext = prefs->getString("/dialogs/extensioneditor/selected-extension"); + if (defaultext.empty()) defaultext = "org.inkscape.input.svg"; + this->setExtension(defaultext); + + show_all_children(); +} + +/** + * Destroys the extension editor dialog. + */ +ExtensionEditor::~ExtensionEditor() += default; + +void +ExtensionEditor::setExtension(Glib::ustring extension_id) { + _selection_search = extension_id; + _page_list_model->foreach_iter(sigc::mem_fun(*this, &ExtensionEditor::setExtensionIter)); + return; +} + +bool +ExtensionEditor::setExtensionIter(const Gtk::TreeModel::iterator &iter) +{ + Gtk::TreeModel::Row row = *iter; + if (row[_page_list_columns._col_id] == _selection_search) { + _page_list.get_selection()->select(iter); + return true; + } + return false; +} + +/** + * Called every time a new extension is selected + * + * This function is set up to handle the signal for a changed extension + * from the tree view in the left pane. It figure out which extension + * is selected and updates the widgets to have data for that extension. + */ +void ExtensionEditor::on_pagelist_selection_changed() +{ + Glib::RefPtr<Gtk::TreeSelection> selection = _page_list.get_selection(); + Gtk::TreeModel::iterator iter = selection->get_selected(); + if (iter) { + /* Get the row info */ + Gtk::TreeModel::Row row = *iter; + Glib::ustring id = row[_page_list_columns._col_id]; + Glib::ustring name = row[_page_list_columns._col_name]; + + /* Set the selection in the preferences */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/dialogs/extensioneditor/selected-extension", id); + + /* Adjust the dialog's title */ + gchar title[500]; + sp_ui_dialog_title_string (Inkscape::Verb::get(SP_VERB_DIALOG_EXTENSIONEDITOR), title); + Glib::ustring utitle(title); + // set_title(utitle + ": " + name); + + /* Clear the notbook pages */ + _notebook_info.remove(); + _notebook_params.remove(); + + Inkscape::Extension::Extension * ext = Inkscape::Extension::db.get(id.c_str()); + + /* Make sure we have all the widgets */ + Gtk::Widget * info = nullptr; + Gtk::Widget * params = nullptr; + + if (ext != nullptr) { + info = ext->get_info_widget(); + params = ext->get_params_widget(); + } + + /* Place them in the pages */ + if (info != nullptr) { + _notebook_info.add(*info); + } + if (params != nullptr) { + _notebook_params.add(*params); + } + + } + + return; +} + +/** + * A function to pass to the iterator in the Extensions Database. + * + * This function is a static function with the prototype required for + * the Extension Database's foreach function. It will get called for + * every extension in the database, and will then turn around and + * call the more object oriented function \c add_extension in the + * ExtensionEditor. + * + * @param in_plug The extension to evaluate. + * @param in_data A pointer to the Extension Editor class. + */ +void ExtensionEditor::dbfunc(Inkscape::Extension::Extension * in_plug, gpointer in_data) +{ + ExtensionEditor * ee = static_cast<ExtensionEditor *>(in_data); + ee->add_extension(in_plug); + return; +} + +/** + * Adds an extension into the tree model. + * + * This function takes the data out of the extension and puts it + * into the tree model for the dialog. + * + * @param ext The extension to add. + * @return The iterator representing the location in the tree model. + */ +Gtk::TreeModel::iterator ExtensionEditor::add_extension(Inkscape::Extension::Extension * ext) +{ + Gtk::TreeModel::iterator iter; + + iter = _page_list_model->append(); + + Gtk::TreeModel::Row row = *iter; + row[_page_list_columns._col_name] = ext->get_name(); + row[_page_list_columns._col_id] = ext->get_id(); + + return iter; +} + +} // 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/extension-editor.h b/src/ui/dialog/extension-editor.h new file mode 100644 index 0000000..403ee1f --- /dev/null +++ b/src/ui/dialog/extension-editor.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Extension editor dialog + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_EXTENSION_EDITOR_H +#define INKSCAPE_UI_DIALOG_EXTENSION_EDITOR_H + +#include "ui/widget/panel.h" + +#include <gtkmm/treestore.h> +#include <gtkmm/treeview.h> +#include <gtkmm/scrolledwindow.h> + +#include "extension/extension.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ExtensionEditor : public UI::Widget::Panel { +public: + ExtensionEditor(); + ~ExtensionEditor() override; + + static ExtensionEditor &getInstance() { return *new ExtensionEditor(); } + +protected: + /** \brief The view of the list of extensions on the left of the dialog */ + Gtk::TreeView _page_list; + /** \brief The model for the list of extensions */ + Glib::RefPtr<Gtk::TreeStore> _page_list_model; + /** \brief The notebook page that contains information */ + Gtk::ScrolledWindow _notebook_info; + /** \brief The notebook page that holds all the parameters */ + Gtk::ScrolledWindow _notebook_params; + + //Pagelist model columns: + class PageListModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + /** \brief Creates the Page List model by adding all of the + members of the class as column records. */ + PageListModelColumns() { + Gtk::TreeModelColumnRecord::add(_col_name); + Gtk::TreeModelColumnRecord::add(_col_id); + } + /** \brief Name of the extension */ + Gtk::TreeModelColumn<Glib::ustring> _col_name; + /** \brief ID of the extension */ + Gtk::TreeModelColumn<Glib::ustring> _col_id; + }; + PageListModelColumns _page_list_columns; + +private: + /** \brief A 'global' variable to help search through and select + an item in the extension list */ + Glib::ustring _selection_search; + + ExtensionEditor(ExtensionEditor const &d) = delete; + ExtensionEditor& operator=(ExtensionEditor const &d) = delete; + + void on_pagelist_selection_changed(); + static void dbfunc (Inkscape::Extension::Extension * in_plug, gpointer in_data); + Gtk::TreeModel::iterator add_extension (Inkscape::Extension::Extension * ext); + bool setExtensionIter(const Gtk::TreeModel::iterator &iter); +public: + void setExtension(Glib::ustring extension_id); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_EXTENSION_EDITOR_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/extensions.cpp b/src/ui/dialog/extensions.cpp new file mode 100644 index 0000000..f39c7d5 --- /dev/null +++ b/src/ui/dialog/extensions.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A simple dialog with information about extensions. + */ +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2005 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extensions.h" +#include "extension/extension.h" +#include <gtkmm/scrolledwindow.h> + +#include "extension/db.h" + + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +using Inkscape::Extension::Extension; + +ExtensionsPanel &ExtensionsPanel::getInstance() +{ + ExtensionsPanel &instance = *new ExtensionsPanel(); + + instance.rescan(); + + return instance; +} + + +ExtensionsPanel::ExtensionsPanel() : + _showAll(false) +{ + Gtk::ScrolledWindow* scroller = new Gtk::ScrolledWindow(); + + _view.set_editable(false); + + scroller->add(_view); + add(*scroller); + + rescan(); + + show_all_children(); +} + +void ExtensionsPanel::set_full(bool full) +{ + if ( full != _showAll ) { + _showAll = full; + rescan(); + } +} + +void ExtensionsPanel::listCB( Inkscape::Extension::Extension * in_plug, gpointer in_data ) +{ + ExtensionsPanel * self = static_cast<ExtensionsPanel*>(in_data); + + const char* stateStr; + Extension::state_t state = in_plug->get_state(); + switch ( state ) { + case Extension::STATE_LOADED: + { + stateStr = "loaded"; + } + break; + case Extension::STATE_UNLOADED: + { + stateStr = "unloaded"; + } + break; + case Extension::STATE_DEACTIVATED: + { + stateStr = "deactivated"; + } + break; + default: + stateStr = "unknown"; + } + + if ( self->_showAll || in_plug->deactivated() ) { + gchar* line = g_strdup_printf( "%s %s\n \"%s\"", stateStr, in_plug->get_name(), in_plug->get_id() ); + + self->_view.get_buffer()->insert( self->_view.get_buffer()->end(), line ); + self->_view.get_buffer()->insert( self->_view.get_buffer()->end(), "\n" ); + g_free(line); + } + + return; +} + +void ExtensionsPanel::rescan() +{ + _view.get_buffer()->set_text("Extensions:\n"); +// g_message("/------------------"); + + Inkscape::Extension::db.foreach(listCB, (gpointer)this); + +// g_message("\\------------------"); +} + +} //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/extensions.h b/src/ui/dialog/extensions.h new file mode 100644 index 0000000..16d2e91 --- /dev/null +++ b/src/ui/dialog/extensions.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A simple dialog with information about extensions + */ +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2005 The Inkscape Organization + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_EXTENSIONS_H +#define SEEN_EXTENSIONS_H + +#include "ui/widget/panel.h" +#include <gtkmm/textview.h> + +namespace Inkscape { +namespace Extension { +class Extension; +} +} + +namespace Inkscape { +namespace UI { +namespace Dialogs { + + +/** + * A panel that displays information about extensions. + */ +class ExtensionsPanel : public Inkscape::UI::Widget::Panel +{ +public: + ExtensionsPanel(); + + static ExtensionsPanel &getInstance(); + + void set_full(bool full); + +private: + ExtensionsPanel(ExtensionsPanel const &) = delete; // no copy + ExtensionsPanel &operator=(ExtensionsPanel const &) = delete; // no assign + + static void listCB(Inkscape::Extension::Extension *in_plug, gpointer in_data); + + void rescan(); + + bool _showAll; + Gtk::TextView _view; +}; + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +#endif // SEEN_EXTENSIONS_H 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..8989171 --- /dev/null +++ b/src/ui/dialog/filedialog.h @@ -0,0 +1,254 @@ +// 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, + 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..5874cac --- /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::_previewEnabledCB)); + + svgexportCheckbox.set_label(Glib::ustring(_("Export as SVG 1.1 per settings in Preference 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::_previewEnabledCB() +{ + bool enabled = previewCheckbox.get_active(); + set_preview_widget_active(enabled); + if (enabled) { + _updatePreviewCallback(); + } else { + // Clears out any current preview image. + svgPreview.showNoPreview(); + } +} + +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() +{ + Glib::ustring fileName = get_preview_filename(); + bool enabled = previewCheckbox.get_active(); + + if (fileName.empty()) { + fileName = get_preview_uri(); + } + + if (enabled && !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 + if (Inkscape::IO::file_test(INKSCAPE_EXAMPLESDIR, G_FILE_TEST_EXISTS) && + Inkscape::IO::file_test(INKSCAPE_EXAMPLESDIR, G_FILE_TEST_IS_DIR) && g_path_is_absolute(INKSCAPE_EXAMPLESDIR)) { + add_shortcut_folder(INKSCAPE_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) +{ + 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; + } + + //###### Add the file types menu + // createFilterMenu(); + + //###### 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)); + } + + if (_dialogType != CUSTOM_TYPE) + createFileTypeMenu(); + + fileTypeComboBox.set_size_request(200, 40); + fileTypeComboBox.signal_changed().connect(sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileTypeChangedCallback)); + + + 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(dynamic_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::createFileTypeMenu() +{ + Inkscape::Extension::DB::OutputList extension_list; + Inkscape::Extension::db.get_output_list(extension_list); + knownExtensions.clear(); + + for (auto omod : extension_list) { + // FIXME: would be nice to grey them out instead of not listing them + if (omod->deactivated()) + 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..430f6b6 --- /dev/null +++ b/src/ui/dialog/filedialogimpl-gtkmm.h @@ -0,0 +1,313 @@ +// 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 user changing preview checkbox + */ + void _previewEnabledCB(); + + /** + * 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 patter (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::HBox childBox; + Gtk::VBox checksBox; + + Gtk::CheckButton fileTypeCheckbox; + + /** + * Callback for user input into fileNameEntry + */ + void fileTypeChangedCallback(); + + /** + * Create a filter menu for this type of dialog + */ + void createFileTypeMenu(); + + + /** + * 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..8c37176 --- /dev/null +++ b/src/ui/dialog/filedialogimpl-win32.cpp @@ -0,0 +1,1937 @@ +// 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 <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); + SPDocument *svgDoc = SPDocument::createNewDoc (utf8string, 0); + g_free(utf8string); + + // Check the document loaded properly + if (svgDoc == NULL) { + return false; + } + if (svgDoc->getRoot() == NULL) + { + svgDoc->doUnref(); + 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; + + // Now get the resized values + const int scaledSvgWidth = round(scaleFactor * svgWidth_px); + const int scaledSvgHeight = round(scaleFactor * svgHeight_px); + + const double dpi = 96*scaleFactor; + Inkscape::Pixbuf * pixbuf = sp_generate_internal_bitmap(svgDoc, NULL, 0, 0, svgWidth_px, svgHeight_px, scaledSvgWidth, scaledSvgHeight, dpi, dpi, (guint32) 0xffffff00, NULL); + + // Tidy up + svgDoc->doUnref(); + if (pixbuf == NULL) { + return false; + } + + // Create the GDK pixbuf + _mutex->lock(); + _preview_bitmap_image = Glib::wrap(pixbuf->getPixbufRaw()); + _preview_document_width = svgWidth_px; + _preview_document_height = svgHeight_px; + _preview_image_width = scaledSvgWidth; + _preview_image_height = scaledSvgHeight; + _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; + +#if !GLIB_CHECK_VERSION(2,32,0) + if(!Glib::thread_supported()) + Glib::thread_init(); +#endif + + _result = false; + _finished = false; + _file_selected = false; + _main_loop = g_main_loop_new(g_main_context_default(), FALSE); + +#if GLIB_CHECK_VERSION(2,32,0) + _mutex = new Glib::Threads::Mutex(); + if(Glib::Threads::Thread::create(sigc::mem_fun(*this, &FileOpenDialogImplWin32::GetOpenFileName_thread))) +#else + _mutex = new Glib::Mutex(); + if(Glib::Thread::create(sigc::mem_fun(*this, &FileOpenDialogImplWin32::GetOpenFileName_thread), true)) +#endif + { + while(1) + { + g_main_context_iteration(g_main_context_default(), FALSE); + + if(_mutex->trylock()) + { + // 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); + } + } + + // Tidy up + delete _mutex; + _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"), + _title_label(NULL), + _title_edit(NULL) +{ + FileSaveDialog::myDocTitle = docTitle; + 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; + + for (Inkscape::Extension::DB::OutputList::iterator current_item = extension_list.begin(); + current_item != extension_list.end(); ++current_item) + { + Inkscape::Extension::Output *omod = *current_item; + if (omod->deactivated()) 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() +{ +#if !GLIB_CHECK_VERSION(2,32,0) + if(!Glib::thread_supported()) + Glib::thread_init(); +#endif + + _result = false; + _main_loop = g_main_loop_new(g_main_context_default(), FALSE); + + if(_main_loop != NULL) + { +#if GLIB_CHECK_VERSION(2,32,0) + if(Glib::Threads::Thread::create(sigc::mem_fun(*this, &FileSaveDialogImplWin32::GetSaveFileName_thread))) +#else + if(Glib::Thread::create(sigc::mem_fun(*this, &FileSaveDialogImplWin32::GetSaveFileName_thread), true)) +#endif + g_main_loop_run(_main_loop); + + if(_result && _extension) + appendExtension(myFilename, (Inkscape::Extension::Output*)_extension); + } + + 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..9dedaa3 --- /dev/null +++ b/src/ui/dialog/filedialogimpl-win32.h @@ -0,0 +1,393 @@ +// 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 <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 browing 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 browing 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 patter (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 +#if GLIB_CHECK_VERSION(2,32,0) + Glib::Threads::Mutex *_mutex; +#else + Glib::Mutex *_mutex; +#endif + + + /// 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(); + + /// 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..1de7ede --- /dev/null +++ b/src/ui/dialog/fill-and-stroke.cpp @@ -0,0 +1,207 @@ +// 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 "verbs.h" + +#include "svg/css-ostringstream.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/widget/notebook-page.h" + +#include "widgets/fill-style.h" +#include "widgets/paint-selector.h" +#include "widgets/stroke-style.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +FillAndStroke::FillAndStroke() + : UI::Widget::Panel("/dialogs/fillstroke", SP_VERB_DIALOG_FILL_STROKE) + , _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(SP_VERB_DIALOG_FILL_STROKE, "fillstroke", + UI::Widget::SimpleFilterModifier::ISOLATION | + UI::Widget::SimpleFilterModifier::BLEND | + UI::Widget::SimpleFilterModifier::BLUR | + UI::Widget::SimpleFilterModifier::OPACITY) + , deskTrack() + , targetDesktop(nullptr) + , fillWdgt(nullptr) + , strokeWdgt(nullptr) + , desktopChangeConn() +{ + Gtk::Box *contents = _getContents(); + contents->set_spacing(2); + contents->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(); + + contents->pack_end(_composite_settings, Gtk::PACK_SHRINK); + + show_all_children(); + + _composite_settings.setSubject(&_subject); + + // Connect this up last + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &FillAndStroke::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +FillAndStroke::~FillAndStroke() +{ + _composite_settings.setSubject(nullptr); + + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +void FillAndStroke::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void FillAndStroke::setTargetDesktop(SPDesktop *desktop) +{ + if (targetDesktop != desktop) { + targetDesktop = desktop; + if (fillWdgt) { + sp_fill_style_widget_set_desktop(fillWdgt, desktop); + } + if (strokeWdgt) { + sp_fill_style_widget_set_desktop(strokeWdgt, desktop); + } + if (strokeStyleWdgt) { + sp_stroke_style_widget_set_desktop(strokeStyleWdgt, desktop); + } + _composite_settings.setSubject(&_subject); + } +} + +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(sp_fill_style_widget_new()); + _page_fill->table().attach(*fillWdgt, 0, 0, 1, 1); +} + +void +FillAndStroke::_layoutPageStrokePaint() +{ + strokeWdgt = Gtk::manage(sp_stroke_style_paint_widget_new()); + _page_stroke_paint->table().attach(*strokeWdgt, 0, 0, 1, 1); +} + +void +FillAndStroke::_layoutPageStrokeStyle() +{ + strokeStyleWdgt = sp_stroke_style_line_widget_new(); + strokeStyleWdgt->set_hexpand(); + strokeStyleWdgt->set_halign(Gtk::ALIGN_START); + + _page_stroke_style->table().attach(*strokeStyleWdgt, 0, 0, 1, 1); +} + +void +FillAndStroke::showPageFill() +{ + present(); + _notebook.set_current_page(0); + _savePagePref(0); + +} + +void +FillAndStroke::showPageStrokePaint() +{ + present(); + _notebook.set_current_page(1); + _savePagePref(1); +} + +void +FillAndStroke::showPageStrokeStyle() +{ + present(); + _notebook.set_current_page(2); + _savePagePref(2); + +} + +Gtk::HBox& +FillAndStroke::_createPageTabLabel(const Glib::ustring& label, const char *label_image) +{ + Gtk::HBox *_tab_label_box = Gtk::manage(new Gtk::HBox(false, 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..f425969 --- /dev/null +++ b/src/ui/dialog/fill-and-stroke.h @@ -0,0 +1,100 @@ +// 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 "ui/widget/panel.h" +#include "ui/widget/object-composite-settings.h" +#include "ui/dialog/desktop-tracker.h" + +#include <gtkmm/notebook.h> +#include "ui/widget/style-subject.h" + +namespace Inkscape { +namespace UI { + +namespace Widget { +class NotebookPage; +} + +namespace Dialog { + +class FillAndStroke : public UI::Widget::Panel { +public: + FillAndStroke(); + ~FillAndStroke() override; + + static FillAndStroke &getInstance() { return *new FillAndStroke(); } + + + void setDesktop(SPDesktop *desktop) override; + + //void selectionChanged(Inkscape::Selection *selection); + + 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::HBox &_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: + FillAndStroke(FillAndStroke const &d) = delete; + FillAndStroke& operator=(FillAndStroke const &d) = delete; + + void setTargetDesktop(SPDesktop *desktop); + + DesktopTracker deskTrack; + SPDesktop *targetDesktop; + Gtk::Widget *fillWdgt; + Gtk::Widget *strokeWdgt; + Gtk::Widget *strokeStyleWdgt; + sigc::connection desktopChangeConn; +}; + +} // 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-editor.cpp b/src/ui/dialog/filter-editor.cpp new file mode 100644 index 0000000..0fae983 --- /dev/null +++ b/src/ui/dialog/filter-editor.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Filter Effects dialog. + */ +/* Authors: + * Marc Jeanmougin + * + * Copyright (C) 2017 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <string> + +#include <gtkmm.h> + +#include <gdkmm/display.h> +#include <gdkmm/seat.h> + +#include <glibmm/convert.h> +#include <glibmm/error.h> +#include <glibmm/i18n.h> +#include <glibmm/main.h> +#include <glibmm/stringutils.h> + +#include "desktop.h" +#include "dialog-manager.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "filter-editor.h" +#include "filter-enums.h" +#include "inkscape.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "io/sys.h" +#include "io/resource.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 "style.h" + +#include "svg/svg-color.h" + +#include "ui/dialog/filedialog.h" +#include "ui/widget/spinbutton.h" + +using namespace Inkscape::Filters; +using namespace Inkscape::IO::Resource; +namespace Inkscape { +namespace UI { +namespace Dialog { + +FilterEditorDialog::FilterEditorDialog() : UI::Widget::Panel("/dialogs/filtereffects", SP_VERB_DIALOG_FILTER_EFFECTS) +{ + + const std::string req_widgets[] = {"FilterEditor", "FilterList", "FilterFERX", "FilterFERY", "FilterFERH", "FilterFERW", "FilterPreview", "FilterPrimitiveDescImage", "FilterPrimitiveList", "FilterPrimitiveDescText", "FilterPrimitiveAdd"}; + Glib::ustring gladefile = get_filename(UIS, "dialog-filter-editor.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; + } + + Gtk::Object* test; + for(std::string w:req_widgets) { + builder->get_widget(w,test); + if(!test){ + g_warning("Required widget %s does not exist", w.c_str()); + return; + } + } + + builder->get_widget("FilterEditor", FilterEditor); + _getContents()->add(*FilterEditor); + +//test + Gtk::ComboBox *OptionList; + builder->get_widget("OptionList",OptionList); + FilterStore = builder->get_object("FilterStore"); + Glib::RefPtr<Gtk::ListStore> fs = Glib::RefPtr<Gtk::ListStore>::cast_static(FilterStore); + Gtk::TreeModel::Row row = *(fs->append()); + + + + + +} +FilterEditorDialog::~FilterEditorDialog()= default; + + + + + + +} // Never put these namespaces together unless you are using gcc 6+ +} +} // P.S. This is for Inkscape::UI::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/filter-editor.h b/src/ui/dialog/filter-editor.h new file mode 100644 index 0000000..e20ecbf --- /dev/null +++ b/src/ui/dialog/filter-editor.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Filter Editor dialog + */ +/* Authors: + * Marc Jeanmougin + * + * Copyright (C) 2017 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_FILTER_EDITOR_H +#define INKSCAPE_UI_DIALOG_FILTER_EDITOR_H + +#include <gtkmm/notebook.h> +#include <gtkmm/sizegroup.h> +#include <gtkmm/builder.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/combobox.h> +#include <gtkmm/drawingarea.h> +#include <gtkmm/liststore.h> + +#include <gtkmm/paned.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/treeview.h> + +#include "ui/widget/panel.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class FilterEditorDialog : public UI::Widget::Panel { +public: + + FilterEditorDialog(); + ~FilterEditorDialog() override; + + static FilterEditorDialog &getInstance() + { return *new FilterEditorDialog(); } + +// void set_attrs_locked(const bool); +private: + Glib::RefPtr<Gtk::Builder> builder; + Glib::RefPtr<Glib::Object> FilterStore; + Gtk::Box *FilterEditor; +}; +} +} +} +#endif diff --git a/src/ui/dialog/filter-effects-dialog.cpp b/src/ui/dialog/filter-effects-dialog.cpp new file mode 100644 index 0000000..cff7e3b --- /dev/null +++ b/src/ui/dialog/filter-effects-dialog.cpp @@ -0,0 +1,3114 @@ +// 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 "dialog-manager.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 "selection-chemistry.h" +#include "verbs.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 "style.h" + +#include "svg/svg-color.h" + +#include "ui/dialog/filedialog.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 SPAttributeEnum 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 SPAttributeEnum 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 SPAttributeEnum a = SP_ATTR_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::HBox +{ +public: + MultiSpinButton(double lower, double upper, double step_inc, + double climb_rate, int digits, std::vector<SPAttributeEnum> attrs, std::vector<double> default_values, std::vector<char*> tip_text) + { + 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::HBox, public AttrWidget +{ +public: + DualSpinButton(char* def, double lower, double upper, double step_inc, + double climb_rate, int digits, const SPAttributeEnum a, char* tt1, char* tt2) + : AttrWidget(a, def), //TO-DO: receive default num-opt-num as parameter in the constructor + _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 SPAttributeEnum 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 SPAttributeEnum 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 SPAttributeEnum 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"); + dynamic_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(SP_ATTR_VALUES), + // TRANSLATORS: this dialog is accessible via menu Filters - Filter editor + _matrix(SP_ATTR_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("", 0, 0, 1, 0.1, 0.01, 2, SP_ATTR_VALUES), + _angle("", 0, 0, 360, 0.1, 0.01, 1, SP_ATTR_VALUES), + _label(C_("Label", "None"), Gtk::ALIGN_START), + _use_stored(false), + _saturation_store(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 + { + 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); + break; + case COLORMATRIX_HUEROTATE: + add(_angle); + if(_use_stored) + _angle.set_value(_angle_store); + else + _angle.set_from_attribute(o); + 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); + break; + } + _use_stored = true; + } + } + + Glib::ustring get_as_attribute() const override + { + const Widget* w = get_child(); + if(w == &_label) + return ""; + else + return dynamic_cast<const AttrWidget*>(w)->get_as_attribute(); + } + + 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::HBox, public AttrWidget +{ +public: + FileOrElementChooser(const SPAttributeEnum a) + : AttrWidget(a) + { + 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(""); + } + } + + void set_desktop(SPDesktop* d){ + _desktop = d; + } + +private: + void select_svg_element(){ + Inkscape::Selection* sel = _desktop->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( + *_desktop->getToplevel(), + open_path, + Inkscape::UI::Dialog::SVG_TYPES,/*TODO: any image, not just svg*/ + (char const *)_("Select an image to be used as feImage 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; + SPDesktop* _desktop; +}; + +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::VBox(false, 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, ""); + } + + void add_notimplemented() + { + Gtk::Label* lbl = Gtk::manage(new Gtk::Label(_("This SVG filter effect is not yet implemented in Inkscape."))); + 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 SPAttributeEnum 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 SPAttributeEnum 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 SPAttributeEnum 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 SPAttributeEnum 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 SPAttributeEnum 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 SPAttributeEnum 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 SPAttributeEnum 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 SPAttributeEnum attr1, const SPAttributeEnum 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<SPAttributeEnum> 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 SPAttributeEnum attr1, const SPAttributeEnum attr2, + const SPAttributeEnum 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<SPAttributeEnum> 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 SPAttributeEnum attr, const Glib::ustring& label) + { + FileOrElementChooser* foech = new FileOrElementChooser(attr); + foech->set_desktop(_dialog.getDesktop()); + add_widget(foech, label); + add_attr_widget(foech); + return foech; + } + + // ComboBoxEnum + template<typename T> ComboBoxEnum<T>* add_combo(T default_value, const SPAttributeEnum 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 SPAttributeEnum 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::HBox *hb = Gtk::manage(new Gtk::HBox); + 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::VBox*> _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(SP_ATTR_INVALID), + _dialog(d), + _settings(d, _box, sigc::mem_fun(*this, &ComponentTransferValues::set_func_attr), COMPONENTTRANSFER_TYPE_ERROR), + _type(ComponentTransferTypeConverter, SP_ATTR_TYPE, false), + _channel(channel), + _funcNode(nullptr) + { + 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, SP_ATTR_SLOPE, _("Slope"), -10, 10, 0.1, 0.01, 2); + _settings.add_spinscale(0, SP_ATTR_INTERCEPT, _("Intercept"), -10, 10, 0.1, 0.01, 2); + + _settings.type(COMPONENTTRANSFER_TYPE_GAMMA); + _settings.add_spinscale(1, SP_ATTR_AMPLITUDE, _("Amplitude"), 0, 10, 0.1, 0.01, 2); + _settings.add_spinscale(1, SP_ATTR_EXPONENT, _("Exponent"), 0, 10, 0.1, 0.01, 2); + _settings.add_spinscale(0, SP_ATTR_OFFSET, _("Offset"), -10, 10, 0.1, 0.01, 2); + + _settings.type(COMPONENTTRANSFER_TYPE_TABLE); + _settings.add_entry(SP_ATTR_TABLEVALUES, _("Table")); + + _settings.type(COMPONENTTRANSFER_TYPE_DISCRETE); + _settings.add_entry(SP_ATTR_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, SP_VERB_DIALOG_FILTER_EFFECTS, _("New transfer function type")); + 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::VBox _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(SP_ATTR_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) + { + _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, SP_ATTR_AZIMUTH, _("Azimuth:"), 0, 360, 1, 1, 0, _("Direction angle for the light source on the XY plane, in degrees")); + _settings.add_spinscale(0, SP_ATTR_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, SP_ATTR_X, SP_ATTR_Y, SP_ATTR_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, SP_ATTR_X, SP_ATTR_Y, SP_ATTR_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, + SP_ATTR_POINTSATX, SP_ATTR_POINTSATY, SP_ATTR_POINTSATZ, + _("Points At:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate")); + _settings.add_spinscale(1, SP_ATTR_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, SP_ATTR_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::VBox& 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, SP_VERB_DIALOG_FILTER_EFFECTS, _("New light source")); + 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::VBox _box; + Settings _settings; + Gtk::HBox _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) + : _desktop(nullptr), + _deskTrack(), + _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 *item = Gtk::manage(new Gtk::MenuItem(_("R_ename"), true)); + item->signal_activate().connect(sigc::mem_fun(*this, &FilterModifier::rename_filter)); + item->show(); + _menu->append(*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()); + + desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &FilterModifier::setTargetDesktop) ); + _deskTrack.connect(GTK_WIDGET(gobj())); + + update_filters(); +} + +FilterEffectsDialog::FilterModifier::~FilterModifier() +{ + _selectChangedConn.disconnect(); + _selectModifiedConn.disconnect(); + _resource_changed.disconnect(); + _doc_replaced.disconnect(); +} + +void FilterEffectsDialog::FilterModifier::setTargetDesktop(SPDesktop *desktop) +{ + if (_desktop != desktop) { + if (_desktop) { + _selectChangedConn.disconnect(); + _selectModifiedConn.disconnect(); + _doc_replaced.disconnect(); + _resource_changed.disconnect(); + _dialog.setDesktop(nullptr); + } + _desktop = desktop; + if (desktop) { + if (desktop->selection) { + _selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &FilterModifier::on_change_selection))); + _selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &FilterModifier::on_modified_selection))); + } + _doc_replaced = desktop->connectDocumentReplaced( sigc::mem_fun(*this, &FilterModifier::on_document_replaced)); + _resource_changed = desktop->getDocument()->connectResourcesChanged("filter",sigc::mem_fun(*this, &FilterModifier::update_filters)); + _dialog.setDesktop(desktop); + + update_filters(); + } + } +} + +// When the document changes, update connection to resources +void FilterEffectsDialog::FilterModifier::on_document_replaced(SPDesktop * /*desktop*/, SPDocument *document) +{ + if (_resource_changed) { + _resource_changed.disconnect(); + } + if (document) + { + _resource_changed = document->connectResourcesChanged("filter",sigc::mem_fun(*this, &FilterModifier::update_filters)); + } + + update_filters(); +} + +// When the selection changes, show the active filter(s) in the dialog +void FilterEffectsDialog::FilterModifier::on_change_selection() +{ + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + update_selection(selection); +} + +void FilterEffectsDialog::FilterModifier::on_modified_selection( guint flags ) +{ + if (flags & ( SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_PARENT_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG) ) { + on_change_selection(); + } +} + +// 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<SPObject*> 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, SP_VERB_DIALOG_FILTER_EFFECTS, _("Rename filter")); + 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(); + SPFilter* filter = (*iter)[_columns.filter]; + Inkscape::Selection *sel = desktop->getSelection(); + + /* If this filter is the only one used in the selection, unset it */ + if((*iter)[_columns.sel] == 1) + filter = nullptr; + + auto itemlist= sel->items(); + for(auto i=itemlist.begin(); itemlist.end() != i; ++i) { + SPItem * item = *i; + SPStyle *style = item->style; + g_assert(style != nullptr); + + if (filter) { + 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, SP_VERB_DIALOG_FILTER_EFFECTS, _("Apply filter")); + } +} + + +void FilterEffectsDialog::FilterModifier::update_counts() +{ + for(const auto & i : _model->children()) { + SPFilter* f = SP_FILTER(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() +{ + SPDesktop* desktop = _dialog.getDesktop(); + SPDocument* document = desktop->getDocument(); + + // Workaround for 1.0, not needed in 1.1 (which properly disconnects signals) + if (!document) { + return; + } + + std::vector<SPObject *> filters = document->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(desktop->selection); + _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); + + _menu->popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void FilterEffectsDialog::FilterModifier::add_filter() +{ + SPDocument* doc = _dialog.getDesktop()->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, SP_VERB_DIALOG_FILTER_EFFECTS, _("Add filter")); +} + +void FilterEffectsDialog::FilterModifier::remove_filter() +{ + SPFilter *filter = get_selected_filter(); + + if(filter) { + SPDocument* doc = filter->document; + + // Delete all references to this filter + std::vector<SPItem*> x,y; + std::vector<SPItem*> all = get_all_items(x, _desktop->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, SP_VERB_DIALOG_FILTER_EFFECTS, _("Remove filter")); + + 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, SP_VERB_DIALOG_FILTER_EFFECTS, _("Duplicate filter")); + + update_filters(); + } +} + +void FilterEffectsDialog::FilterModifier::rename_filter() +{ + _list.set_cursor(_model->get_path(_list.get_selection()->get_selected()), *_list.get_column(1), true); +} + +FilterEffectsDialog::CellRendererConnection::CellRendererConnection() + : Glib::ObjectBase(typeid(CellRendererConnection)), + _primitive(*this, "primitive", nullptr), + _text_width(0) +{} + +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.getDesktop()->getDocument(), SP_VERB_DIALOG_FILTER_EFFECTS, + _("Remove filter primitive")); + + 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 = gtk_widget_get_style_context(GTK_WIDGET(gobj())); + GdkRGBA bg_color, fg_color; + gtk_style_context_get_color(sc, GTK_STATE_FLAG_NORMAL, &fg_color); + gtk_style_context_get_background_color(sc, GTK_STATE_FLAG_NORMAL, &bg_color); + GdkRGBA orig_color {fg_color}; + + auto lerp = [](double v0, double v1, double t){ return (1.0 - t) * v0 + t * v1; }; + fg_color.red = lerp(bg_color.red, orig_color.red, 0.95); + fg_color.green = lerp(bg_color.green, orig_color.green, 0.95); + fg_color.blue = lerp(bg_color.blue, orig_color.blue, 0.95); + bg_color.red = lerp(bg_color.red, orig_color.red, 0.05); + bg_color.green = lerp(bg_color.green, orig_color.green, 0.05); + bg_color.blue = lerp(bg_color.blue, orig_color.blue, 0.05); + GdkRGBA mid_color { + lerp(bg_color.red, fg_color.red, 0.65), + lerp(bg_color.green, fg_color.green, 0.65), + lerp(bg_color.blue, fg_color.blue, 0.65), + fg_color.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->cobj(), &bg_color); + cr->rectangle(x, 0, get_input_type_width(), vis.get_height()); + cr->fill_preserve(); + + gdk_cairo_set_source_rgba(cr->cobj(), &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->cobj(), &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()); + cairo_clip(cr->cobj()); + } + + 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(); + cairo_set_line_width (cr->cobj(),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->cobj(), &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->cobj(), 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, i, text_start_x, outline_x, con_poly[2].get_y(), row_count, 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->cobj(), 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, SP_ATTR_IN, text_start_x, outline_x, con_poly[2].get_y(), row_count, 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->cobj(), 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, SP_ATTR_IN2, text_start_x, outline_x, con_poly[2].get_y(), row_count, fg_color, mid_color); + } + } + } + + // Draw drag connection + if(row_prim == prim && _in_drag) { + cr->save(); + gdk_cairo_set_source_rgba(cr->cobj(), &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 int attr, + const int text_start_x, const int x1, const int y1, + const int row_count, const GdkRGBA fg_color, const GdkRGBA mid_color) +{ + cr->save(); + + int src_id = 0; + Gtk::TreeIter res = find_result(input, attr, src_id); + + 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->cobj(), &mid_color); + else + gdk_cairo_set_source_rgba(cr->cobj(), &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->cobj(), &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 int attr, int& src_id) +{ + 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 == attr && SP_IS_FEMERGENODE(&o)) { + image = SP_FEMERGENODE(&o)->input; + found = true; + } + ++c; + } + if(!found) + return target; + } + else { + if(attr == SP_ATTR_IN) + image = prim->image_in; + else if(attr == SP_ATTR_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, SP_VERB_DIALOG_FILTER_EFFECTS, + _("Remove merge node")); + (*get_selection()->get_selected())[_columns.primitive] = prim; + } else { + _dialog.set_attr(&o, SP_ATTR_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, SP_ATTR_IN, in_val); + (*get_selection()->get_selected())[_columns.primitive] = prim; + } + } + else { + if(_in_drag == 1) + _dialog.set_attr(prim, SP_ATTR_IN, in_val); + else if(_in_drag == 2) + _dialog.set_attr(prim, SP_ATTR_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, SP_VERB_DIALOG_FILTER_EFFECTS, _("Reorder filter primitive")); +} + +// 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 = dynamic_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 = dynamic_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() + : UI::Widget::Panel("/dialogs/filtereffects", SP_VERB_DIALOG_FILTER_EFFECTS), + _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 = 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::HBox* infobox = Gtk::manage(new Gtk::HBox(/*homogeneous:*/false, /*spacing:*/4)); + Gtk::HBox* hb_prims = Gtk::manage(new Gtk::HBox); + Gtk::VBox* vb_prims = Gtk::manage(new Gtk::VBox); + Gtk::VBox* vb_desc = Gtk::manage(new Gtk::VBox); + + Gtk::VBox* prim_vbox_p = Gtk::manage(new Gtk::VBox); + Gtk::VBox* prim_vbox_i = Gtk::manage(new Gtk::VBox); + + 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); + _getContents()->add(*hpaned); + + _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); + _infobox_desc.set_size_request(300, -1); + + vb_desc->pack_start(_infobox_desc, true, true); + + infobox->pack_start(_infobox_icon, false, false); + infobox->pack_start(*vb_desc, true, true); + + //_sw_infobox->set_size_request(-1, -1); + _sw_infobox->set_size_request(300, -1); + _sw_infobox->add(*infobox); + + //vb_prims->set_size_request(-1, 50); + 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); + _getContents()->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)); + + show_all_children(); + init_settings_widgets(); + _primitive_list.update(); + update_primitive_infobox(); +} + +FilterEffectsDialog::~FilterEffectsDialog() +{ + delete _settings; + delete _filter_general_settings; +} + +void FilterEffectsDialog::set_attrs_locked(const bool l) +{ + _locked = l; +} + +void FilterEffectsDialog::show_all_vfunc() +{ + UI::Widget::Panel::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); + _filter_general_settings->add_multispinbutton(/*default x:*/ (double) -0.1, /*default y:*/ (double) -0.1, SP_ATTR_X, SP_ATTR_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")); + _filter_general_settings->add_multispinbutton(/*default width:*/ (double) 1.2, /*default height:*/ (double) 1.2, SP_ATTR_WIDTH, SP_ATTR_HEIGHT, _("Dimensions:"), 0, 1000, 0.01, 0.1, 2, _("Width of filter effects region"), _("Height of filter effects region")); + + _settings->type(NR_FILTER_BLEND); + _settings->add_combo(SP_CSS_BLEND_NORMAL, SP_ATTR_MODE, _("Mode:"), SPBlendModeConverter); + + _settings->type(NR_FILTER_COLORMATRIX); + ComboBoxEnum<FilterColorMatrixType>* colmat = _settings->add_combo(COLORMATRIX_MATRIX, SP_ATTR_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); + _settings->add_componenttransfervalues(_("R:"), SPFeFuncNode::R); + _settings->add_componenttransfervalues(_("G:"), SPFeFuncNode::G); + _settings->add_componenttransfervalues(_("B:"), SPFeFuncNode::B); + _settings->add_componenttransfervalues(_("A:"), SPFeFuncNode::A); + + _settings->type(NR_FILTER_COMPOSITE); + _settings->add_combo(COMPOSITE_OVER, SP_ATTR_OPERATOR, _("Operator:"), CompositeOperatorConverter); + _k1 = _settings->add_spinscale(0, SP_ATTR_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, SP_ATTR_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, SP_ATTR_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, SP_ATTR_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", SP_ATTR_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, SP_ATTR_TARGETX, SP_ATTR_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(SP_ATTR_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, SP_ATTR_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, SP_ATTR_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, SP_ATTR_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, SP_ATTR_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, SP_PROP_LIGHTING_COLOR, _("Diffuse Color:"), _("Defines the color of the light source")); + _settings->add_spinscale(1, SP_ATTR_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, SP_ATTR_DIFFUSECONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model.")); + _settings->add_dualspinscale(SP_ATTR_KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1); + _settings->add_lightsource(); + + _settings->type(NR_FILTER_DISPLACEMENTMAP); + _settings->add_spinscale(0, SP_ATTR_SCALE, _("Scale:"), 0, 100, 1, 0.01, 1, _("This defines the intensity of the displacement effect.")); + _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SP_ATTR_XCHANNELSELECTOR, _("X displacement:"), DisplacementMapChannelConverter, _("Color component that controls the displacement in the X direction")); + _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SP_ATTR_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, SP_PROP_FLOOD_COLOR, _("Flood Color:"), _("The whole filter region will be filled with this color.")); + _settings->add_spinscale(1, SP_PROP_FLOOD_OPACITY, _("Opacity:"), 0, 1, 0.1, 0.01, 2); + + _settings->type(NR_FILTER_GAUSSIANBLUR); + _settings->add_dualspinscale(SP_ATTR_STDDEVIATION, _("Standard Deviation:"), 0.01, 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, SP_ATTR_OPERATOR, _("Operator:"), MorphologyOperatorConverter, _("Erode: performs \"thinning\" of input image.\nDilate: performs \"fattenning\" of input image.")); + _settings->add_dualspinscale(SP_ATTR_RADIUS, _("Radius:"), 0, 100, 1, 0.01, 1); + + _settings->type(NR_FILTER_IMAGE); + _settings->add_fileorelement(SP_ATTR_XLINK_HREF, _("Source of Image:")); + _image_x = _settings->add_entry(SP_ATTR_X,_("X"),_("X")); + _image_x->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_x_changed)); + //This commented because we want the default empty value of X or Y and couldent get it from SpinButton + //_image_y = _settings->add_spinbutton(0, SP_ATTR_Y, _("Y:"), -DBL_MAX, DBL_MAX, 1, 1, 5, _("Y")); + _image_y = _settings->add_entry(SP_ATTR_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, SP_ATTR_PRESERVEALPHA, _("Preserve Alpha"), "true", "false", _("If set, the alpha channel won't be altered by this filter primitive.")); + _settings->add_spinscale(0, SP_ATTR_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, SP_ATTR_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, SP_PROP_LIGHTING_COLOR, _("Specular Color:"), _("Defines the color of the light source")); + _settings->add_spinscale(1, SP_ATTR_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, SP_ATTR_SPECULARCONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model.")); + _settings->add_spinscale(1, SP_ATTR_SPECULAREXPONENT, _("Exponent:"), 1, 50, 1, 0.01, 1, _("Exponent for specular term, larger is more \"shiny\".")); + _settings->add_dualspinscale(SP_ATTR_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, SP_ATTR_STITCHTILES, _("Stitch Tiles"), "stitch", "noStitch"); + _settings->add_combo(TURBULENCE_TURBULENCE, SP_ATTR_TYPE, _("Type:"), TurbulenceTypeConverter, _("Indicates whether the filter primitive should perform a noise or turbulence function.")); + _settings->add_dualspinscale(SP_ATTR_BASEFREQUENCY, _("Base Frequency:"), 0, 1, 0.001, 0.01, 3); + _settings->add_spinscale(1, SP_ATTR_NUMOCTAVES, _("Octaves:"), 1, 10, 1, 1, 0); + _settings->add_spinscale(0, SP_ATTR_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, SP_VERB_DIALOG_FILTER_EFFECTS, _("Add filter primitive")); + } +} + +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(_("The <b>feBlend</b> filter primitive provides 4 image blending modes: 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(_("The <b>feColorMatrix</b> filter primitive applies a matrix transformation to color of each rendered pixel. This allows for effects like turning object to grayscale, modifying color saturation and changing color hue.")); + break; + case(NR_FILTER_COMPONENTTRANSFER): + _infobox_icon.set_from_icon_name("feComponentTransfer-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feComponentTransfer</b> filter primitive manipulates the input's color components (red, green, blue, and alpha) according to particular transfer functions, allowing operations like 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(_("The <b>feComposite</b> filter primitive composites two images using one of the Porter-Duff blending modes or the arithmetic mode described in SVG standard. Porter-Duff blending modes are essentially logical operations between the corresponding pixel values of the images.")); + break; + case(NR_FILTER_CONVOLVEMATRIX): + _infobox_icon.set_from_icon_name("feConvolveMatrix-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feConvolveMatrix</b> lets you specify a Convolution to be applied on the image. Common effects created using convolution matrices are blur, sharpening, embossing and edge detection. Note that while gaussian blur can be created using this filter primitive, the special gaussian blur primitive is faster and resolution-independent.")); + break; + case(NR_FILTER_DIFFUSELIGHTING): + _infobox_icon.set_from_icon_name("feDiffuseLighting-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feDiffuseLighting</b> and feSpecularLighting filter primitives create \"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(_("The <b>feDisplacementMap</b> filter primitive displaces the pixels in the first input using the second input as a displacement map, that shows from how far the pixel should come from. 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(_("The <b>feFlood</b> filter primitive fills the region with a given color and opacity. It is usually used as an 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(_("The <b>feGaussianBlur</b> filter primitive uniformly blurs its input. It is commonly used together with feOffset 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(_("The <b>feImage</b> filter primitive fills the region with an external image or another part of the document.")); + break; + case(NR_FILTER_MERGE): + _infobox_icon.set_from_icon_name("feMerge-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feMerge</b> filter primitive composites several temporary images inside the filter primitive to a single image. It uses normal alpha compositing for this. This is equivalent to using several feBlend primitives in 'normal' mode or several feComposite 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(_("The <b>feMorphology</b> filter primitive 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(_("The <b>feOffset</b> filter primitive offsets the image by an user-defined amount. For example, this is useful for drop shadows, where the shadow is in a slightly different position than the actual object.")); + break; + case(NR_FILTER_SPECULARLIGHTING): + _infobox_icon.set_from_icon_name("feSpecularLighting-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feDiffuseLighting</b> and <b>feSpecularLighting</b> filter primitives create \"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(_("The <b>feTile</b> filter primitive 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(_("The <b>feTurbulence</b> filter primitive renders Perlin noise. This kind of noise is useful in simulating several nature phenomena like clouds, fire and smoke and in generating complex textures like 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, SP_VERB_DIALOG_FILTER_EFFECTS, _("Duplicate filter primitive")); + + _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 = atof( text.c_str() ); + 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 SPAttributeEnum 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(), SP_VERB_DIALOG_FILTER_EFFECTS, + _("Set filter primitive attribute")); + } + + _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()); +} + +} // 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..0ed28d1 --- /dev/null +++ b/src/ui/dialog/filter-effects-dialog.h @@ -0,0 +1,346 @@ +// 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 <memory> + +#include <gtkmm/notebook.h> + +#include <gtkmm/paned.h> +#include <gtkmm/scrolledwindow.h> + +#include "attributes.h" +#include "display/nr-filter-types.h" +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/combo-enums.h" +#include "ui/widget/panel.h" +#include "ui/widget/spin-scale.h" + +#include "xml/helper-observer.h" + +class SPFilter; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class EntryAttr; +//class SpinButtonAttr; +class DualSpinButton; +class MultiSpinButton; +class FilterEffectsDialog : public UI::Widget::Panel { +public: + + FilterEffectsDialog(); + ~FilterEffectsDialog() override; + + static FilterEffectsDialog &getInstance() + { return *new FilterEffectsDialog(); } + + void set_attrs_locked(const bool); +protected: + void show_all_vfunc() override; +private: + + class FilterModifier : public Gtk::VBox + { + public: + FilterModifier(FilterEffectsDialog&); + ~FilterModifier() override; + + 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 setTargetDesktop(SPDesktop *desktop); + + void on_document_replaced(SPDesktop *desktop, SPDocument *document); + void on_change_selection(); + void on_modified_selection( guint flags ); + + void update_selection(Selection *); + 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 update_filters(); + void filter_list_button_release(GdkEventButton*); + void add_filter(); + void remove_filter(); + void duplicate_filter(); + void rename_filter(); + + /** + * Stores the current desktop. + */ + SPDesktop *_desktop; + + /** + * Auxiliary widget to keep track of desktop changes for the floating dialog. + */ + DesktopTracker _deskTrack; + + /** + * Link to callback function for a change in desktop (window). + */ + sigc::connection desktopChangeConn; + sigc::connection _selectChangedConn; + sigc::connection _selectModifiedConn; + sigc::connection _doc_replaced; + sigc::connection _resource_changed; + + 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; + int _text_width; + }; + + 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 int attr, int& src_id); + int find_index(const Gtk::TreeIter& target); + void draw_connection(const Cairo::RefPtr<Cairo::Context>& cr, + const Gtk::TreeIter&, const int attr, const int text_start_x, + const int x1, const int y1, const int row_count, + const GdkRGBA fg_color, const GdkRGBA 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 SPAttributeEnum, const gchar* val); + void update_settings_view(); + void update_filter_general_settings_view(); + void update_settings_sensitivity(); + void update_color_matrix(); + 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::VBox _settings_tab2; + Gtk::VBox _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; + + // 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..a6d53bb --- /dev/null +++ b/src/ui/dialog/find.cpp @@ -0,0 +1,1076 @@ +// 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 "message-stack.h" +#include "selection-chemistry.h" +#include "text-editing.h" +#include "verbs.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/dialog-events.h" + +#include "xml/attribute-record.h" +#include "xml/node-iterators.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +Find::Find() + : UI::Widget::Panel("/dialogs/find", SP_VERB_DIALOG_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")), + vbox_searchin(false, false), + 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")), + 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")), + + status(""), + button_find(_("_Find")), + button_replace(_("_Replace All")), + _action_replace(false), + blocked(false), + desktop(nullptr), + deskTrack() + +{ + _left_size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + _right_size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_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_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_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); + _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); + + Gtk::Box *contents = _getContents(); + contents->set_spacing(6); + contents->pack_start(entry_find, false, false); + contents->pack_start(entry_replace, false, false); + contents->pack_start(hbox_searchin, false, false); + contents->pack_start(expander_options, false, false); + contents->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_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(); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &Find::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); + + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + SPItem *item = selection->singleItem(); + if (item) { + if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) { + gchar *str; + str = sp_te_get_string_multiline (item); + entry_find.getEntry()->set_text(str); + } + } + + button_find.set_can_default(); + //button_find.grab_default(); // activatable by Enter + entry_find.getEntry()->grab_focus(); +} + +Find::~Find() +{ + desktopChangeConn.disconnect(); + selectChangedConn.disconnect(); + deskTrack.disconnect(); +} + +void Find::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void Find::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (this->desktop) { + selectChangedConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &Find::onSelectionChange))); + } + } +} + +void Find::onSelectionChange() +{ + 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; + 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 + strlen(replace) + 1); + } + 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_text_match (SPItem *item, const gchar *find, bool exact, bool casematch, bool replace/*=false*/) +{ + if (item->getRepr() == nullptr) { + return false; + } + + if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) { + const gchar *item_text = sp_te_get_string_multiline (item); + if (item_text == nullptr) { + return false; + } + bool found = find_strcmp(item_text, 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; + } + + gchar* replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + gsize n = find_strcmp_pos(item_text, 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 + strlen(find)); + sp_te_replace(item, _begin_w, _end_w, replace_text); + item_text = sp_te_get_string_multiline (item); + n = find_strcmp_pos(item_text, ufind.c_str(), exact, casematch, n + strlen(replace_text) + 1); + } + + g_free(replace_text); + } + + 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; + } + + Inkscape::Util::List<Inkscape::XML::AttributeRecord const> iter = item->getRepr()->attributeList(); + for (; iter; ++iter) { + 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 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); + } + } + } + } + } + + } + + 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) && !desktop->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 + } + + for (auto& child: r->children) { + SPItem *item = dynamic_cast<SPItem *>(&child); + if (item && !child.cloned && !desktop->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 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->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() +{ + + 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->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->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(), SP_VERB_CONTEXT_TEXT, _("Replace text or property")); + } + + } 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..0eeeee0 --- /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 "ui/widget/panel.h" +#include "ui/widget/entry.h" +#include "ui/widget/frame.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/desktop-tracker.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 UI::Widget::Panel { +public: + Find(); + ~Find() 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 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(); + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + + /** + * Called when desktop selection changes + */ + void onSelectionChange(); + +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::HBox hbox_searchin; + Gtk::VBox vbox_scope; + Gtk::VBox 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::VBox vbox_options1; + Gtk::VBox vbox_options2; + Gtk::HBox hbox_options; + Gtk::VBox 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::HBox hbox_properties; + Gtk::VBox vbox_properties1; + Gtk::VBox 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::VBox vbox_types1; + Gtk::VBox vbox_types2; + Gtk::HBox 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::HBox hbox_text; + + /** + * Action Buttons and status + */ + Gtk::Label status; + Gtk::Button button_find; + Gtk::Button button_replace; + Gtk::ButtonBox box_buttons; + Gtk::HBox hboxbutton_row; + + /** + * Finding or replacing + */ + bool _action_replace; + bool blocked; + + SPDesktop *desktop; + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; + 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/floating-behavior.cpp b/src/ui/dialog/floating-behavior.cpp new file mode 100644 index 0000000..804f1f0 --- /dev/null +++ b/src/ui/dialog/floating-behavior.cpp @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Floating dialog implementation. + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/dialog.h> +#include <glibmm/main.h> + +#include "floating-behavior.h" +#include "dialog.h" + +#include "inkscape.h" +#include "desktop.h" +#include "ui/dialog-events.h" +#include "verbs.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { +namespace Behavior { + +FloatingBehavior::FloatingBehavior(Dialog &dialog) : + Behavior(dialog), + _d (new Gtk::Dialog(_dialog._title)) + ,_dialog_active(_d->property_is_active()) + ,_trans_focus(Inkscape::Preferences::get()->getDoubleLimited("/dialogs/transparency/on-focus", 0.95, 0.0, 1.0)) + ,_trans_blur(Inkscape::Preferences::get()->getDoubleLimited("/dialogs/transparency/on-blur", 0.50, 0.0, 1.0)) + ,_trans_time(Inkscape::Preferences::get()->getIntLimited("/dialogs/transparency/animate-time", 100, 0, 5000)) +{ + hide(); + + signal_delete_event().connect(sigc::mem_fun(_dialog, &Inkscape::UI::Dialog::Dialog::_onDeleteEvent)); + + sp_transientize(GTK_WIDGET(_d->gobj())); + _dialog.retransientize_suppress = false; +} + +FloatingBehavior::~FloatingBehavior() +{ + delete _d; + _d = nullptr; +} + +Behavior * +FloatingBehavior::create(Dialog &dialog) +{ + return new FloatingBehavior(dialog); +} + +inline FloatingBehavior::operator Gtk::Widget &() { return *_d; } +inline GtkWidget *FloatingBehavior::gobj() { return GTK_WIDGET(_d->gobj()); } +inline Gtk::Box* FloatingBehavior::get_vbox() { + return _d->get_content_area(); +} +inline void FloatingBehavior::present() { _d->present(); } +inline void FloatingBehavior::hide() { _d->hide(); } +inline void FloatingBehavior::show() { _d->show(); } +inline void FloatingBehavior::show_all_children() { _d->show_all_children(); } +inline void FloatingBehavior::resize(int width, int height) { _d->resize(width, height); } +inline void FloatingBehavior::move(int x, int y) { _d->move(x, y); } +inline void FloatingBehavior::set_position(Gtk::WindowPosition position) { _d->set_position(position); } +inline void FloatingBehavior::set_size_request(int width, int height) { _d->set_size_request(width, height); } +inline void FloatingBehavior::size_request(Gtk::Requisition &requisition) { + Gtk::Requisition requisition_natural; + _d->get_preferred_size(requisition, requisition_natural); +} +inline void FloatingBehavior::get_position(int &x, int &y) { _d->get_position(x, y); } +inline void FloatingBehavior::get_size(int &width, int &height) { _d->get_size(width, height); } +inline void FloatingBehavior::set_title(Glib::ustring title) { _d->set_title(title); } +inline void FloatingBehavior::set_sensitive(bool sensitive) { _d->set_sensitive(sensitive); } + +Glib::SignalProxy0<void> FloatingBehavior::signal_show() { return _d->signal_show(); } +Glib::SignalProxy0<void> FloatingBehavior::signal_hide() { return _d->signal_hide(); } +Glib::SignalProxy1<bool, GdkEventAny *> FloatingBehavior::signal_delete_event () { return _d->signal_delete_event(); } + + +void +FloatingBehavior::onHideF12() +{ + _dialog.save_geometry(); + hide(); +} + +void +FloatingBehavior::onShowF12() +{ + show(); + _dialog.read_geometry(); +} + +void +FloatingBehavior::onShutdown() +{ + _dialog.save_status(!_dialog._user_hidden, 1, GDL_DOCK_TOP); // Make sure 1 == DockItem::FLOATING_STATE +} + +void +FloatingBehavior::onDesktopActivated (SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint transient_policy = prefs->getIntLimited("/options/transientpolicy/value", 1, 0, 2); + +#ifdef _WIN32 // Win32 special code to enable transient dialogs + transient_policy = 2; +#endif + + if (!transient_policy) + return; + + GtkWindow *dialog_win = GTK_WINDOW(_d->gobj()); + + if (_dialog.retransientize_suppress) { + /* if retransientizing of this dialog is still forbidden after + * previous call warning turned off because it was confusingly fired + * when loading many files from command line + */ + + // g_warning("Retranzientize aborted! You're switching windows too fast!"); + return; + } + + if (dialog_win) + { + _dialog.retransientize_suppress = true; // disallow other attempts to retranzientize this dialog + + desktop->setWindowTransient (dialog_win); + + /* + * This enables "aggressive" transientization, + * i.e. dialogs always emerging on top when you switch documents. Note + * however that this breaks "click to raise" policy of a window + * manager because the switched-to document will be raised at once + * (so that its transients also could raise) + */ + if (transient_policy == 2 && ! _dialog._hiddenF12 && !_dialog._user_hidden) { + // without this, a transient window not always emerges on top + gtk_window_present (dialog_win); + } + } + + // we're done, allow next retransientizing not sooner than after 120 msec + g_timeout_add (120, (GSourceFunc) sp_retransientize_again, (gpointer) &_dialog); +} + + +} // namespace Behavior +} // 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/floating-behavior.h b/src/ui/dialog/floating-behavior.h new file mode 100644 index 0000000..d4e9ebe --- /dev/null +++ b/src/ui/dialog/floating-behavior.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A floating dialog implementation. + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef INKSCAPE_UI_DIALOG_FLOATING_BEHAVIOR_H +#define INKSCAPE_UI_DIALOG_FLOATING_BEHAVIOR_H + +#include <glibmm/property.h> +#include "behavior.h" + +namespace Gtk { +class Dialog; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { +namespace Behavior { + +class FloatingBehavior : public Behavior { + +public: + static Behavior *create(Dialog &dialog); + + ~FloatingBehavior() override; + + /** Gtk::Dialog methods */ + operator Gtk::Widget &() override; + GtkWidget *gobj() override; + void present() override; + Gtk::Box *get_vbox() override; + void show() override; + void hide() override; + void show_all_children() override; + void resize(int width, int height) override; + void move(int x, int y) override; + void set_position(Gtk::WindowPosition) override; + void set_size_request(int width, int height) override; + void size_request(Gtk::Requisition &requisition) override; + void get_position(int &x, int &y) override; + void get_size(int& width, int &height) override; + void set_title(Glib::ustring title) override; + void set_sensitive(bool sensitive) override; + + /** Gtk::Dialog signal proxies */ + Glib::SignalProxy0<void> signal_show() override; + Glib::SignalProxy0<void> signal_hide() override; + Glib::SignalProxy1<bool, GdkEventAny *> signal_delete_event() override; + + /** Custom signal handlers */ + void onHideF12() override; + void onShowF12() override; + void onDesktopActivated(SPDesktop *desktop) override; + void onShutdown() override; + +private: + FloatingBehavior(Dialog& dialog); + + Gtk::Dialog *_d; //< the actual dialog + + Glib::PropertyProxy_ReadOnly<bool> _dialog_active; //< Variable proxy to track whether the dialog is the active window + float _trans_focus; //< The percentage opacity when the dialog is focused + float _trans_blur; //< The percentage opactiy when the dialog is not focused + int _trans_time; //< The amount of time (in ms) for the dialog to change it's transparency +}; + +} // namespace Behavior +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_FLOATING_BEHAVIOR_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..107c534 --- /dev/null +++ b/src/ui/dialog/font-substitution.h @@ -0,0 +1,58 @@ +// 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> + +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..0603f48 --- /dev/null +++ b/src/ui/dialog/glyphs.cpp @@ -0,0 +1,822 @@ +// 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 "verbs.h" + +#include "libnrtype/font-instance.h" +#include "libnrtype/font-lister.h" + +#include "object/sp-flowtext.h" +#include "object/sp-text.h" + +#include "ui/widget/font-selector.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() : + Inkscape::UI::Widget::Panel("/dialogs/glyphs", SP_VERB_DIALOG_GLYPHS), + store(Gtk::ListStore::create(*getColumns())), + deskTrack(), + instanceConns(), + desktopConns() +{ + auto table = new Gtk::Grid(); + table->set_row_spacing(4); + table->set_column_spacing(4); + _getContents()->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 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 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::HBox *box = new Gtk::HBox(); + + entry = new 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(*Gtk::manage(entry), Gtk::PACK_SHRINK); + + Gtk::Label *pad = new Gtk::Label(" "); + box->pack_start(*Gtk::manage(pad), Gtk::PACK_SHRINK); + + label = new Gtk::Label(" "); + box->pack_start(*Gtk::manage(label), Gtk::PACK_SHRINK); + + pad = new Gtk::Label(""); + box->pack_start(*Gtk::manage(pad), Gtk::PACK_EXPAND_WIDGET); + + insertBtn = new 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(*Gtk::manage(insertBtn), Gtk::PACK_SHRINK); + box->set_hexpand(); + table->attach( *Gtk::manage(box), 0, row, 3, 1); + + row++; + +// ------------------------------- + + + show_all_children(); + + // Connect this up last + conn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &GlyphsPanel::setTargetDesktop) ); + instanceConns.push_back(conn); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +GlyphsPanel::~GlyphsPanel() +{ + for (auto & instanceConn : instanceConns) { + instanceConn.disconnect(); + } + instanceConns.clear(); + for (auto & desktopConn : desktopConns) { + desktopConn.disconnect(); + } + desktopConns.clear(); +} + + +void GlyphsPanel::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void GlyphsPanel::setTargetDesktop(SPDesktop *desktop) +{ + if (targetDesktop != desktop) { + if (targetDesktop) { + for (auto & desktopConn : desktopConns) { + desktopConn.disconnect(); + } + desktopConns.clear(); + } + + targetDesktop = desktop; + + if (targetDesktop && targetDesktop->selection) { + sigc::connection conn = desktop->selection->connectChanged(sigc::hide(sigc::bind(sigc::mem_fun(*this, &GlyphsPanel::readSelection), true, true))); + desktopConns.push_back(conn); + + // Text selection within selected items has changed: + conn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::bind(sigc::mem_fun(*this, &GlyphsPanel::readSelection), true, false))); + desktopConns.push_back(conn); + + // Must check flags, so can't call performUpdate() directly. + conn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &GlyphsPanel::selectionModifiedCB))); + desktopConns.push_back(conn); + + readSelection(true, true); + } + } +} + +// Append selected glyphs to selected text +void GlyphsPanel::insertText() +{ + SPItem *textItem = nullptr; + auto itemlist= targetDesktop->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; + gchar *str = sp_te_get_string_multiline(textItem); + if (str) { + combined = str; + g_free(str); + str = nullptr; + } + combined += glyphs; + sp_te_set_repr_text_multiline(textItem, combined.c_str()); + DocumentUndo::done(targetDesktop->doc(), SP_VERB_CONTEXT_TEXT, _("Append 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::selectionModifiedCB(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); +} + +void GlyphsPanel::calcCanInsert() +{ + int items = 0; + auto itemlist= targetDesktop->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..3307683 --- /dev/null +++ b/src/ui/dialog/glyphs.h @@ -0,0 +1,97 @@ +// 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 "ui/widget/panel.h" +#include <gtkmm/treemodel.h> +#include "ui/dialog/desktop-tracker.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 Inkscape::UI::Widget::Panel +{ +public: + GlyphsPanel(); + ~GlyphsPanel() override; + + static GlyphsPanel& getInstance(); + + void setDesktop(SPDesktop *desktop) 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 setTargetDesktop(SPDesktop *desktop); + void selectionModifiedCB(guint flags); + void readSelection( bool updateStyle, bool updateContent ); + void calcCanInsert(); + void insertText(); + + + Glib::RefPtr<Gtk::ListStore> store; + Gtk::IconView *iconView; + Gtk::Entry *entry; + Gtk::Label *label; + Gtk::Button *insertBtn; + Gtk::ComboBoxText *scriptCombo; + Gtk::ComboBoxText *rangeCombo; + Inkscape::UI::Widget::FontSelector *fontSelector; + SPDesktop *targetDesktop; + DesktopTracker deskTrack; + + std::vector<sigc::connection> instanceConns; + std::vector<sigc::connection> desktopConns; +}; + + +} // 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..81baf78 --- /dev/null +++ b/src/ui/dialog/grid-arrange-tab.cpp @@ -0,0 +1,776 @@ +// 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 <glibmm/i18n.h> + +#include <gtkmm/grid.h> + +#include <2geom/transforms.h> + +#include "verbs.h" +#include "preferences.h" +#include "inkscape.h" + +#include "document.h" +#include "document-undo.h" +#include "desktop.h" +//#include "sp-item-transform.h" FIXME +#include "ui/dialog/tile.h" // for Inkscape::UI::Dialog::ArrangeDialog + + /* + * Sort items by their x co-ordinates, taking account of y (keeps rows intact) + * + * <0 *elem1 goes before *elem2 + * 0 *elem1 == *elem2 + * >0 *elem1 goes after *elem2 + */ + static bool sp_compare_x_position(SPItem *first, SPItem *second) + { + using Geom::X; + using Geom::Y; + + Geom::OptRect a = first->documentVisualBounds(); + Geom::OptRect b = second->documentVisualBounds(); + + if ( !a || !b ) { + // FIXME? + return false; + } + + double const a_height = a->dimensions()[Y]; + double const b_height = b->dimensions()[Y]; + + bool a_in_b_vert = false; + if ((a->min()[Y] < b->min()[Y] + 0.1) && (a->min()[Y] > b->min()[Y] - b_height)) { + a_in_b_vert = true; + } else if ((b->min()[Y] < a->min()[Y] + 0.1) && (b->min()[Y] > a->min()[Y] - a_height)) { + a_in_b_vert = true; + } else if (b->min()[Y] == a->min()[Y]) { + a_in_b_vert = true; + } else { + a_in_b_vert = false; + } + + if (!a_in_b_vert) { // a and b are not in the same row + return (a->min()[Y] < b->min()[Y]); + } + return (a->min()[X] < b->min()[X]); + } + + /* + * Sort items by their y co-ordinates. + */ + static bool sp_compare_y_position(SPItem *first, SPItem *second) + { + Geom::OptRect a = first->documentVisualBounds(); + Geom::OptRect b = second->documentVisualBounds(); + + if ( !a || !b ) { + // FIXME? + return false; + } + + if (a->min()[Geom::Y] > b->min()[Geom::Y]) { + return false; + } + if (a->min()[Geom::Y] < b->min()[Geom::Y]) { + return true; + } + + return false; + } + + + namespace Inkscape { + namespace UI { + namespace Dialog { + + + //######################################################################### + //## E V E N T S + //######################################################################### + + /* + * + * This arranges the selection in a grid pattern. + * + */ + + void GridArrangeTab::arrange() + { + + int cnt,row_cnt,col_cnt,a,row,col; + double grid_left,grid_top,col_width,row_height,paddingx,paddingy,width, height, new_x, new_y; + double total_col_width,total_row_height; + col_width = 0; + row_height = 0; + total_col_width=0; + total_row_height=0; + + // check for correct numbers in the row- and col-spinners + on_col_spinbutton_changed(); + on_row_spinbutton_changed(); + + // set padding to manual values + paddingx = XPadding.getValue("px"); + paddingy = YPadding.getValue("px"); + + std::vector<double> row_heights; + std::vector<double> col_widths; + std::vector<double> row_ys; + std::vector<double> col_xs; + + int NoOfCols = NoOfColsSpinner.get_value_as_int(); + int NoOfRows = NoOfRowsSpinner.get_value_as_int(); + + width = 0; + for (a=0;a<NoOfCols; a++){ + col_widths.push_back(width); + } + + height = 0; + for (a=0;a<NoOfRows; a++){ + row_heights.push_back(height); + } + grid_left = 99999; + grid_top = 99999; + + SPDesktop *desktop = Parent->getDesktop(); + desktop->getDocument()->ensureUpToDate(); + + Inkscape::Selection *selection = desktop->getSelection(); + std::vector<SPItem*> items; + if (selection) { + items.insert(items.end(), selection->items().begin(), selection->items().end()); + } + + for(auto item : items){ + Geom::OptRect b = item->documentVisualBounds(); + if (!b) { + continue; + } + + width = b->dimensions()[Geom::X]; + height = b->dimensions()[Geom::Y]; + + if (b->min()[Geom::X] < grid_left) { + grid_left = b->min()[Geom::X]; + } + if (b->min()[Geom::Y] < grid_top) { + grid_top = b->min()[Geom::Y]; + } + if (width > col_width) { + col_width = width; + } + if (height > row_height) { + row_height = height; + } + } + + + // require the sorting done before we can calculate row heights etc. + + g_return_if_fail(selection); + std::vector<SPItem*> sorted(selection->items().begin(), selection->items().end()); + sort(sorted.begin(),sorted.end(),sp_compare_y_position); + sort(sorted.begin(),sorted.end(),sp_compare_x_position); + + + // Calculate individual Row and Column sizes if necessary + + + cnt=0; + const std::vector<SPItem*> sizes(sorted); + for (auto item : sizes) { + Geom::OptRect b = item->documentVisualBounds(); + if (b) { + width = b->dimensions()[Geom::X]; + height = b->dimensions()[Geom::Y]; + if (width > col_widths[(cnt % NoOfCols)]) { + col_widths[(cnt % NoOfCols)] = width; + } + if (height > row_heights[(cnt / NoOfCols)]) { + row_heights[(cnt / NoOfCols)] = height; + } + } + + cnt++; + } + + + /// 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)); + } + + #ifdef DEBUG_GRID_ARRANGE + g_print("\n cx = %f cy= %f gridleft=%f",cx,cy,grid_left); + #endif + + // Calculate total widths and heights, allowing for columns and rows non uniformly sized. + + if (ColumnWidthButton.get_active()){ + total_col_width = col_width * NoOfCols; + col_widths.clear(); + for (a=0;a<NoOfCols; a++){ + col_widths.push_back(col_width); + } + } else { + for (a = 0; a < (int)col_widths.size(); a++) + { + total_col_width += col_widths[a] ; + } + } + + if (RowHeightButton.get_active()){ + total_row_height = row_height * NoOfRows; + row_heights.clear(); + for (a=0;a<NoOfRows; a++){ + row_heights.push_back(row_height); + } + } else { + for (a = 0; a < (int)row_heights.size(); a++) + { + total_row_height += row_heights[a] ; + } + } + + + Geom::OptRect sel_bbox = selection->visualBounds(); + // Fit to bbox, calculate padding between rows accordingly. + if ( sel_bbox && !SpaceManualRadioButton.get_active() ){ +#ifdef DEBUG_GRID_ARRANGE +g_print("\n row = %f col = %f selection x= %f selection y = %f", total_row_height,total_col_width, b.extent(Geom::X), b.extent(Geom::Y)); +#endif + paddingx = (sel_bbox->width() - total_col_width) / (NoOfCols -1); + paddingy = (sel_bbox->height() - total_row_height) / (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. + + for (a=0;a<NoOfCols; a++){ + if (a<1) col_xs.push_back(0); + else col_xs.push_back(col_widths[a-1]+paddingx+col_xs[a-1]); + } + + + for (a=0;a<NoOfRows; a++){ + if (a<1) row_ys.push_back(0); + else row_ys.push_back(row_heights[a-1]+paddingy+row_ys[a-1]); + } + + cnt=0; + std::vector<SPItem*>::iterator it = sorted.begin(); + for (row_cnt=0; ((it != sorted.end()) && (row_cnt<NoOfRows)); ++row_cnt) { + + std::vector<SPItem *> current_row; + col_cnt = 0; + for(;it!=sorted.end()&&col_cnt<NoOfCols;++it) { + current_row.push_back(*it); + col_cnt++; + } + + for (auto item:current_row) { + Inkscape::XML::Node *repr = item->getRepr(); + Geom::OptRect b = item->documentVisualBounds(); + Geom::Point min; + if (b) { + width = b->dimensions()[Geom::X]; + height = b->dimensions()[Geom::Y]; + min = b->min(); + } else { + width = height = 0; + min = Geom::Point(0, 0); + } + + row = cnt / NoOfCols; + col = cnt % NoOfCols; + + new_x = grid_left + (((col_widths[col] - width)/2)*HorizAlign) + col_xs[col]; + 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(), SP_VERB_SELECTION_ARRANGE, + _("Arrange in a grid")); + +} + + +//######################################################################### +//## E V E N T S +//######################################################################### + +/** + * changed value in # of columns spinbox. + */ +void GridArrangeTab::on_row_spinbutton_changed() +{ + // 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; + g_return_if_fail( selection ); + + int selcount = (int) boost::distance(selection->items()); + + double PerCol = ceil(selcount / NoOfColsSpinner.get_value()); + NoOfRowsSpinner.set_value(PerCol); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/NoOfCols", NoOfColsSpinner.get_value()); + updating=false; +} + +/** + * changed value in # of rows spinbox. + */ +void GridArrangeTab::on_col_spinbutton_changed() +{ + // 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; + g_return_if_fail(selection); + + int selcount = (int) boost::distance(selection->items()); + + double PerRow = ceil(selcount / NoOfRowsSpinner.get_value()); + NoOfColsSpinner.set_value(PerRow); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/NoOfCols", PerRow); + + updating=false; +} + +/** + * 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; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // 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); + prefs->setInt("/dialogs/gridtiler/NoOfCols", NoOfCols); + } + } else { + double PerRow = ceil(sqrt(selcount)); + double PerCol = ceil(sqrt(selcount)); + NoOfRowsSpinner.set_value(PerRow); + NoOfColsSpinner.set_value(PerCol); + prefs->setInt("/dialogs/gridtiler/NoOfCols", static_cast<int>(PerCol)); + } + } + + updating = false; +} + + +//######################################################################### +//## 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(); + + // could not do this in gtkmm - there's no Gtk::SizeGroup public constructor (!) + GtkSizeGroup *_col1 = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + GtkSizeGroup *_col2 = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + GtkSizeGroup *_col3 = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + + { + // Selection Change signal + INKSCAPE.signal_selection_changed.connect(sigc::hide<0>(sigc::mem_fun(*this, &GridArrangeTab::updateSelection))); + } + + Gtk::Box *contents = this; + +#define MARGIN 2 + + //##Set up the panel + + SPDesktop *desktop = Parent->getDesktop(); + + Inkscape::Selection *selection = desktop ? desktop->selection : nullptr; + g_return_if_fail( selection ); + int selcount = 1; + if (!selection->isEmpty()) { + selcount = (int) boost::distance(selection->items()); + } + + + /*#### Number of Rows ####*/ + + double PerRow = ceil(sqrt(selcount)); + double PerCol = ceil(sqrt(selcount)); + + #ifdef DEBUG_GRID_ARRANGE + g_print("/n PerRox = %f PerCol = %f selcount = %d",PerRow,PerCol,selcount); + #endif + + 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.set_value(PerCol); + 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); + gtk_size_group_add_widget(_col1, (GtkWidget *) NoOfRowsBox.gobj()); + + 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); + gtk_size_group_add_widget(_col2, GTK_WIDGET(XByYLabelVBox.gobj())); + + /*#### 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.set_value(PerRow); + 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); + gtk_size_group_add_widget(_col3, GTK_WIDGET(NoOfColsBox.gobj())); + + 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); + + //## The OK button FIXME + /*TileOkButton = addResponseButton(C_("Rows and columns dialog","_Arrange"), GTK_RESPONSE_APPLY); + TileOkButton->set_use_underline(true); + TileOkButton->set_tooltip_text(_("Arrange selected objects"));*/ + + show_all_children(); +} + +} //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..ff6afb8 --- /dev/null +++ b/src/ui/dialog/grid-arrange-tab.h @@ -0,0 +1,148 @@ +// 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> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ArrangeDialog; + +/** + * Dialog for tiling an object + */ +class GridArrangeTab : public ArrangeTab { +public: + GridArrangeTab(ArrangeDialog *parent); + ~GridArrangeTab() override = default;; + + /** + * 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 userHidden; + bool updating; + + Gtk::Box TileBox; + Gtk::Button *TileOkButton; + Gtk::Button *TileCancelButton; + + // 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::VBox 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; +}; + +} //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..03743b8 --- /dev/null +++ b/src/ui/dialog/guides.cpp @@ -0,0 +1,358 @@ +// 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 "verbs.h" + +#include "include/gtkmm_version.h" + +#include "object/sp-guide.h" +#include "object/sp-namedview.h" + +#include "display/guideline.h" + +#include "ui/dialog-events.h" +#include "ui/tools/tool-base.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() +{ + 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); + + DocumentUndo::done(_guide->document, SP_VERB_NONE, + _("Set guide properties")); +} + +void GuidelinePropertiesDialog::_onDelete() +{ + SPDocument *doc = _guide->document; + sp_guide_remove(_guide); + DocumentUndo::done(doc, SP_VERB_NONE, + _("Delete guide")); +} + +void GuidelinePropertiesDialog::_onDuplicate() +{ + _guide->duplicate(); + DocumentUndo::done(_guide->document, SP_VERB_NONE, _("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 + _spin_button_x.setDigits(3); + _spin_button_x.setIncrements(1.0, 10.0); + _spin_button_x.setRange(-1e6, 1e6); + _spin_button_y.setDigits(3); + _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.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->lockguides; + if(global_guides_lock){ + _locked_toggle.set_sensitive(false); + } + _locked_toggle.set_active(_guide->getLocked()); + + // don't know what this exactly does, but it results in that the dialog closes when entering a value and pressing enter (see LP bug 484187) + g_signal_connect_swapped(G_OBJECT(_spin_button_x.getWidget()->gobj()), "activate", + G_CALLBACK(gtk_window_activate_default), gobj()); + g_signal_connect_swapped(G_OBJECT(_spin_button_y.getWidget()->gobj()), "activate", + G_CALLBACK(gtk_window_activate_default), gobj()); + g_signal_connect_swapped(G_OBJECT(_spin_angle.getWidget()->gobj()), "activate", + G_CALLBACK(gtk_window_activate_default), gobj()); + + + // 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] ) ); + } + + { + // FIXME holy crap!!! + Inkscape::XML::Node *repr = _guide->getRepr(); + const gchar *guide_id = repr->attribute("id"); + gchar *label = g_strdup_printf(_("Guideline ID: %s"), guide_id); + _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; +} + +} +} +} + +/* + 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..abfc7d3 --- /dev/null +++ b/src/ui/dialog/guides.h @@ -0,0 +1,103 @@ +// 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 _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; +}; + +} // 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..f8e73e3 --- /dev/null +++ b/src/ui/dialog/icon-preview.cpp @@ -0,0 +1,682 @@ +// 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 "verbs.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(); + _getContents()->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() : + UI::Widget::Panel("/dialogs/iconpreview", SP_VERB_VIEW_ICON_PREVIEW), + deskTrack(), + desktop(nullptr), + document(nullptr), + drawing(nullptr), + visionkey(0), + timer(nullptr), + renderTimer(nullptr), + pending(false), + minDelay(0.1), + targetId(), + hot(1), + selectionButton(nullptr), + desktopChangeConn(), + docReplacedConn(), + docModConn(), + selChangedConn() +{ + 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::VBox* magBox = new Gtk::VBox(); + + 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::VBox *verts = new Gtk::VBox(); + Gtk::HBox *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 ); + + GdkPixbuf *pb = gdk_pixbuf_new_from_data( pixMem[i], GDK_COLORSPACE_RGB, TRUE, 8, sizes[i], sizes[i], stride, /*(GdkPixbufDestroyNotify)g_free*/nullptr, nullptr ); + GtkImage* img = GTK_IMAGE( gtk_image_new_from_pixbuf( pb ) ); + images[i] = Glib::wrap(img); + 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::HBox()); + 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 ); + + + _getContents()->pack_start(iconBox, Gtk::PACK_SHRINK); + + show_all_children(); + + // Connect this up last + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &IconPreviewPanel::setDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +IconPreviewPanel::~IconPreviewPanel() +{ + setDesktop(nullptr); + if (timer) { + timer->stop(); + delete timer; + timer = nullptr; + } + if ( renderTimer ) { + renderTimer->stop(); + delete renderTimer; + renderTimer = nullptr; + } + + selChangedConn.disconnect(); + docModConn.disconnect(); + docReplacedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.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::setDesktop( SPDesktop* desktop ) +{ + Panel::setDesktop(desktop); + + SPDocument *newDoc = (desktop) ? desktop->doc() : nullptr; + + if ( desktop != this->desktop ) { + docReplacedConn.disconnect(); + selChangedConn.disconnect(); + + this->desktop = Panel::getDesktop(); + if ( this->desktop ) { + docReplacedConn = this->desktop->connectDocumentReplaced(sigc::hide<0>(sigc::mem_fun(this, &IconPreviewPanel::setDocument))); + if ( this->desktop->selection && Inkscape::Preferences::get()->getBool("/iconpreview/autoRefresh", true) ) { + selChangedConn = this->desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(this, &IconPreviewPanel::queueRefresh))); + } + } + } + setDocument(newDoc); + deskTrack.setBase(desktop); +} + +void IconPreviewPanel::setDocument( SPDocument *document ) +{ + if (this->document != document) { + docModConn.disconnect(); + if (drawing) { + this->document->getRoot()->invoke_hide(visionkey); + delete drawing; + drawing = nullptr; + } + this->document = document; + if (this->document) { + drawing = new Inkscape::Drawing(); + visionkey = SPItem::display_key_new(1); + drawing->setRoot(this->document->getRoot()->invoke_show(*drawing, visionkey, SP_ITEM_SHOW_DISPLAY)); + + if ( Inkscape::Preferences::get()->getBool("/iconpreview/autoRefresh", true) ) { + docModConn = this->document->connectModified(sigc::hide(sigc::mem_fun(this, &IconPreviewPanel::queueRefresh))); + } + queueRefresh(); + } + } +} + +void IconPreviewPanel::refreshPreview() +{ + SPDesktop *desktop = getDesktop(); + 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 ( desktop && desktop->doc() ) { +#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()) ? desktop->doc()->getObjectById( targetId.c_str() ) : nullptr; + if ( !target ) { + targetId.clear(); + Inkscape::Selection * sel = desktop->getSelection(); + if ( sel ) { + //g_message("found a selection to play with"); + + auto items = sel->items(); + for(auto i=items.begin();!target && i!=items.end();++i){ + SPItem* item = *i; + gchar const *id = item->getId(); + if ( id ) { + targetId = id; + target = item; + } + } + } + } + } else { + target = desktop->currentRoot(); + } + 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()); + + SPNamedView *nv = sp_document_namedview(doc, nullptr); + float bg_r = SP_RGBA32_R_F(nv->pagecolor); + float bg_g = SP_RGBA32_G_F(nv->pagecolor); + float bg_b = SP_RGBA32_B_F(nv->pagecolor); + float bg_a = SP_RGBA32_A_F(nv->pagecolor); + + cairo_t *cr = cairo_create(s); + cairo_set_source_rgba(cr, bg_r, bg_g, bg_b, bg_a); + 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..938bbbf --- /dev/null +++ b/src/ui/dialog/icon-preview.h @@ -0,0 +1,119 @@ +// 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/label.h> +#include <gtkmm/paned.h> +#include <gtkmm/image.h> +#include <gtkmm/togglebutton.h> +#include <gtkmm/toggletoolbutton.h> + +#include "ui/widget/panel.h" +#include "desktop-tracker.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 UI::Widget::Panel +{ +public: + IconPreviewPanel(); + //IconPreviewPanel(Glib::ustring const &label); + ~IconPreviewPanel() override; + + static IconPreviewPanel& getInstance(); + + void setDesktop( SPDesktop* desktop ) override; + void refreshPreview(); + void modeToggled(); + +private: + IconPreviewPanel(IconPreviewPanel const &) = delete; // no copy + IconPreviewPanel &operator=(IconPreviewPanel const &) = delete; // no assign + + + DesktopTracker deskTrack; + SPDesktop *desktop; + SPDocument *document; + Drawing *drawing; + unsigned int visionkey; + Glib::Timer *timer; + Glib::Timer *renderTimer; + bool pending; + gdouble minDelay; + + Gtk::VBox 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 desktopChangeConn; + sigc::connection docReplacedConn; + sigc::connection docModConn; + sigc::connection selChangedConn; + + + void setDocument( SPDocument *document ); + 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..72a8107 --- /dev/null +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -0,0 +1,2772 @@ +// 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> + * + * 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 <gio/gio.h> +#include <glibmm/fileutils.h> +#include <glibmm/i18n.h> +#include <glibmm/markup.h> +#include <glibmm/miscutils.h> +#include <gtk/gtksettings.h> +#include <gtkmm/cssprovider.h> +#include <gtkmm/main.h> +#include <gtkmm/recentinfo.h> +#include <gtkmm/recentmanager.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 "shortcuts.h" +#include "verbs.h" + +/* #include "display/cairo-utils.h" */ +#include "display/canvas-grid.h" +#include "display/nr-filter-gaussian.h" + +#include "extension/internal/gdkpixbuf-input.h" + +#include "include/gtkmm_version.h" + +#include "io/resource.h" +#include "io/sys.h" + +#include "object/color-profile.h" +#include "style.h" +#include "svg/svg-color.h" +#include "ui/interface.h" +#include "ui/widget/style-swatch.h" +#include "widgets/desktop-widget.h" +#include <fstream> + +#if HAVE_ASPELL +# 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::PrefSpinButton; +using Inkscape::UI::Widget::StyleSwatch; +using Inkscape::CMSSystem; + +#define REMOVE_SPACES(x) \ + x.erase(0, x.find_first_not_of(' ')); \ + x.erase(x.find_last_not_of(' ') + 1); + +InkscapePreferences::InkscapePreferences() + : UI::Widget::Panel ("/dialogs/preferences", SP_VERB_DIALOG_DISPLAY), + _minimum_width(0), + _minimum_height(0), + _natural_width(0), + _natural_height(0), + _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); + _getContents()->add(*sb); + show_all_children(); + Gtk::Requisition sreq; + Gtk::Requisition sreq_natural; + sb->get_preferred_size(sreq_natural, sreq); + _sb_width = sreq.width; + _getContents()->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); + _getContents()->add(*hbox_list_page); + + //Pagelist + Gtk::Frame* list_frame = Gtk::manage(new Gtk::Frame()); + Gtk::ScrolledWindow* scrolled_window = Gtk::manage(new Gtk::ScrolledWindow()); + hbox_list_page->pack_start(*list_frame, false, true, 0); + _page_list.set_headers_visible(false); + scrolled_window->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + scrolled_window->set_propagate_natural_width(); + scrolled_window->set_propagate_natural_height(); + scrolled_window->add(_page_list); + list_frame->set_shadow_type(Gtk::SHADOW_IN); + list_frame->add(*scrolled_window); + _page_list_model = Gtk::TreeStore::create(_page_list_columns); + _page_list.set_model(_page_list_model); + _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); + + //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_IN); + title_frame->set_shadow_type(Gtk::SHADOW_IN); + + initPageTools(); + initPageUI(); + initPageBehavior(); + initPageIO(); + + initPageSystem(); + initPageBitmaps(); + initPageRendering(); + initPageSpellcheck(); + + + signalPresent().connect(sigc::mem_fun(*this, &InkscapePreferences::_presentPages)); + + //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(); +} + +InkscapePreferences::~InkscapePreferences() += default; + +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); + + _path_tools = _page_list.get_model()->get_path(iter_tools); + + _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_use_abs_size.init ( _("Width is in absolute units"), "/tools/calligraphic/abs_width", 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 + _path_shapes = _page_list.get_model()->get_path(iter_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_use_abs_size, "", + _("If on, pen width is in absolute units (px) independent of zoom; otherwise pen width depends on zoom so that it looks the same at any zoom")); + _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")); + + 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_gradienteditor.init( _("Use legacy Gradient Editor"), "/dialogs/gradienteditor/showlegacy", false); + _page_gradient.add_line( false, "", _misc_gradienteditor, "", + _("When on, the Gradient Edit button in the Fill & Stroke dialog will show the legacy Gradient Editor dialog, when off the Gradient Tool will be used"), 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); + + + //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 +} + +static void _inkscape_fill_gtk(const gchar *path, GHashTable *t) +{ + const gchar *dir_entry; + GDir *dir = g_dir_open(path, 0, NULL); + + if (!dir) + return; + + while ((dir_entry = g_dir_read_name(dir))) { + gchar *filename = g_build_filename(path, dir_entry, "gtk-3.0", "gtk.css", NULL); + + if (g_file_test(filename, G_FILE_TEST_IS_REGULAR) && !g_hash_table_contains(t, dir_entry)) + g_hash_table_add(t, g_strdup(dir_entry)); + + g_free(filename); + } + + g_dir_close(dir); +} + +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"); + if (themeiconname == prefs->getString("/theme/defaultIconTheme")) { + themeiconname = "hicolor"; + } + Glib::ustring prefix = ""; + if (prefs->getBool("/theme/darkTheme", false)) { + prefix = ".dark "; + } + Glib::ustring higlight = get_filename(ICONS, Glib::ustring(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)); + REMOVE_SPACES(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)); + REMOVE_SPACES(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)); + REMOVE_SPACES(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)); + REMOVE_SPACES(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"); + if (!prefs->getBool("/theme/symbolicIcons", false)) { + _symbolic_base_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/symbolicDefaultColors", true) || + !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) { + auto const screen = Gdk::Screen::get_default(); + if (INKSCAPE.colorizeprovider) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.colorizeprovider); + } + // This colors are setted on style.css of inkscape + Gdk::RGBA base_color = _symbolic_base_color.get_style_context()->get_color(); + // This is a hack to fix a proble style is not updated enoght fast on + // chage from dark to bright themes + if (themechange) { + base_color = _symbolic_base_color.get_style_context()->get_background_color(); + } + 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 base_color_sp(base_color.get_red(), base_color.get_green(), base_color.get_blue()); + 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()); + guint32 colorsetbase = base_color_sp.toRGBA32(base_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_base_color.setRgba32(colorsetbase); + _symbolic_success_color.setRgba32(colorsetsuccess); + _symbolic_warning_color.setRgba32(colorsetwarning); + _symbolic_error_color.setRgba32(colorseterror); + prefs->setUInt("/theme/" + themeiconname + "/symbolicBaseColor", colorsetbase); + prefs->setUInt("/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess); + prefs->setUInt("/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning); + prefs->setUInt("/theme/" + themeiconname + "/symbolicErrorColor", colorseterror); + if (prefs->getBool("/theme/symbolicDefaultColors", true)) { + _symbolic_base_color.setSensitive(false); + _symbolic_success_color.setSensitive(false); + _symbolic_warning_color.setSensitive(false); + _symbolic_error_color.setSensitive(false); + /* _complementary_colors->get_style_context()->add_class("disabled"); */ + } + changeIconsColors(); + } else { + _symbolic_base_color.setSensitive(true); + _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"); + 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.colorizeprovider) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.colorizeprovider); + } + Gtk::CssProvider::create(); + Glib::ustring css_str = ""; + if (prefs->getBool("/theme/symbolicIcons", false)) { + css_str = INKSCAPE.get_symbolic_colors(); + } + try { + INKSCAPE.colorizeprovider->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.colorizeprovider, + 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); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + 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.colorizeprovider) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.colorizeprovider); + } + _symbolic_base_colors.set_sensitive(false); + } + INKSCAPE.signal_change_theme.emit(); +} + +void InkscapePreferences::themeChange() +{ + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + if (window) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool darktheme = prefs->getBool("/theme/preferDarkTheme", false); + Glib::ustring themename = prefs->getString("/theme/gtkTheme"); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + GtkSettings *settings = gtk_settings_get_default(); + g_object_set(settings, "gtk-theme-name", themename.c_str(), NULL); + g_object_set(settings, "gtk-application-prefer-dark-theme", darktheme, NULL); + bool dark = themename.find(":dark") != std::string::npos; + if (!dark) { + Glib::RefPtr<Gtk::StyleContext> stylecontext = window->get_style_context(); + Gdk::RGBA rgba; + bool background_set = stylecontext->lookup_color("theme_bg_color", rgba); + if (background_set && (0.299 * rgba.get_red() + 0.587 * rgba.get_green() + 0.114 * rgba.get_blue()) < 0.5) { + dark = true; + } + } + Gtk::Widget *dialog_window = Glib::wrap(gobj()); + 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.signal_change_theme.emit(); + resetIconsColors(toggled); + } +} + +void InkscapePreferences::symbolicThemeCheck() +{ + using namespace Inkscape::IO::Resource; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + bool symbolic = false; + GtkSettings *settings = gtk_settings_get_default(); + if (settings) { + if (themeiconname != "") { + g_object_set(settings, "gtk-icon-theme-name", themeiconname.c_str(), NULL); + } + } + if (prefs->getString("/theme/defaultIconTheme") != prefs->getString("/theme/iconTheme")) { + 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 == prefs->getString("/theme/iconTheme")) { +#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_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_base_color.get_parent()->get_parent()->show(); + _symbolic_success_color.get_parent()->get_parent()->show(); + } + } + if (symbolic) { + if (prefs->getBool("/theme/symbolicDefaultColors", 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); + _path_ui = _page_list.get_model()->get_path(iter_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 (requires restart):"), _ui_languages, "", + _("Set the language for menus and number formats"), false); + + 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); + + _ui_zoom_correction.init(300, 30, 0.01, 500.0, 1.0, 10.0, 1.0); + _page_ui.add_line( false, _("_Zoom correction factor (in %):"), _ui_zoom_correction, "", + _("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"), true); + + _ui_partialdynamic.init( _("Enable dynamic relayout for incomplete sections"), "/options/workarounds/dynamicnotdone", false); + _page_ui.add_line( false, "", _ui_partialdynamic, "", + _("When on, will allow dynamic layout of components that are not completely finished being refactored"), true); + + /* show infobox */ + _show_filters_info_box.init( _("Show filter primitives infobox (requires restart)"), "/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")); + + { + Glib::ustring dockbarstyleLabels[] = {_("Icons only"), _("Text only"), _("Icons and text")}; + int dockbarstyleValues[] = {0, 1, 2}; + + /* dockbar style */ + _dockbar_style.init( "/options/dock/dockbarstyle", dockbarstyleLabels, dockbarstyleValues, G_N_ELEMENTS(dockbarstyleLabels), 0); + _page_ui.add_line(false, _("Dockbar style (requires restart):"), _dockbar_style, "", + _("Selects whether the vertical bars on the dockbar will show text labels, icons, or both"), false); + + Glib::ustring switcherstyleLabels[] = {_("Text only"), _("Icons only"), _("Icons and text")}; /* see bug #1098437 */ + int switcherstyleValues[] = {0, 1, 2}; + + /* switcher style */ + _switcher_style.init( "/options/dock/switcherstyle", switcherstyleLabels, switcherstyleValues, G_N_ELEMENTS(switcherstyleLabels), 0); + _page_ui.add_line(false, _("Switcher style (requires restart):"), _switcher_style, "", + _("Selects whether the dockbar switcher will show text labels, icons, or both"), false); + } + + _ui_yaxisdown.init( _("Origin at upper left with y-axis pointing down (requires restart)"), "/options/yaxisdown", true); + _page_ui.add_line( false, "", _ui_yaxisdown, "", + _("When off, origin is at lower left corner and y-axis points up"), true); + + _ui_rotationlock.init(_("Lock canvas rotation by default"), "/options/rotationlock", false); + _page_ui.add_line(false, "", _ui_rotationlock, "", + _("When enabled, common actions which normally rotate the canvas no longer do so by default"), true); + + + _mouse_grabsize.init("/options/grabsize/value", 1, 7, 1, 2, 3, 0); + _page_ui.add_line(false, _("_Handle size:"), _mouse_grabsize, "", + _("Set the relative size of node handles"), true); + + // Theme + _page_theme.add_group_header(_("Theme changes")); + { + using namespace Inkscape::IO::Resource; + GHashTable *t; + GHashTableIter iter; + gchar *theme, *path; + gchar **builtin_themes; + GList *list, *l; + guint i; + const gchar *const *dirs; + + t = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + /* Builtin themes */ + builtin_themes = g_resources_enumerate_children("/org/gtk/libgtk/theme", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL); + for (i = 0; builtin_themes[i] != NULL; i++) { + if (g_str_has_suffix(builtin_themes[i], "/")) + g_hash_table_add(t, g_strndup(builtin_themes[i], strlen(builtin_themes[i]) - 1)); + } + g_strfreev(builtin_themes); + + path = g_build_filename(g_get_user_data_dir(), "themes", NULL); + _inkscape_fill_gtk(path, t); + g_free(path); + + path = g_build_filename(g_get_home_dir(), ".themes", NULL); + _inkscape_fill_gtk(path, t); + g_free(path); + + dirs = g_get_system_data_dirs(); + for (i = 0; dirs[i]; i++) { + path = g_build_filename(dirs[i], "themes", NULL); + _inkscape_fill_gtk(path, t); + g_free(path); + } + + list = NULL; + g_hash_table_iter_init(&iter, t); + while (g_hash_table_iter_next(&iter, (gpointer *)&theme, NULL)) + list = g_list_insert_sorted(list, theme, (GCompareFunc)strcmp); + + std::vector<Glib::ustring> labels; + std::vector<Glib::ustring> values; + for (l = list; l; l = l->next) { + theme = (gchar *)l->data; + labels.emplace_back(theme); + values.emplace_back(theme); + } + labels.emplace_back(_("Use system theme")); + values.push_back(prefs->getString("/theme/defaultTheme")); + g_list_free(list); + g_hash_table_destroy(t); + + _gtk_theme.init("/theme/gtkTheme", labels, values, "Adwaita"); + _page_theme.add_line(false, _("Change Gtk theme:"), _gtk_theme, "", "", false); + _gtk_theme.signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::themeChange)); + } + _sys_user_themes_dir_copy.init(g_build_filename(g_get_user_data_dir(), "themes", NULL), _("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())); + _dark_theme.init(_("Use dark theme"), "/theme/preferDarkTheme", false); + _page_theme.add_line(true, "", _dark_theme, "", _("Use dark theme"), true); + _dark_theme.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::themeChange)); + // Icons + _page_theme.add_group_header(_("Display icons")); + { + using namespace Inkscape::IO::Resource; + auto folders = get_foldernames(ICONS, { "application" }); + std::vector<Glib::ustring> labels; + std::vector<Glib::ustring> values; + 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 incase 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); + } + + labels.push_back(folder); + values.push_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()); + labels.emplace_back(_("Use system icons")); + values.push_back(prefs->getString("/theme/defaultIconTheme")); + _icon_theme.init("/theme/iconTheme", labels, values, "hicolor"); + _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"); + _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 colors for icons"), "/theme/symbolicDefaultColors", true); + _symbolic_base_colors.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::resetIconsColorsWrapper)); + _page_theme.add_line(true, "", _symbolic_base_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 color for icons. Some icons changes need reload"), 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, _("Highlights"), + _("Highlights colors, some symbolic icon themes use it. Some icons changes need reload"), + 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, some symbolic icon themes use it. Some icons changes need reload"), + false); + { + Glib::ustring sizeLabels[] = { C_("Icon size", "Larger"), C_("Icon size", "Large"), C_("Icon size", "Small"), + C_("Icon size", "Smaller") }; + int sizeValues[] = { 3, 2, 0, 1 }; + // "Larger" is 3 to not break existing preference files. Should fix in GTK3 + + _misc_small_tools.init("/toolbox/tools/small", sizeLabels, sizeValues, G_N_ELEMENTS(sizeLabels), 0); + _page_theme.add_line(false, _("Toolbox icon size:"), _misc_small_tools, "", + _("Set the size for the tool icons (requires restart)"), false); + + _misc_small_toolbar.init("/toolbox/small", sizeLabels, sizeValues, G_N_ELEMENTS(sizeLabels), 0); + _page_theme.add_line(false, _("Control bar icon size:"), _misc_small_toolbar, "", + _("Set the size for the icons in tools' control bars to use (requires restart)"), false); + + _misc_small_secondary.init("/toolbox/secondary", sizeLabels, sizeValues, G_N_ELEMENTS(sizeLabels), 1); + _page_theme.add_line(false, _("Secondary toolbar icon size:"), _misc_small_secondary, "", + _("Set the size for the icons in secondary toolbars to use (requires restart)"), 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 theme determines which icons to display by using the 'show-icons' attribute in its 'menus.xml' file. (requires restart)"), false); + } + + this->AddPage(_page_theme, _("Theme"), iter_ui, PREFS_PAGE_UI_THEME); + symbolicThemeCheck(); + // 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_save_dialog_pos_on.init ( _("Save and restore dialogs status"), "/options/savedialogposition/value", 1, true, nullptr); + _win_save_dialog_pos_off.init ( _("Don't save dialogs status"), "/options/savedialogposition/value", 0, false, &_win_save_dialog_pos_on); + + _win_dockable.init ( _("Dockable"), "/options/dialogtype/value", 1, true, nullptr); + _win_floating.init ( _("Floating"), "/options/dialogtype/value", 0, false, &_win_dockable); + + + _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_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", 0, false, nullptr); + _win_ontop_normal.init ( _("Normal"), "/options/transientpolicy/value", 1, true, &_win_ontop_none); + _win_ontop_agressive.init ( _("Aggressive"), "/options/transientpolicy/value", 2, false, &_win_ontop_none); + + { + 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 geometry (size and position)")); + _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)")); + + _page_windows.add_group_header( _("Saving dialogs status")); + _page_windows.add_line( true, "", _win_save_dialog_pos_off, "", + _("Don't save dialogs status")); + _page_windows.add_line( true, "", _win_save_dialog_pos_on, "", + _("Save and restore dialogs status (the last open windows dialogs are saved when it closes)")); + + + + _page_windows.add_group_header( _("Dialog behavior (requires restart)")); + _page_windows.add_line( true, "", _win_dockable, "", + _("Dockable")); + _page_windows.add_line( true, "", _win_floating, "", + _("Floating")); +#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 + +#ifndef _WIN32 // non-Win32 special code to enable transient dialogs + _page_windows.add_group_header( _("Dialogs on top:")); + + _page_windows.add_line( true, "", _win_ontop_none, "", + _("Dialogs are treated as regular windows")); + _page_windows.add_line( true, "", _win_ontop_normal, "", + _("Dialogs stay on top of document windows")); + _page_windows.add_line( true, "", _win_ontop_agressive, "", + _("Same as Normal but may work better with some window managers")); +#endif + + _page_windows.add_group_header( _("Miscellaneous")); +#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_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(5); + _grids_xy_origin_y.set_digits(5); + _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(5); + _grids_xy_spacing_y.set_digits(5); + _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(5); + _grids_axonom_origin_y.set_digits(5); + _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(5); + _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); + + initKeyboardShortcuts(iter_ui); +} + +#if defined(HAVE_LIBLCMS2) +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); +} +#endif // defined(HAVE_LIBLCMS2) + +void InkscapePreferences::initPageIO() +{ + Gtk::TreeModel::iterator iter_io = this->AddPage(_page_io, _("Input/Output"), PREFS_PAGE_IO); + _path_io = _page_list.get_model()->get_path(iter_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_comment.init( _("Add label comments to printing output"), "/printing/debug/show-label-comments", false); + _page_io.add_line( false, "", _misc_comment, "", + _("When on, a comment will be added to the raw print output, marking the rendered output for an object with its label"), 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); + + // 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 (requires restart)"), + _("How close on the screen you need to be to an object to be able to grab it with mouse (in screen pixels)"), false); + _mouse_thres.init ( "/options/dragtolerance/value", 0.0, 20.0, 1.0, 1.0, 4.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 (requires restart)"), "/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)")); + + _mouse_switch_on_ext_input.init( _("Switch tool based on tablet device (requires restart)"), "/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)")); + 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 Mesh Gradient JavaScript polyfill."), "/options/svgexport/mesh_insertpolyfill", true ); + _svgexport_insert_hatch_polyfill.init( _("Insert Hatch Paint Server JavaScript polyfill."), "/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 JavaScript polyfill to render meshes."), false); + _page_svgexport.add_line( false, "", _svgexport_insert_hatch_polyfill, "", _("Adds JavaScript polyfill to render hatches (linear and absolute paths)."), 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( _("Replace markers with 'auto_start_reverse'."), "/options/svgexport/marker_autostartreverse", false); + _svgexport_remove_marker_context_paint.init( _("Replace markers using 'context_paint' or 'context_fill'."), "/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 start of path."), false); + _page_svgexport.add_line( false, "", _svgexport_remove_marker_context_paint, "", _("SVG 2 allows markers to automatically match stroke color."), 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}; + +#if !defined(HAVE_LIBLCMS2) + Gtk::Label* lbl = new Gtk::Label(_("(Note: Color management has been disabled in this build)")); + _page_cms.add_line( false, "", *lbl, "", "", true); +#endif // !defined(HAVE_LIBLCMS2) + + _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(HAVE_LIBLCMS2) + _page_cms.add_line( true, "", _cms_proof_preserveblack, +#if defined(cmsFLAGS_PRESERVEBLACK) + "", +#else + _("(LittleCMS 1.15 or later required)"), +#endif // defined(cmsFLAGS_PRESERVEBLACK) + _("Preserve K channel in CMYK -> CMYK transforms"), false); +#endif // !defined(HAVE_LIBLCMS2) + +#if !defined(cmsFLAGS_PRESERVEBLACK) + _cms_proof_preserveblack.set_sensitive( false ); +#endif // !defined(cmsFLAGS_PRESERVEBLACK) + + +#if defined(HAVE_LIBLCMS2) + { + 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) ); +#else + // disable it, but leave it visible + _cms_intent.set_sensitive( false ); + _cms_display_profile.set_sensitive( false ); + _cms_from_display.set_sensitive( false ); + _cms_softproof.set_sensitive( false ); + _cms_gamutwarn.set_sensitive( false ); + _cms_gamutcolor.set_sensitive( false ); + _cms_proof_intent.set_sensitive( false ); + _cms_proof_profile.set_sensitive( false ); + _cms_proof_blackpoint.set_sensitive( false ); + _cms_proof_preserveblack.set_sensitive( false ); +#endif // defined(HAVE_LIBLCMS2) + + this->AddPage(_page_cms, _("Color management"), iter_io, PREFS_PAGE_IO_CMS); + + // Autosave options + _save_autosave_enable.init( _("Enable autosave (requires restart)"), "/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, 100.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 + * + * FIXME! + * AutoSave::restart() should be called AFTER the values have been changed + * (which cannot be guaranteed from here) - use a PrefObserver somewhere. + */ + _save_autosave_enable.signal_toggled( ).connect( sigc::ptr_fun(Inkscape::AutoSave::restart), true); + _save_autosave_interval.signal_changed().connect( sigc::ptr_fun(Inkscape::AutoSave::restart), true); + + this->AddPage(_page_autosave, _("Autosave"), iter_io, PREFS_PAGE_IO_AUTOSAVE); +} + +void InkscapePreferences::initPageBehavior() +{ + Gtk::TreeModel::iterator iter_behavior = this->AddPage(_page_behavior, _("Behavior"), PREFS_PAGE_BEHAVIOR); + _path_behavior = _page_list.get_model()->get_path(iter_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_layer_deselects.init ( _("Deselect upon layer change"), "/options/selection/layerdeselect", 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_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)")); + + _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")); + + 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_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_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); + + _dash_scale.init(_("Scale dashes with stroke"), "/options/dash/scale", true); + _page_dashes.add_line(false, "", _dash_scale, "", _("When changing stroke width, scale dash array")); + + this->AddPage(_page_dashes, _("Dashes"), iter_behavior, PREFS_PAGE_BEHAVIOR_DASHES); + + // 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")); + _wheel_zoom.init ( _("Mouse wheel zooms by default"), "/options/wheelzooms/value", false); + _page_scrolling.add_line( false, "", _wheel_zoom, "", + _("When on, mouse wheel zooms without Ctrl and scrolls canvas with Ctrl; when off, it zooms with Ctrl and scrolls without Ctrl")); + this->AddPage(_page_scrolling, _("Scrolling"), iter_behavior, PREFS_PAGE_BEHAVIOR_SCROLLING); + + // Snapping options + _page_snapping.add_group_header( _("Snap defaults")); + + _snap_default.init( _("Enable snapping in new documents"), "/options/snapdefault/value", true); + _page_snapping.add_line( true, "", _snap_default, "", + _("Initial state of snapping in new documents and non-Inkscape SVGs. Snap status is subsequently saved per-document.")); + + _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); + + _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 on, clicking the middle mouse button (usually the mouse wheel) makes zoom.")); + _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_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); +} + +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, _("(requires restart)"), + _("Configure number of processors/threads to use when rendering filters"), false); + + // 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 xray 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, _("Rendering XRay radius:"), _rendering_xray_radius, "", + _("XRay mode radius preview"), false); + + { + // if these GTK constants ever change, consider adding a compatibility shim to SPCanvas::addIdle() + static_assert(G_PRIORITY_HIGH_IDLE == 100, "G_PRIORITY_HIGH_IDLE must be 100 to match preferences.xml"); + static_assert(G_PRIORITY_DEFAULT_IDLE == 200, "G_PRIORITY_DEFAULT_IDLE must be 200 to match preferences.xml"); + + Glib::ustring redrawPriorityLabels[] = {_("Responsive"), _("Conservative")}; + int redrawPriorityValues[] = {G_PRIORITY_HIGH_IDLE, G_PRIORITY_DEFAULT_IDLE}; + + // redraw priority + _rendering_redraw_priority.init("/options/redrawpriority/value", redrawPriorityLabels, redrawPriorityValues, G_N_ELEMENTS(redrawPriorityLabels), 0); + _page_rendering.add_line(false, _("Redraw while editing:"), _rendering_redraw_priority, "", + _("Set how quickly the canvas display is updated while editing objects"), 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")); + + 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.")); + + { + 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"), _("Embed"), _("Link")}; + Glib::ustring values[] = {"include", "embed", "link"}; + _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) +{ + std::vector<Glib::ustring> fileNames; + std::vector<Glib::ustring> fileLabels; + + sp_shortcut_get_file_names(&fileLabels, &fileNames); + + _kb_filelist.init( "/options/kbshortcuts/shortcutfile", &fileLabels[0], &fileNames[0], fileLabels.size(), fileNames[0]); + + Glib::ustring tooltip(_("Select a file of predefined shortcuts to use. Any customized shortcuts you create will be added separately to ")); + tooltip += Glib::ustring(IO::Resource::get_path(IO::Resource::USER, IO::Resource::KEYS, "default.xml")); + + _page_keyshortcuts.add_line( false, _("Shortcut file:"), _kb_filelist, "", tooltip.c_str(), false); + + _kb_search.init("/options/kbshortcuts/value", true); + _page_keyshortcuts.add_line( false, _("Search:"), _kb_search, "", "", true); + + _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)); + + _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); + + _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)); + + _kb_tree.get_column(2)->set_resizable(true); + _kb_tree.get_column(2)->set_clickable(true); + + _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) ); + + Gtk::ScrolledWindow* scroller = new Gtk::ScrolledWindow(); + scroller->add(_kb_tree); + + int row = 3; + + scroller->set_hexpand(); + scroller->set_vexpand(); + _page_keyshortcuts.attach(*scroller, 0, row, 2, 1); + + row++; + + 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 Shortcuts"), 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.shortcutid] = 0; + (*iter_group)[_kb_columns.user_set] = 0; + +} + +void InkscapePreferences::onKBList() +{ + sp_shortcut_init(); + onKBListKeyboardShortcuts(); +} + +void InkscapePreferences::onKBReset() +{ + sp_shortcuts_delete_all_from_file(); + sp_shortcut_init(); + onKBListKeyboardShortcuts(); +} + +void InkscapePreferences::onKBImport() +{ + if (sp_shortcut_file_import()) { + onKBListKeyboardShortcuts(); + } +} + +void InkscapePreferences::onKBExport() +{ + sp_shortcut_file_export(); +} + +bool InkscapePreferences::onKBSearchKeyEvent(GdkEventKey * /*event*/) +{ + _kb_filter->refilter(); + return FALSE; +} + +void InkscapePreferences::onKBTreeCleared(const Glib::ustring& path) +{ + Gtk::TreeModel::iterator iter = _kb_filter->get_iter(path); + Glib::ustring id = (*iter)[_kb_columns.id]; + unsigned int const current_shortcut_id = (*iter)[_kb_columns.shortcutid]; + + // Remove current shortcut from file + sp_shortcut_delete_from_file(id.c_str(), current_shortcut_id); + + sp_shortcut_init(); + onKBListKeyboardShortcuts(); + +} + +void InkscapePreferences::onKBTreeEdited (const Glib::ustring& path, guint accel_key, Gdk::ModifierType accel_mods, guint hardware_keycode) +{ + Gtk::TreeModel::iterator iter = _kb_filter->get_iter(path); + + Glib::ustring id = (*iter)[_kb_columns.id]; + Glib::ustring current_shortcut = (*iter)[_kb_columns.shortcut]; + unsigned int const current_shortcut_id = (*iter)[_kb_columns.shortcutid]; + + Inkscape::Verb *const verb = Inkscape::Verb::getbyid(id.c_str()); + if (!verb) { + return; + } + + unsigned int const new_shortcut_id = sp_shortcut_get_from_gdk_event(accel_key, accel_mods, hardware_keycode); + if (new_shortcut_id && (new_shortcut_id != current_shortcut_id)) { + // check if there is currently a verb assigned to this shortcut; if yes ask if the shortcut should be reassigned + Inkscape::Verb *current_verb = sp_shortcut_get_verb(new_shortcut_id); + if (current_verb) { + Glib::ustring verb_name = _(current_verb->get_name()); + Glib::ustring::size_type pos = 0; + while ((pos = verb_name.find('_', pos)) != verb_name.npos) { // strip mnemonics + verb_name.erase(pos, 1); + } + Glib::ustring message = Glib::ustring::compose(_("Keyboard shortcut \"%1\"\nis already assigned to \"%2\""), + sp_shortcut_get_label(new_shortcut_id), verb_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; + } + } + + // Delete current shortcut if it existed + sp_shortcut_delete_from_file(id.c_str(), current_shortcut_id); + // Delete any references to the new shortcut + sp_shortcut_delete_from_file(id.c_str(), new_shortcut_id); + // Add the new shortcut + sp_shortcut_add_to_file(id.c_str(), new_shortcut_id); + + sp_shortcut_init(); + 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 foreground=\"blue\"> " + shortcut + " </span>").c_str(); + } else { + accel->property_markup() = Glib::ustring("<span> " + shortcut + " </span>").c_str(); + } +} + +void InkscapePreferences::onKBListKeyboardShortcuts() +{ + // 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(); + + std::vector<Verb *>verbs = Inkscape::Verb::getList(); + + for (auto verb : verbs) { + + if (!verb) { + continue; + } + if (!verb->get_name()){ + continue; + } + + Gtk::TreeStore::Path path; + if (_kb_store->iter_is_valid(_kb_store->get_iter("0"))) { + path = _kb_store->get_path(_kb_store->get_iter("0")); + } + + // Find this group in the tree + Glib::ustring group = verb->get_group() ? _(verb->get_group()) : _("Misc"); + Glib::ustring verb_id = verb->get_id(); + if (verb_id .compare(0,26,"org.inkscape.effect.filter") == 0) { + group = _("Filters"); + } + Gtk::TreeStore::iterator iter_group; + bool found = false; + while (path) { + iter_group = _kb_store->get_iter(path); + if (!_kb_store->iter_is_valid(iter_group)) { + break; + } + Glib::ustring name = (*iter_group)[_kb_columns.name]; + if ((*iter_group)[_kb_columns.name] == group) { + found = true; + break; + } + path.next(); + } + + if (!found) { + // Add the group if not there + iter_group = _kb_store->append(); + (*iter_group)[_kb_columns.name] = group; + (*iter_group)[_kb_columns.shortcut] = ""; + (*iter_group)[_kb_columns.id] = ""; + (*iter_group)[_kb_columns.description] = ""; + (*iter_group)[_kb_columns.shortcutid] = 0; + (*iter_group)[_kb_columns.user_set] = 0; + } + + // Remove the key accelerators from the verb name + Glib::ustring name = _(verb->get_name()); + std::string::size_type k = 0; + while((k=name.find('_',k))!=name.npos) { + name.erase(k, 1); + } + + // Get the shortcut label + unsigned int shortcut_id = sp_shortcut_get_primary(verb); + Glib::ustring shortcut_label = ""; + if (shortcut_id != GDK_KEY_VoidSymbol) { + gchar* str = sp_shortcut_get_label(shortcut_id); + if (str) { + shortcut_label = Glib::Markup::escape_text(str); + g_free(str); + str = nullptr; + } + } + // Add the verb to the group + Gtk::TreeStore::iterator row = _kb_store->append(iter_group->children()); + (*row)[_kb_columns.name] = name; + (*row)[_kb_columns.shortcut] = shortcut_label; + (*row)[_kb_columns.description] = verb->get_short_tip() ? _(verb->get_short_tip()) : ""; + (*row)[_kb_columns.shortcutid] = shortcut_id; + (*row)[_kb_columns.id] = verb->get_id(); + (*row)[_kb_columns.user_set] = sp_shortcut_is_user_set(verb); + + if (selected_id == verb->get_id()) { + 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); + } + } + + // 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"))); + } + +} + +void InkscapePreferences::initPageSpellcheck() +{ +#if HAVE_ASPELL + + std::vector<Glib::ustring> languages; + std::vector<Glib::ustring> langValues; + + languages.emplace_back(C_("Spellchecker language", "None")); + langValues.emplace_back(""); + + for (auto const &lang : SpellCheck::get_available_langs()) { + languages.emplace_back(lang); + langValues.emplace_back(lang); + } + + _spell_language.init( "/dialogs/spellcheck/lang", &languages[0], &langValues[0], languages.size(), languages[0]); + _page_spellcheck.add_line( false, _("Language:"), _spell_language, "", + _("Set the main spell check language"), false); + + _spell_language2.init( "/dialogs/spellcheck/lang2", &languages[0], &langValues[0], languages.size(), languages[0]); + _page_spellcheck.add_line( false, _("Second language:"), _spell_language2, "", + _("Set the second spell check language; checking will only stop on words unknown in ALL chosen languages"), false); + + _spell_language3.init( "/dialogs/spellcheck/lang3", &languages[0], &langValues[0], languages.size(), languages[0]); + _page_spellcheck.add_line( false, _("Third language:"), _spell_language3, "", + _("Set the third spell check language; checking will only stop on words unknown in ALL chosen languages"), false); + + _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 +} + +static void appendList( Glib::ustring& tmp, const gchar* const*listing ) +{ + for (const gchar* const* ptr = listing; *ptr; ptr++) { + tmp += *ptr; + 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, _("(requires restart)"), + _("Factor by which the event clock is skewed from the actual time (0.9766 on some systems)"), false); + 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); + + _sys_user_extension_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::EXTENSIONS, ""), + _("Open extensions folder")); + _page_system.add_line(true, _("User extensions: "), _sys_user_extension_dir, "", + _("Location of the user’s extensions"), true); + + _sys_user_themes_dir.init(g_build_filename(g_get_user_data_dir(), "themes", NULL), _("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( INKSCAPE_DATADIR_REAL ); + _sys_data.set_editable(false); + _page_system.add_line(true, _("Inkscape data: "), _sys_data, "", _("Location of Inkscape data"), true); + + _sys_extension_dir.set_text(INKSCAPE_EXTENSIONDIR); + _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; + appendList( tmp, g_get_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); + + tmp = ""; + gchar** paths = nullptr; + gint count = 0; + gtk_icon_theme_get_search_path(gtk_icon_theme_get_default(), &paths, &count); + appendList( tmp, paths ); + g_strfreev(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; + _getContents()->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; +} + +bool InkscapePreferences::PresentPage(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]) + { + if (desired_page >= PREFS_PAGE_TOOLS && desired_page <= PREFS_PAGE_TOOLS_CONNECTOR) + _page_list.expand_row(_path_tools, false); + if (desired_page >= PREFS_PAGE_TOOLS_SHAPES && desired_page <= PREFS_PAGE_TOOLS_SHAPES_SPIRAL) + _page_list.expand_row(_path_shapes, false); + if (desired_page >= PREFS_PAGE_UI && desired_page <= PREFS_PAGE_UI_KEYBOARD_SHORTCUTS) + _page_list.expand_row(_path_ui, false); + if (desired_page >= PREFS_PAGE_BEHAVIOR && desired_page <= PREFS_PAGE_BEHAVIOR_MASKS) + _page_list.expand_row(_path_behavior, false); + if (desired_page >= PREFS_PAGE_IO && desired_page <= PREFS_PAGE_IO_OPENCLIPART) + _page_list.expand_row(_path_io, false); + _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::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(); + while (Gtk::Main::events_pending()) + { + Gtk::Main::iteration(); + } + this->show_all_children(); + if (prefs->getInt("/dialogs/preferences/page", 0) == PREFS_PAGE_UI_THEME) { + symbolicThemeCheck(); + } + } +} + +void InkscapePreferences::_presentPages() +{ + _page_list_model->foreach_iter(sigc::mem_fun(*this, &InkscapePreferences::PresentPage)); +} + +} // 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..af7aa26 --- /dev/null +++ b/src/ui/dialog/inkscape-preferences.h @@ -0,0 +1,620 @@ +// 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 + +#include <iostream> +#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/frame.h> +#include <gtkmm/notebook.h> +#include <gtkmm/textview.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/liststore.h> +#include <gtkmm/treemodel.h> +#include <gtkmm/treemodelfilter.h> + +#include "ui/widget/panel.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_WINDOWS, + PREFS_PAGE_UI_GRIDS, + PREFS_PAGE_UI_KEYBOARD_SHORTCUTS, + PREFS_PAGE_BEHAVIOR, + PREFS_PAGE_BEHAVIOR_SELECTING, + PREFS_PAGE_BEHAVIOR_TRANSFORMS, + PREFS_PAGE_BEHAVIOR_DASHES, + 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_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 +}; + +namespace Gtk { +class Scale; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class InkscapePreferences : public UI::Widget::Panel { +public: + ~InkscapePreferences() override; + + static InkscapePreferences &getInstance() { return *new InkscapePreferences(); } + +protected: + Gtk::Frame _page_frame; + Gtk::Label _page_title; + Gtk::TreeView _page_list; + Glib::RefPtr<Gtk::TreeStore> _page_list_model; + + //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; + + Gtk::TreeModel::Path _path_tools; + Gtk::TreeModel::Path _path_shapes; + Gtk::TreeModel::Path _path_ui; + Gtk::TreeModel::Path _path_behavior; + Gtk::TreeModel::Path _path_io; + + 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_theme; + UI::Widget::DialogPage _page_windows; + UI::Widget::DialogPage _page_grids; + + UI::Widget::DialogPage _page_behavior; + UI::Widget::DialogPage _page_select; + UI::Widget::DialogPage _page_transforms; + UI::Widget::DialogPage _page_dashes; + 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_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; + UI::Widget::PrefCheckButton _wheel_zoom; + + 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::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; + + 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::PrefCheckButton _symbolic_icons; + UI::Widget::PrefCheckButton _symbolic_base_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; + + Gtk::Button _apply_theme; + + 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_save_geom_off; + UI::Widget::PrefRadioButton _win_save_geom; + UI::Widget::PrefRadioButton _win_save_geom_prefs; + 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_use_abs_size; + 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::PrefRadioButton _mask_grouping_none; + UI::Widget::PrefRadioButton _mask_grouping_separate; + UI::Widget::PrefRadioButton _mask_grouping_all; + UI::Widget::PrefCheckButton _mask_ungrouping; + + 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; + UI::Widget::PrefCheckButton _show_filters_info_box; + UI::Widget::PrefCombo _dockbar_style; + UI::Widget::PrefCombo _switcher_style; + UI::Widget::PrefCheckButton _rendering_image_outline; + UI::Widget::PrefSpinButton _rendering_cache_size; + UI::Widget::PrefSpinButton _rendering_tile_multiplier; + UI::Widget::PrefSpinButton _rendering_xray_radius; + UI::Widget::PrefCombo _rendering_redraw_priority; + UI::Widget::PrefSpinButton _filter_multi_threaded; + + UI::Widget::PrefCheckButton _trans_scale_stroke; + UI::Widget::PrefCheckButton _trans_scale_corner; + UI::Widget::PrefCheckButton _trans_gradient; + UI::Widget::PrefCheckButton _trans_pattern; + UI::Widget::PrefRadioButton _trans_optimized; + UI::Widget::PrefRadioButton _trans_preserved; + + UI::Widget::PrefCheckButton _dash_scale; + + 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_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::PrefSpinButton _importexport_export_res; + UI::Widget::PrefSpinButton _importexport_import_res; + UI::Widget::PrefCheckButton _importexport_import_res_override; + UI::Widget::PrefSlider _snap_delay; + UI::Widget::PrefSlider _snap_weight; + UI::Widget::PrefSlider _snap_persistence; + 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 _misc_forkvectors; + UI::Widget::PrefCheckButton _misc_gradienteditor; + UI::Widget::PrefSpinButton _misc_gradientangle; + UI::Widget::PrefCheckButton _misc_scripts; + UI::Widget::PrefCheckButton _misc_namedicon_delay; + + // System page + // Gtk::Button *_apply_theme; + 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_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; + 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_partialdynamic; + UI::Widget::ZoomCorrRulerSlider _ui_zoom_correction; + UI::Widget::PrefCheckButton _ui_yaxisdown; + UI::Widget::PrefCheckButton _ui_rotationlock; + + //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::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; + + + /* + * Keyboard shortcut members + */ + class ModelColumns: public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns() { + add(name); + add(id); + add(shortcut); + add(description); + add(shortcutid); + 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<unsigned int> shortcutid; + 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; + gboolean _kb_shortcuts_loaded; + + 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); + bool PresentPage(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 on_reset_open_recent_clicked(); + void on_reset_prefs_clicked(); + + 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); + + void _presentPages(); + + /* + * 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); + +private: + void themeChange(); + 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); + + InkscapePreferences(); + InkscapePreferences(InkscapePreferences const &d); + InkscapePreferences operator=(InkscapePreferences const &d); + bool _init; +}; + +} // 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..438914d --- /dev/null +++ b/src/ui/dialog/input.cpp @@ -0,0 +1,1792 @@ +// 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/panel.h" +#include "ui/widget/frame.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" + +/* 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", +" ", +" .................... ", +" ..++++++++++++++++++.. ", +" .++++++++++++++++++++. ", +" .++++++++++++++++++++. ", +" ..++++++++++++++++++.. ", +" .................... ", +" "}; + +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::VBox + { + 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::VBox detailsBox; + Gtk::HBox titleFrame; + Gtk::Label titleLabel; + Inkscape::UI::Widget::Frame axisFrame; + Inkscape::UI::Widget::Frame keysFrame; + Gtk::VBox axisVBox; + Gtk::ComboBoxText modeCombo; + Gtk::Label modeLabel; + Gtk::HBox 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; + + GdkInputSource 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((GdkInputSource)-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() +{ + Gtk::Box *contents = _getContents(); + + 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); + contents->pack_start(topHolder); + } else { + contents->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::VBox(), + confDeviceStore(Gtk::TreeStore::create(getCols())), + confDeviceIter(), + confDeviceTree(confDeviceStore), + confDeviceScroller(), + watcher(*this), + useExt(_("_Use pressure-sensitive tablet (requires restart)"), true), + save(_("_Save"), true), + detailsBox(false, 4), + titleFrame(false, 4), + titleLabel(""), + axisFrame(_("Axes")), + keysFrame(_("Keys")), + modeLabel(_("Mode:")), + modeBox(false, 4) + +{ + + + 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 co-ordinates 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 ) +{ + guint numAxes = gdk_device_get_n_axes(dev); + + 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; + + GdkInputSource source = gdk_device_get_source(device); + const gchar *name = gdk_device_get_name(device); + + 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; + + GdkInputSource source = lastSourceSeen; + Glib::ustring devName = lastDevnameSeen; + Glib::ustring key; + gint hotButton = -1; + + switch ( event->type ) { + case GDK_KEY_PRESS: + case GDK_KEY_RELEASE: + { + GdkEventKey* keyEvt = reinterpret_cast<GdkEventKey*>(event); + gchar* name = gtk_accelerator_name(keyEvt->keyval, static_cast<GdkModifierType>(keyEvt->state)); + keyVal.set_label(name); +// g_message("%d KEY state:0x%08x 0x%04x [%s]", keyEvt->type, keyEvt->state, keyEvt->keyval, name); + g_free(name); + } + break; + case GDK_BUTTON_PRESS: + modmod = 1; + // fallthrough + case GDK_BUTTON_RELEASE: + { + GdkEventButton* btnEvt = reinterpret_cast<GdkEventButton*>(event); + if ( btnEvt->device ) { + key = getKeyFor(btnEvt->device); + source = gdk_device_get_source(btnEvt->device); + devName = gdk_device_get_name(btnEvt->device); + 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); + } + gchar* name = gtk_accelerator_name(0, static_cast<GdkModifierType>(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") + +// ); + g_free(name); + } + break; + case GDK_MOTION_NOTIFY: + { + GdkEventMotion* btnMtn = reinterpret_cast<GdkEventMotion*>(event); + if ( btnMtn->device ) { + key = getKeyFor(btnMtn->device); + source = gdk_device_get_source(btnMtn->device); + devName = gdk_device_get_name(btnMtn->device); + mapAxesValues(key, btnMtn->axes, btnMtn->device); + } + gchar* name = gtk_accelerator_name(0, static_cast<GdkModifierType>(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) +// ); + g_free(name); + } + 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..a756cc5 --- /dev/null +++ b/src/ui/dialog/input.h @@ -0,0 +1,47 @@ +// 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 "verbs.h" +#include "ui/widget/panel.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class InputDialog : public UI::Widget::Panel +{ +public: + static InputDialog &getInstance(); + + InputDialog() : UI::Widget::Panel("/dialogs/inputdevices", SP_VERB_DIALOG_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..708c90e --- /dev/null +++ b/src/ui/dialog/knot-properties.cpp @@ -0,0 +1,207 @@ +// 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 "ui/dialog/knot-properties.h" + +#include <boost/lexical_cast.hpp> +#include <glibmm/i18n.h> +#include <glibmm/main.h> +#include "inkscape.h" +#include "util/units.h" +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "layer-manager.h" + +#include "selection-chemistry.h" + +//#include "event-context.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +KnotPropertiesDialog::KnotPropertiesDialog() + : _desktop(nullptr), + _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() { + + _setDesktop(nullptr); +} + +void KnotPropertiesDialog::showDialog(SPDesktop *desktop, const SPKnot *pt, Glib::ustring const unit_name) +{ + KnotPropertiesDialog *dialog = new KnotPropertiesDialog(); + dialog->_setDesktop(desktop); + 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() +{ + _setDesktop(nullptr); + 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); +} + +void KnotPropertiesDialog::_setDesktop(SPDesktop *desktop) { + if (desktop) { + Inkscape::GC::anchor (desktop); + } + if (_desktop) { + Inkscape::GC::release (_desktop); + } + _desktop = desktop; +} + +} // 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..fb88fad --- /dev/null +++ b/src/ui/dialog/knot-properties.h @@ -0,0 +1,97 @@ +// 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 "knot.h" +#include "ui/tools/measure-tool.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + + +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: + + SPDesktop *_desktop; + 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 _setDesktop(SPDesktop *desktop); + 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..083f104 --- /dev/null +++ b/src/ui/dialog/layer-properties.cpp @@ -0,0 +1,424 @@ +// 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 "verbs.h" +#include "selection-chemistry.h" +#include "ui/icon-names.h" +#include "ui/widget/imagetoggler.h" +#include "ui/tools/tool-base.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +LayerPropertiesDialog::LayerPropertiesDialog() + : _strategy(nullptr), + _desktop(nullptr), + _layer(nullptr), + _position_visible(false), + _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(sigc::mem_fun(*this, &LayerPropertiesDialog::_close)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &LayerPropertiesDialog::_apply)); + + signal_delete_event().connect( + sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &LayerPropertiesDialog::_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(); +} + +LayerPropertiesDialog::~LayerPropertiesDialog() { + + _setDesktop(nullptr); + _setLayer(nullptr); +} + +void LayerPropertiesDialog::_showDialog(LayerPropertiesDialog::Strategy &strategy, + SPDesktop *desktop, SPObject *layer) +{ + LayerPropertiesDialog *dialog = new LayerPropertiesDialog(); + + dialog->_strategy = &strategy; + dialog->_setDesktop(desktop); + dialog->_setLayer(layer); + + dialog->_strategy->setup(*dialog); + + dialog->set_modal(true); + desktop->setWindowTransient (dialog->gobj()); + dialog->property_destroy_with_parent() = true; + + dialog->show(); + dialog->present(); +} + +void +LayerPropertiesDialog::_apply() +{ + g_assert(_strategy != nullptr); + + _strategy->perform(*this); + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_NONE, + _("Add layer")); + + _close(); +} + +void +LayerPropertiesDialog::_close() +{ + _setLayer(nullptr); + _setDesktop(nullptr); + destroy_(); + Glib::signal_idle().connect( + sigc::bind_return( + sigc::bind(sigc::ptr_fun<void*, void>(&::operator delete), this), + false + ) + ); +} + +void +LayerPropertiesDialog::_setup_position_controls() { + if ( nullptr == _layer || _desktop->currentRoot() == _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, + sigc::mem_fun(*this, &LayerPropertiesDialog::_prepareLabelRenderer)); + + 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"))); + + _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(); +} + +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); + + Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::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 ); + } + + Inkscape::UI::Widget::ImageToggler * renderer = Gtk::manage( new Inkscape::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( sigc::mem_fun(*this, &LayerPropertiesDialog::_handleKeyEvent), false ); + _tree.signal_button_press_event().connect_notify( sigc::mem_fun(*this, &LayerPropertiesDialog::_handleButtonEvent) ); + + _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->currentLayer(); + _store->clear(); + _addLayer( document, SP_OBJECT(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(); + _layout_table.attach(_scroller, 0, 1, 2, 1); + + show_all_children(); +} + +void LayerPropertiesDialog::_addLayer( SPDocument* doc, SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level ) +{ + int _maxNestDepth = 20; + if ( _desktop && _desktop->layer_manager && layer && (level < _maxNestDepth) ) { + unsigned int counter = _desktop->layer_manager->childCount(layer); + for ( unsigned int i = 0; i < counter; i++ ) { + SPObject *child = _desktop->layer_manager->nthChildOf(layer, i); + if ( child ) { +#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); + + //_checkTreeSelection(); + } + + _addLayer( doc, 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: { + _strategy->perform(*this); + _close(); + return true; + } + break; + } + return false; +} + +void LayerPropertiesDialog::_handleButtonEvent(GdkEventButton* event) +{ + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + _strategy->perform(*this); + _close(); + } +} + +/** 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.c_str(); +} + +void LayerPropertiesDialog::Rename::setup(LayerPropertiesDialog &dialog) { + SPDesktop *desktop=dialog._desktop; + dialog.set_title(_("Rename Layer")); + gchar const *name = desktop->currentLayer()->label(); + dialog._layer_name_entry.set_text(( name ? name : _("Layer") )); + dialog._apply_button.set_label(_("_Rename")); +} + +void LayerPropertiesDialog::Rename::perform(LayerPropertiesDialog &dialog) { + SPDesktop *desktop=dialog._desktop; + Glib::ustring name(dialog._layer_name_entry.get_text()); + if (name.empty()) + return; + desktop->layer_manager->renameLayer( desktop->currentLayer(), + (gchar *)name.c_str(), + FALSE + ); + DocumentUndo::done(desktop->getDocument(), SP_VERB_NONE, + _("Rename layer")); + // TRANSLATORS: This means "The layer has been renamed" + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Renamed layer")); +} + +void LayerPropertiesDialog::Create::setup(LayerPropertiesDialog &dialog) { + dialog.set_title(_("Add Layer")); + + // Set the initial name to the "next available" layer name + LayerManager *mgr = dialog._desktop->layer_manager; + Glib::ustring newName = mgr->getNextLayerName(nullptr, dialog._desktop->currentLayer()->label()); + dialog._layer_name_entry.set_text(newName.c_str()); + dialog._apply_button.set_label(_("_Add")); + dialog._setup_position_controls(); +} + +void LayerPropertiesDialog::Create::perform(LayerPropertiesDialog &dialog) { + SPDesktop *desktop=dialog._desktop; + + LayerRelativePosition position = LPOS_ABOVE; + + if (dialog._position_visible) { + Gtk::ListStore::iterator activeRow(dialog._layer_position_combo.get_active()); + position = activeRow->get_value(dialog._dropdown_columns.position); + } + Glib::ustring name(dialog._layer_name_entry.get_text()); + if (name.empty()) + return; + + SPObject *new_layer=Inkscape::create_layer(desktop->currentRoot(), dialog._layer, position); + + if (!name.empty()) { + desktop->layer_manager->renameLayer( new_layer, (gchar *)name.c_str(), TRUE ); + } + desktop->getSelection()->clear(); + desktop->setCurrentLayer(new_layer); + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("New layer created.")); +} + +void LayerPropertiesDialog::Move::setup(LayerPropertiesDialog &dialog) { + dialog.set_title(_("Move to Layer")); + //TODO: find an unused layer number, forming name from _("Layer ") + "%d" + dialog._layer_name_entry.set_text(_("Layer")); + dialog._apply_button.set_label(_("_Move")); + dialog._setup_layers_controls(); +} + +void LayerPropertiesDialog::Move::perform(LayerPropertiesDialog &dialog) { + + SPObject *moveto = dialog._selectedLayer(); + dialog._desktop->selection->toLayer(moveto); +} + +void LayerPropertiesDialog::_setDesktop(SPDesktop *desktop) { + if (desktop) { + Inkscape::GC::anchor (desktop); + } + if (_desktop) { + Inkscape::GC::release (_desktop); + } + _desktop = desktop; +} + +void LayerPropertiesDialog::_setLayer(SPObject *layer) { + if (layer) { + sp_object_ref(layer, nullptr); + } + if (_layer) { + sp_object_unref(_layer, nullptr); + } + _layer = layer; +} + +} // 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/layer-properties.h b/src/ui/dialog/layer-properties.h new file mode 100644 index 0000000..7cdd130 --- /dev/null +++ b/src/ui/dialog/layer-properties.h @@ -0,0 +1,177 @@ +// 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-fns.h" +#include "ui/widget/layer-selector.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +class LayerPropertiesDialog : public Gtk::Dialog { + public: + LayerPropertiesDialog(); + ~LayerPropertiesDialog() override; + + Glib::ustring getName() const { return "LayerPropertiesDialog"; } + + static void showRename(SPDesktop *desktop, SPObject *layer) { + _showDialog(Rename::instance(), desktop, layer); + } + static void showCreate(SPDesktop *desktop, SPObject *layer) { + _showDialog(Create::instance(), desktop, layer); + } + static void showMove(SPDesktop *desktop, SPObject *layer) { + _showDialog(Move::instance(), desktop, layer); + } + +protected: + struct Strategy { + virtual ~Strategy() = default; + virtual void setup(LayerPropertiesDialog &)=0; + virtual void perform(LayerPropertiesDialog &)=0; + }; + struct Rename : public Strategy { + static Rename &instance() { static Rename instance; return instance; } + void setup(LayerPropertiesDialog &dialog) override; + void perform(LayerPropertiesDialog &dialog) override; + }; + struct Create : public Strategy { + static Create &instance() { static Create instance; return instance; } + void setup(LayerPropertiesDialog &dialog) override; + void perform(LayerPropertiesDialog &dialog) override; + }; + struct Move : public Strategy { + static Move &instance() { static Move instance; return instance; } + void setup(LayerPropertiesDialog &dialog) override; + void perform(LayerPropertiesDialog &dialog) override; + }; + + friend struct Rename; + friend struct Create; + friend struct Move; + + Strategy *_strategy; + SPDesktop *_desktop; + SPObject *_layer; + + 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; + + 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; + + static LayerPropertiesDialog &_instance() { + static LayerPropertiesDialog instance; + return instance; + } + + void _setDesktop(SPDesktop *desktop); + void _setLayer(SPObject *layer); + + static void _showDialog(Strategy &strategy, 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( SPDocument* doc, SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level ); + SPObject* _selectedLayer(); + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton* event); + +private: + LayerPropertiesDialog(LayerPropertiesDialog const &) = delete; // no copy + LayerPropertiesDialog &operator=(LayerPropertiesDialog const &) = delete; // 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/layers.cpp b/src/ui/dialog/layers.cpp new file mode 100644 index 0000000..a268426 --- /dev/null +++ b/src/ui/dialog/layers.cpp @@ -0,0 +1,1004 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple panel for layers + * + * Authors: + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2006,2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "layers.h" + +#include <gtkmm/icontheme.h> +#include <gtkmm/separatormenuitem.h> +#include <glibmm/main.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "layer-fns.h" +#include "layer-manager.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "helper/action.h" +#include "ui/icon-loader.h" + +#include "include/gtkmm_version.h" + +#include "object/sp-root.h" + +#include "svg/css-ostringstream.h" + +#include "ui/contextmenu.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/tools/tool-base.h" +#include "ui/widget/imagetoggler.h" + +//#define DUMP_LAYERS 1 + +namespace Inkscape { +namespace UI { +namespace Dialog { + +LayersPanel& LayersPanel::getInstance() +{ + return *new LayersPanel(); +} + +enum { + COL_VISIBLE = 1, + COL_LOCKED +}; + +enum { + BUTTON_NEW = 0, + BUTTON_RENAME, + BUTTON_TOP, + BUTTON_BOTTOM, + BUTTON_UP, + BUTTON_DOWN, + BUTTON_DUPLICATE, + BUTTON_DELETE, + BUTTON_SOLO, + BUTTON_SHOW_ALL, + BUTTON_HIDE_ALL, + BUTTON_LOCK_OTHERS, + BUTTON_LOCK_ALL, + BUTTON_UNLOCK_ALL, + DRAGNDROP +}; + +class LayersPanel::InternalUIBounce +{ +public: + int _actionCode; + SPObject* _target; +}; + +void LayersPanel::_styleButton( Gtk::Button& btn, SPDesktop *desktop, unsigned int code, char const* iconName, char const* fallback ) +{ + bool set = false; + + if ( 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); + set = true; + } + + if ( desktop ) { + Verb *verb = Verb::get( code ); + if ( verb ) { + SPAction *action = verb->get_action(Inkscape::ActionContext(desktop)); + if ( !set && action && action->image ) { + GtkWidget *child = sp_get_icon_image(action->image, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show( child ); + btn.add( *Gtk::manage(Glib::wrap(child)) ); + set = true; + } + + if ( action && action->tip ) { + btn.set_tooltip_text (action->tip); + } + } + } + + if ( !set && fallback ) { + btn.set_label( fallback ); + } +} + + +Gtk::MenuItem& LayersPanel::_addPopupItem( SPDesktop *desktop, unsigned int code, int id ) +{ + Verb *verb = Verb::get( code ); + g_assert(verb); + SPAction *action = verb->get_action(Inkscape::ActionContext(desktop)); + + Gtk::MenuItem* item = Gtk::manage(new Gtk::MenuItem()); + + Gtk::Label *label = Gtk::manage(new Gtk::Label(action->name, true)); + label->set_xalign(0.0); + + if (_show_contextmenu_icons && action->image) { + item->set_name("ImageMenuItem"); // custom name to identify our "ImageMenuItems" + Gtk::Image *icon = Gtk::manage(sp_get_icon_image(action->image, Gtk::ICON_SIZE_MENU)); + + // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can only hold one child + Gtk::Box *box = Gtk::manage(new Gtk::Box()); + box->pack_start(*icon, false, false, 0); + box->pack_start(*label, true, true, 0); + item->add(*box); + } else { + item->add(*label); + } + + item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &LayersPanel::_takeAction), id)); + _popupMenu.append(*item); + + return *item; +} + +void LayersPanel::_fireAction( unsigned int code ) +{ + if ( _desktop ) { + Verb *verb = Verb::get( code ); + if ( verb ) { + SPAction *action = verb->get_action(Inkscape::ActionContext(_desktop)); + if ( action ) { + sp_action_perform( action, nullptr ); +// } else { +// g_message("no action"); + } +// } else { +// g_message("no verb for %u", code); + } +// } else { +// g_message("no active desktop"); + } +} + +// SP_VERB_LAYER_NEXT, +// SP_VERB_LAYER_PREV, +void LayersPanel::_takeAction( int val ) +{ + if ( !_pending ) { + _pending = new InternalUIBounce(); + _pending->_actionCode = val; + _pending->_target = _selectedLayer(); + Glib::signal_timeout().connect( sigc::mem_fun(*this, &LayersPanel::_executeAction), 0 ); + } +} + +bool LayersPanel::_executeAction() +{ + // Make sure selected layer hasn't changed since the action was triggered + if ( _pending + && ( + (_pending->_actionCode == BUTTON_NEW || _pending->_actionCode == DRAGNDROP) + || !( (_desktop && _desktop->currentLayer()) + && (_desktop->currentLayer() != _pending->_target) + ) + ) + ) { + int val = _pending->_actionCode; +// SPObject* target = _pending->_target; + + switch ( val ) { + case BUTTON_NEW: + { + _fireAction( SP_VERB_LAYER_NEW ); + } + break; + case BUTTON_RENAME: + { + _fireAction( SP_VERB_LAYER_RENAME ); + } + break; + case BUTTON_TOP: + { + _fireAction( SP_VERB_LAYER_TO_TOP ); + } + break; + case BUTTON_BOTTOM: + { + _fireAction( SP_VERB_LAYER_TO_BOTTOM ); + } + break; + case BUTTON_UP: + { + _fireAction( SP_VERB_LAYER_RAISE ); + } + break; + case BUTTON_DOWN: + { + _fireAction( SP_VERB_LAYER_LOWER ); + } + break; + case BUTTON_DUPLICATE: + { + _fireAction( SP_VERB_LAYER_DUPLICATE ); + } + break; + case BUTTON_DELETE: + { + _fireAction( SP_VERB_LAYER_DELETE ); + } + break; + case BUTTON_SOLO: + { + _fireAction( SP_VERB_LAYER_SOLO ); + } + break; + case BUTTON_SHOW_ALL: + { + _fireAction( SP_VERB_LAYER_SHOW_ALL ); + } + break; + case BUTTON_HIDE_ALL: + { + _fireAction( SP_VERB_LAYER_HIDE_ALL ); + } + break; + case BUTTON_LOCK_OTHERS: + { + _fireAction( SP_VERB_LAYER_LOCK_OTHERS ); + } + break; + case BUTTON_LOCK_ALL: + { + _fireAction( SP_VERB_LAYER_LOCK_ALL ); + } + break; + case BUTTON_UNLOCK_ALL: + { + _fireAction( SP_VERB_LAYER_UNLOCK_ALL ); + } + break; + case DRAGNDROP: + { + _doTreeMove( ); + } + break; + } + + delete _pending; + _pending = nullptr; + } + + return false; +} + +class LayersPanel::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; +}; + +void LayersPanel::_updateLayer( SPObject *layer ) { + _store->foreach( sigc::bind<SPObject*>(sigc::mem_fun(*this, &LayersPanel::_checkForUpdated), layer) ); +} + +bool LayersPanel::_checkForUpdated(const Gtk::TreePath &/*path*/, const Gtk::TreeIter& iter, SPObject* layer) +{ + bool stopGoing = false; + Gtk::TreeModel::Row row = *iter; + if ( layer == row[_model->_colObject] ) + { + /* + * We get notified of layer update here (from layer->setLabel()) before layer->label() is set + * with the correct value (sp-object bug?). So use the inkscape:label attribute instead which + * has the correct value (bug #168351) + */ + //row[_model->_colLabel] = layer->label() ? layer->label() : layer->defaultLabel(); + gchar const *label = layer->getAttribute("inkscape:label"); + row[_model->_colLabel] = label ? label : layer->defaultLabel(); + row[_model->_colVisible] = SP_IS_ITEM(layer) ? !SP_ITEM(layer)->isHidden() : false; + row[_model->_colLocked] = SP_IS_ITEM(layer) ? SP_ITEM(layer)->isLocked() : false; + + stopGoing = true; + } + + return stopGoing; +} + +void LayersPanel::_selectLayer( SPObject *layer ) { + if ( !layer || (_desktop && _desktop->doc() && (layer == _desktop->doc()->getRoot())) ) { + if ( _tree.get_selection()->count_selected_rows() != 0 ) { + _tree.get_selection()->unselect_all(); + } + } else { + _store->foreach( sigc::bind<SPObject*>(sigc::mem_fun(*this, &LayersPanel::_checkForSelected), layer) ); + } + + _checkTreeSelection(); +} + +bool LayersPanel::_checkForSelected(const Gtk::TreePath &path, const Gtk::TreeIter& iter, SPObject* layer) +{ + bool stopGoing = false; + + Gtk::TreeModel::Row row = *iter; + if ( layer == row[_model->_colObject] ) + { + _tree.expand_to_path( path ); + + Glib::RefPtr<Gtk::TreeSelection> select = _tree.get_selection(); + + select->select(iter); + + stopGoing = true; + } + + return stopGoing; +} + +void LayersPanel::_layersChanged() +{ +// g_message("_layersChanged()"); + if (_desktop) { + SPDocument* document = _desktop->doc(); + g_return_if_fail(document != nullptr); // bug #158: Crash on File>Quit + SPRoot* root = document->getRoot(); + if ( root ) { + _selectedConnection.block(); + if ( _desktop->layer_manager && _desktop->layer_manager->includes( root ) ) { + SPObject* target = _desktop->currentLayer(); + _store->clear(); + + #if DUMP_LAYERS + g_message("root:%p {%s} [%s]", root, root->id, root->label() ); + #endif // DUMP_LAYERS + _addLayer( document, root, nullptr, target, 0 ); + } + _selectedConnection.unblock(); + } + } +} + +void LayersPanel::_addLayer( SPDocument* doc, SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level ) +{ + if ( _desktop && _desktop->layer_manager && layer && (level < _maxNestDepth) ) { + unsigned int counter = _desktop->layer_manager->childCount(layer); + for ( unsigned int i = 0; i < counter; i++ ) { + SPObject *child = _desktop->layer_manager->nthChildOf(layer, i); + if ( child ) { +#if DUMP_LAYERS + g_message(" %3d layer:%p {%s} [%s]", level, child, child->getId(), 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->defaultLabel(); + 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); + + _checkTreeSelection(); + } + + _addLayer( doc, child, &row, target, level + 1 ); + } + } + } +} + +SPObject* LayersPanel::_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; +} + +void LayersPanel::_pushTreeSelectionToCurrent() +{ + // TODO hunt down the possible API abuse in getting NULL + if ( _desktop && _desktop->layer_manager && _desktop->currentRoot() ) { + SPObject* inTree = _selectedLayer(); + if ( inTree ) { + SPObject* curr = _desktop->currentLayer(); + if ( curr != inTree ) { + _desktop->layer_manager->setCurrentLayer( inTree ); + } + } else { + _desktop->layer_manager->setCurrentLayer( _desktop->doc()->getRoot() ); + } + } +} + +void LayersPanel::_checkTreeSelection() +{ + bool sensitive = false; + bool sensitiveNonTop = false; + bool sensitiveNonBottom = false; + if ( _tree.get_selection()->count_selected_rows() > 0 ) { + sensitive = true; + + SPObject* inTree = _selectedLayer(); + if ( inTree ) { + + sensitiveNonTop = (Inkscape::next_layer(inTree->parent, inTree) != nullptr); + sensitiveNonBottom = (Inkscape::previous_layer(inTree->parent, inTree) != nullptr); + + } + } + + + for (auto & it : _watching) { + it->set_sensitive( sensitive ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( sensitiveNonTop ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( sensitiveNonBottom ); + } +} + +void LayersPanel::_preToggle( GdkEvent const *event ) +{ + + if ( _toggleEvent ) { + gdk_event_free(_toggleEvent); + _toggleEvent = nullptr; + } + + if ( event && (event->type == GDK_BUTTON_PRESS) ) { + // Make a copy so we can keep it around. + _toggleEvent = gdk_event_copy(const_cast<GdkEvent*>(event)); + } +} + +void LayersPanel::_toggled( Glib::ustring const& str, int targetCol ) +{ + g_return_if_fail(_desktop != nullptr); + + Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(str); + Gtk::TreeModel::Row row = *iter; + + Glib::ustring tmp = row[_model->_colLabel]; + + SPObject* obj = row[_model->_colObject]; + SPItem* item = ( obj && SP_IS_ITEM(obj) ) ? SP_ITEM(obj) : nullptr; + if ( item ) { + switch ( targetCol ) { + case COL_VISIBLE: + { + bool newValue = !row[_model->_colVisible]; + row[_model->_colVisible] = newValue; + item->setHidden( !newValue ); + item->updateRepr(); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_LAYERS, + newValue? _("Unhide layer") : _("Hide layer")); + } + break; + + case COL_LOCKED: + { + bool newValue = !row[_model->_colLocked]; + row[_model->_colLocked] = newValue; + item->setLocked( newValue ); + item->updateRepr(); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_LAYERS, + newValue? _("Lock layer") : _("Unlock layer")); + } + break; + } + } + Inkscape::SelectionHelper::fixSelection(_desktop); +} + +bool LayersPanel::_handleButtonEvent(GdkEventButton* event) +{ + static unsigned doubleclick = 0; + + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 3) ) { + // TODO - fix to a better is-popup function + Gtk::TreeModel::Path path; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + if ( _tree.get_path_at_pos( x, y, path ) ) { + _checkTreeSelection(); + + _popupMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } + } + + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 1) + && (event->state & GDK_MOD1_MASK)) { + // Alt left click on the visible/lock columns - eat this event to keep row selection + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (col == _tree.get_column(COL_VISIBLE-1) || + col == _tree.get_column(COL_LOCKED-1)) { + return true; + } + } + } + + // TODO - ImageToggler doesn't seem to handle Shift/Alt clicks - so we deal with them here. + if ( (event->type == GDK_BUTTON_RELEASE) && (event->button == 1) + && (event->state & (GDK_SHIFT_MASK | GDK_MOD1_MASK))) { + + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (event->state & GDK_SHIFT_MASK) { + // Shift left click on the visible/lock columns toggles "solo" mode + if (col == _tree.get_column(COL_VISIBLE - 1)) { + _takeAction(BUTTON_SOLO); + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + _takeAction(BUTTON_LOCK_OTHERS); + } + } else if (event->state & GDK_MOD1_MASK) { + // Alt+left click on the visible/lock columns toggles "solo" mode and preserves selection + Gtk::TreeModel::iterator iter = _store->get_iter(path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + SPObject *obj = row[_model->_colObject]; + if (col == _tree.get_column(COL_VISIBLE - 1)) { + _desktop->toggleLayerSolo( obj ); + DocumentUndo::maybeDone(_desktop->doc(), "layer:solo", SP_VERB_LAYER_SOLO, _("Toggle layer solo")); + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + _desktop->toggleLockOtherLayers( obj ); + DocumentUndo::maybeDone(_desktop->doc(), "layer:lockothers", SP_VERB_LAYER_LOCK_OTHERS, _("Lock other layers")); + } + } + } + } + } + + + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + doubleclick = 1; + } + + return false; +} + +/* + * Drap and drop within the tree + * Save the drag source and drop target SPObjects and if its a drag between layers or into (sublayer) a layer + */ +bool LayersPanel::_handleDragDrop(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int x, int y, guint /*time*/) +{ + int cell_x = 0, cell_y = 0; + Gtk::TreeModel::Path target_path; + Gtk::TreeView::Column *target_column; + SPObject *selected = _selectedLayer(); + + _dnd_into = false; + _dnd_target = nullptr; + _dnd_source = ( selected && SP_IS_ITEM(selected) ) ? SP_ITEM(selected) : nullptr; + + if (_tree.get_path_at_pos (x, y, target_path, target_column, cell_x, cell_y)) { + // Are we before, inside or after the drop layer + Gdk::Rectangle rect; + _tree.get_background_area (target_path, *target_column, rect); + int cell_height = rect.get_height(); + _dnd_into = (cell_y > (int)(cell_height * 1/3) && cell_y <= (int)(cell_height * 2/3)); + if (cell_y > (int)(cell_height * 2/3)) { + Gtk::TreeModel::Path next_path = target_path; + next_path.next(); + if (_store->iter_is_valid(_store->get_iter(next_path))) { + target_path = next_path; + } else { + // Dragging to the "end" + Gtk::TreeModel::Path up_path = target_path; + up_path.up(); + if (_store->iter_is_valid(_store->get_iter(up_path))) { + // Drop into parent + target_path = up_path; + _dnd_into = true; + } else { + // Drop into the top level + _dnd_target = nullptr; + } + } + } + Gtk::TreeModel::iterator iter = _store->get_iter(target_path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + SPObject *obj = row[_model->_colObject]; + _dnd_target = ( obj && SP_IS_ITEM(obj) ) ? SP_ITEM(obj) : nullptr; + } + } + + _takeAction(DRAGNDROP); + + return false; +} + +/* + * Move a layer in response to a drag & drop action + */ +void LayersPanel::_doTreeMove( ) +{ + if (_dnd_source && _dnd_source->getRepr() ) { + if(!_dnd_target){ + _dnd_source->doWriteTransform(_dnd_source->i2doc_affine() * _dnd_source->document->getRoot()->i2doc_affine().inverse()); + }else{ + SPItem* parent = _dnd_into ? _dnd_target : dynamic_cast<SPItem*>(_dnd_target->parent); + if(parent){ + Geom::Affine move = _dnd_source->i2doc_affine() * parent->i2doc_affine().inverse(); + _dnd_source->doWriteTransform(move); + } + } + _dnd_source->moveTo(_dnd_target, _dnd_into); + _selectLayer(_dnd_source); + _dnd_source = nullptr; + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Move layer")); + } +} + + +void LayersPanel::_handleEdited(const Glib::ustring& path, const Glib::ustring& new_text) +{ + Gtk::TreeModel::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + _renameLayer(row, new_text); +} + +void LayersPanel::_renameLayer(Gtk::TreeModel::Row row, const Glib::ustring& name) +{ + if ( row && _desktop && _desktop->layer_manager) { + SPObject* obj = row[_model->_colObject]; + if ( obj ) { + gchar const* oldLabel = obj->label(); + if ( !name.empty() && (!oldLabel || name != oldLabel) ) { + _desktop->layer_manager->renameLayer( obj, name.c_str(), FALSE ); + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Rename layer")); + } + + } + } +} + +bool LayersPanel::_rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool currentlySelected ) +{ + bool val = true; + if ( !currentlySelected && _toggleEvent ) + { + GdkEvent* event = gtk_get_current_event(); + if ( event ) { + // (keep these checks separate, so we know when to call gdk_event_free() + if ( event->type == GDK_BUTTON_PRESS ) { + GdkEventButton const* target = reinterpret_cast<GdkEventButton const*>(_toggleEvent); + GdkEventButton const* evtb = reinterpret_cast<GdkEventButton const*>(event); + + if ( (evtb->window == target->window) + && (evtb->send_event == target->send_event) + && (evtb->time == target->time) + && (evtb->state == target->state) + ) + { + // Ooooh! It's a magic one + val = false; + } + } + gdk_event_free(event); + } + } + return val; +} + +/** + * Constructor + */ +LayersPanel::LayersPanel() : + UI::Widget::Panel("/dialogs/layers", SP_VERB_DIALOG_LAYERS), + deskTrack(), + _maxNestDepth(20), + _desktop(nullptr), + _model(nullptr), + _pending(nullptr), + _toggleEvent(nullptr), + _compositeSettings(SP_VERB_DIALOG_LAYERS, "layers", + UI::Widget::SimpleFilterModifier::ISOLATION | + UI::Widget::SimpleFilterModifier::BLEND | + UI::Widget::SimpleFilterModifier::OPACITY | + UI::Widget::SimpleFilterModifier::BLUR), + desktopChangeConn() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _maxNestDepth = prefs->getIntLimited("/dialogs/layers/maxDepth", 20, 1, 1000); + + ModelColumns *zoop = new ModelColumns(); + _model = zoop; + + _store = Gtk::TreeStore::create( *zoop ); + + _tree.set_model( _store ); + _tree.set_headers_visible(false); + _tree.set_reorderable(true); + _tree.enable_model_drag_dest (Gdk::ACTION_MOVE); + + Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) ); + int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1; + eyeRenderer->signal_pre_toggle().connect( sigc::mem_fun(*this, &LayersPanel::_preToggle) ); + eyeRenderer->signal_toggled().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_toggled), (int)COL_VISIBLE) ); + eyeRenderer->property_activatable() = true; + Gtk::TreeViewColumn* col = _tree.get_column(visibleColNum); + if ( col ) { + col->add_attribute( eyeRenderer->property_active(), _model->_colVisible ); + } + + + Inkscape::UI::Widget::ImageToggler * renderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked")) ); + int lockedColNum = _tree.append_column("lock", *renderer) - 1; + renderer->signal_pre_toggle().connect( sigc::mem_fun(*this, &LayersPanel::_preToggle) ); + renderer->signal_toggled().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_toggled), (int)COL_LOCKED) ); + renderer->property_activatable() = true; + col = _tree.get_column(lockedColNum); + if ( col ) { + col->add_attribute( renderer->property_active(), _model->_colLocked ); + } + + _text_renderer = Gtk::manage(new Gtk::CellRendererText()); + int nameColNum = _tree.append_column("Name", *_text_renderer) - 1; + _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.set_search_column(nameColNum + 1); + + _compositeSettings.setSubject(&_subject); + + _selectedConnection = _tree.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &LayersPanel::_pushTreeSelectionToCurrent) ); + _tree.get_selection()->set_select_function( sigc::mem_fun(*this, &LayersPanel::_rowSelectFunction) ); + + _tree.signal_drag_drop().connect( sigc::mem_fun(*this, &LayersPanel::_handleDragDrop), false); + + _text_renderer->property_editable() = true; + _text_renderer->signal_edited().connect( sigc::mem_fun(*this, &LayersPanel::_handleEdited) ); + + _tree.signal_button_press_event().connect( sigc::mem_fun(*this, &LayersPanel::_handleButtonEvent), false ); + _tree.signal_button_release_event().connect( sigc::mem_fun(*this, &LayersPanel::_handleButtonEvent), 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); + } + + _watching.push_back( &_compositeSettings ); + + _layersPage.pack_start( _scroller, Gtk::PACK_EXPAND_WIDGET ); + _layersPage.pack_end(_compositeSettings, Gtk::PACK_SHRINK); + _layersPage.pack_end(_buttonsRow, Gtk::PACK_SHRINK); + + _getContents()->pack_start(_layersPage, Gtk::PACK_EXPAND_WIDGET); + + SPDesktop* targetDesktop = getDesktop(); + + Gtk::Button* btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_NEW, INKSCAPE_ICON("list-add"), C_("Layers", "New") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_NEW) ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_TO_BOTTOM, INKSCAPE_ICON("go-bottom"), C_("Layers", "Bot") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_BOTTOM) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_LOWER, INKSCAPE_ICON("go-down"), C_("Layers", "Dn") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_DOWN) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_RAISE, INKSCAPE_ICON("go-up"), C_("Layers", "Up") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_UP) ); + _watchingNonTop.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_TO_TOP, INKSCAPE_ICON("go-top"), C_("Layers", "Top") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_TOP) ); + _watchingNonTop.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + +// btn = Gtk::manage( new Gtk::Button("Dup") ); +// btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_DUPLICATE) ); +// _buttonsRow.add( *btn ); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_DELETE, INKSCAPE_ICON("list-remove"), _("X") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_DELETE) ); + _watching.push_back( btn ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + _buttonsRow.pack_start(_buttonsSecondary, Gtk::PACK_EXPAND_WIDGET); + _buttonsRow.pack_end(_buttonsPrimary, Gtk::PACK_EXPAND_WIDGET); + + + + // ------------------------------------------------------- + { + _show_contextmenu_icons = prefs->getBool("/theme/menuIcons_layers", true); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_NEW, (int)BUTTON_NEW ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_RENAME, (int)BUTTON_RENAME ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SOLO, (int)BUTTON_SOLO ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SHOW_ALL, (int)BUTTON_SHOW_ALL ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_HIDE_ALL, (int)BUTTON_HIDE_ALL ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_OTHERS, (int)BUTTON_LOCK_OTHERS ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_ALL, (int)BUTTON_LOCK_ALL ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_UNLOCK_ALL, (int)BUTTON_UNLOCK_ALL ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watchingNonTop.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_RAISE, (int)BUTTON_UP ) ); + _watchingNonBottom.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOWER, (int)BUTTON_DOWN ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_DUPLICATE, (int)BUTTON_DUPLICATE ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_DELETE, (int)BUTTON_DELETE ) ); + + _popupMenu.show_all_children(); + + // Install CSS to shift icons into the space reserved for toggles (i.e. check and radio items). + _popupMenu.signal_map().connect(sigc::mem_fun(static_cast<ContextMenu*>(&_popupMenu), &ContextMenu::ShiftIcons)); + } + // ------------------------------------------------------- + + + + for (auto & it : _watching) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( false ); + } + + setDesktop( targetDesktop ); + + show_all_children(); + + // restorePanelPrefs(); + + // Connect this up last + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &LayersPanel::setDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +LayersPanel::~LayersPanel() +{ + setDesktop(nullptr); + + _compositeSettings.setSubject(nullptr); + + if ( _model ) + { + delete _model; + _model = nullptr; + } + + if (_pending) { + delete _pending; + _pending = nullptr; + } + + if ( _toggleEvent ) + { + gdk_event_free( _toggleEvent ); + _toggleEvent = nullptr; + } + + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + + +void LayersPanel::setDesktop( SPDesktop* desktop ) +{ + Panel::setDesktop(desktop); + + if ( desktop != _desktop ) { + _layerChangedConnection.disconnect(); + _layerUpdatedConnection.disconnect(); + _changedConnection.disconnect(); + if ( _desktop ) { + _desktop = nullptr; + } + + _desktop = Panel::getDesktop(); + if ( _desktop ) { + //setLabel( _desktop->doc()->name ); + + LayerManager *mgr = _desktop->layer_manager; + if ( mgr ) { + _layerChangedConnection = mgr->connectCurrentLayerChanged( sigc::mem_fun(*this, &LayersPanel::_selectLayer) ); + _layerUpdatedConnection = mgr->connectLayerDetailsChanged( sigc::mem_fun(*this, &LayersPanel::_updateLayer) ); + _changedConnection = mgr->connectChanged( sigc::mem_fun(*this, &LayersPanel::_layersChanged) ); + } + + _layersChanged(); + } + } + deskTrack.setBase(desktop); +} + + + +} //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/layers.h b/src/ui/dialog/layers.h new file mode 100644 index 0000000..8a66eef --- /dev/null +++ b/src/ui/dialog/layers.h @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple dialog for layer UI. + * + * Authors: + * Jon A. Cruz + * + * Copyright (C) 2006,2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_LAYERS_PANEL_H +#define SEEN_LAYERS_PANEL_H + +#include <gtkmm/box.h> +#include <gtkmm/treeview.h> +#include <gtkmm/treestore.h> +#include <gtkmm/scrolledwindow.h> +#include "ui/widget/spinbutton.h" +#include "ui/widget/panel.h" +#include "ui/widget/object-composite-settings.h" +#include "desktop-tracker.h" +#include "ui/widget/style-subject.h" + +class SPObject; + +namespace Inkscape { + +class LayerManager; + +namespace UI { +namespace Dialog { + + +/** + * A panel that displays layers. + */ +class LayersPanel : public UI::Widget::Panel +{ +public: + LayersPanel(); + ~LayersPanel() override; + + static LayersPanel& getInstance(); + + void setDesktop( SPDesktop* desktop ) override; + +private: + class ModelColumns; + class InternalUIBounce; + + LayersPanel(LayersPanel const &) = delete; // no copy + LayersPanel &operator=(LayersPanel const &) = delete; // no assign + + void _styleButton( Gtk::Button& btn, SPDesktop *desktop, unsigned int code, char const* iconName, char const* fallback ); + void _fireAction( unsigned int code ); + Gtk::MenuItem& _addPopupItem( SPDesktop *desktop, unsigned int code, int id ); + + void _preToggle( GdkEvent const *event ); + void _toggled( Glib::ustring const& str, int targetCol ); + + bool _handleButtonEvent(GdkEventButton *event); + bool _handleKeyEvent(GdkEventKey *event); + bool _handleDragDrop(const Glib::RefPtr<Gdk::DragContext>& context, int x, int y, guint time); + void _handleEdited(const Glib::ustring& path, const Glib::ustring& new_text); + void _handleEditingCancelled(); + + void _doTreeMove(); + void _renameLayer(Gtk::TreeModel::Row row, const Glib::ustring& name); + + void _pushTreeSelectionToCurrent(); + void _checkTreeSelection(); + + void _takeAction( int val ); + bool _executeAction(); + + bool _rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b ); + + void _updateLayer(SPObject *layer); + bool _checkForUpdated(const Gtk::TreePath &path, const Gtk::TreeIter& iter, SPObject* layer); + + void _selectLayer(SPObject *layer); + bool _checkForSelected(const Gtk::TreePath& path, const Gtk::TreeIter& iter, SPObject* layer); + + void _layersChanged(); + void _addLayer( SPDocument* doc, SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level ); + + SPObject* _selectedLayer(); + + // Hooked to the layer manager: + sigc::connection _layerChangedConnection; + sigc::connection _layerUpdatedConnection; + sigc::connection _changedConnection; + sigc::connection _addedConnection; + sigc::connection _removedConnection; + + // Internal + sigc::connection _selectedConnection; + + DesktopTracker deskTrack; + int _maxNestDepth; + SPDesktop* _desktop; + ModelColumns* _model; + InternalUIBounce* _pending; + gboolean _dnd_into; + SPItem* _dnd_source; + SPItem* _dnd_target; + GdkEvent* _toggleEvent; + bool _show_contextmenu_icons; + + Glib::RefPtr<Gtk::TreeStore> _store; + 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::Box _buttonsRow; + Gtk::Box _buttonsPrimary; + Gtk::Box _buttonsSecondary; + Gtk::ScrolledWindow _scroller; + Gtk::Menu _popupMenu; + Inkscape::UI::Widget::SpinButton _spinBtn; + Gtk::VBox _layersPage; + + UI::Widget::StyleSubject::CurrentLayer _subject; + UI::Widget::ObjectCompositeSettings _compositeSettings; + sigc::connection desktopChangeConn; +}; + + + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + + +#endif // SEEN_LAYERS_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/livepatheffect-add.cpp b/src/ui/dialog/livepatheffect-add.cpp new file mode 100644 index 0000000..8dbe30a --- /dev/null +++ b/src/ui/dialog/livepatheffect-add.cpp @@ -0,0 +1,933 @@ +// 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 "desktop.h" +#include "dialog.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 <cmath> +#include <glibmm/i18n.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); + } + } +} + +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) +{ + Glib::ustring gladefile = get_filename(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 filter 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(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 = _(converter.get_label(data->id).c_str()); + const Glib::ustring untranslated_label = converter.get_untranslated_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)); + _LPEDialogSelector->set_transient_for(*window); + _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.signal_change_theme.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); + } + } + 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, please disable the favorites star")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } else { + _LPEInfo->set_text(_("This is your favorite effects")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } + } else { + _LPEInfo->set_text(_("Your search do a empty result, please try again")); + _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(_("Your search do a empty result, please try again")); + _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(_("Your search do a empty result, please try again")); + _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_right(25); + overlay->set_margin_right(5); + } else { + icon->set_pixel_size(60); + icon->set_margin_right(0); + overlay->set_margin_right(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_right(40); + } else { + icon->set_pixel_size(60); + icon->set_margin_right(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()); + 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..472f9ac --- /dev/null +++ b/src/ui/dialog/livepatheffect-editor.cpp @@ -0,0 +1,634 @@ +// 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 "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "livepatheffect-add.h" +#include "path-chemistry.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "helper/action.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-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 lpeeditor_selection_changed (Inkscape::Selection * selection, gpointer data) +{ + LivePathEffectEditor *lpeeditor = static_cast<LivePathEffectEditor *>(data); + lpeeditor->lpe_list_locked = false; + lpeeditor->onSelectionChanged(selection); + lpeeditor->_on_button_release(nullptr); //to force update widgets +} + +void lpeeditor_selection_modified (Inkscape::Selection * selection, guint /*flags*/, gpointer data) +{ + + LivePathEffectEditor *lpeeditor = static_cast<LivePathEffectEditor *>(data); + lpeeditor->lpe_list_locked = false; + lpeeditor->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() + : UI::Widget::Panel("/dialogs/livepatheffect", SP_VERB_DIALOG_LIVE_PATH_EFFECT), + deskTrack(), + lpe_list_locked(false), + effectwidget(nullptr), + status_label("", Gtk::ALIGN_CENTER), + effectcontrol_frame(""), + button_add(), + button_remove(), + button_up(), + button_down(), + current_desktop(nullptr), + current_lpeitem(nullptr), + current_lperef(nullptr) +{ + Gtk::Box *contents = _getContents(); + contents->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); + + effectapplication_hbox.set_spacing(4); + 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_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_up ); + toolbar_hbox.add( button_down ); + + //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); + + contents->pack_start(effectlist_vbox, true, true); + contents->pack_start(status_label, false, false); + contents->pack_start(effectcontrol_frame, false, false); + + effectcontrol_frame.hide(); + + // 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_up.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onUp)); + button_down.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onDown)); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &LivePathEffectEditor::setDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); +} + +LivePathEffectEditor::~LivePathEffectEditor() +{ + if (effectwidget) { + effectcontrol_vbox.remove(*effectwidget); + delete effectwidget; + effectwidget = nullptr; + } + + if (current_desktop) { + selection_changed_connection.disconnect(); + selection_modified_connection.disconnect(); + } +} + +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(); + 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); + } +} + + +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(); + + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + if ( item ) { + 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 *orig = use->get_original(); + 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; + } + } +} + + +void +LivePathEffectEditor::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + + if ( desktop == current_desktop ) { + return; + } + + if (current_desktop) { + selection_changed_connection.disconnect(); + selection_modified_connection.disconnect(); + } + + lpe_list_locked = false; + current_desktop = desktop; + if (desktop) { + Inkscape::Selection *selection = desktop->getSelection(); + selection_changed_connection = selection->connectChanged( + sigc::bind (sigc::ptr_fun(&lpeeditor_selection_changed), this ) ); + selection_modified_connection = selection->connectModified( + sigc::bind (sigc::ptr_fun(&lpeeditor_selection_modified), this ) ); + + onSelectionChanged(selection); + } else { + onSelectionChanged(nullptr); + } +} + +/*######################################################################## +# 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() +{ + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + if (item) { + if ( dynamic_cast<SPLPEItem *>(item) ) { + // show effectlist dialog + using Inkscape::UI::Dialog::LivePathEffectAdd; + LivePathEffectAdd::show(current_desktop); + if ( !LivePathEffectAdd::isApplied()) { + return; + } + + SPDocument *doc = current_desktop->doc(); + + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data = + LivePathEffectAdd::getActiveData(); + if (!data) { + return; + } + item = sel->singleItem(); // get new item + + LivePathEffect::Effect::createAndApply(data->key.c_str(), doc, item); + + DocumentUndo::done(doc, SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Create and apply path effect")); + + lpe_list_locked = false; + onSelectionChanged(sel); + } 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 + sel->set(orig); + + // delete clone but remember its id and transform + gchar *id = g_strdup(item->getRepr()->attribute("id")); + gchar *transform = g_strdup(item->getRepr()->attribute("transform")); + item->deleteObject(false); + item = nullptr; + + // run sp_selection_clone_original_path_lpe + sel->cloneOriginalPathLPE(true); + + SPItem *new_item = sel->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); + new_item->setAttribute("transform", transform); + } + g_free(id); + g_free(transform); + + /// \todo Add the LPE stack of the original path? + + SPDocument *doc = current_desktop->doc(); + DocumentUndo::done(doc, SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Create and apply Clone original path effect")); + + lpe_list_locked = false; + onSelectionChanged(sel); + } + } + } + } + } +} + +void +LivePathEffectEditor::onRemove() +{ + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->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( current_desktop->getDocument(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Remove path effect") ); + lpe_list_locked = false; + onSelectionChanged(sel); + } + } + +} + +void LivePathEffectEditor::onUp() +{ + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if ( lpeitem ) { + Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE(); + lpeitem->upCurrentPathEffect(); + DocumentUndo::done( current_desktop->getDocument(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Move path effect up") ); + + effect_list_reload(lpeitem); + if (lpe) { + showParams(*lpe); + lpe_list_locked = true; + selectInList(lpe); + } + } + } +} + +void LivePathEffectEditor::onDown() +{ + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if ( lpeitem ) { + Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE(); + lpeitem->downCurrentPathEffect(); + + DocumentUndo::done( current_desktop->getDocument(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Move path effect down") ); + 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(); + 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 (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 + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + if (item) { + sel->clear(); + sel->add(item); + Inkscape::UI::Tools::sp_update_helperpath(); + } + } + } + } + } +} + +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; + + 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"); + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if ( lpeitem ) { + lpeobjref->lpeobject->get_lpe()->doOnVisibilityToggled(lpeitem); + } + } + DocumentUndo::done( current_desktop->getDocument(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + newValue ? _("Activate path effect") : _("Deactivate path effect")); + } +} + +} // 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..30524e4 --- /dev/null +++ b/src/ui/dialog/livepatheffect-editor.h @@ -0,0 +1,152 @@ +// 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 "ui/widget/panel.h" + +#include <gtkmm/label.h> +#include <gtkmm/frame.h> +#include "ui/widget/combo-enums.h" +#include "ui/widget/frame.h" +#include "object/sp-item.h" +#include "live_effects/effect-enum.h" +#include <gtkmm/liststore.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/treeview.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/toolbar.h> +#include <gtkmm/buttonbox.h> +#include "ui/dialog/desktop-tracker.h" + +class SPDesktop; +class SPLPEItem; + +namespace Inkscape { + +namespace LivePathEffect { + class Effect; + class LPEObjectReference; +} + +namespace UI { +namespace Dialog { + +class LivePathEffectEditor : public UI::Widget::Panel { +public: + LivePathEffectEditor(); + ~LivePathEffectEditor() override; + + static LivePathEffectEditor &getInstance() { return *new LivePathEffectEditor(); } + + void onSelectionChanged(Inkscape::Selection *sel); + void onSelectionModified(Inkscape::Selection *sel); + virtual void on_effect_selection_changed(); + void setDesktop(SPDesktop *desktop) override; + +private: + + /** + * Auxiliary widget to keep track of desktop changes for the floating dialog. + */ + DesktopTracker deskTrack; + + /** + * Link to callback function for a change in desktop (window). + */ + sigc::connection desktopChangeConn; + sigc::connection selection_changed_connection; + sigc::connection selection_modified_connection; + + // void add_entry(const char* name ); + 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(); + + 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<LivePathEffect::LPEObjectReference *> lperef; + Gtk::TreeModelColumn<bool> col_visible; + }; + + bool lpe_list_locked; + //Inkscape::UI::Widget::ComboBoxEnum<LivePathEffect::EffectType> combo_effecttype; + + Gtk::Widget * effectwidget; + Gtk::Label status_label; + UI::Widget::Frame effectcontrol_frame; + Gtk::HBox effectapplication_hbox; + Gtk::VBox effectcontrol_vbox; + Gtk::EventBox effectcontrol_eventbox; + Gtk::VBox 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_up; + Gtk::Button button_down; + + SPDesktop * current_desktop; + + SPLPEItem * current_lpeitem; + + 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); + LivePathEffectEditor& operator=(LivePathEffectEditor const &d); +}; + +} // 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..bbf4b31 --- /dev/null +++ b/src/ui/dialog/lpe-fillet-chamfer-properties.cpp @@ -0,0 +1,276 @@ +// 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() + : _desktop(nullptr), + _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 tha max aloable 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 tha max aloable 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() +{ + + _setDesktop(nullptr); +} + +void FilletChamferPropertiesDialog::showDialog( + SPDesktop *desktop, + double _amount, + const Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity *pt, + bool _use_distance, + bool _aprox_radius, + Satellite _satellite) +{ + FilletChamferPropertiesDialog *dialog = new FilletChamferPropertiesDialog(); + + dialog->_setDesktop(desktop); + dialog->_setUseDistance(_use_distance); + dialog->_setAprox(_aprox_radius); + dialog->_setAmount(_amount); + dialog->_setSatellite(_satellite); + 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) { + _satellite.satellite_type = FILLET; + } else if (_fillet_chamfer_type_inverse_fillet.get_active() == true) { + _satellite.satellite_type = INVERSE_FILLET; + } else if (_fillet_chamfer_type_inverse_chamfer.get_active() == true) { + _satellite.satellite_type = INVERSE_CHAMFER; + } else { + _satellite.satellite_type = CHAMFER; + } + if (_flexible) { + if (d_pos > 99.99999 || d_pos < 0) { + d_pos = 0; + } + d_pos = d_pos / 100; + } + _satellite.amount = d_pos; + size_t steps = (size_t)_fillet_chamfer_chamfer_subdivisions.get_value(); + if (steps < 1) { + steps = 1; + } + _satellite.steps = steps; + _knotpoint->knot_set_offset(_satellite); + } + _close(); +} + +void FilletChamferPropertiesDialog::_close() +{ + _setDesktop(nullptr); + 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::_setSatellite(Satellite satellite) +{ + 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 (satellite.is_time) { + position = _amount * 100; + _flexible = true; + _fillet_chamfer_position_label.set_label(_("Position (%):")); + } else { + _flexible = false; + std::string 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(satellite.steps); + if (satellite.satellite_type == FILLET) { + _fillet_chamfer_type_fillet.set_active(true); + } else if (satellite.satellite_type == INVERSE_FILLET) { + _fillet_chamfer_type_inverse_fillet.set_active(true); + } else if (satellite.satellite_type == CHAMFER) { + _fillet_chamfer_type_chamfer.set_active(true); + } else if (satellite.satellite_type == INVERSE_CHAMFER) { + _fillet_chamfer_type_inverse_chamfer.set_active(true); + } + _satellite = satellite; +} + +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; +} + +void FilletChamferPropertiesDialog::_setDesktop(SPDesktop *desktop) +{ + if (desktop) { + Inkscape::GC::anchor(desktop); + } + if (_desktop) { + Inkscape::GC::release(_desktop); + } + _desktop = desktop; +} + +} // 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..26a0569 --- /dev/null +++ b/src/ui/dialog/lpe-fillet-chamfer-properties.h @@ -0,0 +1,114 @@ +// 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/satellitesarray.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, + Satellite _satellite); + +protected: + + SPDesktop *_desktop; + 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 _setDesktop(SPDesktop *desktop); + void _setPt(const Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity *pt); + void _setUseDistance(bool use_knot_distance); + void _setAprox(bool aprox_radius); + void _setAmount(double amount); + void _setSatellite(Satellite satellite); + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); + + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton *event); + + void _apply(); + void _close(); + bool _flexible; + Satellite _satellite; + 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..1e5d1b1 --- /dev/null +++ b/src/ui/dialog/lpe-powerstroke-properties.cpp @@ -0,0 +1,199 @@ +// 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() + : _desktop(nullptr), + _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() { + + _setDesktop(nullptr); +} + +void PowerstrokePropertiesDialog::showDialog(SPDesktop *desktop, Geom::Point knotpoint, const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt) +{ + PowerstrokePropertiesDialog *dialog = new PowerstrokePropertiesDialog(); + + dialog->_setDesktop(desktop); + 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() +{ + _setDesktop(nullptr); + 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); +} + +void PowerstrokePropertiesDialog::_setDesktop(SPDesktop *desktop) { + if (desktop) { + Inkscape::GC::anchor (desktop); + } + if (_desktop) { + Inkscape::GC::release (_desktop); + } + _desktop = desktop; +} + +} // 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..0bf6539 --- /dev/null +++ b/src/ui/dialog/lpe-powerstroke-properties.h @@ -0,0 +1,92 @@ +// 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: + + SPDesktop *_desktop; + 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 _setDesktop(SPDesktop *desktop); + 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..a4210ca --- /dev/null +++ b/src/ui/dialog/memory.cpp @@ -0,0 +1,247 @@ +// 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" +#include "verbs.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() + : UI::Widget::Panel("/dialogs/memory", SP_VERB_HELP_MEMORY), + _private(*(new Memory::Private())) +{ + _getContents()->pack_start(_private.view); + + _private.update(); + + addResponseButton(_("Recalculate"), Gtk::RESPONSE_APPLY); + + show_all_children(); + + signal_show().connect(sigc::mem_fun(_private, &Private::start_update_task)); + signal_hide().connect(sigc::mem_fun(_private, &Private::stop_update_task)); + + _private.start_update_task(); +} + +Memory::~Memory() { + delete &_private; +} + +void Memory::_apply() { + GC::Core::gcollect(); + _private.update(); +} + +} // 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..5e6a917 --- /dev/null +++ b/src/ui/dialog/memory.h @@ -0,0 +1,54 @@ +// 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/widget/panel.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class Memory : public UI::Widget::Panel { +public: + Memory(); + ~Memory() override; + + static Memory &getInstance() { return *new Memory(); } + +protected: + void _apply() override; + +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..85ba840 --- /dev/null +++ b/src/ui/dialog/messages.cpp @@ -0,0 +1,216 @@ +// 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" +#include "verbs.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() + : UI::Widget::Panel("/dialogs/messages", SP_VERB_DIALOG_DEBUG), + buttonClear(_("_Clear"), _("Clear log messages")), + checkCapture(_("Capture log messages"), _("Capture log messages")) +{ + Gtk::Box *contents = _getContents(); + + /* + * 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); + contents->pack_start(textScroll); + + buttonBox.set_spacing(6); + buttonBox.pack_start(checkCapture, true, true, 6); + buttonBox.pack_end(buttonClear, false, false, 10); + contents->pack_start(buttonBox, Gtk::PACK_SHRINK); + + // sick of this thing shrinking too much + set_size_request(400, 300); + + 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..b3ad393 --- /dev/null +++ b/src/ui/dialog/messages.h @@ -0,0 +1,103 @@ +// 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 <gtkmm/box.h> +#include <gtkmm/textview.h> +#include <gtkmm/button.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/menubar.h> +#include <gtkmm/menu.h> +#include <gtkmm/scrolledwindow.h> + +#include <glibmm/i18n.h> + +#include "ui/widget/panel.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class Messages : public UI::Widget::Panel { +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::HBox 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..f129854 --- /dev/null +++ b/src/ui/dialog/object-attributes.cpp @@ -0,0 +1,218 @@ +// 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 "verbs.h" + +#include "object/sp-anchor.h" +#include "object/sp-image.h" + +#include "ui/dialog/object-attributes.h" +#include "ui/dialog/dialog-manager.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 () : + UI::Widget::Panel("/dialogs/objectattr/", SP_VERB_DIALOG_ATTR), + blocked (false), + CurrentItem(nullptr), + attrTable(Gtk::manage(new SPAttributeTable())), + desktop(nullptr), + deskTrack(), + selectChangedConn(), + subselChangedConn(), + selectModifiedConn() +{ + attrTable->show(); + widget_setup(); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &ObjectAttributes::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +ObjectAttributes::~ObjectAttributes () +{ + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +void ObjectAttributes::widget_setup () +{ + if (blocked) + { + return; + } + + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->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::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (this->desktop) { + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &ObjectAttributes::widget_setup))); + subselChangedConn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &ObjectAttributes::widget_setup))); + + // Must check flags, so can't call widget_setup() directly. + selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &ObjectAttributes::selectionModifiedCB))); + } + widget_setup(); + } +} + +void ObjectAttributes::selectionModifiedCB( 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..ad718ea --- /dev/null +++ b/src/ui/dialog/object-attributes.h @@ -0,0 +1,123 @@ +// 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/desktop-tracker.h" +#include "ui/widget/panel.h" + +class SPAttributeTable; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A dialog widget to show object attributes (currently for images and links). + */ +class ObjectAttributes : public Widget::Panel { +public: + ObjectAttributes (); + ~ObjectAttributes () 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; + + /** + * Stores the current desktop. + */ + SPDesktop *desktop; + + /** + * Auxiliary widget to keep track of desktop changes for the floating dialog. + */ + DesktopTracker deskTrack; + + /** + * Link to callback function for a change in desktop (window). + */ + sigc::connection desktopChangeConn; + + /** + * Link to callback function for a selection change. + */ + sigc::connection selectChangedConn; + sigc::connection subselChangedConn; + + /** + * Link to callback function for a modification of the selected object. + */ + sigc::connection selectModifiedConn; + + /** + * Callback function invoked by the desktop tracker in case of a modification of the selected object. + */ + void selectionModifiedCB( guint flags ); + + /* + * Can be invoked for setting the desktop. Currently not used. + */ + // void setDesktop(SPDesktop *desktop); + + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + +}; + +} +} +} + +#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..b2594a0 --- /dev/null +++ b/src/ui/dialog/object-properties.cpp @@ -0,0 +1,605 @@ +// 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 "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "verbs.h" +#include "style.h" +#include "style-enums.h" + +#include "object/sp-image.h" + +#include "widgets/sp-attribute-widget.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +ObjectProperties::ObjectProperties() + : UI::Widget::Panel("/dialogs/object/", SP_VERB_DIALOG_ITEM) + , _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) + , _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())) + , _desktop(nullptr) +{ + //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:"); + + _desktop_changed_connection = _desktop_tracker.connectDesktopChanged( + sigc::mem_fun(*this, &ObjectProperties::_setTargetDesktop) + ); + _desktop_tracker.connect(GTK_WIDGET(gobj())); + + _init(); +} + +ObjectProperties::~ObjectProperties() +{ + _subselection_changed_connection.disconnect(); + _selection_changed_connection.disconnect(); + _desktop_changed_connection.disconnect(); + _desktop_tracker.disconnect(); +} + +void ObjectProperties::_init() +{ + Gtk::Box *contents = _getContents(); + contents->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); + + contents->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); + grid_top->attach(_label_id, 0, 0, 1, 1); + + /* 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); + grid_top->attach(_entry_id, 1, 0, 1, 1); + + _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); + grid_top->attach(_label_label, 0, 1, 1, 1); + + /* 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); + grid_top->attach(_entry_label, 1, 1, 1, 1); + + _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); + grid_top->attach(_label_title, 0, 2, 1, 1); + + /* 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); + grid_top->attach(_entry_title, 1, 2, 1, 1); + + _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)); + + /* 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); + contents->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); + grid_top->attach(_label_dpi, 0, 3, 1, 1); + + /* Create the entry box for the SVG DPI */ + _spin_dpi.set_digits(2); + _spin_dpi.set_range(1, 1200); + grid_top->attach(_spin_dpi, 1, 3, 1, 1); + + _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); + grid_top->attach(_label_image_rendering, 0, 4, 1, 1); + + /* 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 (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); + grid_top->attach(_combo_image_rendering, 1, 4, 1, 1); + + _label_image_rendering.set_mnemonic_widget(_combo_image_rendering); + + _combo_image_rendering.signal_changed().connect( + sigc::mem_fun(this, &ObjectProperties::_imageRenderingChanged) + ); + + + + /* Check boxes */ + Gtk::HBox *hb_checkboxes = Gtk::manage(new Gtk::HBox()); + contents->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); + contents->pack_start(_exp_interactivity, Gtk::PACK_SHRINK); + + show_all(); + update(); +} + +void ObjectProperties::update() +{ + if (_blocked || !_desktop) { + return; + } + if (SP_ACTIVE_DESKTOP != _desktop) { + return; + } + + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + Gtk::Box *contents = _getContents(); + + if (!selection->singleItem()) { + contents->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(); + return; + } else { + contents->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 */ + + 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 = SP_ACTIVE_DESKTOP->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 (SP_ACTIVE_DOCUMENT->getObjectById(id) != nullptr) { + _label_id.set_text(_("Id exists! ")); + } else { + SPException ex; + _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" ")); + SP_EXCEPTION_INIT(&ex); + item->setAttribute("id", id, &ex); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, _("Set object ID")); + } + 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(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _("Set object label")); + } + + /* Retrieve the title */ + if (obj->setTitle(_entry_title.get_text().c_str())) { + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _("Set object title")); + } + + /* 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(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, _("Set image DPI")); + } + + /* 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(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _("Set object description")); + } + + _blocked = false; +} + +void ObjectProperties::_imageRenderingChanged() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->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(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _("Set image rendering option")); + } + sp_repr_css_attr_unref(css); + + _blocked = false; +} + +void ObjectProperties::_sensitivityToggled() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + g_return_if_fail(item != nullptr); + + _blocked = true; + item->setLocked(_cb_lock.get_active()); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _cb_lock.get_active() ? _("Lock object") : _("Unlock object")); + _blocked = false; +} + +void ObjectProperties::_aspectRatioToggled() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->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(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, _("Set preserve ratio")); + } + _blocked = false; +} + +void ObjectProperties::_hiddenToggled() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + g_return_if_fail(item != nullptr); + + _blocked = true; + item->setExplicitlyHidden(_cb_hide.get_active()); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _cb_hide.get_active() ? _("Hide object") : _("Unhide object")); + _blocked = false; +} + +void ObjectProperties::_setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + _desktop_tracker.setBase(desktop); +} + +void ObjectProperties::_setTargetDesktop(SPDesktop *desktop) +{ + if (this->_desktop != desktop) { + if (this->_desktop) { + _subselection_changed_connection.disconnect(); + _selection_changed_connection.disconnect(); + } + this->_desktop = desktop; + if (desktop && desktop->selection) { + _selection_changed_connection = desktop->selection->connectChanged( + sigc::hide(sigc::mem_fun(*this, &ObjectProperties::update)) + ); + _subselection_changed_connection = desktop->connectToolSubselectionChanged( + sigc::hide(sigc::mem_fun(*this, &ObjectProperties::update)) + ); + } + update(); + } +} + +} +} +} + +/* + 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..a1974c0 --- /dev/null +++ b/src/ui/dialog/object-properties.h @@ -0,0 +1,148 @@ +// 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 "ui/widget/panel.h" +#include "ui/widget/frame.h" + +#include <gtkmm/checkbutton.h> +#include <gtkmm/entry.h> +#include <gtkmm/expander.h> +#include <gtkmm/frame.h> +#include <gtkmm/spinbutton.h> +#include <gtkmm/textview.h> +#include <gtkmm/comboboxtext.h> + +#include "ui/dialog/desktop-tracker.h" + +class SPAttributeTable; +class SPDesktop; +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 Widget::Panel { +public: + ObjectProperties(); + ~ObjectProperties() override; + + static ObjectProperties &getInstance() { return *new ObjectProperties(); } + + /// Updates entries and other child widgets on selection change, object modification, etc. + void update(); + +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_image_rendering; // the label for 'image-rendering' + 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 + + SPDesktop *_desktop; + DesktopTracker _desktop_tracker; + sigc::connection _desktop_changed_connection; + sigc::connection _selection_changed_connection; + sigc::connection _subselection_changed_connection; + + /// Constructor auxiliary function creating the child widgets. + void _init(); + + /// Sets object properties (ID, label, title, description) on user input. + void _labelChanged(); + + /// 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(); + + /// Can be invoked for setting the desktop. Currently not used. + void _setDesktop(SPDesktop *desktop); + + /// Is invoked by the desktop tracker when the desktop changes. + void _setTargetDesktop(SPDesktop *desktop); +}; + +} +} +} + +#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..f9e14e7 --- /dev/null +++ b/src/ui/dialog/objects.cpp @@ -0,0 +1,2363 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple panel for objects (originally developed for Ponyscape, an Inkscape derivative) + * + * Authors: + * Theodore Janeczko + * Tweaked by Liam P White for use in Inkscape + * 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. + */ + +#include "objects.h" + +#include <gtkmm/icontheme.h> +#include <gtkmm/imagemenuitem.h> +#include <gtkmm/separatormenuitem.h> +#include <glibmm/main.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 "shortcuts.h" +#include "verbs.h" + +#include "helper/action.h" +#include "ui/icon-loader.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/contextmenu.h" +#include "ui/dialog-events.h" +#include "ui/icon-names.h" +#include "ui/selected-color.h" +#include "ui/tools-switch.h" +#include "ui/tools/node-tool.h" +#include "ui/widget/clipmaskicon.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/highlight-picker.h" +#include "ui/widget/imagetoggler.h" +#include "ui/widget/insertordericon.h" +#include "ui/widget/layertypeicon.h" + +#include "xml/node-observer.h" + +//#define DUMP_LAYERS 1 + +namespace Inkscape { +namespace UI { +namespace Dialog { + +using Inkscape::XML::Node; + +/** + * Gets an instance of the Objects panel + */ +ObjectsPanel& ObjectsPanel::getInstance() +{ + return *new ObjectsPanel(); +} + +/** + * Column enumeration + */ +enum { + COL_VISIBLE = 1, + COL_LOCKED, + COL_TYPE, +// COL_INSERTORDER, + COL_CLIPMASK, + COL_HIGHLIGHT +}; + +/** + * Button enumeration + */ +enum { + BUTTON_NEW = 0, + BUTTON_RENAME, + BUTTON_TOP, + BUTTON_BOTTOM, + BUTTON_UP, + BUTTON_DOWN, + BUTTON_DUPLICATE, + BUTTON_DELETE, + BUTTON_SOLO, + BUTTON_SHOW_ALL, + BUTTON_HIDE_ALL, + BUTTON_LOCK_OTHERS, + BUTTON_LOCK_ALL, + BUTTON_UNLOCK_ALL, + BUTTON_SETCLIP, + BUTTON_CLIPGROUP, +// BUTTON_SETINVCLIP, + BUTTON_UNSETCLIP, + BUTTON_SETMASK, + BUTTON_UNSETMASK, + BUTTON_GROUP, + BUTTON_UNGROUP, + BUTTON_COLLAPSE_ALL, + DRAGNDROP, + UPDATE_TREE +}; + +/** + * Xml node observer for observing objects in the document + */ +class ObjectsPanel::ObjectWatcher : public Inkscape::XML::NodeObserver { +public: + /** + * Creates a new object watcher + * @param pnl The panel to which the object watcher belongs + * @param obj The object to watch + */ + ObjectWatcher(ObjectsPanel* pnl, SPObject* obj) : + _pnl(pnl), + _obj(obj), + _repr(obj->getRepr()), + _highlightAttr(g_quark_from_string("inkscape:highlight-color")), + _lockedAttr(g_quark_from_string("sodipodi:insensitive")), + _labelAttr(g_quark_from_string("inkscape:label")), + _groupAttr(g_quark_from_string("inkscape:groupmode")), + _styleAttr(g_quark_from_string("style")), + _clipAttr(g_quark_from_string("clip-path")), + _maskAttr(g_quark_from_string("mask")) + { + _repr->addObserver(*this); + } + + ~ObjectWatcher() override { + _repr->removeObserver(*this); + } + + void notifyChildAdded( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChangedWrapper( _obj ); + } + } + void notifyChildRemoved( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChangedWrapper( _obj ); + } + } + void notifyChildOrderChanged( Node &/*node*/, Node &/*child*/, Node */*old_prev*/, Node */*new_prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChangedWrapper( _obj ); + } + } + void notifyContentChanged( Node &/*node*/, Util::ptr_shared /*old_content*/, Util::ptr_shared /*new_content*/ ) override {} + void notifyAttributeChanged( Node &node, GQuark name, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ ) override { + /* Weird things happen on undo! we get notified about the child being removed, but after that we still get + * notified for attributes being changed on this XML node! In that case the corresponding SPObject might already + * have been deleted and the pointer to might be invalid, leading to a segfault if we're not carefull. + * So after we initiated the update of the treeview using _objectsChangedWrapper() in notifyChildRemoved(), the + * _pending_update flag is set, and we will no longer process any notifyAttributeChanged() + * Reproducing the crash: new document -> open objects panel -> draw freehand line -> undo -> segfault (but only + * if we don't check for _pending_update) */ + if ( _pnl && (!_pnl->_pending_update) && _obj ) { + if ( name == _lockedAttr || name == _labelAttr || name == _highlightAttr || name == _groupAttr || name == _styleAttr || name == _clipAttr || name == _maskAttr ) { + _pnl->_updateObject(_obj, name == _highlightAttr); + if ( name == _styleAttr ) { + _pnl->_updateComposite(); + } + } + } + } + + /** + * Objects panel to which this watcher belongs + */ + ObjectsPanel* _pnl; + + /** + * The object that is being observed + */ + SPObject* _obj; + + /** + * The xml representation of the object that is being observed + */ + Inkscape::XML::Node* _repr; + + /* These are quarks which define the attributes that we are observing */ + GQuark _highlightAttr; + GQuark _lockedAttr; + GQuark _labelAttr; + GQuark _groupAttr; + GQuark _styleAttr; + GQuark _clipAttr; + GQuark _maskAttr; +}; + +class ObjectsPanel::InternalUIBounce +{ +public: + int _actionCode; + sigc::connection _signal; +}; + +class ObjectsPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + + ModelColumns() + { + add(_colObject); + add(_colVisible); + add(_colLocked); + add(_colLabel); + add(_colType); + add(_colHighlight); + add(_colClipMask); + add(_colPrevSelectionState); + //add(_colInsertOrder); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<SPItem*> _colObject; + Gtk::TreeModelColumn<Glib::ustring> _colLabel; + Gtk::TreeModelColumn<bool> _colVisible; + Gtk::TreeModelColumn<bool> _colLocked; + Gtk::TreeModelColumn<int> _colType; + Gtk::TreeModelColumn<guint32> _colHighlight; + Gtk::TreeModelColumn<int> _colClipMask; + Gtk::TreeModelColumn<bool> _colPrevSelectionState; + //Gtk::TreeModelColumn<int> _colInsertOrder; +}; + +/** + * Stylizes a button using the given icon name and tooltip + */ +void ObjectsPanel::_styleButton(Gtk::Button& btn, char const* iconName, char const* tooltip) +{ + 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); + btn.set_tooltip_text (tooltip); +} + +/** + * Adds an item to the pop-up (right-click) menu + * @param desktop The active destktop + * @param code Action code + * @param id Button id for callback function + * @return The generated menu item + */ +Gtk::MenuItem& ObjectsPanel::_addPopupItem( SPDesktop *desktop, unsigned int code, int id ) +{ + Verb *verb = Verb::get( code ); + g_assert(verb); + SPAction *action = verb->get_action(Inkscape::ActionContext(desktop)); + + Gtk::MenuItem* item = Gtk::manage(new Gtk::MenuItem()); + + Gtk::Label *label = Gtk::manage(new Gtk::Label(action->name, true)); + label->set_xalign(0.0); + + if (_show_contextmenu_icons && action->image) { + item->set_name("ImageMenuItem"); // custom name to identify our "ImageMenuItems" + Gtk::Image *icon = Gtk::manage(sp_get_icon_image(action->image, Gtk::ICON_SIZE_MENU)); + + // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can only hold one child + Gtk::Box *box = Gtk::manage(new Gtk::Box()); + box->pack_start(*icon, false, false, 0); + box->pack_start(*label, true, true, 0); + item->add(*box); + } else { + item->add(*label); + } + + item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_takeAction), id)); + _popupMenu.append(*item); + + return *item; +} + +/** + * Attach a watcher to the XML node of an item, which will signal us in case of changes to that item or node + * @param item The item of which the XML node is to be watched + */ +void ObjectsPanel::_addWatcher(SPItem *item) { + bool used = true; // Any newly created watcher is obviously being used + auto iter = _objectWatchers.find(item); + if (iter == _objectWatchers.end()) { // If not found then watcher doesn't exist yet + ObjectsPanel::ObjectWatcher *w = new ObjectsPanel::ObjectWatcher(this, item); + _objectWatchers.emplace(item, std::make_pair(w, used)); + } else { // Found; no need to create a new watcher; just flag it as "in use" + (*iter).second.second = used; + } +} + +/** + * Delete the watchers, which signal us in case of changes to the item being watched + * @param only_unused Only delete those watchers that are no longer in use + */ +void ObjectsPanel::_removeWatchers(bool only_unused = false) { + // Delete all watchers (optionally only those which are not in use) + auto iter = _objectWatchers.begin(); + while (iter != _objectWatchers.end()) { + bool used = (*iter).second.second; + bool delete_watcher = (!only_unused) || (only_unused && !used); + if ( delete_watcher ) { + ObjectsPanel::ObjectWatcher *w = (*iter).second.first; + delete w; + iter = _objectWatchers.erase(iter); + } else { + // It must be in use, so the used "field" should be set to true; + // However, when _removeWatchers is being called, we will already have processed the complete queue ... + g_assert(_tree_update_queue.empty()); + // .. and we can preemptively flag it as unused for the processing of the next queue + (*iter).second.second = false; // It will be set to true again by _addWatcher, if in use + iter++; + } + } +} +/** + * Call function for asynchronous invocation of _objectsChanged + */ +void ObjectsPanel::_objectsChangedWrapper(SPObject */*obj*/) { + // We used to call _objectsChanged with a reference to _obj, + // but since _obj wasn't used, I'm dropping that for now + _takeAction(UPDATE_TREE); +} + +/** + * Callback function for when an object changes. Essentially refreshes the entire tree + * @param obj Object which was changed (currently not used as the entire tree is recreated) + */ +void ObjectsPanel::_objectsChanged(SPObject */*obj*/) +{ + if (_desktop) { + //Get the current document's root and use that to enumerate the tree + SPDocument* document = _desktop->doc(); + SPRoot* root = document->getRoot(); + if ( root ) { + _selectedConnection.block(); // Will be unblocked after the queue has been processed fully + _documentChangedCurrentLayer.block(); + + //Clear the tree store + _store->clear(); // This will increment it's stamp, making all old iterators + _tree_cache.clear(); // invalid. So we will also clear our own cache, as well + _tree_update_queue.clear(); // as any remaining update queue + + // Temporarily detach the TreeStore from the TreeView to slightly reduce flickering, and to speed up + // Note: if we truly want to eliminate the flickering, we should implement double buffering on the _store, + // but maybe this is a bit too much effort/bloat for too little gain? + _tree.unset_model(); + + //Add all items recursively; we will do this asynchronously, by first filling a queue, which is rather fast + _queueObject( root, nullptr ); + //However, the processing of this queue is slow, so this is done at a low priority and in small chunks. Using + //only small chunks keeps Inkscape responsive, for example while using the spray tool. After processing each + //of the chunks, Inkscape will check if there are other tasks with a high priority, for example when user is + //spraying. If so, the sprayed objects will be added first, and the whole updating will be restarted before + //it even finished. + _paths_to_be_expanded.clear(); + _processQueue_sig.disconnect(); // Might be needed in case objectsChanged is called directly, and not through objectsChangedWrapper() + _processQueue_sig = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_processQueue), 0, Glib::PRIORITY_DEFAULT_IDLE+100); + } + } +} + +/** + * Recursively adds the children of the given item to the tree + * @param obj Root object to add to the tree + * @param parentRow Parent tree row (or NULL if adding to tree root) + */ +void ObjectsPanel::_queueObject(SPObject* obj, Gtk::TreeModel::Row* parentRow) +{ + bool already_expanded = false; + + for(auto& child: obj->children) { + if (SP_IS_ITEM(&child)) { + //Add the item to the tree, basically only creating an empty row in the tree view + Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend(); + + //Add the item to a queue, so we can fill in the data in each row asynchronously + //at a later stage. See the comments in _objectsChanged() for more details + bool expand = SP_IS_GROUP(obj) && SP_GROUP(obj)->expanded() && (not already_expanded); + _tree_update_queue.emplace_back(SP_ITEM(&child), iter, expand); + + already_expanded = expand || already_expanded; // We need to expand only a single child in each group + + //If the item is a group, recursively add its children + if (SP_IS_GROUP(&child)) { + Gtk::TreeModel::Row row = *iter; + _queueObject(&child, &row); + } + } + } +} + +/** + * Walks through the queue in small chunks, and fills in the rows in the tree view accordingly + * @return False if the queue has been fully emptied + */ +bool ObjectsPanel::_processQueue() { + auto queue_iter = _tree_update_queue.begin(); + auto queue_end = _tree_update_queue.end(); + int count = 0; + + while (queue_iter != queue_end) { + //The queue is a list of tuples; expand the tuples + SPItem *item = std::get<0>(*queue_iter); + Gtk::TreeModel::iterator iter = std::get<1>(*queue_iter); + bool expanded = std::get<2>(*queue_iter); + //Add the object to the tree view and tree cache + _addObjectToTree(item, *iter, expanded); + _tree_cache.emplace(item, *iter); + + /* Update the watchers; No watcher shall be deleted before the processing of the queue has + * finished; we need to keep watching for items that might have been deleted while the queue, + * which is being processed on idle, was not yet empty. This is because when an item is deleted, the + * queue is still holding a pointer to it. The NotifyChildRemoved method of the watcher will stop the + * processing of the queue and prevent a segmentation fault, but only if there is a watcher in place*/ + _addWatcher(item); + + queue_iter = _tree_update_queue.erase(queue_iter); + count++; + if (count == 100 && (!_tree_update_queue.empty())) { + return true; // we have not yet reached the end of the queue, so return true to keep the timeout signal alive + } + } + + //We have reached the end of the queue, and it is safe to remove any watchers + _removeWatchers(true); // ... but only remove those that are no longer in use + + // Now we can bring the tree view back to life safely + _tree.set_model(_store); // Attach the store again to the tree view this sets search columns as -1 + _tree.set_search_column(_model->_colLabel);//set search column again + + // Expand the tree; this is kept outside of _addObjectToTree() and _processQueue() to allow + // temporarily detaching the store from the tree, which slightly reduces flickering + for (auto path: _paths_to_be_expanded) { + _tree.expand_to_path(path); + _tree.collapse_row(path); + } + + _blockAllSignals(false); + _objectsSelected(_desktop->selection); //Set the tree selection; will also invoke _checkTreeSelection() + _pending_update = false; + return false; // Return false to kill the timeout signal that kept calling _processQueue +} + +/** + * Fills in the details of an item in the already existing row of the tree view + * @param item Item of which the name, visibility, lock status, etc, will be filled in + * @param row Row where the item is residing + * @param expanded True if the item is part of a group that is shown as expanded in the tree view + */ +void ObjectsPanel::_addObjectToTree(SPItem* item, const Gtk::TreeModel::Row &row, bool expanded) +{ + SPGroup * group = SP_IS_GROUP(item) ? SP_GROUP(item) : nullptr; + + row[_model->_colObject] = item; + gchar const * label = item->label() ? item->label() : item->getId(); + row[_model->_colLabel] = label ? label : item->defaultLabel(); + row[_model->_colVisible] = !item->isHidden(); + row[_model->_colLocked] = !item->isSensitive(); + row[_model->_colType] = group ? (group->layerMode() == SPGroup::LAYER ? 2 : 1) : 0; + row[_model->_colHighlight] = item->isHighlightSet() ? item->highlight_color() : item->highlight_color() & 0xffffff00; + row[_model->_colClipMask] = item ? ( + (item->getClipObject() ? 1 : 0) | + (item->getMaskObject() ? 2 : 0) + ) : 0; + row[_model->_colPrevSelectionState] = false; + //row[_model->_colInsertOrder] = group ? (group->insertBottom() ? 2 : 1) : 0; + + //If our parent object is a group and it's expanded, expand the tree + if (expanded) { + _paths_to_be_expanded.emplace_back(_store->get_path(row)); + } +} + +/** + * Updates an item in the tree and optionally recursively updates the item's children + * @param obj The item to update in the tree + * @param recurse Whether to recurse through the item's children + */ +void ObjectsPanel::_updateObject( SPObject *obj, bool recurse ) { + Gtk::TreeModel::iterator tree_iter; + if (_findInTreeCache(SP_ITEM(obj), tree_iter)) { + Gtk::TreeModel::Row row = *tree_iter; + + //We found our item in the tree; now update it! + SPItem * item = SP_IS_ITEM(obj) ? SP_ITEM(obj) : nullptr; + SPGroup * group = SP_IS_GROUP(obj) ? SP_GROUP(obj) : nullptr; + + gchar const * label = obj->label() ? obj->label() : obj->getId(); + row[_model->_colLabel] = label ? label : obj->defaultLabel(); + row[_model->_colVisible] = item ? !item->isHidden() : false; + row[_model->_colLocked] = item ? !item->isSensitive() : false; + row[_model->_colType] = group ? (group->layerMode() == SPGroup::LAYER ? 2 : 1) : 0; + row[_model->_colHighlight] = item ? (item->isHighlightSet() ? item->highlight_color() : item->highlight_color() & 0xffffff00) : 0; + row[_model->_colClipMask] = item ? ( + (item->getClipObject() ? 1 : 0) | + (item->getMaskObject() ? 2 : 0) + ) : 0; + //row[_model->_colInsertOrder] = group ? (group->insertBottom() ? 2 : 1) : 0; + + if (recurse){ + for (auto& iter: obj->children) { + _updateObject(&iter, recurse); + } + } + } +} + +/** + * Updates the composite controls for the selected item + */ +void ObjectsPanel::_updateComposite() { + if (!_blockCompositeUpdate) + { + //Set the default values + bool setValues = true; + + //Get/set the values + _tree.get_selection()->selected_foreach_iter(sigc::bind<bool *>(sigc::mem_fun(*this, &ObjectsPanel::_compositingChanged), &setValues)); + + } +} + +/** + * Sets the compositing values for the first selected item in the tree + * @param iter Current tree item + * @param setValues Whether to set the compositing values + */ +void ObjectsPanel::_compositingChanged( const Gtk::TreeModel::iterator& iter, bool *setValues ) +{ + if (iter) { + Gtk::TreeModel::Row row = *iter; + SPItem *item = row[_model->_colObject]; + if (*setValues) + { + _setCompositingValues(item); + *setValues = false; + } + } +} + +/** + * Occurs when the current desktop selection changes + * @param sel The current selection + */ +void ObjectsPanel::_objectsSelected( Selection *sel ) { + + bool setOpacity = true; + _selectedConnection.block(); + + _tree.get_selection()->unselect_all(); + _store->foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_clearPrevSelectionState)); + + SPItem *item = nullptr; + auto items = sel->items(); + for(auto i=items.begin(); i!=items.end(); ++i){ + item = *i; + if (setOpacity) + { + _setCompositingValues(item); + setOpacity = false; + } + _updateObjectSelected(item, (*i)==items.back(), false); + } + if (!item) { + if (_desktop->currentLayer() && SP_IS_ITEM(_desktop->currentLayer())) { + item = SP_ITEM(_desktop->currentLayer()); + _setCompositingValues(item); + _updateObjectSelected(item, false, true); + } + } + _selectedConnection.unblock(); + _checkTreeSelection(); +} + +/** + * Helper function for setting the compositing values + * @param item Item to use for setting the compositing values + */ +void ObjectsPanel::_setCompositingValues(SPItem *item) +{ + // Block the connections to avoid interference + _isolationConnection.block(); + _opacityConnection.block(); + _blendConnection.block(); + _blurConnection.block(); + + // Set the isolation + auto isolation = item->style->isolation.set ? item->style->isolation.value : SP_CSS_ISOLATION_AUTO; + _filter_modifier.set_isolation_mode(isolation, true); + // Set the opacity + double opacity = (item->style->opacity.set ? SP_SCALE24_TO_FLOAT(item->style->opacity.value) : 1); + opacity *= 100; // Display in percent. + _filter_modifier.set_opacity_value(opacity); + // Set the blend mode + if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) { + _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, true); + } else { + _filter_modifier.set_blend_mode(item->style->mix_blend_mode.value, true); + } + if (_filter_modifier.get_blend_mode() == SP_CSS_BLEND_NORMAL) { + if (!item->style->mix_blend_mode.set && item->style->filter.set && item->style->getFilter()) { + auto blend = filter_get_legacy_blend(item); + _filter_modifier.set_blend_mode(blend, true); + } + } + SPGaussianBlur *spblur = nullptr; + if (item->style->getFilter()) { + for (auto& primitive_obj: item->style->getFilter()->children) { + if (!SP_IS_FILTER_PRIMITIVE(&primitive_obj)) { + break; + } + if (SP_IS_GAUSSIANBLUR(&primitive_obj) && !spblur) { + //Get the blur value + spblur = SP_GAUSSIANBLUR(&primitive_obj); + } + } + } + + //Set the blur value + double blur_value = 0; + if (spblur) { + Geom::OptRect bbox = item->bounds(SPItem::GEOMETRIC_BBOX); // calculating the bbox is expensive; only do this if we have an spblur in the first place + if (bbox) { + double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct? + blur_value = spblur->stdDeviation.getNumber() * 400 / perimeter; + } + } + _filter_modifier.set_blur_value(blur_value); + + //Unblock connections + _isolationConnection.unblock(); + _blurConnection.unblock(); + _blendConnection.unblock(); + _opacityConnection.unblock(); +} + +// See the comment in objects.h for _tree_cache +/** + * Find the specified item in the tree cache + * @param iter Current tree item + * @param tree_iter Tree_iter will point to the row in which the tree item was found + * @return True if found + */ +bool ObjectsPanel::_findInTreeCache(SPItem* item, Gtk::TreeModel::iterator &tree_iter) { + if (not item) { + return false; + } + + try { + tree_iter = _tree_cache.at(item); + } + catch (std::out_of_range) { + // Apparently, item cannot be found in the tree_cache, which could mean that + // - the tree and/or tree_cache are out-dated or in the process of being updated. + // - a layer is selected, which is not visible in the objects panel (see _objectsSelected()) + // Anyway, this doesn't seem all that critical, so no warnings; just return false + return false; + } + + /* If the row in the tree has been deleted, and an old tree_cache is being used, then we will + * get a segmentation fault crash somewhere here; so make sure iters don't linger around! + * We can only check the validity as done below, but this is rather slow according to the + * documentation (adds 0.25 s for a 2k long tree). But better safe than sorry + */ + if (not _store->iter_is_valid(tree_iter)) { + g_critical("Invalid iterator to Gtk::tree in objects panel; just prevented a segfault!"); + return false; + } + + return true; +} + + +/** + * Find the specified item in the tree store and (de)select it, optionally scrolling to the item + * @param item Item to select in the tree + * @param scrollto Whether to scroll to the item + * @param expand If true, the path in the tree towards item will be expanded + */ +void ObjectsPanel::_updateObjectSelected(SPItem* item, bool scrollto, bool expand) +{ + Gtk::TreeModel::iterator tree_iter; + if (_findInTreeCache(item, tree_iter)) { + Gtk::TreeModel::Row row = *tree_iter; + + //We found the item! Expand to the path and select it in the tree. + Gtk::TreePath path = _store->get_path(tree_iter); + _tree.expand_to_path( path ); + if (!expand) + // but don't expand itself, just the path + _tree.collapse_row(path); + + Glib::RefPtr<Gtk::TreeSelection> select = _tree.get_selection(); + + select->select(tree_iter); + row[_model->_colPrevSelectionState] = true; + if (scrollto) { + //Scroll to the item in the tree + _tree.scroll_to_row(path, 0.5); + } + } +} + +/** + * Pushes the current tree selection to the canvas + */ +void ObjectsPanel::_pushTreeSelectionToCurrent() +{ + if ( _desktop && _desktop->currentRoot() ) { + //block connections for selection and compositing values to prevent interference + _selectionChangedConnection.block(); + _documentChangedCurrentLayer.block(); + //Clear the selection and then iterate over the tree selection, pushing each item to the desktop + _desktop->selection->clear(); + if (_tree.get_selection()->count_selected_rows() == 0) { + _store->foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_clearPrevSelectionState)); + } + bool setOpacity = true; + bool first_pass = true; + _store->foreach_iter(sigc::bind<bool *>(sigc::mem_fun(*this, &ObjectsPanel::_selectItemCallback), &setOpacity, &first_pass)); + first_pass = false; + _store->foreach_iter(sigc::bind<bool *>(sigc::mem_fun(*this, &ObjectsPanel::_selectItemCallback), &setOpacity, &first_pass)); + + //unblock connections, unless we were already blocking them beforehand + _selectionChangedConnection.unblock(); + _documentChangedCurrentLayer.unblock(); + + _checkTreeSelection(); + } +} + +/** + * Helper function for pushing the current tree selection to the current desktop + * @param iter Current tree item + * @param setCompositingValues Whether to set the compositing values + */ +bool ObjectsPanel::_selectItemCallback(const Gtk::TreeModel::iterator& iter, bool *setCompositingValues, bool *first_pass) +{ + Gtk::TreeModel::Row row = *iter; + bool selected = _tree.get_selection()->is_selected(iter); + if (selected) { // All items selected in the treeview will be added to the current selection + /* Adding/removing only the items that were selected or deselected since the previous call to _pushTreeSelectionToCurrent() + * is very slow on large documents, because _desktop->selection->remove(item) needs to traverse the whole ObjectSet to find + * the item to be removed. When all N objects are selected in a document, clearing the whole selection would require O(N^2) + * That's why we simply clear the complete selection using _desktop->selection->clear(), and re-add all items one by one. + * This is much faster. + */ + + /* On the first pass, we will add only the items that were selected before too. Then, on the second pass, we will add the + * newly selected items such that the last selected items will be actually last. This is needed for example when the user + * wants to align relative to the last selected item. + */ + if (*first_pass == row[_model->_colPrevSelectionState]) { + SPItem *item = row[_model->_colObject]; + if (!SP_IS_GROUP(item) || SP_GROUP(item)->layerMode() != SPGroup::LAYER) { + //If the item is not a layer, then select it and set the current layer to its parent (if it's the first item) + if (_desktop->selection->isEmpty()) { + _desktop->setCurrentLayer(item->parent); + } + _desktop->selection->add(item); + } else { + //If the item is a layer, set the current layer + if (_desktop->selection->isEmpty()) { + _desktop->setCurrentLayer(item); + } + } + if (*setCompositingValues) { + //Only set the compositing values for the first item <-- TODO: We have this comment here, but this has not actually been implemented? + _setCompositingValues(item); + *setCompositingValues = false; + } + } + } + + if (not *first_pass) { + row[_model->_colPrevSelectionState] = selected; + } + + return false; +} + +bool ObjectsPanel::_clearPrevSelectionState( const Gtk::TreeModel::iterator& iter) { + Gtk::TreeModel::Row row = *iter; + row[_model->_colPrevSelectionState] = false; + SPItem *item = row[_model->_colObject]; + return false; +} + +/** + * Handles button sensitivity + */ +void ObjectsPanel::_checkTreeSelection() +{ + bool sensitive = _tree.get_selection()->count_selected_rows() > 0; + //TODO: top/bottom sensitivity + bool sensitiveNonTop = true; + bool sensitiveNonBottom = true; + + for (auto & it : _watching) { + it->set_sensitive( sensitive ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( sensitiveNonTop ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( sensitiveNonBottom ); + } + + _tree.set_reorderable(sensitive); // Reorderable means that we allow drag-and-drop, but we only allow that when at least one row is selected +} + +/** + * Sets visibility of items in the tree + * @param iter Current item in the tree + * @param visible Whether the item should be visible or not + */ +void ObjectsPanel::_setVisibleIter( const Gtk::TreeModel::iterator& iter, const bool visible ) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + item->setHidden( !visible ); + row[_model->_colVisible] = visible; + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Sets sensitivity of items in the tree + * @param iter Current item in the tree + * @param locked Whether the item should be locked + */ +void ObjectsPanel::_setLockedIter( const Gtk::TreeModel::iterator& iter, const bool locked ) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + item->setLocked( locked ); + row[_model->_colLocked] = locked; + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * 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) +{ + if (!_desktop) + return false; + + unsigned int shortcut; + shortcut = sp_shortcut_get_for_event(event); + + switch (shortcut) { + // how to get users key binding for the action “start-interactive-search” ?? + // ctrl+f is just the default + case GDK_KEY_f | SP_SHORTCUT_CONTROL_MASK: + return false; + break; + // shall we slurp ctrl+w to close panel? + + // defocus: + case GDK_KEY_Escape: + if (_desktop->canvas) { + gtk_widget_grab_focus (GTK_WIDGET(_desktop->canvas)); + return true; + } + break; + } + + // invoke user defined shortcuts first + bool done = sp_shortcut_invoke(shortcut, _desktop); + if (done) + return true; + + // handle events for the treeview + bool empty = _desktop->selection->isEmpty(); + + switch (Inkscape::UI::Tools::get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + { + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn *focus_column = nullptr; + + _tree.get_cursor(path, focus_column); + if (focus_column == _name_column && !_text_renderer->property_editable()) { + //Rename item + _text_renderer->property_editable() = true; + _tree.set_cursor(path, *_name_column, true); + grab_focus(); + return true; + } + return false; + break; + } + } + return false; +} + +/** + * Handles mouse events + * @param event Mouse event from GDK + * @return whether to eat the event (om nom nom) + */ +bool ObjectsPanel::_handleButtonEvent(GdkEventButton* event) +{ + static unsigned doubleclick = 0; + static bool overVisible = false; + + //Right mouse button was clicked, launch the pop-up menu + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 3) ) { + Gtk::TreeModel::Path path; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + if ( _tree.get_path_at_pos( x, y, path ) ) { + _checkTreeSelection(); + _popupMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + + if (_tree.get_selection()->is_selected(path)) { + return true; + } + } + } + + //Left mouse button was pressed! In order to handle multiple item drag & drop, + //we need to defer selection by setting the select function so that the tree doesn't + //automatically select anything. In order to handle multiple item icon clicking, + //we need to eat the event. There might be a better way to do both of these... + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 1)) { + overVisible = false; + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (col == _tree.get_column(COL_VISIBLE-1)) { + //Click on visible column, eat this event to keep row selection + overVisible = true; + return true; + } else if (col == _tree.get_column(COL_LOCKED-1) || + col == _tree.get_column(COL_TYPE-1) || + //col == _tree.get_column(COL_INSERTORDER - 1) || + col == _tree.get_column(COL_HIGHLIGHT-1)) { + //Click on an icon column, eat this event to keep row selection + return true; + } else if ( !(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) & _tree.get_selection()->is_selected(path) ) { + //Click on a selected item with no modifiers, defer selection to the mouse-up by + //setting the select function to _noSelection + _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &ObjectsPanel::_noSelection)); + _defer_target = path; + } + } + } + + //Restore the selection function to allow tree selection on mouse button release + if ( event->type == GDK_BUTTON_RELEASE) { + _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &ObjectsPanel::_rowSelectFunction)); + } + + //CellRenderers do not have good support for dealing with multiple items, so + //we handle all events on them here + if ( (event->type == GDK_BUTTON_RELEASE) && (event->button == 1)) { + + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (_defer_target) { + //We had deferred a selection target, select it here (assuming no drag & drop) + if (_defer_target == path && !(event->x == 0 && event->y == 0)) + { + _tree.set_cursor(path, *col, false); + } + _defer_target = Gtk::TreeModel::Path(); + } + else { + if (event->state & GDK_SHIFT_MASK) { + // Shift left click on the visible/lock columns toggles "solo" mode + if (col == _tree.get_column(COL_VISIBLE - 1)) { + _takeAction(BUTTON_SOLO); + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + _takeAction(BUTTON_LOCK_OTHERS); + } + } else if (event->state & GDK_MOD1_MASK) { + // Alt+left click on the visible/lock columns toggles "solo" mode and preserves selection + Gtk::TreeModel::iterator iter = _store->get_iter(path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + SPItem *item = row[_model->_colObject]; + if (col == _tree.get_column(COL_VISIBLE - 1)) { + _desktop->toggleLayerSolo( item ); + DocumentUndo::maybeDone(_desktop->doc(), "layer:solo", SP_VERB_LAYER_SOLO, _("Toggle layer solo")); + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + _desktop->toggleLockOtherLayers( item ); + DocumentUndo::maybeDone(_desktop->doc(), "layer:lockothers", SP_VERB_LAYER_LOCK_OTHERS, _("Lock other layers")); + } + } + } else { + Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + SPItem* item = row[_model->_colObject]; + + if (col == _tree.get_column(COL_VISIBLE - 1)) { + if (overVisible) { + //Toggle visibility + bool newValue = !row[_model->_colVisible]; + if (_tree.get_selection()->is_selected(path)) + { + //If the current row is selected, toggle the visibility + //for all selected items + _tree.get_selection()->selected_foreach_iter(sigc::bind<bool>(sigc::mem_fun(*this, &ObjectsPanel::_setVisibleIter), newValue)); + } + else + { + //If the current row is not selected, toggle just its visibility + row[_model->_colVisible] = newValue; + item->setHidden(!newValue); + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, + newValue? _("Unhide objects") : _("Hide objects")); + overVisible = false; + } + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + //Toggle locking + bool newValue = !row[_model->_colLocked]; + if (_tree.get_selection()->is_selected(path)) + { + //If the current row is selected, toggle the sensitivity for + //all selected items + _tree.get_selection()->selected_foreach_iter(sigc::bind<bool>(sigc::mem_fun(*this, &ObjectsPanel::_setLockedIter), newValue)); + } + else + { + //If the current row is not selected, toggle just its sensitivity + row[_model->_colLocked] = newValue; + item->setLocked( newValue ); + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, + newValue? _("Lock objects") : _("Unlock objects")); + + } else if (col == _tree.get_column(COL_TYPE - 1)) { + if (SP_IS_GROUP(item)) + { + //Toggle the current item between a group and a layer + SPGroup * g = SP_GROUP(item); + bool newValue = g->layerMode() == SPGroup::LAYER; + row[_model->_colType] = newValue ? 1: 2; + g->setLayerMode(newValue ? SPGroup::GROUP : SPGroup::LAYER); + g->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, + newValue? _("Layer to group") : _("Group to layer")); + _pushTreeSelectionToCurrent(); + } + } /*else if (col == _tree.get_column(COL_INSERTORDER - 1)) { + if (SP_IS_GROUP(item)) + { + //Toggle the current item's insert order + SPGroup * g = SP_GROUP(item); + bool newValue = !g->insertBottom(); + row[_model->_colInsertOrder] = newValue ? 2: 1; + g->setInsertBottom(newValue); + g->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, + newValue? _("Set insert mode bottom") : _("Set insert mode top")); + } + }*/ else if (col == _tree.get_column(COL_HIGHLIGHT - 1)) { + //Clear the highlight targets + _highlight_target.clear(); + if (_tree.get_selection()->is_selected(path)) + { + //If the current item is selected, store all selected items + //in the highlight source + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_storeHighlightTarget)); + } else { + //If the current item is not selected, store only it in the highlight source + _storeHighlightTarget(iter); + } + if (_selectedColor) + { + //Set up the color selector + SPColor color; + color.set( row[_model->_colHighlight] ); + _selectedColor->setColorAlpha(color, SP_RGBA32_A_F(row[_model->_colHighlight])); + } + //Show the color selector dialog + _colorSelectorDialog.show(); + } + } + } + } + } + + //Second mouse button press, set double click status for when the mouse is released + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + doubleclick = 1; + } + + //Double click on mouse button release, if we're over the label column, edit + //the item name + if ( event->type == GDK_BUTTON_RELEASE && doubleclick) { + doubleclick = 0; + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) && col == _name_column) { + // Double click on the Layer name, enable editing + _text_renderer->property_editable() = true; + _tree.set_cursor (path, *_name_column, true); + grab_focus(); + } + } + + return false; +} + +/** + * Stores items in the highlight target vector to manipulate with the color selector + * @param iter Current tree item to store + */ +void ObjectsPanel::_storeHighlightTarget(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + _highlight_target.push_back(item); + } +} + +/* + * Drag and drop within the tree + */ +bool ObjectsPanel::_handleDragDrop(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int x, int y, guint /*time*/) +{ + //Set up our defaults and clear the source vector + _dnd_into = false; + _dnd_target = nullptr; + _dnd_source.clear(); + _dnd_source_includes_layer = false; + + //Add all selected items to the source vector + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_storeDragSource)); + + bool cancel_dnd = false; + bool dnd_to_top_at_end = false; + + Gtk::TreeModel::Path target_path; + Gtk::TreeViewDropPosition pos; + if (_tree.get_dest_row_at_pos(x, y, target_path, pos)) { + // SPItem::moveTo() will be used to move the selected items to their new position, but + // moveTo() can only "drop before"; we therefore need to find the next path and drop + // the selection just before it, instead of "dropping after" the target path + if (pos == Gtk::TREE_VIEW_DROP_AFTER) { + Gtk::TreeModel::Path next_path = target_path; + if (_tree.row_expanded(next_path)) { + next_path.down(); // The next path is at a lower level in the hierarchy, i.e. in a layer or group + } else { + next_path.next(); // The next path is at the same level + } + // A next path might however not be present, if we're dropping at the end of the tree view + if (_store->iter_is_valid(_store->get_iter(next_path))) { + target_path = next_path; + } else { + // Dragging to the "end" of the treeview ; we'll get the parent group or layer of the last + // item, and drop into that parent + Gtk::TreeModel::Path up_path = target_path; + up_path.up(); + if (_store->iter_is_valid(_store->get_iter(up_path))) { + // Drop into the parent of the last item + target_path = up_path; + _dnd_into = true; + } else { + // Drop into the top level, completely at the end of the treeview; + dnd_to_top_at_end = true; + } + } + } + + if (dnd_to_top_at_end) { + g_assert(_dnd_target == nullptr); + } else { + // Find the SPItem corresponding to the target_path/row at which we're dropping our selection + Gtk::TreeModel::iterator iter = _store->get_iter(target_path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + _dnd_target = row[_model->_colObject]; //Set the drop target + if ((pos == Gtk::TREE_VIEW_DROP_INTO_OR_BEFORE) or (pos == Gtk::TREE_VIEW_DROP_INTO_OR_AFTER)) { + // Trying to drop into a layer or group + if (SP_IS_GROUP(_dnd_target)) { + _dnd_into = true; + } else { + // If the target is not a group (or layer), then we cannot drop into it (unless we + // would create a group on the fly), so we will cancel the drag and drop action. + cancel_dnd = true; + } + } + // If the source selection contains a layer however, then it can not be dropped ... + bool c1 = target_path.size() > 1; // .. below the top-level + bool c2 = SP_IS_GROUP(_dnd_target) and _dnd_into; // .. or in any group (at the top level) + if (_dnd_source_includes_layer and (c1 or c2)) { + cancel_dnd = true; + } + } else { + cancel_dnd = true; + } + } + } + + if (not cancel_dnd) { + _takeAction(DRAGNDROP); + } + + return true; // If True: then we're signaling here that nothing needs to be done by the TreeView; we're updating ourselves.. +} + +/** + * Stores all selected items as the drag source + * @param iter Current tree item + */ +void ObjectsPanel::_storeDragSource(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) { + _dnd_source.push_back(item); + if (SP_IS_GROUP(item) && (SP_GROUP(item)->layerMode() == SPGroup::LAYER)) { + _dnd_source_includes_layer = true; + } + } +} + +/* + * Move a selection of items in response to a drag & drop action + */ +void ObjectsPanel::_doTreeMove( ) +{ + g_assert(_desktop != nullptr); + g_assert(_document != nullptr); + + std::vector<gchar *> idvector; + + //Clear the desktop selection + _desktop->selection->clear(); + while (!_dnd_source.empty()) + { + SPItem *obj = _dnd_source.back(); + _dnd_source.pop_back(); + + if (obj != _dnd_target) { + //Store the object id (for selection later) and move the object + idvector.push_back(g_strdup(obj->getId())); + obj->moveTo(_dnd_target, _dnd_into); + } + } + //Select items + while (!idvector.empty()) { + //Grab the id from the vector, get the item in the document and select it + gchar * id = idvector.back(); + idvector.pop_back(); + SPObject *obj = _document->getObjectById(id); + g_free(id); + if (obj && SP_IS_ITEM(obj)) { + SPItem *item = SP_ITEM(obj); + if (!SP_IS_GROUP(item) || SP_GROUP(item)->layerMode() != SPGroup::LAYER) + { + if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(item->parent); + _desktop->selection->add(item); + } + else + { + if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(item); + } + } + } + + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Moved objects")); +} + +/** + * Prevents the treeview from emiting and responding to most signals; needed when it's not up to date + */ +void ObjectsPanel::_blockAllSignals(bool should_block = true) { + + // incoming signals + _documentChangedCurrentLayer.block(should_block); + _isolationConnection.block(should_block); + _opacityConnection.block(should_block); + _blendConnection.block(should_block); + _blurConnection.block(should_block); + if (_pending && should_block) { + // Kill any pending UI event, e.g. a delete or drag 'n drop action, which could + // become unpredictable after the tree has been updated + _pending->_signal.disconnect(); + } + + _selectionChangedConnection.block(should_block); + // outgoing signal + _selectedConnection.block(should_block); + + // These are not blocked: desktopChangeConn, _documentChangedConnection +} + +/** + * Fires the action verb + */ +void ObjectsPanel::_fireAction( unsigned int code ) +{ + if ( _desktop ) { + Verb *verb = Verb::get( code ); + if ( verb ) { + SPAction *action = verb->get_action(_desktop); + if ( action ) { + sp_action_perform( action, nullptr ); + } + } + } +} + +bool ObjectsPanel::_executeUpdate() { + _objectsChanged(nullptr); + return false; +} + +/** + * Executes the given button action during the idle time + */ +void ObjectsPanel::_takeAction( int val ) +{ + if (val == UPDATE_TREE) { + _pending_update = true; + // We might already have been updating the tree, but new data is available now + // so we will then first cancel the old update before scheduling a new one + _processQueue_sig.disconnect(); + _executeUpdate_sig.disconnect(); + _blockAllSignals(true); + //_store->clear(); + _tree_cache.clear(); + _executeUpdate_sig = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_executeUpdate), 500, Glib::PRIORITY_DEFAULT_IDLE+50); + // In the spray tool, updating the tree competes in priority with the redrawing of the canvas, + // see SPCanvas::addIdle(), which is set to UPDATE_PRIORITY (=G_PRIORITY_DEFAULT_IDLE). We + // should take a lower priority (= higher value) to keep the spray tool updating longer, and to prevent + // the objects-panel from clogging the processor; however, once the spraying slows down, the tree might + // get updated anyway. + } else if ( !_pending ) { + _pending = new InternalUIBounce(); + _pending->_actionCode = val; + _pending->_signal = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_executeAction), 0 ); + } +} + +/** + * Executes the pending button action + */ +bool ObjectsPanel::_executeAction() +{ + // Make sure selected layer hasn't changed since the action was triggered + if ( _document && _pending) + { + int val = _pending->_actionCode; +// SPObject* target = _pending->_target; + + switch ( val ) { + case BUTTON_NEW: + { + _fireAction( SP_VERB_LAYER_NEW ); + } + break; + case BUTTON_RENAME: + { + _fireAction( SP_VERB_LAYER_RENAME ); + } + break; + case BUTTON_TOP: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_TO_TOP ); + } + else + { + _fireAction( SP_VERB_SELECTION_TO_FRONT); + } + } + break; + case BUTTON_BOTTOM: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_TO_BOTTOM ); + } + else + { + _fireAction( SP_VERB_SELECTION_TO_BACK); + } + } + break; + case BUTTON_UP: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_RAISE ); + } + else + { + _fireAction( SP_VERB_SELECTION_STACK_UP ); + } + } + break; + case BUTTON_DOWN: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_LOWER ); + } + else + { + _fireAction( SP_VERB_SELECTION_STACK_DOWN ); + } + } + break; + case BUTTON_DUPLICATE: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_DUPLICATE ); + } + else + { + _fireAction( SP_VERB_EDIT_DUPLICATE ); + } + } + break; + case BUTTON_DELETE: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_DELETE ); + } + else + { + _fireAction( SP_VERB_EDIT_DELETE ); + } + } + break; + case BUTTON_SOLO: + { + _fireAction( SP_VERB_LAYER_SOLO ); + } + break; + case BUTTON_SHOW_ALL: + { + _fireAction( SP_VERB_LAYER_SHOW_ALL ); + } + break; + case BUTTON_HIDE_ALL: + { + _fireAction( SP_VERB_LAYER_HIDE_ALL ); + } + break; + case BUTTON_LOCK_OTHERS: + { + _fireAction( SP_VERB_LAYER_LOCK_OTHERS ); + } + break; + case BUTTON_LOCK_ALL: + { + _fireAction( SP_VERB_LAYER_LOCK_ALL ); + } + break; + case BUTTON_UNLOCK_ALL: + { + _fireAction( SP_VERB_LAYER_UNLOCK_ALL ); + } + break; + case BUTTON_CLIPGROUP: + { + _fireAction ( SP_VERB_OBJECT_CREATE_CLIP_GROUP ); + } + case BUTTON_SETCLIP: + { + _fireAction( SP_VERB_OBJECT_SET_CLIPPATH ); + } + break; + case BUTTON_UNSETCLIP: + { + _fireAction( SP_VERB_OBJECT_UNSET_CLIPPATH ); + } + break; + case BUTTON_SETMASK: + { + _fireAction( SP_VERB_OBJECT_SET_MASK ); + } + break; + case BUTTON_UNSETMASK: + { + _fireAction( SP_VERB_OBJECT_UNSET_MASK ); + } + break; + case BUTTON_GROUP: + { + _fireAction( SP_VERB_SELECTION_GROUP ); + } + break; + case BUTTON_UNGROUP: + { + _fireAction( SP_VERB_SELECTION_UNGROUP ); + } + break; + case BUTTON_COLLAPSE_ALL: + { + for (auto& obj: _document->getRoot()->children) { + if (SP_IS_GROUP(&obj)) { + _setCollapsed(SP_GROUP(&obj)); + } + } + _objectsChanged(_document->getRoot()); + } + break; + case DRAGNDROP: + { + _doTreeMove( ); + // The notifyChildOrderChanged signal will ensure that the TreeView gets updated + } + break; + } + + delete _pending; + _pending = nullptr; + } + + return false; +} + +/** + * Handles an unsuccessful item label edit (escape pressed, etc.) + */ +void ObjectsPanel::_handleEditingCancelled() +{ + _text_renderer->property_editable() = 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) +{ + Gtk::TreeModel::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + _renameObject(row, new_text); + _text_renderer->property_editable() = false; +} + +/** + * Renames an item in the tree + * @param row Tree row + * @param name New label to give to the item + */ +void ObjectsPanel::_renameObject(Gtk::TreeModel::Row row, const Glib::ustring& name) +{ + if ( row && _desktop) { + SPItem* item = row[_model->_colObject]; + if ( item ) { + gchar const* oldLabel = item->label(); + if ( !name.empty() && (!oldLabel || name != oldLabel) ) { + item->setLabel(name.c_str()); + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Rename object")); + } + } + } +} + +/** + * A row selection function used by the tree that doesn't allow any new items to be selected. + * Currently, this is used to allow multi-item drag & drop. + */ +bool ObjectsPanel::_noSelection( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool /*currentlySelected*/ ) +{ + return false; +} + +/** + * Default row selection function taken from the layers dialog + */ +bool ObjectsPanel::_rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool currentlySelected ) +{ + bool val = true; + if ( !currentlySelected && _toggleEvent ) + { + GdkEvent* event = gtk_get_current_event(); + if ( event ) { + // (keep these checks separate, so we know when to call gdk_event_free() + if ( event->type == GDK_BUTTON_PRESS ) { + GdkEventButton const* target = reinterpret_cast<GdkEventButton const*>(_toggleEvent); + GdkEventButton const* evtb = reinterpret_cast<GdkEventButton const*>(event); + + if ( (evtb->window == target->window) + && (evtb->send_event == target->send_event) + && (evtb->time == target->time) + && (evtb->state == target->state) + ) + { + // Ooooh! It's a magic one + val = false; + } + } + gdk_event_free(event); + } + } + return val; +} + +/** + * Sets a group to be collapsed and recursively collapses its children + * @param group The group to collapse + */ +void ObjectsPanel::_setCollapsed(SPGroup * group) +{ + group->setExpanded(false); + group->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + for (auto& iter: group->children) { + if (SP_IS_GROUP(&iter)) { + _setCollapsed(SP_GROUP(&iter)); + } + } +} + +/** + * Sets a group to be expanded or collapsed + * @param iter Current tree item + * @param isexpanded Whether to expand or collapse + */ +void ObjectsPanel::_setExpanded(const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& /*path*/, bool isexpanded) +{ + Gtk::TreeModel::Row row = *iter; + + SPItem* item = row[_model->_colObject]; + if (item && SP_IS_GROUP(item)) + { + if (isexpanded) + { + //If we're expanding, simply perform the expansion + SP_GROUP(item)->setExpanded(isexpanded); + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + else + { + //If we're collapsing, we need to recursively collapse, so call our helper function + _setCollapsed(SP_GROUP(item)); + } + } +} + +/** + * Callback for when the highlight color is changed + * @param csel Color selector + * @param cp Objects panel + */ +void ObjectsPanel::_highlightPickerColorMod() +{ + SPColor color; + float alpha = 0; + _selectedColor->colorAlpha(color, alpha); + + guint32 rgba = color.toRGBA32( alpha ); + + //Set the highlight color for all items in the _highlight_target (all selected items) + for (auto target : _highlight_target) + { + target->setHighlightColor(rgba); + target->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + DocumentUndo::maybeDone(SP_ACTIVE_DOCUMENT, "highlight", SP_VERB_DIALOG_OBJECTS, _("Set object highlight color")); +} + +/** + * Callback for when the opacity value is changed + */ +void ObjectsPanel::_opacityValueChanged() +{ + _blockCompositeUpdate = true; + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_opacityChangedIter)); + DocumentUndo::maybeDone(_document, "opacity", SP_VERB_DIALOG_OBJECTS, _("Set object opacity")); + _blockCompositeUpdate = false; +} + +/** + * Change the opacity of the selected items in the tree + * @param iter Current tree item + */ +void ObjectsPanel::_opacityChangedIter(const Gtk::TreeIter& iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + item->style->opacity.set = TRUE; + item->style->opacity.value = SP_SCALE24_FROM_FLOAT(_filter_modifier.get_opacity_value() / 100); + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Callback for when the isolation value is changed + */ +void ObjectsPanel::_isolationValueChanged() +{ + _blockCompositeUpdate = true; + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_isolationChangedIter)); + DocumentUndo::maybeDone(_document, "isolation", SP_VERB_DIALOG_OBJECTS, _("Set object isolation")); + _blockCompositeUpdate = false; +} + +/** + * Change the isolation of the selected items in the tree + * @param iter Current tree item + */ +void ObjectsPanel::_isolationChangedIter(const Gtk::TreeIter &iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem *item = row[_model->_colObject]; + if (item) { + item->style->isolation.set = TRUE; + item->style->isolation.value = _filter_modifier.get_isolation_mode(); + if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) { + item->style->mix_blend_mode.set = TRUE; + item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL; + _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false); + } + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Callback for when the blend mode is changed + */ +void ObjectsPanel::_blendValueChanged() +{ + _blockCompositeUpdate = true; + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_blendChangedIter)); + DocumentUndo::done(_document, SP_VERB_DIALOG_OBJECTS, _("Set object blend mode")); + _blockCompositeUpdate = false; +} + +/** + * Sets the blend mode of the selected tree items + * @param iter Current tree item + * @param blendmode Blend mode to set + */ +void ObjectsPanel::_blendChangedIter(const Gtk::TreeIter &iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + // < 1.0 filter based blend removal + if (!item->style->mix_blend_mode.set && item->style->filter.set && item->style->getFilter()) { + remove_filter_legacy_blend(item); + } + item->style->mix_blend_mode.set = TRUE; + if (_filter_modifier.get_blend_mode() && + item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) + { + item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL; + _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false); + } else { + item->style->mix_blend_mode.value = _filter_modifier.get_blend_mode(); + } + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Callback for when the blur value has changed + */ +void ObjectsPanel::_blurValueChanged() +{ + _blockCompositeUpdate = true; + _tree.get_selection()->selected_foreach_iter(sigc::bind<double>(sigc::mem_fun(*this, &ObjectsPanel::_blurChangedIter), _filter_modifier.get_blur_value())); + DocumentUndo::maybeDone(_document, "blur", SP_VERB_DIALOG_OBJECTS, _("Set object blur")); + _blockCompositeUpdate = false; +} + +/** + * Sets the blur value for the selected items in the tree + * @param iter Current tree item + * @param blur Blur value to set + */ +void ObjectsPanel::_blurChangedIter(const Gtk::TreeIter& iter, double blur) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + //Since blur and blend are both filters, we need to set both at the same time + SPStyle *style = item->style; + if (style) { + Geom::OptRect bbox = item->bounds(SPItem::GEOMETRIC_BBOX); + double radius; + if (bbox) { + double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct? + radius = blur * perimeter / 400; + } else { + radius = 0; + } + + if (radius != 0) { + // The modify function expects radius to be in display pixels. + Geom::Affine i2d (item->i2dt_affine()); + double expansion = i2d.descrim(); + radius *= expansion; + SPFilter *filter = modify_filter_gaussian_blur_from_item(_document, item, radius); + sp_style_set_property_url(item, "filter", filter, false); + } else if (item->style->filter.set && item->style->getFilter()) { + for (auto& primitive: item->style->getFilter()->children) { + if (!SP_IS_FILTER_PRIMITIVE(&primitive)) { + break; + } + if (SP_IS_GAUSSIANBLUR(&primitive)) { + primitive.deleteObject(); + break; + } + } + if (!item->style->getFilter()->firstChild()) { + remove_filter(item, false); + } + } + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } +} + +/** + * Constructor + */ +ObjectsPanel::ObjectsPanel() : + UI::Widget::Panel("/dialogs/objects", SP_VERB_DIALOG_OBJECTS), + _rootWatcher(nullptr), + _deskTrack(), + _desktop(nullptr), + _document(nullptr), + _model(nullptr), + _pending(nullptr), + _pending_update(false), + _toggleEvent(nullptr), + _defer_target(), + _visibleHeader(C_("Visibility", "V")), + _lockHeader(C_("Lock", "L")), + _typeHeader(C_("Type", "T")), + _clipmaskHeader(C_("Clip and mask", "CM")), + _highlightHeader(C_("Highlight", "HL")), + _nameHeader(_("Label")), + _filter_modifier( UI::Widget::SimpleFilterModifier::ISOLATION | + UI::Widget::SimpleFilterModifier::BLEND | + UI::Widget::SimpleFilterModifier::BLUR | + UI::Widget::SimpleFilterModifier::OPACITY ), + _colorSelectorDialog("dialogs.colorpickerwindow") +{ + //Create the tree model and store + ModelColumns *zoop = new ModelColumns(); + _model = zoop; + + _store = Gtk::TreeStore::create( *zoop ); + + //Set up the tree + _tree.set_model( _store ); + _tree.set_headers_visible(true); + _tree.set_reorderable(false); // Reorderable means that we allow drag-and-drop, but we only allow that when at least one row is selected + _tree.enable_model_drag_dest (Gdk::ACTION_MOVE); + + //Create the column CellRenderers + //Visible + Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) ); + int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1; + eyeRenderer->property_activatable() = true; + Gtk::TreeViewColumn* col = _tree.get_column(visibleColNum); + if ( col ) { + col->add_attribute( eyeRenderer->property_active(), _model->_colVisible ); + // In order to get tooltips on header, we must create our own label. + _visibleHeader.set_tooltip_text(_("Toggle visibility of Layer, Group, or Object.")); + _visibleHeader.show(); + col->set_widget( _visibleHeader ); + } + + //Locked + Inkscape::UI::Widget::ImageToggler * renderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked")) ); + int lockedColNum = _tree.append_column("lock", *renderer) - 1; + renderer->property_activatable() = true; + col = _tree.get_column(lockedColNum); + if ( col ) { + col->add_attribute( renderer->property_active(), _model->_colLocked ); + _lockHeader.set_tooltip_text(_("Toggle lock of Layer, Group, or Object.")); + _lockHeader.show(); + col->set_widget( _lockHeader ); + } + + //Type + Inkscape::UI::Widget::LayerTypeIcon * typeRenderer = Gtk::manage( new Inkscape::UI::Widget::LayerTypeIcon()); + int typeColNum = _tree.append_column("type", *typeRenderer) - 1; + typeRenderer->property_activatable() = true; + col = _tree.get_column(typeColNum); + if ( col ) { + col->add_attribute( typeRenderer->property_active(), _model->_colType ); + _typeHeader.set_tooltip_text(_("Type: Layer, Group, or Object. Clicking on Layer or Group icon, toggles between the two types.")); + _typeHeader.show(); + col->set_widget( _typeHeader ); + } + + //Insert order (LiamW: unused) + /*Inkscape::UI::Widget::InsertOrderIcon * insertRenderer = Gtk::manage( new Inkscape::UI::Widget::InsertOrderIcon()); + int insertColNum = _tree.append_column("type", *insertRenderer) - 1; + col = _tree.get_column(insertColNum); + if ( col ) { + col->add_attribute( insertRenderer->property_active(), _model->_colInsertOrder ); + }*/ + + //Clip/mask + Inkscape::UI::Widget::ClipMaskIcon * clipRenderer = Gtk::manage( new Inkscape::UI::Widget::ClipMaskIcon()); + int clipColNum = _tree.append_column("clipmask", *clipRenderer) - 1; + col = _tree.get_column(clipColNum); + if ( col ) { + col->add_attribute( clipRenderer->property_active(), _model->_colClipMask ); + _clipmaskHeader.set_tooltip_text(_("Is object clipped and/or masked?")); + _clipmaskHeader.show(); + col->set_widget( _clipmaskHeader ); + } + + //Highlight + Inkscape::UI::Widget::HighlightPicker * highlightRenderer = Gtk::manage( new Inkscape::UI::Widget::HighlightPicker()); + int highlightColNum = _tree.append_column("highlight", *highlightRenderer) - 1; + col = _tree.get_column(highlightColNum); + if ( col ) { + col->add_attribute( highlightRenderer->property_active(), _model->_colHighlight ); + _highlightHeader.set_tooltip_text(_("Highlight color of outline in Node tool. Click to set. If alpha is zero, use inherited color.")); + _highlightHeader.show(); + col->set_widget( _highlightHeader ); + } + + //Label + _text_renderer = Gtk::manage(new Gtk::CellRendererText()); + int nameColNum = _tree.append_column("Name", *_text_renderer) - 1; + _name_column = _tree.get_column(nameColNum); + if( _name_column ) { + _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); + _nameHeader.set_tooltip_text(_("Layer/Group/Object label (inkscape:label). Double-click to set. Default value is object 'id'.")); + _nameHeader.show(); + _name_column->set_widget( _nameHeader ); + } + + //Set the expander and search columns + _tree.set_expander_column( *_tree.get_column(nameColNum) ); + _tree.set_search_column(_model->_colLabel); + // use ctrl+f to start search + _tree.set_enable_search(false); + + //Set up the tree selection + _tree.get_selection()->set_mode(Gtk::SELECTION_MULTIPLE); + _selectedConnection = _tree.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &ObjectsPanel::_pushTreeSelectionToCurrent) ); + _tree.get_selection()->set_select_function( sigc::mem_fun(*this, &ObjectsPanel::_rowSelectFunction) ); + + //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_drag_drop().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleDragDrop), false); + _tree.signal_row_collapsed().connect( sigc::bind<bool>(sigc::mem_fun(*this, &ObjectsPanel::_setExpanded), false)); + _tree.signal_row_expanded().connect( sigc::bind<bool>(sigc::mem_fun(*this, &ObjectsPanel::_setExpanded), true)); + + //Set up the label editing signals + _text_renderer->signal_edited().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleEdited) ); + _text_renderer->signal_editing_canceled().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleEditingCancelled) ); + + //Set up the scroller window and pack the page + _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( _scroller, Gtk::PACK_EXPAND_WIDGET ); + + //Set up the compositing items + _blendConnection = _filter_modifier.signal_blend_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_blendValueChanged)); + _blurConnection = _filter_modifier.signal_blur_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_blurValueChanged)); + _opacityConnection = _filter_modifier.signal_opacity_changed().connect( sigc::mem_fun(*this, &ObjectsPanel::_opacityValueChanged)); + _isolationConnection = _filter_modifier.signal_isolation_changed().connect( + sigc::mem_fun(*this, &ObjectsPanel::_isolationValueChanged)); + //Pack the compositing functions and the button row + _page.pack_end(_filter_modifier, Gtk::PACK_SHRINK); + _page.pack_end(_buttonsRow, Gtk::PACK_SHRINK); + + //Pack into the panel contents + _getContents()->pack_start(_page, Gtk::PACK_EXPAND_WIDGET); + + SPDesktop* targetDesktop = getDesktop(); + + //Set up the button row + + + //Add object/layer + Gtk::Button* btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("list-add"), _("Add layer...")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_NEW) ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + //Remove object + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("list-remove"), _("Remove object")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_DELETE) ); + _watching.push_back( btn ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + //Move to bottom + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("go-bottom"), _("Move To Bottom")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_BOTTOM) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + //Move down + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("go-down"), _("Move Down")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_DOWN) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + //Move up + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("go-up"), _("Move Up")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_UP) ); + _watchingNonTop.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + //Move to top + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("go-top"), _("Move To Top")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_TOP) ); + _watchingNonTop.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + //Collapse all + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("format-indent-less"), _("Collapse All")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_COLLAPSE_ALL) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + _buttonsRow.pack_start(_buttonsSecondary, Gtk::PACK_EXPAND_WIDGET); + _buttonsRow.pack_end(_buttonsPrimary, Gtk::PACK_EXPAND_WIDGET); + + _watching.push_back(&_filter_modifier); + + //Set up the pop-up menu + // ------------------------------------------------------- + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _show_contextmenu_icons = prefs->getBool("/theme/menuIcons_objects", true); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_RENAME, (int)BUTTON_RENAME ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_NEW, (int)BUTTON_NEW ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SOLO, (int)BUTTON_SOLO ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SHOW_ALL, (int)BUTTON_SHOW_ALL ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_HIDE_ALL, (int)BUTTON_HIDE_ALL ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_OTHERS, (int)BUTTON_LOCK_OTHERS ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_ALL, (int)BUTTON_LOCK_ALL ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_UNLOCK_ALL, (int)BUTTON_UNLOCK_ALL ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watchingNonTop.push_back( &_addPopupItem(targetDesktop, SP_VERB_SELECTION_STACK_UP, (int)BUTTON_UP) ); + _watchingNonBottom.push_back( &_addPopupItem(targetDesktop, SP_VERB_SELECTION_STACK_DOWN, (int)BUTTON_DOWN) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_SELECTION_GROUP, (int)BUTTON_GROUP ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_SELECTION_UNGROUP, (int)BUTTON_UNGROUP ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_CLIPPATH, (int)BUTTON_SETCLIP ) ); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_CREATE_CLIP_GROUP, (int)BUTTON_CLIPGROUP ) ); + + //will never be implemented + //_watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_INVERSE_CLIPPATH, (int)BUTTON_SETINVCLIP ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_UNSET_CLIPPATH, (int)BUTTON_UNSETCLIP ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_MASK, (int)BUTTON_SETMASK ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_UNSET_MASK, (int)BUTTON_UNSETMASK ) ); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_EDIT_DUPLICATE, (int)BUTTON_DUPLICATE ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_EDIT_DELETE, (int)BUTTON_DELETE ) ); + + _popupMenu.show_all_children(); + + // Install CSS to shift icons into the space reserved for toggles (i.e. check and radio items). + _popupMenu.signal_map().connect(sigc::mem_fun(static_cast<ContextMenu*>(&_popupMenu), &ContextMenu::ShiftIcons)); + } + // ------------------------------------------------------- + + //Set initial sensitivity of buttons + for (auto & it : _watching) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( false ); + } + + //Set up the color selection dialog + GtkWidget *dlg = GTK_WIDGET(_colorSelectorDialog.gobj()); + sp_transientize(dlg); + + _colorSelectorDialog.hide(); + _colorSelectorDialog.set_title (_("Select Highlight Color")); + _colorSelectorDialog.set_border_width (4); + _colorSelectorDialog.property_modal() = true; + _selectedColor.reset(new Inkscape::UI::SelectedColor); + Gtk::Widget *color_selector = Gtk::manage(new Inkscape::UI::Widget::ColorNotebook(*_selectedColor)); + _colorSelectorDialog.get_content_area()->pack_start ( + *color_selector, true, true, 0); + + _selectedColor->signal_dragged.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod)); + _selectedColor->signal_released.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod)); + _selectedColor->signal_changed.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod)); + + color_selector->show(); + + setDesktop( targetDesktop ); + + show_all_children(); + + //Connect the desktop changed connection + desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &ObjectsPanel::setDesktop) ); + _deskTrack.connect(GTK_WIDGET(gobj())); +} + +/** + * Callback method that will be called when the desktop is destroyed + */ +void ObjectsPanel::_desktopDestroyed(SPDesktop* /*desktop*/) { + // We need to make sure that we're not trying to update the tree after the desktop has vanished, e.g. + // when closing Inkscape. Preferably, we would have done so in the destructor of the ObjectsPanel. But + // as this destructor is never ever called, we will do this by attaching to the desktop_destroyed signal + // instead + _processQueue_sig.disconnect(); + _executeUpdate_sig.disconnect(); + _desktop = nullptr; +} + +/** + * Destructor + */ +ObjectsPanel::~ObjectsPanel() +{ + // Never being called, not even when closing Inkscape? + + //Close the highlight selection dialog + _colorSelectorDialog.hide(); + + //Set the desktop to null, which will disconnect all object watchers + setDesktop(nullptr); + + if ( _model ) + { + delete _model; + _model = nullptr; + } + + if (_pending) { + delete _pending; + _pending = nullptr; + } + + if ( _toggleEvent ) + { + gdk_event_free( _toggleEvent ); + _toggleEvent = nullptr; + } + + desktopChangeConn.disconnect(); + _deskTrack.disconnect(); +} + +/** + * Sets the current document + */ +void ObjectsPanel::setDocument(SPDesktop* /*desktop*/, SPDocument* document) +{ + //Clear all object watchers + _removeWatchers(); + + //Delete the root watcher + if (_rootWatcher) + { + _rootWatcher->_repr->removeObserver(*_rootWatcher); + delete _rootWatcher; + _rootWatcher = nullptr; + } + + _document = document; + + if (document && document->getRoot() && document->getRoot()->getRepr()) + { + //Create a new root watcher for the document and then call _objectsChanged to fill the tree + _rootWatcher = new ObjectsPanel::ObjectWatcher(this, document->getRoot()); + document->getRoot()->getRepr()->addObserver(*_rootWatcher); + _objectsChanged(document->getRoot()); + } +} + +/** + * Set the current panel desktop + */ +void ObjectsPanel::setDesktop( SPDesktop* desktop ) +{ + Panel::setDesktop(desktop); + + if ( desktop != _desktop ) { + _documentChangedConnection.disconnect(); + _documentChangedCurrentLayer.disconnect(); + _selectionChangedConnection.disconnect(); + if ( _desktop ) { + _desktop = nullptr; + } + + _desktop = Panel::getDesktop(); + if ( _desktop ) { + //Connect desktop signals + _documentChangedConnection = _desktop->connectDocumentReplaced( sigc::mem_fun(*this, &ObjectsPanel::setDocument)); + + _documentChangedCurrentLayer = _desktop->connectCurrentLayerChanged( sigc::mem_fun(*this, &ObjectsPanel::_objectsChangedWrapper)); + + _selectionChangedConnection = _desktop->selection->connectChanged( sigc::mem_fun(*this, &ObjectsPanel::_objectsSelected)); + + _desktopDestroyedConnection = _desktop->connectDestroy( sigc::mem_fun(*this, &ObjectsPanel::_desktopDestroyed)); + + setDocument(_desktop, _desktop->doc()); + } else { + setDocument(nullptr, nullptr); + } + } + _deskTrack.setBase(desktop); +} +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +//should be okay to put these here because they are never referenced anywhere else +using namespace Inkscape::UI::Tools; + +void SPItem::setHighlightColor(guint32 const color) +{ + g_free(_highlightColor); + if (color & 0x000000ff) + { + _highlightColor = g_strdup_printf("%u", color); + } + else + { + _highlightColor = nullptr; + } + + NodeTool *tool = nullptr; + if (SP_ACTIVE_DESKTOP ) { + Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context; + if (INK_IS_NODE_TOOL(ec)) { + tool = static_cast<NodeTool*>(ec); + tools_switch(tool->desktop, TOOLS_NODES); + } + } +} + +void SPItem::unsetHighlightColor() +{ + g_free(_highlightColor); + _highlightColor = nullptr; + NodeTool *tool = nullptr; + if (SP_ACTIVE_DESKTOP ) { + Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context; + if (INK_IS_NODE_TOOL(ec)) { + tool = static_cast<NodeTool*>(ec); + tools_switch(tool->desktop, TOOLS_NODES); + } + } +} + +/* + 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..dff1498 --- /dev/null +++ b/src/ui/dialog/objects.h @@ -0,0 +1,279 @@ +// 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/treeview.h> +#include <gtkmm/treestore.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/scale.h> +#include <gtkmm/dialog.h> +#include "ui/widget/spinbutton.h" +#include "ui/widget/panel.h" +#include "desktop-tracker.h" +#include "ui/widget/style-subject.h" +#include "selection.h" +#include "ui/widget/filter-effect-chooser.h" + +class SPObject; +class SPGroup; +struct SPColorSelector; + +namespace Inkscape { + +namespace UI { + +class SelectedColor; + +namespace Dialog { + + +/** + * A panel that displays objects. + */ +class ObjectsPanel : public UI::Widget::Panel +{ +public: + ObjectsPanel(); + ~ObjectsPanel() override; + + static ObjectsPanel& getInstance(); + + void setDesktop( SPDesktop* desktop ) override; + void setDocument( SPDesktop* desktop, SPDocument* document); + +private: + //Internal Classes: + class ModelColumns; + class InternalUIBounce; + class ObjectWatcher; + + //Connections, Watchers, Trackers: + + //Document root watcher + ObjectsPanel::ObjectWatcher* _rootWatcher; + + //All object watchers + std::map<SPItem*, std::pair<ObjectsPanel::ObjectWatcher*, bool> > _objectWatchers; + + //Connection for when the desktop changes + sigc::connection desktopChangeConn; + + //Connection for when the desktop is destroyed (I.e. its deconstructor is called) + sigc::connection _desktopDestroyedConnection; + + //Connection for when the document changes + sigc::connection _documentChangedConnection; + + //Connection for when the active layer changes + sigc::connection _documentChangedCurrentLayer; + + //Connection for when the active selection in the document changes + sigc::connection _selectionChangedConnection; + + //Connection for when the selection in the dialog changes + sigc::connection _selectedConnection; + + //Connections for when the opacity/blend/blur of the active selection in the document changes + sigc::connection _isolationConnection; + sigc::connection _opacityConnection; + sigc::connection _blendConnection; + sigc::connection _blurConnection; + + sigc::connection _processQueue_sig; + sigc::connection _executeUpdate_sig; + + //Desktop tracker for grabbing the desktop changed connection + DesktopTracker _deskTrack; + + //Members: + + //The current desktop + SPDesktop* _desktop; + + //The current document + SPDocument* _document; + + //Tree data model + ModelColumns* _model; + + //Prevents the composite controls from updating + bool _blockCompositeUpdate; + + // + InternalUIBounce* _pending; + bool _pending_update; + + //Whether the drag & drop was dragged into an item + gboolean _dnd_into; + + //List of drag & drop source items + std::vector<SPItem*> _dnd_source; + + //Drag & drop target item + SPItem* _dnd_target; + + // Whether the drag sources include a layer + bool _dnd_source_includes_layer; + + //List of items to change the highlight on + std::vector<SPItem*> _highlight_target; + + //Show icons in the context menu + bool _show_contextmenu_icons; + + //GUI Members: + + GdkEvent* _toggleEvent; + + Gtk::TreeModel::Path _defer_target; + + Glib::RefPtr<Gtk::TreeStore> _store; + std::list<std::tuple<SPItem*, Gtk::TreeModel::iterator, bool> > _tree_update_queue; + //When the user selects an item in the document, we need to find that item in the tree view + //and highlight it. When looking up a specific item in the tree though, we don't want to have + //to iterate through the whole list, as this would take too long if the list is very long. So + //we will use a std::map for this instead, which is much faster (and call it _tree_cache). It + //would have been cleaner to create our own custom tree model, as described here + //https://en.wikibooks.org/wiki/GTK%2B_By_Example/Tree_View/Tree_Models + std::map<SPItem*, Gtk::TreeModel::iterator> _tree_cache; + std::list<SPItem *> _selected_objects_order; // ordered by time of selection + std::list<Gtk::TreePath> _paths_to_be_expanded; + + 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::Box _buttonsRow; + Gtk::Box _buttonsPrimary; + Gtk::Box _buttonsSecondary; + Gtk::ScrolledWindow _scroller; + Gtk::Menu _popupMenu; + Inkscape::UI::Widget::SpinButton _spinBtn; + Gtk::VBox _page; + + Gtk::Label _visibleHeader; + Gtk::Label _lockHeader; + Gtk::Label _typeHeader; + Gtk::Label _clipmaskHeader; + Gtk::Label _highlightHeader; + Gtk::Label _nameHeader; + + /* Composite Settings (blend, blur, opacity). */ + Inkscape::UI::Widget::SimpleFilterModifier _filter_modifier; + + Gtk::Dialog _colorSelectorDialog; + std::unique_ptr<Inkscape::UI::SelectedColor> _selectedColor; + + //Methods: + + ObjectsPanel(ObjectsPanel const &) = delete; // no copy + ObjectsPanel &operator=(ObjectsPanel const &) = delete; // no assign + + void _styleButton( Gtk::Button& btn, char const* iconName, char const* tooltip ); + void _fireAction( unsigned int code ); + + Gtk::MenuItem& _addPopupItem( SPDesktop *desktop, unsigned int code, int id ); + + void _setVisibleIter( const Gtk::TreeModel::iterator& iter, const bool visible ); + void _setLockedIter( const Gtk::TreeModel::iterator& iter, const bool locked ); + + bool _handleButtonEvent(GdkEventButton *event); + bool _handleKeyEvent(GdkEventKey *event); + + void _storeHighlightTarget(const Gtk::TreeModel::iterator& iter); + void _storeDragSource(const Gtk::TreeModel::iterator& iter); + bool _handleDragDrop(const Glib::RefPtr<Gdk::DragContext>& context, int x, int y, guint time); + void _handleEdited(const Glib::ustring& path, const Glib::ustring& new_text); + void _handleEditingCancelled(); + + void _doTreeMove(); + void _renameObject(Gtk::TreeModel::Row row, const Glib::ustring& name); + + void _pushTreeSelectionToCurrent(); + bool _selectItemCallback(const Gtk::TreeModel::iterator& iter, bool *setOpacity, bool *first_pass); + bool _clearPrevSelectionState(const Gtk::TreeModel::iterator& iter); + void _desktopDestroyed(SPDesktop* desktop); + + void _checkTreeSelection(); + + void _blockAllSignals(bool should_block); + void _takeAction( int val ); + bool _executeAction(); + bool _executeUpdate(); + + void _setExpanded( const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& path, bool isexpanded ); + void _setCollapsed(SPGroup * group); + + bool _noSelection( Glib::RefPtr<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b ); + bool _rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b ); + + void _compositingChanged( const Gtk::TreeModel::iterator& iter, bool *setValues ); + void _updateComposite(); + void _setCompositingValues(SPItem *item); + + bool _findInTreeCache(SPItem* item, Gtk::TreeModel::iterator &tree_iter); + void _updateObject(SPObject *obj, bool recurse); + + void _objectsSelected(Selection *sel); + void _updateObjectSelected(SPItem* item, bool scrollto, bool expand); + + void _removeWatchers(bool only_unused); + void _addWatcher(SPItem* item); + void _objectsChangedWrapper(SPObject *obj); + void _objectsChanged(SPObject *obj); + bool _processQueue(); + void _queueObject(SPObject* obj, Gtk::TreeModel::Row* parentRow); + void _addObjectToTree(SPItem* item, const Gtk::TreeModel::Row &parentRow, bool expanded); + + void _isolationChangedIter(const Gtk::TreeIter &iter); + void _isolationValueChanged(); + + void _opacityChangedIter(const Gtk::TreeIter& iter); + void _opacityValueChanged(); + + void _blendChangedIter(const Gtk::TreeIter &iter); + void _blendValueChanged(); + + void _blurChangedIter(const Gtk::TreeIter& iter, double blur); + void _blurValueChanged(); + + void _highlightPickerColorMod(); +}; + + + +} //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..ec49312 --- /dev/null +++ b/src/ui/dialog/paint-servers.cpp @@ -0,0 +1,505 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Paint Servers dialog + */ +/* Authors: + * Valentin Ionita + * + * Copyright (C) 2019 Valentin Ionita + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include <iostream> +#include <map> +#include <utility> + +#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 "verbs.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 "object/sp-shape.h" +#include "ui/cache/svg_preview_cache.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> +)====="; + +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); + } +}; + +PaintServersColumns *PaintServersDialog::getColumns() { return new PaintServersColumns(); } + +// Constructor +PaintServersDialog::PaintServersDialog(gchar const *prefsPath) + : Inkscape::UI::Widget::Panel(prefsPath, SP_VERB_DIALOG_PAINT) + , desktop(SP_ACTIVE_DESKTOP) + , target_selected(true) + , ALLDOCS(_("All paint servers")) + , CURRENTDOC(_("Current document")) +{ + current_store = ALLDOCS; + + store[ALLDOCS] = Gtk::ListStore::create(*getColumns()); + store[CURRENTDOC] = Gtk::ListStore::create(*getColumns()); + + // 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); + _getContents()->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 Gtk::ComboBoxText()); + dropdown->append(ALLDOCS); + dropdown->append(CURRENTDOC); + document_map[CURRENTDOC] = desktop->getDocument(); + 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 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, 300); + 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); + grid->attach(*scroller, 0, 2, 2, 1); + + // Events + target_dropdown->signal_changed().connect( + sigc::mem_fun(*this, &PaintServersDialog::on_target_changed) + ); + + dropdown->signal_changed().connect( + sigc::mem_fun(*this, &PaintServersDialog::on_document_changed) + ); + + icon_view->signal_item_activated().connect( + sigc::mem_fun(*this, &PaintServersDialog::on_item_activated) + ); + + desktop->getDocument()->getDefs()->connectModified( + sigc::mem_fun(*this, &PaintServersDialog::load_current_document) + ); + + // 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)); + + // Load paint servers from resource files + load_sources(); +} + +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 paint servers from all the files associated +void PaintServersDialog::load_sources() +{ + + // Extract paints from the current file + load_document(desktop->getDocument()); + + // Extract out paints from files in share/paint. + for (auto &path : get_filenames(Inkscape::IO::Resource::PAINT, { ".svg" })) { + SPDocument *document = SPDocument::createNewDoc(path.c_str(), FALSE); + + load_document(document); + } +} + +Glib::RefPtr<Gdk::Pixbuf> PaintServersDialog::get_pixbuf(SPDocument *document, Glib::ustring 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::load_document: 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 = dynamic_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; +} + +// Load paint server from the given document +void PaintServersDialog::load_document(SPDocument *document) +{ + PaintServersColumns *columns = getColumns(); + Glib::ustring document_title; + if (!document->getRoot()->title()) { + document_title = CURRENTDOC; + } else { + document_title = Glib::ustring(document->getRoot()->title()); + } + bool has_server_elements = false; + + // Find all paints + std::vector<Glib::ustring> paints; + recurse_find_paint(document->getRoot(), paints); + + // Sort and remove duplicates. + std::sort(paints.begin(), paints.end()); + paints.erase(std::unique(paints.begin(), paints.end()), paints.end()); + + if (paints.size() && store.find(document_title) == store.end()) { + store[document_title] = Gtk::ListStore::create(*getColumns()); + } + + // iterating though servers + for (auto paint : paints) { + Glib::RefPtr<Gdk::Pixbuf> pixbuf(nullptr); + Glib::ustring id; + pixbuf = get_pixbuf(document, paint, &id); + if (!pixbuf) { + continue; + } + + // Save as a ListStore column + Gtk::ListStore::iterator iter = store[ALLDOCS]->append(); + (*iter)[columns->id] = id; + (*iter)[columns->paint] = paint; + (*iter)[columns->pixbuf] = pixbuf; + (*iter)[columns->document] = document_title; + + iter = store[document_title]->append(); + (*iter)[columns->id] = id; + (*iter)[columns->paint] = paint; + (*iter)[columns->pixbuf] = pixbuf; + (*iter)[columns->document] = document_title; + has_server_elements = true; + } + + if (has_server_elements && document_map.find(document_title) == document_map.end()) { + document_map[document_title] = document; + dropdown->append(document_title); + } +} + +void PaintServersDialog::load_current_document(SPObject * /*object*/, guint /*flags*/) +{ + std::unique_ptr<PaintServersColumns> columns(getColumns()); + SPDocument *document = desktop->getDocument(); + Glib::RefPtr<Gtk::ListStore> current = store[CURRENTDOC]; + + std::vector<Glib::ustring> paints; + std::vector<Glib::ustring> paints_current; + std::vector<Glib::ustring> paints_missing; + recurse_find_paint(document->getDefs(), paints); + + std::sort(paints.begin(), paints.end()); + paints.erase(std::unique(paints.begin(), paints.end()), paints.end()); + + // Delete the server from the store if it doesn't exist in the current document + for (auto iter = current->children().begin(); iter != current->children().end();) { + Gtk::TreeRow server = *iter; + + if (std::find(paints.begin(), paints.end(), server[columns->paint]) == paints.end()) { + iter = current->erase(server); + } else { + paints_current.push_back(server[columns->paint]); + iter++; + } + } + + for (auto s : paints) { + if (std::find(paints_current.begin(), paints_current.end(), s) == paints_current.end()) { + std::cout << "missing " << s << std::endl; + paints_missing.push_back(s); + } + } + + if (!paints_missing.size()) { + return; + } + + for (auto paint : paints_missing) { + Glib::RefPtr<Gdk::Pixbuf> pixbuf(nullptr); + Glib::ustring id; + pixbuf = get_pixbuf(document, paint, &id); + if (!pixbuf) { + continue; + } + + Gtk::ListStore::iterator iter = current->append(); + (*iter)[columns->id] = id; + (*iter)[columns->paint] = paint; + (*iter)[columns->pixbuf] = pixbuf; + (*iter)[columns->document] = CURRENTDOC; + } +} + +void PaintServersDialog::on_target_changed() +{ + target_selected = !target_selected; +} + +void PaintServersDialog::on_document_changed() +{ + current_store = dropdown->get_active_text(); + icon_view->set_model(store[current_store]); +} + +void PaintServersDialog::on_item_activated(const Gtk::TreeModel::Path& path) +{ + // Get the current selected elements + Selection *selection = desktop->getSelection(); + std::vector<SPObject*> const selected_items(selection->items().begin(), selection->items().end()); + + if (!selected_items.size()) { + return; + } + + PaintServersColumns *columns = getColumns(); + 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 document_title = (*iter)[columns->document]; + SPDocument *document = document_map[document_title]; + SPObject *paint_server = document->getObjectById(id); + SPDocument *document_target = desktop->getDocument(); + + bool paint_server_exists = false; + for (auto server : store[CURRENTDOC]->children()) { + if (server[columns->id] == id) { + paint_server_exists = true; + break; + } + } + + if (!paint_server_exists) { + // Add the paint server to the current document definition + Inkscape::XML::Document *xml_doc = document_target->getReprDoc(); + Inkscape::XML::Node *repr = paint_server->getRepr()->duplicate(xml_doc); + document_target->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] = document_title; + } + + // 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(); + } + + document_target->collectOrphans(); +} + +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..d3b8f58 --- /dev/null +++ b/src/ui/dialog/paint-servers.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Paint Servers dialog + */ +/* Authors: + * Valentin Ionita + * + * 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/widget/panel.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class PaintServersColumns; // For Gtk::ListStore + +/** + * 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 Inkscape::UI::Widget::Panel { + +public: + PaintServersDialog(gchar const *prefsPath = "/dialogs/paint"); + ~PaintServersDialog() override; + + static PaintServersDialog &getInstance() { return *new PaintServersDialog(); }; + PaintServersDialog(PaintServersDialog const &) = delete; + PaintServersDialog &operator=(PaintServersDialog const &) = delete; + + private: + static PaintServersColumns *getColumns(); + void load_sources(); + void load_document(SPDocument *document); + void load_current_document(SPObject *, guint); + Glib::RefPtr<Gdk::Pixbuf> get_pixbuf(SPDocument *, Glib::ustring, Glib::ustring *); + void on_target_changed(); + void on_document_changed(); + void on_item_activated(const Gtk::TreeModel::Path &path); + std::vector<SPObject *> extract_elements(SPObject *item); + + 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; + SPDesktop *desktop; + Gtk::ComboBoxText *target_dropdown; + bool target_selected; +}; + +} // 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/panel-dialog.h b/src/ui/dialog/panel-dialog.h new file mode 100644 index 0000000..eb7d9f3 --- /dev/null +++ b/src/ui/dialog/panel-dialog.h @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A panel holding dialog + */ +/* Authors: + * C 2007 Gustav Broberg <broberg@kth.se> + * C 2012 Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_PANEL_DIALOG_H +#define INKSCAPE_PANEL_DIALOG_H + +#include <glibmm/i18n.h> +#include <gtkmm/dialog.h> + +#include "verbs.h" +#include "dialog.h" +#include "ui/dialog/swatches.h" +#include "ui/dialog/floating-behavior.h" +#include "ui/dialog/dock-behavior.h" +#include "inkscape.h" +#include "desktop.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * Auxiliary class for the link between UI::Dialog::PanelDialog and UI::Dialog::Dialog. + * + * PanelDialog handles signals emitted when a desktop changes, either changing to a + * different desktop or a new document. + */ +class PanelDialogBase { +public: + PanelDialogBase(UI::Widget::Panel &panel, char const */*prefs_path*/, int const /*verb_num*/) : + _panel (panel) { } + + virtual void present() = 0; + virtual ~PanelDialogBase() = default; + + virtual UI::Widget::Panel &getPanel() { return _panel; } + +protected: + + inline virtual void _propagateDocumentReplaced(SPDesktop* desktop, SPDocument *document); + inline virtual void _propagateDesktopActivated(SPDesktop *); + inline virtual void _propagateDesktopDeactivated(SPDesktop *); + + UI::Widget::Panel &_panel; + sigc::connection _document_replaced_connection; +}; + +/** + * Bridges UI::Widget::Panel and UI::Dialog::Dialog. + * + * Where Dialog handles window behaviour, such as closing, position, etc, and where + * Panel is the actual container for dialog child widgets (and from where the dialog + * content is made), PanelDialog links these two classes together to create a + * dockable and floatable dialog. The link with Dialog is made via PanelDialogBase. + */ +template <typename Behavior> +class PanelDialog : public PanelDialogBase, public Inkscape::UI::Dialog::Dialog { + +public: + /** + * Constructor. + * + * @param contents panel with the actual dialog content. + * @param prefs_path characteristic path for loading/saving dialog position. + * @param verb_num the dialog verb. + */ + PanelDialog(UI::Widget::Panel &contents, char const *prefs_path, int const verb_num); + + ~PanelDialog() override = default; + + template <typename T> + static PanelDialog<Behavior> *create(); + + inline void present() override; + +private: + template <typename T> + static PanelDialog<Behavior> *_create(); + + inline void _presentDialog(); + + PanelDialog() = delete; + PanelDialog(PanelDialog<Behavior> const &d) = delete; // no copy + PanelDialog<Behavior>& operator=(PanelDialog<Behavior> const &d) = delete; // no assign +}; + + +void PanelDialogBase::_propagateDocumentReplaced(SPDesktop *desktop, SPDocument *document) +{ + _panel.signalDocumentReplaced().emit(desktop, document); +} + +void PanelDialogBase::_propagateDesktopActivated(SPDesktop *desktop) +{ + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(*this, &PanelDialogBase::_propagateDocumentReplaced)); + _panel.signalActivateDesktop().emit(desktop); +} + +void PanelDialogBase::_propagateDesktopDeactivated(SPDesktop *desktop) +{ + _document_replaced_connection.disconnect(); + _panel.signalDeactiveDesktop().emit(desktop); +} + + +template <typename B> +PanelDialog<B>::PanelDialog(Widget::Panel &panel, char const *prefs_path, int const verb_num) : + PanelDialogBase(panel, prefs_path, verb_num), + Dialog(&B::create, prefs_path, verb_num) +{ + Gtk::Box *vbox = get_vbox(); + _panel.signalResponse().connect(sigc::mem_fun(*this, &PanelDialog::_handleResponse)); + _panel.signalPresent().connect(sigc::mem_fun(*this, &PanelDialog::_presentDialog)); + + vbox->pack_start(_panel, true, true, 0); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + _propagateDesktopActivated(desktop); + + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(*this, &PanelDialog::_propagateDocumentReplaced)); + + show_all_children(); +} + +template <typename B> template <typename P> +PanelDialog<B> *PanelDialog<B>::create() +{ + return _create<P>(); +} + +template <typename B> template <typename P> +PanelDialog<B> *PanelDialog<B>::_create() +{ + UI::Widget::Panel &panel = P::getInstance(); + return new PanelDialog<B>(panel, panel.getPrefsPath(), panel.getVerb()); +} + +template <typename B> +void PanelDialog<B>::present() +{ + _panel.present(); +} + +template <typename B> +void PanelDialog<B>::_presentDialog() +{ + Dialog::present(); +} + +template <> inline +void PanelDialog<Behavior::FloatingBehavior>::present() +{ + Dialog::present(); + _panel.present(); +} + +template <> inline +void PanelDialog<Behavior::FloatingBehavior>::_presentDialog() +{ +} + +/** + * Specialized factory method for panel dialogs with floating behavior in order to make them work as + * singletons, i.e. allow them track the current active desktop. + */ +template <> +template <typename P> +PanelDialog<Behavior::FloatingBehavior> *PanelDialog<Behavior::FloatingBehavior>::create() +{ + auto instance = _create<P>(); + + INKSCAPE.signal_activate_desktop.connect( + sigc::mem_fun(*instance, &PanelDialog<Behavior::FloatingBehavior>::_propagateDesktopActivated) + ); + INKSCAPE.signal_deactivate_desktop.connect( + sigc::mem_fun(*instance, &PanelDialog<Behavior::FloatingBehavior>::_propagateDesktopDeactivated) + ); + + return instance; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_PANEL_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/polar-arrange-tab.cpp b/src/ui/dialog/polar-arrange-tab.cpp new file mode 100644 index 0000000..67f1442 --- /dev/null +++ b/src/ui/dialog/polar-arrange-tab.cpp @@ -0,0 +1,408 @@ +// 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 "verbs.h" + +#include "object/sp-ellipse.h" +#include "object/sp-item-transform.h" + +#include "ui/dialog/polar-arrange-tab.h" +#include "ui/dialog/tile.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); +} + +/** + * 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) + { + // The first selected ellipse is actually the last one in the list + if(SP_IS_GENERICELLIPSE(item)) + referenceEllipse = SP_GENERICELLIPSE(item); + } else { + // The last selected ellipse is actually the first in list + 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(), SP_VERB_SELECTION_ARRANGE, + _("Arrange on ellipse")); +} + +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-colors-preview-dialog.cpp b/src/ui/dialog/print-colors-preview-dialog.cpp new file mode 100644 index 0000000..6371af9 --- /dev/null +++ b/src/ui/dialog/print-colors-preview-dialog.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Print Colors Preview dialog - implementation. + */ +/* Authors: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* +#include "desktop.h" +#include "print-colors-preview-dialog.h" +#include "preferences.h" +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +//Yes, I know we shouldn't hardcode CMYK. This class needs to be refactored +// in order to accommodate spot colors and color components defined using +// ICC colors. --Juca + +void PrintColorsPreviewDialog::toggle_cyan(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/printcolorspreview/cyan", cyan->get_active()); + + SPDesktop *desktop = getDesktop(); + desktop->setDisplayModePrintColorsPreview(); +} + +void PrintColorsPreviewDialog::toggle_magenta(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/printcolorspreview/magenta", magenta->get_active()); + + SPDesktop *desktop = getDesktop(); + desktop->setDisplayModePrintColorsPreview(); +} + +void PrintColorsPreviewDialog::toggle_yellow(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/printcolorspreview/yellow", yellow->get_active()); + + SPDesktop *desktop = getDesktop(); + desktop->setDisplayModePrintColorsPreview(); +} + +void PrintColorsPreviewDialog::toggle_black(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/printcolorspreview/black", black->get_active()); + + SPDesktop *desktop = getDesktop(); + desktop->setDisplayModePrintColorsPreview(); +} + +PrintColorsPreviewDialog::PrintColorsPreviewDialog() + : UI::Widget::Panel("/dialogs/printcolorspreview", SP_VERB_DIALOG_PRINT_COLORS_PREVIEW) +{ + Gtk::VBox* vbox = Gtk::manage(new Gtk::VBox()); + + cyan = new Gtk::ToggleButton(_("Cyan")); + vbox->pack_start( *cyan, false, false ); +// tips.set_tip((*cyan), _("Render cyan separation")); + cyan->signal_clicked().connect( sigc::mem_fun(*this, &PrintColorsPreviewDialog::toggle_cyan) ); + + magenta = new Gtk::ToggleButton(_("Magenta")); + vbox->pack_start( *magenta, false, false ); +// tips.set_tip((*magenta), _("Render magenta separation")); + magenta->signal_clicked().connect( sigc::mem_fun(*this, &PrintColorsPreviewDialog::toggle_magenta) ); + + yellow = new Gtk::ToggleButton(_("Yellow")); + vbox->pack_start( *yellow, false, false ); +// tips.set_tip((*yellow), _("Render yellow separation")); + yellow->signal_clicked().connect( sigc::mem_fun(*this, &PrintColorsPreviewDialog::toggle_yellow) ); + + black = new Gtk::ToggleButton(_("Black")); + vbox->pack_start( *black, false, false ); +// tips.set_tip((*black), _("Render black separation")); + black->signal_clicked().connect( sigc::mem_fun(*this, &PrintColorsPreviewDialog::toggle_black) ); + + gint val; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + val = prefs->getBool("/options/printcolorspreview/cyan"); + cyan->set_active( val != 0 ); + val = prefs->getBool("/options/printcolorspreview/magenta"); + magenta->set_active( val != 0 ); + val = prefs->getBool("/options/printcolorspreview/yellow"); + yellow->set_active( val != 0 ); + val = prefs->getBool("/options/printcolorspreview/black"); + black->set_active( val != 0 ); + + _getContents()->add(*vbox); + _getContents()->show_all(); +} + +PrintColorsPreviewDialog::~PrintColorsPreviewDialog(){} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape +*/ diff --git a/src/ui/dialog/print-colors-preview-dialog.h b/src/ui/dialog/print-colors-preview-dialog.h new file mode 100644 index 0000000..e40f987 --- /dev/null +++ b/src/ui/dialog/print-colors-preview-dialog.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Print Colors Preview dialog + */ +/* Authors: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_PRINT_COLORS_PREVIEW_H +#define INKSCAPE_UI_DIALOG_PRINT_COLORS_PREVIEW_H + +#include "ui/widget/panel.h" +#include "verbs.h" + +#include <gtkmm/box.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class PrintColorsPreviewDialog : public UI::Widget::Panel { +public: + PrintColorsPreviewDialog(); + ~PrintColorsPreviewDialog(); + + static PrintColorsPreviewDialog &getInstance() + { return *new PrintColorsPreviewDialog(); } + +private: + void toggle_cyan(); + void toggle_magenta(); + void toggle_yellow(); + void toggle_black(); + + Gtk::ToggleButton* cyan; + Gtk::ToggleButton* magenta; + Gtk::ToggleButton* yellow; + Gtk::ToggleButton* black; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //#ifndef INKSCAPE_UI_PRINT_COLORS_PREVIEW_H diff --git a/src/ui/dialog/print.cpp b/src/ui/dialog/print.cpp new file mode 100644 index 0000000..006961b --- /dev/null +++ b/src/ui/dialog/print.cpp @@ -0,0 +1,263 @@ +// 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 = 1.0; + sp_repr_get_double (nv, "inkscape:pageopacity", &opacity); + 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: + { + Cairo::RefPtr<Cairo::ImageSurface> png = Cairo::ImageSurface::create_from_png (tmp_png); + cairo_t *cr = gtk_print_context_get_cairo_context (context->gobj()); + cairo_matrix_t m; + cairo_get_matrix(cr, &m); + cairo_scale(cr, Inkscape::Util::Quantity::convert(1, "in", "pt") / dpi, Inkscape::Util::Quantity::convert(1, "in", "pt") / dpi); + // FIXME: why is the origin offset?? + cairo_set_source_surface(cr, png->cobj(), 0, 0); + cairo_paint(cr); + cairo_set_matrix(cr, &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); + + cairo_t *cr = gtk_print_context_get_cairo_context (context->gobj()); + cairo_surface_t *surface = cairo_get_target(cr); + cairo_matrix_t ctm; + cairo_get_matrix(cr, &ctm); + + bool ret = ctx->setSurfaceTarget (surface, 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/save-template-dialog.cpp b/src/ui/dialog/save-template-dialog.cpp new file mode 100644 index 0000000..5a8afa9 --- /dev/null +++ b/src/ui/dialog/save-template-dialog.cpp @@ -0,0 +1,101 @@ +// 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> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +SaveTemplate::SaveTemplate() : + Gtk::Dialog(_("Save Document as Template")), + name_label(_("Name: "), Gtk::ALIGN_START), + author_label(_("Author: "), Gtk::ALIGN_START), + description_label(_("Description: "), Gtk::ALIGN_START), + keywords_label(_("Keywords: "), Gtk::ALIGN_START), + is_default_template(_("Set as default template")) +{ + resize(400, 200); + + name_text.set_hexpand(true); + + grid.attach(name_label, 0, 0, 1, 1); + grid.attach(name_text, 1, 0, 1, 1); + + grid.attach(author_label, 0, 1, 1, 1); + grid.attach(author_text, 1, 1, 1, 1); + + grid.attach(description_label, 0, 2, 1, 1); + grid.attach(description_text, 1, 2, 1, 1); + + grid.attach(keywords_label, 0, 3, 1, 1); + grid.attach(keywords_text, 1, 3, 1, 1); + + auto content_area = get_content_area(); + + content_area->set_spacing(5); + + content_area->add(grid); + content_area->add(is_default_template); + + name_text.signal_changed().connect( sigc::mem_fun(*this, + &SaveTemplate::on_name_changed) ); + + add_button("Cancel", Gtk::RESPONSE_CANCEL); + add_button("Save", Gtk::RESPONSE_OK); + + set_response_sensitive(Gtk::RESPONSE_OK, false); + set_default_response(Gtk::RESPONSE_CANCEL); + + show_all(); +} + +void SaveTemplate::on_name_changed() { + + if (name_text.get_text_length() == 0) { + + set_response_sensitive(Gtk::RESPONSE_OK, false); + } else { + + set_response_sensitive(Gtk::RESPONSE_OK, true); + } +} + +bool SaveTemplate::save_template(Gtk::Window &parentWindow) { + + return sp_file_save_template(parentWindow, name_text.get_text(), + author_text.get_text(), description_text.get_text(), + keywords_text.get_text(), is_default_template.get_active()); +} + +void SaveTemplate::save_document_as_template(Gtk::Window &parentWindow) { + + SaveTemplate dialog; + + auto operation_done = false; + + while (operation_done == false) { + + auto user_response = dialog.run(); + + if (user_response == Gtk::RESPONSE_OK) + operation_done = dialog.save_template(parentWindow); + else + operation_done = true; + } +} + +} +} +} diff --git a/src/ui/dialog/save-template-dialog.h b/src/ui/dialog/save-template-dialog.h new file mode 100644 index 0000000..8cf2d8e --- /dev/null +++ b/src/ui/dialog/save-template-dialog.h @@ -0,0 +1,60 @@ +// 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 <gtkmm/dialog.h> +#include <gtkmm/entry.h> +#include <gtkmm/box.h> +#include <gtkmm/grid.h> +#include <gtkmm/label.h> +#include <gtkmm/checkbutton.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class SaveTemplate : public Gtk::Dialog +{ + +public: + + static void save_document_as_template(Gtk::Window &parentWindow); + +protected: + + void on_name_changed(); + +private: + + Gtk::Grid grid; + + Gtk::Label name_label; + Gtk::Entry name_text; + + Gtk::Label author_label; + Gtk::Entry author_text; + + Gtk::Label description_label; + Gtk::Entry description_text; + + Gtk::Label keywords_label; + Gtk::Entry keywords_text; + + Gtk::CheckButton is_default_template; + + SaveTemplate(); + bool save_template(Gtk::Window &parentWindow); + +}; +} +} +} +#endif diff --git a/src/ui/dialog/selectorsdialog.cpp b/src/ui/dialog/selectorsdialog.cpp new file mode 100644 index 0000000..280bfdf --- /dev/null +++ b/src/ui/dialog/selectorsdialog.cpp @@ -0,0 +1,1508 @@ +// 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 "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 "verbs.h" + +#include "xml/attribute-record.h" +#include "xml/node-observer.h" +#include "xml/sp-css-attr.h" + +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include <map> +#include <regex> +#include <utility> + +// G_MESSAGES_DEBUG=DEBUG_SELECTORSDIALOG gdb ./inkscape +// #define DEBUG_SELECTORSDIALOG +// #define G_LOG_DOMAIN "SELECTORSDIALOG" + +using Inkscape::DocumentUndo; +using Inkscape::Util::List; +using Inkscape::XML::AttributeRecord; + +/** + * This macro is used to remove spaces around selectors or any strings when + * parsing is done to update XML style element or row labels in this dialog. + */ +#define REMOVE_SPACES(x) \ + x.erase(0, x.find_first_not_of(' ')); \ + if (x.size() && x[0] == ',') \ + x.erase(0, 1); \ + if (x.size() && x[x.size() - 1] == ',') \ + x.erase(x.size() - 1, 1); \ + x.erase(x.find_last_not_of(' ') + 1); + +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->_scroollock = 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"); + + _scroollock = 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() + : UI::Widget::Panel("/dialogs/selectors", SP_VERB_DIALOG_SELECTORS) + , _updating(false) + , _textNode(nullptr) + , _scroolpos(0) + , _scroollock(false) + , _desktopTracker() +{ + 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); + + _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(); + + // Document & Desktop + _desktop_changed_connection = + _desktopTracker.connectDesktopChanged(sigc::mem_fun(*this, &SelectorsDialog::_handleDesktopChanged)); + _desktopTracker.connect(GTK_WIDGET(gobj())); + + _document_replaced_connection = + getDesktop()->connectDocumentReplaced(sigc::mem_fun(this, &SelectorsDialog::_handleDocumentReplaced)); + + _selection_changed_connection = getDesktop()->getSelection()->connectChanged( + sigc::hide(sigc::mem_fun(this, &SelectorsDialog::_handleSelectionChanged))); + + // Add watchers + _updateWatchers(getDesktop()); + + // Load tree + _readStyleElement(); + _selectRow(); + + if (!_store->children().empty()) { + _del.show(); + } + show_all(); +} + + +void SelectorsDialog::_vscrool() +{ + if (!_scroollock) { + _scroolpos = _vadj->get_value(); + } else { + _vadj->set_value(_scroolpos); + _scroollock = 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); + _vadj = _scrolled_window_selectors.get_vadjustment(); + _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_vscrool)); + _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 = 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); + Gtk::ScrolledWindow *dialog_scroller = new Gtk::ScrolledWindow(); + dialog_scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + dialog_scroller->set_shadow_type(Gtk::SHADOW_IN); + dialog_scroller->add(*Gtk::manage(contents)); + _getContents()->pack_start(*dialog_scroller, Gtk::PACK_EXPAND_WIDGET); + show_all(); + int widthpos = _paned.property_max_position() - _paned.property_min_position(); + int panedpos = prefs->getInt("/dialogs/selectors/panedpos", widthpos / 2); + _paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_childresized)); + _paned.signal_size_allocate().connect(sigc::mem_fun(*this, &SelectorsDialog::_panedresized)); + _paned.signal_realize().connect(sigc::mem_fun(*this, &SelectorsDialog::_panedrealized)); + _updating = true; + _paned.property_position() = panedpos; + _updating = false; + set_size_request(320, 260); + set_name("SelectorsAndStyleDialog"); +} + +void SelectorsDialog::_panedresized(Gtk::Allocation allocation) +{ + g_debug("SelectorsDialog::_panedresized"); + _resized(); +} + +void SelectorsDialog::_panedrealized() { _style_dialog->readStyleElement(); } + +void SelectorsDialog::_childresized() +{ + g_debug("SelectorsDialog::_childresized"); + _resized(); +} + +void SelectorsDialog::_resized() +{ + g_debug("SelectorsDialog::_resized"); + _scroollock = true; + if (_updating) { + return; + } + _updating = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool dir = !prefs->getBool("/dialogs/selectors/vertical", true); + int max = int(_paned.property_max_position() * 0.95); + int min = int(_paned.property_max_position() * 0.05); + if (_paned.property_position() > max) { + _paned.property_position() = max; + } + if (_paned.property_position() < min) { + _paned.property_position() = min; + } + + prefs->setInt("/dialogs/selectors/panedpos", _paned.property_position()); + _updating = false; +} + +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; +} + +/** + * Class destructor + */ +SelectorsDialog::~SelectorsDialog() +{ + g_debug("SelectorsDialog::~SelectorsDialog"); + _desktop_changed_connection.disconnect(); + _document_replaced_connection.disconnect(); + _selection_changed_connection.disconnect(); +} + + +/** + * @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; + _scroollock = 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]; + REMOVE_SPACES(selector); // Remove leading/trailing spaces + 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; + + + std::vector<SPObject *> objVec; + for (unsigned i = 0; i < tokens.size()-1; i += 2) { + Glib::ustring selector = tokens[i]; + REMOVE_SPACES(selector); // Remove leading/trailing spaces + 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] = objVec; + 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; + // Get list of objects selector matches + objVec = _getObjVec(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; + } + REMOVE_SPACES(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] = objVec; + row[_mColumns._colProperties] = properties; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + // Add as children, objects that match selector. + for (auto &obj : objVec) { + 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] = std::vector<SPObject *>(1, obj); + childrow[_mColumns._colProperties] = ""; // Unused + childrow[_mColumns._colVisible] = true; // Unused + childrow[_mColumns._colSelected] = 400; + } + } + + + _updating = false; + if (rewrite) { + _writeStyleElement(); + } + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _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"); + + _scroollock = true; + _updating = true; + Glib::ustring styleContent = ""; + for (auto& row: _store->children()) { + Glib::ustring selector = row[_mColumns._colSelector]; +#if 0 + REMOVE_SPACES(selector); + size_t len = selector.size(); + if(selector[len-1] == ','){ + selector.erase(len-1); + } + 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()); + INKSCAPE.readStyleSheets(true); + if (empty) { + styleContent = ""; + textNode->setContent(styleContent.c_str()); + } + textNode->setContent(styleContent.c_str()); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_SELECTORS, _("Edited style element.")); + + _updating = false; + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); + g_debug("SelectorsDialog::_writeStyleElement(): | %s |", styleContent.c_str()); +} + +/** + * Update the watchers on objects. + */ +void SelectorsDialog::_updateWatchers(SPDesktop *desktop) +{ + g_debug("SelectorsDialog::_updateWatchers"); + + if (_textNode) { + _textNode->removeObserver(*m_styletextwatcher); + _textNode = nullptr; + } + + if (m_root) { + m_root->removeSubtreeObserver(*m_nodewatcher); + m_root = nullptr; + } + + if (desktop) { + m_root = desktop->getDocument()->getReprRoot(); + m_root->addSubtreeObserver(*m_nodewatcher); + } +} +/* +void sp_get_selector_active(Glib::ustring &selector) +{ + std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[ ]+", selector); + selector = tokensplus[tokensplus.size() - 1]; + // Erase any comma/space + REMOVE_SPACES(selector); + Glib::ustring toadd = Glib::ustring(selector); + Glib::ustring toparse = Glib::ustring(selector); + Glib::ustring tag = ""; + if (toadd[0] != '.' || toadd[0] != '#') { + auto i = std::min(toadd.find("#"), toadd.find(".")); + tag = toadd.substr(0,i-1); + toparse.erase(0, i-1); + } + auto i = toparse.find("#"); + toparse.erase(i, 1); + auto j = toparse.find("#"); + if (j == std::string::npos) { + selector = ""; + } else if (i != std::string::npos) { + Glib::ustring post = toadd.substr(0,i-1); + Glib::ustring pre = toadd.substr(i, (toadd.size()-1)-i); + selector = tag + pre + post; + } +} */ + +Glib::ustring sp_get_selector_classes(Glib::ustring selector) //, SelectorType selectortype, Glib::ustring id = "") +{ + g_debug("SelectorsDialog::sp_get_selector_classes"); + + 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 + REMOVE_SPACES(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]; + std::vector<SPObject *> objVec = _getObjVec(multiselector); + row[_mColumns._colObj] = objVec; + 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 = sp_get_selector_classes(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] = std::vector<SPObject *>(1, obj); + childrow[_mColumns._colProperties] = ""; // Unused + childrow[_mColumns._colVisible] = true; // Unused + childrow[_mColumns._colSelected] = 400; + } + objVec = _getObjVec(multiselector); + row[_mColumns._colSelector] = multiselector; + row[_mColumns._colObj] = objVec; + _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 (List<AttributeRecord const> iter = css_selector->attributeList(); iter; ++iter) { + gchar const *key = g_quark_to_string(iter->key); + css->setAttribute(key, nullptr); + } + 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) { + _scroollock = 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]; + REMOVE_SPACES(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 = sp_get_selector_classes(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; + } + } + REMOVE_SPACES(selector); + if (selector.empty()) { + _store->erase(parent); + + } else { + _store->erase(row); + parent[_mColumns._colSelector] = selector; + parent[_mColumns._colExpand] = true; + parent[_mColumns._colObj] = _getObjVec(selector); + } + } + _updating = false; + + // Add entry to style element + _writeStyleElement(); + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _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; + } + REMOVE_SPACES(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; + Gtk::TreeModel::Children children = row.children(); + if (children.empty() || children.size() == 1) { + _del.show(); + } + std::vector<SPObject *> objVec = row[_mColumns._colObj]; + + for (auto obj : objVec) { + getDesktop()->selection->add(obj); + } + } + _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"); + _scroollock = 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 + REMOVE_SPACES(selectorValue); + if (originalValue.find("@import ") != std::string::npos) { + std::vector<SPObject *> objVecEmpty; + Gtk::TreeModel::Row row = *(_store->prepend()); + row[_mColumns._colSelector] = originalValue; + row[_mColumns._colExpand] = false; + row[_mColumns._colType] = OTHER; + row[_mColumns._colObj] = objVecEmpty; + 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 = sp_get_selector_classes(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); + } + } + } + objVec = _getObjVec(selectorValue); + Gtk::TreeModel::Row row = *(_store->prepend()); + row[_mColumns._colExpand] = true; + row[_mColumns._colType] = SELECTOR; + row[_mColumns._colSelector] = selectorValue; + row[_mColumns._colObj] = objVec; + row[_mColumns._colProperties] = ""; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + for (auto &obj : objVec) { + 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] = std::vector<SPObject *>(1, obj); + childrow[_mColumns._colProperties] = ""; // Unused + childrow[_mColumns._colVisible] = true; // Unused + childrow[_mColumns._colSelected] = 400; + } + } + // Add entry to style element + _writeStyleElement(); + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _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"); + + _scroollock = true; + Glib::RefPtr<Gtk::TreeSelection> refTreeSelection = _treeView.get_selection(); + _treeView.get_selection()->set_mode(Gtk::SELECTION_SINGLE); + Gtk::TreeModel::iterator iter = refTreeSelection->get_selected(); + if (iter) { + _vscrool(); + Gtk::TreeModel::Row row = *iter; + if (row.children().size() > 2) { + return; + } + _updating = true; + _store->erase(iter); + _updating = false; + _writeStyleElement(); + _del.hide(); + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _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) { + _scroollock = 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)) { + _vscrool(); + 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(_scroolpos, _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; +}; + +// ------------------------------------------------------------------- + + +/** + * Handle document replaced. (Happens when a default document is immediately replaced by another + * document in a new window.) + */ +void SelectorsDialog::_handleDocumentReplaced(SPDesktop *desktop, SPDocument * /* document */) +{ + g_debug("SelectorsDialog::handleDocumentReplaced()"); + + _selection_changed_connection.disconnect(); + + _updateWatchers(desktop); + + if (!desktop) + return; + + _selection_changed_connection = desktop->getSelection()->connectChanged( + sigc::hide(sigc::mem_fun(this, &SelectorsDialog::_handleSelectionChanged))); + + _readStyleElement(); + _selectRow(); +} + + +/* + * When a dialog is floating, it is connected to the active desktop. + */ +void SelectorsDialog::_handleDesktopChanged(SPDesktop *desktop) +{ + g_debug("SelectorsDialog::handleDesktopReplaced()"); + + if (getDesktop() == desktop) { + // This will happen after construction of dialog. We've already + // set up signals so just return. + return; + } + + _selection_changed_connection.disconnect(); + _document_replaced_connection.disconnect(); + + setDesktop( desktop ); + + _selection_changed_connection = desktop->getSelection()->connectChanged( + sigc::hide(sigc::mem_fun(this, &SelectorsDialog::_handleSelectionChanged))); + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(this, &SelectorsDialog::_handleDocumentReplaced)); + + _updateWatchers(desktop); + _readStyleElement(); + _selectRow(); +} + + +/* + * Handle a change in which objects are selected in a document. + */ +void SelectorsDialog::_handleSelectionChanged() +{ + g_debug("SelectorsDialog::_handleSelectionChanged()"); + _lastpath.clear(); + _treeView.get_selection()->set_mode(Gtk::SELECTION_MULTIPLE); + _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"); + _treeView.get_selection()->set_mode(Gtk::SELECTION_SINGLE); + _updating = true; + _del.show(); + if (event->type == GDK_BUTTON_RELEASE && event->button == 1) { + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + _selectObjects(x, y); + } + _updating = false; +} + + +/** + * 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() +{ + _scroollock = 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->parent()) { + _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. + + _treeView.get_selection()->unselect_all(); + 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) { + Gtk::TreeModel::Children subchildren = row->children(); + for (auto subrow : subchildren) { + subrow[_mColumns._colSelected] = 400; + } + } + for (auto obj : selection->items()) { + for (auto row : children) { + Gtk::TreeModel::Children subchildren = row->children(); + for (auto subrow : subchildren) { + std::vector<SPObject *> objVec = subrow[_mColumns._colObj]; + if (obj == objVec[0]) { + _treeView.get_selection()->select(row); + row[_mColumns._colVisible] = true; + subrow[_mColumns._colSelected] = 700; + } + } + if (row[_mColumns._colExpand]) { + _treeView.expand_to_path(Gtk::TreePath(row)); + } + } + } + for (auto row : children) { + if (row[_mColumns._colExpand]) { + _treeView.expand_to_path(Gtk::TreePath(row)); + } + } + _vadj->set_value(std::min(_scroolpos, _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..2c0e363 --- /dev/null +++ b/src/ui/dialog/selectorsdialog.h @@ -0,0 +1,209 @@ +// 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 "ui/dialog/desktop-tracker.h" +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/styledialog.h" +#include "ui/widget/panel.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 <ui/widget/panel.h> + +#include "xml/helper-observer.h" + +#include <memory> +#include <vector> + +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 Widget::Panel { + + public: + ~SelectorsDialog() override; + // No default constructor, noncopyable, nonassignable + SelectorsDialog(); + SelectorsDialog(SelectorsDialog const &d) = delete; + SelectorsDialog operator=(SelectorsDialog const &d) = delete; + static SelectorsDialog &getInstance() { return *new SelectorsDialog(); } + + private: + // Monitor <style> element for changes. + class NodeObserver; + + // 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<std::vector<SPObject *> > _colObj; // List of matching objects. + 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; + void _updateWatchers(SPDesktop *); + + // 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 _resized(); + void _childresized(); + void _panedresized(Gtk::Allocation allocation); + + void _selectObjects(int, int); + // Variables + double _scroolpos; + bool _scroollock; + 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. + + // Signals and handlers - External + sigc::connection _document_replaced_connection; + sigc::connection _desktop_changed_connection; + sigc::connection _selection_changed_connection; + + void _handleDocumentReplaced(SPDesktop* desktop, SPDocument *document); + void _handleDesktopChanged(SPDesktop* desktop); + void _handleSelectionChanged(); + void _panedrealized(); + 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); + + DesktopTracker _desktopTracker; + + Inkscape::XML::SignalObserver _objObserver; // Track object in selected row (for style change). + + // Signal and handlers - Internal + void _addSelector(); + void _delSelector(); + bool _handleButtonEvent(GdkEventButton *event); + void _buttonEventsSelectObjs(GdkEventButton *event); + void _selectRow(); // Select row in tree when selection changed. + void _vscrool(); + + // 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..44e8302 --- /dev/null +++ b/src/ui/dialog/spellcheck.cpp @@ -0,0 +1,815 @@ +// 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 "message-stack.h" + +#include "inkscape.h" +#include "document.h" +#include "desktop.h" + +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/inkscape-preferences.h" // for PREFS_PAGE_SPELLCHECK +#include "ui/tools-switch.h" +#include "ui/tools/text-tool.h" + +#include "text-editing.h" +#include "selection-chemistry.h" +#include "display/curve.h" +#include "document-undo.h" +#include "verbs.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 <glibmm/i18n.h> + +#ifdef _WIN32 +#include <windows.h> +#endif + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * Get the list of installed aspell dictionaries/languages + */ +std::vector<std::string> SpellCheck::get_available_langs() +{ + std::vector<std::string> langs; + +#if HAVE_ASPELL + auto *config = new_aspell_config(); + auto const *dlist = get_aspell_dict_info_list(config); + auto *elements = aspell_dict_info_list_elements(dlist); + + for (AspellDictInfo const *entry; (entry = aspell_dict_info_enumeration_next(elements)) != nullptr;) { + // skip duplicates (I get "de_DE" twice) + if (!langs.empty() && langs.back() == entry->name) { + continue; + } + + langs.emplace_back(entry->name); + } + + delete_aspell_dict_info_enumeration(elements); + delete_aspell_config(config); +#endif + + return langs; +} + +static void show_spellcheck_preferences_dialog() +{ + Inkscape::Preferences::get()->setInt("/dialogs/preferences/page", PREFS_PAGE_SPELLCHECK); + SP_ACTIVE_DESKTOP->_dlg_mgr->showDialog("InkscapePreferences"); +} + +SpellCheck::SpellCheck () : + UI::Widget::Panel("/dialogs/spellcheck/", SP_VERB_DIALOG_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(false, 0), + stop_button(_("_Stop"), true), + start_button(_("_Start"), true), + desktop(nullptr), + deskTrack() +{ + _prefs = Inkscape::Preferences::get(); + + // take languages from prefs + for (const char *langkey : { "lang", "lang2", "lang3" }) { + auto lang = _prefs->getString(_prefs_path + langkey); + if (!lang.empty()) { + _langs.push_back(lang); + } + } + + 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("<i>No aspell dictionaries installed</i>"); + } + } + + 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 (auto const &lang : _langs) { + dictionary_combo.append(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 + */ + Gtk::Box *contents = _getContents(); + contents->set_spacing(6); + contents->pack_start (banner_hbox, false, false, 0); + contents->pack_start (suggestion_hbox, true, true, 0); + contents->pack_start (dictionary_hbox, false, false, 0); + contents->pack_start (action_sep, false, false, 6); + contents->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)); + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &SpellCheck::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + + 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(); + + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +void SpellCheck::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void SpellCheck::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + this->desktop = desktop; + if (_working) { + // Stop and start on the new desktop + finished(); + onStart(); + } + } +} + +void SpellCheck::clearRects() +{ + for(auto t : _rects) { + sp_canvas_item_hide(t); + sp_canvas_item_destroy(t); + } + _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 (!desktop) + return; // no desktop to check + + 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 + } + + for (auto& child: r->children) { + if (SP_IS_ITEM (&child) && !child.cloned && !desktop->isLayer(SP_ITEM(&child))) { + if ((hidden || !desktop->itemIsHidden(SP_ITEM(&child))) && (locked || !SP_ITEM(&child)->isLocked())) { + if (SP_IS_TEXT(&child) || SP_IS_FLOWTEXT(&child)) + l.push_back(static_cast<SPItem*>(&child)); + } + } + 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 (gconstpointer a, gconstpointer b)//returns a<b +{ + SPItem *i1 = SP_ITEM(a); + SPItem *i2 = SP_ITEM(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 = (SP_OBJECT(_text))->connectModified(sigc::mem_fun(*this, &SpellCheck::onObjModified)); + _release_connection = (SP_OBJECT(_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() { +#if HAVE_ASPELL + if (_speller) { + aspell_speller_save_all_word_lists(_speller); + delete_aspell_speller(_speller); + _speller = nullptr; + } +#endif +} + +bool SpellCheck::updateSpeller() { +#if HAVE_ASPELL + deleteSpeller(); + + auto lang = dictionary_combo.get_active_text(); + if (!lang.empty()) { + AspellConfig *config = new_aspell_config(); + aspell_config_replace(config, "lang", lang.c_str()); + aspell_config_replace(config, "encoding", "UTF-8"); + AspellCanHaveError *ret = new_aspell_speller(config); + delete_aspell_config(config); + if (aspell_error(ret) != nullptr) { + banner_label.set_text(aspell_error_message(ret)); + delete_aspell_can_have_error(ret); + } else { + _speller = to_aspell_speller(ret); + } + } + + return _speller != nullptr; +#else + return false; +#endif +} + +bool +SpellCheck::init(SPDesktop *d) +{ + desktop = d; + + start_button.set_sensitive(false); + + _stops = 0; + _adds = 0; + clearRects(); + + if (!updateSpeller()) + return false; + + _root = desktop->getDocument()->getRoot(); + + // empty the list of objects we've checked + _seen_objects.clear(); + + // grab first text + nextText(); + + _working = true; + + return true; +} + +void +SpellCheck::finished () +{ + deleteSpeller(); + + clearRects(); + disconnect(); + + //desktop->clearWaitingCursor(); + + 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(); + + desktop = nullptr; + _root = nullptr; + + _working = false; +} + +bool +SpellCheck::nextWord() +{ + if (!_working) + 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 HAVE_ASPELL + // run it by all active spellers + if (_speller) { + have += aspell_speller_check(_speller, _word.c_str(), -1); + } +#endif /* HAVE_ASPELL */ + + if (have == 0) { // not found in any! + _stops ++; + + //desktop->clearWaitingCursor(); + + // 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 path rectangle, red stroke + SPCanvasItem *rect = sp_canvas_bpath_new(desktop->getSketch(), nullptr); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(rect), 0xff0000ff, 3.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(rect), 0, SP_WIND_RULE_NONZERO); + SPCurve *curve = new SPCurve(); + curve->moveto(area.corner(0)); + curve->lineto(area.corner(1)); + curve->lineto(area.corner(2)); + curve->lineto(area.corner(3)); + curve->lineto(area.corner(0)); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(rect), curve); + sp_canvas_item_show(rect); + _rects.push_back(rect); + + // scroll to make it all visible + Geom::Point const center = desktop->get_display_area().midpoint(); + 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 (tools_isactive(desktop, TOOLS_TEXT)) { + 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 HAVE_ASPELL + + // get suggestions + model = Gtk::ListStore::create(tree_columns); + tree_view.set_model(model); + unsigned n_sugg = 0; + + if (_speller) { + const AspellWordList *wl = aspell_speller_suggest(_speller, _word.c_str(), -1); + AspellStringEnumeration * els = aspell_word_list_elements(wl); + const char *sugg; + Gtk::TreeModel::iterator iter; + + while ((sugg = aspell_string_enumeration_next(els)) != nullptr) { + 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); + } + } + delete_aspell_string_enumeration(els); + } + + accept_button.set_sensitive(n_sugg > 0); + +#endif /* HAVE_ASPELL */ + + return true; + + } + return false; +} + + + +void +SpellCheck::deleteLastRect () +{ + if (!_rects.empty()) { + sp_canvas_item_hide(_rects.back()); + sp_canvas_item_destroy(_rects.back()); + _rects.pop_back(); // pop latest-prepended rect + } +} + +void SpellCheck::doSpellcheck () +{ + if (_langs.empty()) { + return; + } + + banner_label.set_markup(_("<i>Checking...</i>")); + + //desktop->setWaitingCursor(); + + 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(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Fix spelling")); + } + } + + deleteLastRect(); + doSpellcheck(); +} + +void +SpellCheck::onIgnore () +{ +#if HAVE_ASPELL + if (_speller) { + aspell_speller_add_to_session(_speller, _word.c_str(), -1); + } +#endif /* HAVE_ASPELL */ + + deleteLastRect(); + doSpellcheck(); +} + +void +SpellCheck::onIgnoreOnce () +{ + deleteLastRect(); + doSpellcheck(); +} + +void +SpellCheck::onAdd () +{ + _adds++; + +#if HAVE_ASPELL + if (_speller) { + aspell_speller_add_to_personal(_speller, _word.c_str(), -1); + } +#endif /* HAVE_ASPELL */ + + deleteLastRect(); + doSpellcheck(); +} + +void +SpellCheck::onStop () +{ + finished(); +} + +void +SpellCheck::onStart () +{ + if (init (SP_ACTIVE_DESKTOP)) + doSpellcheck(); +} + +void SpellCheck::onLanguageChanged() +{ + 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..39d1285 --- /dev/null +++ b/src/ui/dialog/spellcheck.h @@ -0,0 +1,301 @@ +// 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 <vector> +#include <set> + +#include <gtkmm/box.h> +#include <gtkmm/button.h> +#include <gtkmm/buttonbox.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/separator.h> +#include <gtkmm/liststore.h> +#include <gtkmm/treeview.h> + +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/panel.h" + +#include "text-editing.h" + +#if HAVE_ASPELL +#include <aspell.h> +#endif /* HAVE_ASPELL */ + +class SPDesktop; +class SPObject; +class SPItem; +class SPCanvasItem; + +namespace Inkscape { +class Preferences; + +namespace UI { +namespace Dialog { + +/** + * + * A dialog widget to checking spelling of text elements in the document + * Uses ASpell and one of the languages set in the users preference file + * + */ +class SpellCheck : public Widget::Panel { +public: + SpellCheck (); + ~SpellCheck () override; + + static SpellCheck &getInstance() { return *new SpellCheck(); } + + static std::vector<std::string> get_available_langs(); + +private: + + /** + * 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 (gconstpointer a, gconstpointer b); + SPItem *getText (SPObject *root); + void nextText (); + + /** + * Initialize the controls and aspell + */ + bool init (SPDesktop *desktop); + + /** + * 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(); + + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + + SPObject *_root; + +#if HAVE_ASPELL + AspellSpeller *_speller = nullptr; +#endif /* HAVE_ASPELL */ + + /** + * list of canvasitems (currently just rects) that mark misspelled things on canvas + */ + std::vector<SPCanvasItem *> _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<std::string> _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::HBox suggestion_hbox; + Gtk::VBox 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; + Gtk::ComboBoxText dictionary_combo; + Gtk::HBox dictionary_hbox; + Gtk::Separator action_sep; + Gtk::Button stop_button; + Gtk::Button start_button; + Gtk::ButtonBox actionbutton_hbox; + + SPDesktop * desktop; + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; + + 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/styledialog.cpp b/src/ui/dialog/styledialog.cpp new file mode 100644 index 0000000..872b3bf --- /dev/null +++ b/src/ui/dialog/styledialog.cpp @@ -0,0 +1,1690 @@ +// 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 "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 "verbs.h" +#include "xml/attribute-record.h" +#include "xml/node-observer.h" +#include "xml/sp-css-attr.h" + +#include <map> +#include <regex> +#include <utility> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +// G_MESSAGES_DEBUG=DEBUG_STYLEDIALOG gdb ./inkscape +// #define DEBUG_STYLEDIALOG +// #define G_LOG_DOMAIN "STYLEDIALOG" + +using Inkscape::DocumentUndo; +using Inkscape::Util::List; +using Inkscape::XML::AttributeRecord; + +/** + * This macro is used to remove spaces around selectors or any strings when + * parsing is done to update XML style element or row labels in this dialog. + */ +#define REMOVE_SPACES(x) \ + x.erase(0, x.find_first_not_of(' ')); \ + x.erase(x.find_last_not_of(' ') + 1); + +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; + + 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::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 notifyContentChanged(Inkscape::XML::Node &node, + Inkscape::Util::ptr_shared old_content, + Inkscape::Util::ptr_shared new_content) override{ + if ( _styledialog && _repr && _textNode == node) { + _styledialog->_stylesheetChanged( node ); + } + }; + */ + 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) +{ + readStyleElement(); +} + +void StyleDialog::_nodeRemoved(Inkscape::XML::Node &repr) +{ + if (_textNode == &repr) { + _textNode = nullptr; + } + + readStyleElement(); +} + +void StyleDialog::_nodeChanged(Inkscape::XML::Node &object) +{ + g_debug("StyleDialog::_nodeChanged"); + readStyleElement(); +} + +/* void +StyleDialog::_stylesheetChanged( Inkscape::XML::Node &repr ) { + std::cout << "Style tag modified" << std::endl; + 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() + : UI::Widget::Panel("/dialogs/style", SP_VERB_DIALOG_STYLE) + , _updating(false) + , _textNode(nullptr) + , _scroolpos(0) + , _desktopTracker() + , _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); + Gtk::Label *infotoggler = Gtk::manage(new Gtk::Label(_("Edit Full Stylesheet"))); + infotoggler->get_style_context()->add_class("inksmall"); + _vadj = _scrolledWindow.get_vadjustment(); + _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &StyleDialog::_vscrool)); + _mainBox.set_orientation(Gtk::ORIENTATION_VERTICAL); + + _getContents()->pack_start(_mainBox, Gtk::PACK_EXPAND_WIDGET); + // Document & Desktop + _desktop_changed_connection = + _desktopTracker.connectDesktopChanged(sigc::mem_fun(*this, &StyleDialog::_handleDesktopChanged)); + _desktopTracker.connect(GTK_WIDGET(gobj())); + + _document_replaced_connection = + getDesktop()->connectDocumentReplaced(sigc::mem_fun(this, &StyleDialog::_handleDocumentReplaced)); + + _selection_changed_connection = getDesktop()->getSelection()->connectChanged( + sigc::hide(sigc::mem_fun(this, &StyleDialog::_handleSelectionChanged))); + + // Add watchers + _updateWatchers(getDesktop()); + + // Load tree + readStyleElement(); +} + +void StyleDialog::_vscrool() +{ + if (!_scroollock) { + _scroolpos = _vadj->get_value(); + } else { + _vadj->set_value(_scroolpos); + _scroollock = false; + } +} + +Glib::ustring StyleDialog::fixCSSSelectors(Glib::ustring selector) +{ + g_debug("SelectorsDialog::fixCSSSelectors"); + REMOVE_SPACES(selector); + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", selector); + Glib::ustring my_selector = selector + " {"; // Parsing fails sometimes without '{'. Fix me + CRSelector *cr_selector = cr_selector_parse_from_buf((guchar *)my_selector.c_str(), CR_UTF_8); + for (auto token : tokens) { + REMOVE_SPACES(token); + std::vector<Glib::ustring> subtokens = Glib::Regex::split_simple("[ ]+", token); + for (auto subtoken : subtokens) { + REMOVE_SPACES(subtoken); + Glib::ustring my_selector = subtoken + " {"; // Parsing fails sometimes without '{'. Fix me + CRSelector *cr_selector = cr_selector_parse_from_buf((guchar *)my_selector.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 ""; +} + +/** + * Class destructor + */ +StyleDialog::~StyleDialog() +{ + g_debug("StyleDialog::~StyleDialog"); + _desktop_changed_connection.disconnect(); + _document_replaced_connection.disconnect(); + _selection_changed_connection.disconnect(); +} + +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"); + + if (_updating) + return; // Don't read if we wrote style element. + _updating = true; + _scroollock = true; + Inkscape::XML::Node *textNode = _getStyleTextNode(); + SPDocument *document = SP_ACTIVE_DOCUMENT; + + // 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 = getDesktop()->getSelection(); + SPObject *obj = nullptr; + if (selection->objects().size() == 1) { + obj = selection->objects().back(); + } + if (!obj) { + obj = getDesktop()->getDocument()->getXMLDialogSelectedObject(); + if (obj && !obj->getRepr()) { + obj = nullptr; // treat detached object as no selection + } + } + + Glib::ustring gladefile = get_filename(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())) { + 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] = iter->get_value(); + row[_mColumns._colStrike] = false; + row[_mColumns._colOwner] = Glib::ustring("Current value"); + row[_mColumns._colHref] = nullptr; + row[_mColumns._colLinked] = false; + if (is_url(iter->get_value().c_str())) { + Glib::ustring id = iter->get_value(); + id = id.substr(5, id.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]; + REMOVE_SPACES(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 undertand + // 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 != SP_STYLE_SRC_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"); + 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 != SP_STYLE_SRC_UNSET) { + auto key = iter->id(); + if (key != SP_PROP_FONT && key != SP_ATTR_D && key != SP_PROP_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(_("Invalid property set")); + if (!value.empty()) { + tooltiptext = Glib::ustring(_("Used in ") + _owner_style[row[_mColumns._colName]]); + } + 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) { + Inkscape::Selection *selection = getDesktop()->getSelection(); + getDesktop()->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; + + REMOVE_SPACES(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) { + REMOVE_SPACES(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"); + if (_updating) { + return; + } + _scroollock = true; + Inkscape::Selection *selection = getDesktop()->getSelection(); + SPObject *obj = nullptr; + if (selection->objects().size() == 1) { + obj = selection->objects().back(); + } + if (!obj) { + obj = getDesktop()->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 = styleContent + "\n" + selector + " { \n"; + } + selectorpos = _deleted_pos; + for (auto &row : store->children()) { + selector = row[_mColumns._colSelector]; + selectorpos = row[_mColumns._colSelectorPos]; + Glib::ustring opencomment = ""; + Glib::ustring closecomment = ""; + if (selector != "style_properties" && selector != "attributes") { + opencomment = row[_mColumns._colActive] ? " " : " /*"; + closecomment = row[_mColumns._colActive] ? "\n" : "*/\n"; + } + Glib::ustring name = row[_mColumns._colName]; + Glib::ustring value = row[_mColumns._colValue]; + if (!(name.empty() && value.empty())) { + styleContent = styleContent + opencomment + name + ":" + value + ";" + closecomment; + } + } + if (selector != "style_properties" && selector != "attributes") { + styleContent = styleContent + "}"; + } + if (selector == "style_properties") { + _updating = true; + obj->getRepr()->setAttribute("style", styleContent, false); + _updating = false; + } else if (selector == "attributes") { + for (auto iter : obj->style->properties()) { + auto key = iter->id(); + if (key != SP_PROP_FONT && key != SP_ATTR_D && key != SP_PROP_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 name = row[_mColumns._colName]; + Glib::ustring 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()); + INKSCAPE.readStyleSheets(true); + if (empty) { + textNode->setContent(""); + } + } + _updating = false; + readStyleElement(); + SPDocument *document = SP_ACTIVE_DOCUMENT; + for (auto iter : document->getObjectsBySelector(selector)) { + iter->style->readFromObject(iter); + iter->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_STYLE, _("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); +} +/*Harcode 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; + _scroollock = 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"); + _scroollock = 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"); + + _scroollock = 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 = std::min(finalname.find(";"), finalname.find(":")); + 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 enoght 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->setAttribute(name, nullptr); + 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); + } + } */ + } 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"); + + _scroollock = 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"); + + _scroollock = 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; +} + +/** + * Update the watchers on objects. + */ +void StyleDialog::_updateWatchers(SPDesktop *desktop) + +{ + g_debug("StyleDialog::_updateWatchers"); + + if (_textNode) { + _textNode->removeObserver(*m_styletextwatcher); + _textNode = nullptr; + } + + if (m_root) { + m_root->removeSubtreeObserver(*m_nodewatcher); + m_root = nullptr; + } + + if (desktop) { + m_root = desktop->getDocument()->getReprRoot(); + m_root->addSubtreeObserver(*m_nodewatcher); + } +} + + +/** + * @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 getDesktop()->getDocument()->getObjectsBySelector(selector); +} + +void StyleDialog::_closeDialog(Gtk::Dialog *textDialogPtr) { textDialogPtr->response(Gtk::RESPONSE_OK); } + + +/** + * Handle document replaced. (Happens when a default document is immediately replaced by another + * document in a new window.) + */ +void StyleDialog::_handleDocumentReplaced(SPDesktop *desktop, SPDocument * /* document */) +{ + g_debug("StyleDialog::handleDocumentReplaced()"); + + _selection_changed_connection.disconnect(); + + _updateWatchers(desktop); + + if (!desktop) + return; + + _selection_changed_connection = + desktop->getSelection()->connectChanged(sigc::hide(sigc::mem_fun(this, &StyleDialog::_handleSelectionChanged))); + + readStyleElement(); +} + + +/* + * When a dialog is floating, it is connected to the active desktop. + */ +void StyleDialog::_handleDesktopChanged(SPDesktop *desktop) +{ + g_debug("StyleDialog::handleDesktopReplaced()"); + + if (getDesktop() == desktop) { + // This will happen after construction of dialog. We've already + // set up signals so just return. + return; + } + + _selection_changed_connection.disconnect(); + _document_replaced_connection.disconnect(); + setDesktop(desktop); + + _selection_changed_connection = + desktop->getSelection()->connectChanged(sigc::hide(sigc::mem_fun(this, &StyleDialog::_handleSelectionChanged))); + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(this, &StyleDialog::_handleDocumentReplaced)); + + _updateWatchers(desktop); + readStyleElement(); +} + + +/* + * Handle a change in which objects are selected in a document. + */ +void StyleDialog::_handleSelectionChanged() +{ + g_debug("StyleDialog::_handleSelectionChanged()"); + _scroolpos = 0; + _vadj->set_value(0); + 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..2d96d48 --- /dev/null +++ b/src/ui/dialog/styledialog.h @@ -0,0 +1,208 @@ +// 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 "style-enums.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 <ui/widget/panel.h> + +#include "ui/dialog/desktop-tracker.h" + +#include "xml/helper-observer.h" + +#include <memory> +#include <vector> + +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 Widget::Panel { + + public: + ~StyleDialog() override; + // No default constructor, noncopyable, nonassignable + StyleDialog(); + StyleDialog(StyleDialog const &d) = delete; + StyleDialog operator=(StyleDialog const &d) = delete; + + 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 _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 _vscrool(); + bool _scroollock; + double _scroolpos; + Glib::ustring _current_selector; + + // Update watchers + std::unique_ptr<Inkscape::XML::NodeObserver> m_nodewatcher; + std::unique_ptr<Inkscape::XML::NodeObserver> m_styletextwatcher; + void _updateWatchers(SPDesktop *); + + // 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 + + // Signals and handlers - External + sigc::connection _document_replaced_connection; + sigc::connection _desktop_changed_connection; + sigc::connection _selection_changed_connection; + + void _handleDocumentReplaced(SPDesktop *desktop, SPDocument *document); + void _handleDesktopChanged(SPDesktop *desktop); + void _handleSelectionChanged(); + void _closeDialog(Gtk::Dialog *textDialogPtr); + DesktopTracker _desktopTracker; +}; + +} // 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..9f191d1 --- /dev/null +++ b/src/ui/dialog/svg-fonts-dialog.cpp @@ -0,0 +1,1067 @@ +// 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 <gtkmm/scale.h> +#include <gtkmm/notebook.h> +#include <gtkmm/imagemenuitem.h> +#include <glibmm/stringutils.h> +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "document-undo.h" +#include "selection.h" +#include "svg-fonts-dialog.h" +#include "verbs.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-missing-glyph.h" +#include "svg/svg.h" +#include "xml/repr.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); + cr->show_text (_text.c_str()); + + // Draw some lines to show line area. + cr->set_source_rgb( 0.5, 0.5, 0.5 ); + cr->move_to ( 0, 10); + cr->line_to (_x, 10); + cr->stroke(); + cr->move_to ( 0, _y-10); + cr->line_to (_x, _y-10); + cr->stroke(); + } + return true; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/* +Gtk::HBox* SvgFontsDialog::AttrEntry(gchar* lbl, const SPAttributeEnum attr){ + Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox()); + hbox->add(* Gtk::manage(new Gtk::Label(lbl)) ); + Gtk::Entry* entry = Gtk::manage(new Gtk::Entry()); + hbox->add(* entry ); + hbox->show_all(); + + entry->signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_attr_changed)); + return hbox; +} +*/ + +SvgFontsDialog::AttrEntry::AttrEntry(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttributeEnum attr){ + this->dialog = d; + this->attr = attr; + entry.set_tooltip_text(tooltip); + auto label = new Gtk::Label(lbl); + this->pack_start(*Gtk::manage(label), false, false, 4); + this->pack_end(entry, true, true); + this->show_all(); + + entry.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::AttrEntry::on_attr_changed)); +} + +void SvgFontsDialog::AttrEntry::set_text(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(){ + + SPObject* o = nullptr; + for (auto& node: dialog->get_selected_spfont()->children) { + switch(this->attr){ + case SP_PROP_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(), SP_VERB_DIALOG_SVG_FONTS, + _("Set SVG Font attribute")); + } + +} + +SvgFontsDialog::AttrSpin::AttrSpin(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttributeEnum attr) { + + this->dialog = d; + this->attr = attr; + spin.set_tooltip_text(tooltip); + auto label = new Gtk::Label(lbl); + this->set_border_width(2); + this->set_spacing(6); + this->pack_start(*Gtk::manage(label), false, false); + this->pack_end(spin, true, true); + this->show_all(); + spin.set_range(0, 4096); + spin.set_increments(16, 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(){ + + SPObject* o = nullptr; + switch (this->attr) { + + // <font> attributes + case SP_ATTR_HORIZ_ORIGIN_X: + case SP_ATTR_HORIZ_ORIGIN_Y: + case SP_ATTR_HORIZ_ADV_X: + case SP_ATTR_VERT_ORIGIN_X: + case SP_ATTR_VERT_ORIGIN_Y: + case SP_ATTR_VERT_ADV_Y: + o = this->dialog->get_selected_spfont(); + break; + + // <font-face> attributes + case SP_ATTR_UNITS_PER_EM: + case SP_ATTR_ASCENT: + case SP_ATTR_DESCENT: + case SP_ATTR_CAP_HEIGHT: + case SP_ATTR_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(), SP_VERB_DIALOG_SVG_FONTS, + _("Set SVG Font attribute")); + } + +} + +Gtk::HBox* SvgFontsDialog::AttrCombo(gchar* lbl, const SPAttributeEnum /*attr*/){ + Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox()); + hbox->add(* Gtk::manage(new Gtk::Label(lbl)) ); + hbox->add(* Gtk::manage(new Gtk::ComboBox()) ); + hbox->show_all(); + return hbox; +} + +/* +Gtk::HBox* SvgFontsDialog::AttrSpin(gchar* lbl){ + Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox()); + hbox->add(* Gtk::manage(new Gtk::Label(lbl)) ); + hbox->add(* Gtk::manage(new Inkscape::UI::Widget::SpinBox()) ); + hbox->show_all(); + return hbox; +}*/ + +/*** SvgFontsDialog ***/ + +GlyphComboBox::GlyphComboBox()= default; + +void GlyphComboBox::update(SPFont* spfont){ + if (!spfont) return; + + this->remove_all(); + + for (auto& node: spfont->children) { + if (SP_IS_GLYPH(&node)){ + this->append((static_cast<SPGlyph*>(&node))->unicode); + } + } +} + +void SvgFontsDialog::on_kerning_value_changed(){ + if (!get_selected_kerning_pair()) { + return; + } + + SPDocument* document = this->getDesktop()->getDocument(); + + //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(document, undokey.c_str(), SP_VERB_DIALOG_SVG_FONTS, _("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::create_glyphs_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem) +{ + auto mi = Gtk::manage(new 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()){ + global_vbox.set_sensitive(true); + glyphs_vbox.set_sensitive(true); + kerning_vbox.set_sensitive(true); + } else { + global_vbox.set_sensitive(false); + glyphs_vbox.set_sensitive(false); + kerning_vbox.set_sensitive(false); + } +} + +/* Add all fonts in the document to the combobox. */ +void SvgFontsDialog::update_fonts() +{ + SPDesktop* desktop = this->getDesktop(); + SPDocument* document = desktop->getDocument(); + std::vector<SPObject *> fonts = document->getResourceList( "font" ); + + _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); + const gchar* lbl = f->label(); + const gchar* id = f->getId(); + row[_columns.label] = lbl ? lbl : (id ? id : "font"); + } + + 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) 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::on_font_selection_changed(){ + SPFont* spfont = this->get_selected_spfont(); + if (!spfont) return; + + SvgFont* svgfont = this->get_selected_svgfont(); + first_glyph.update(spfont); + second_glyph.update(spfont); + kerning_preview.set_svgfont(svgfont); + _font_da.set_svgfont(svgfont); + _font_da.redraw(); + + kerning_slider->set_range(0, spfont->horiz_adv_x); + kerning_slider->set_draw_value(false); + kerning_slider->set_value(0); + + update_global_settings_tab(); + populate_glyphs_box(); + populate_kerning_pairs_box(); + update_sensitiveness(); +} + +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; +} + +SPGlyph* SvgFontsDialog::get_selected_glyph() +{ + Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected(); + if(i) + return (*i)[_GlyphsListColumns.glyph_node]; + return nullptr; +} + +Gtk::VBox* SvgFontsDialog::global_settings_tab(){ + _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*) _("Horiz. Advance X"), _("Average amount of horizontal space each letter takes up."), SP_ATTR_HORIZ_ADV_X); + _horiz_origin_x_spin = new AttrSpin( this, (gchar*) _("Horiz. Origin X"), _("Average horizontal origin location for each letter."), SP_ATTR_HORIZ_ORIGIN_X); + _horiz_origin_y_spin = new AttrSpin( this, (gchar*) _("Horiz. Origin Y"), _("Average vertical origin location for each letter."), SP_ATTR_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."), SP_PROP_FONT_FAMILY); + _units_per_em_spin = new AttrSpin( this, (gchar*) _("Units per em"), _("Number of display units each letter takes up."), SP_ATTR_UNITS_PER_EM); + _ascent_spin = new AttrSpin( this, (gchar*) _("Ascent:"), _("Amount of space taken up by accenders like the tall line on the letter 'h'."), SP_ATTR_ASCENT); + _descent_spin = new AttrSpin( this, (gchar*) _("Descent:"), _("Amount of space taken up by decenders like the tail on the letter 'g'."), SP_ATTR_DESCENT); + _cap_height_spin = new AttrSpin( this, (gchar*) _("Cap Height:"), _("The height of a capital letter above the baseline like the letter 'H' or 'I'."), SP_ATTR_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'."), SP_ATTR_X_HEIGHT); + + //_descent_spin->set_range(-4096,0); + _font_label->set_use_markup(); + _font_face_label->set_use_markup(); + + global_vbox.set_border_width(2); + global_vbox.pack_start(*_font_label); + global_vbox.pack_start(*_horiz_adv_x_spin); + global_vbox.pack_start(*_horiz_origin_x_spin); + global_vbox.pack_start(*_horiz_origin_y_spin); + global_vbox.pack_start(*_font_face_label); + global_vbox.pack_start(*_familyname_entry); + global_vbox.pack_start(*_units_per_em_spin); + global_vbox.pack_start(*_ascent_spin); + global_vbox.pack_start(*_descent_spin); + global_vbox.pack_start(*_cap_height_spin); + global_vbox.pack_start(*_x_height_spin); + +/* global_vbox->add(*AttrCombo((gchar*) _("Style:"), SP_PROP_FONT_STYLE)); + global_vbox->add(*AttrCombo((gchar*) _("Variant:"), SP_PROP_FONT_VARIANT)); + global_vbox->add(*AttrCombo((gchar*) _("Weight:"), SP_PROP_FONT_WEIGHT)); +*/ + + return &global_vbox; +} + +void +SvgFontsDialog::populate_glyphs_box() +{ + if (!_GlyphsListStore) return; + _GlyphsListStore->clear(); + + SPFont* spfont = this->get_selected_spfont(); + _glyphs_observer.set(spfont); + + for (auto& node: spfont->children) { + if (SP_IS_GLYPH(&node)){ + Gtk::TreeModel::Row row = *(_GlyphsListStore->append()); + row[_GlyphsListColumns.glyph_node] = static_cast<SPGlyph*>(&node); + row[_GlyphsListColumns.glyph_name] = (static_cast<SPGlyph*>(&node))->glyph_name; + row[_GlyphsListColumns.unicode] = (static_cast<SPGlyph*>(&node))->unicode; + row[_GlyphsListColumns.advance] = (static_cast<SPGlyph*>(&node))->horiz_adv_x; + } + } +} + +void +SvgFontsDialog::populate_kerning_pairs_box() +{ + if (!_KerningPairsListStore) return; + _KerningPairsListStore->clear(); + + SPFont* spfont = this->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); + } + } +} + +SPGlyph *new_glyph(SPDocument* document, SPFont *font, const int count) +{ + g_return_val_if_fail(font != nullptr, NULL); + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + // create a new glyph + Inkscape::XML::Node *repr; + repr = xml_doc->createElement("svg:glyph"); + + std::ostringstream os; + os << _("glyph") << " " << count; + repr->setAttribute("glyph-name", os.str()); + + // Append the new glyph node to the current font + font->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + + // get corresponding object + SPGlyph *g = SP_GLYPH( document->getObjectByRepr(repr) ); + + g_assert(g != nullptr); + g_assert(SP_IS_GLYPH(g)); + + return g; +} + +void SvgFontsDialog::update_glyphs(){ + SPFont* font = get_selected_spfont(); + if (!font) return; + populate_glyphs_box(); + populate_kerning_pairs_box(); + first_glyph.update(font); + second_glyph.update(font); + get_selected_svgfont()->refresh(); + _font_da.redraw(); +} + +void SvgFontsDialog::add_glyph(){ + const int count = _GlyphsListStore->children().size(); + SPDocument* doc = this->getDesktop()->getDocument(); + /* SPGlyph* glyph =*/ new_glyph(doc, get_selected_spfont(), count+1); + + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Add glyph")); + + update_glyphs(); +} + +Geom::PathVector +SvgFontsDialog::flip_coordinate_system(Geom::PathVector pathv){ + double units_per_em = 1024; + for (auto& obj: get_selected_spfont()->children) { + if (SP_IS_FONTFACE(&obj)){ + //XML Tree being directly used here while it shouldn't be. + sp_repr_get_double(obj.getRepr(), "units-per-em", &units_per_em); + } + } + double baseline_offset = units_per_em - get_selected_spfont()->horiz_origin_y; + //This matrix flips y-axis and places the origin at baseline + Geom::Affine m(Geom::Coord(1),Geom::Coord(0),Geom::Coord(0),Geom::Coord(-1),Geom::Coord(0),Geom::Coord(baseline_offset)); + return pathv*m; +} + +void SvgFontsDialog::set_glyph_description_from_selected_path(){ + SPDesktop* desktop = this->getDesktop(); + if (!desktop) { + g_warning("SvgFontsDialog: No active desktop"); + return; + } + + Inkscape::MessageStack *msgStack = desktop->getMessageStack(); + SPDocument* doc = desktop->getDocument(); + Inkscape::Selection* sel = desktop->getSelection(); + if (sel->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 = sel->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")); + + //XML Tree being directly used here while it shouldn't be. + gchar *str = sp_svg_write_path (flip_coordinate_system(pathv)); + glyph->setAttribute("d", str); + g_free(str); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Set glyph curves")); + + update_glyphs(); +} + +void SvgFontsDialog::missing_glyph_description_from_selected_path(){ + SPDesktop* desktop = this->getDesktop(); + if (!desktop) { + g_warning("SvgFontsDialog: No active desktop"); + return; + } + + Inkscape::MessageStack *msgStack = desktop->getMessageStack(); + SPDocument* doc = desktop->getDocument(); + Inkscape::Selection* sel = desktop->getSelection(); + if (sel->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 = sel->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? + + Geom::PathVector pathv = sp_svg_read_pathv(node->attribute("d")); + + for (auto& obj: get_selected_spfont()->children) { + if (SP_IS_MISSING_GLYPH(&obj)){ + + //XML Tree being directly used here while it shouldn't be. + gchar *str = sp_svg_write_path (flip_coordinate_system(pathv)); + obj.setAttribute("d", str); + g_free(str); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Set glyph curves")); + } + } + + update_glyphs(); +} + +void SvgFontsDialog::reset_missing_glyph_description(){ + SPDesktop* desktop = this->getDesktop(); + if (!desktop) { + g_warning("SvgFontsDialog: No active desktop"); + return; + } + + SPDocument* doc = desktop->getDocument(); + 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(doc, SP_VERB_DIALOG_SVG_FONTS, _("Reset missing-glyph")); + } + } + + update_glyphs(); +} + +void SvgFontsDialog::glyph_name_edit(const Glib::ustring&, const Glib::ustring& str){ + Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected(); + if (!i) return; + + SPGlyph* glyph = (*i)[_GlyphsListColumns.glyph_node]; + //XML Tree being directly used here while it shouldn't be. + glyph->setAttribute("glyph-name", str); + + SPDocument* doc = this->getDesktop()->getDocument(); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Edit glyph name")); + + update_glyphs(); +} + +void SvgFontsDialog::glyph_unicode_edit(const Glib::ustring&, const Glib::ustring& str){ + Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected(); + if (!i) return; + + SPGlyph* glyph = (*i)[_GlyphsListColumns.glyph_node]; + //XML Tree being directly used here while it shouldn't be. + glyph->setAttribute("unicode", str); + + SPDocument* doc = this->getDesktop()->getDocument(); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Set glyph unicode")); + + update_glyphs(); +} + +void SvgFontsDialog::glyph_advance_edit(const Glib::ustring&, const Glib::ustring& str){ + Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected(); + if (!i) return; + + SPGlyph* glyph = (*i)[_GlyphsListColumns.glyph_node]; + //XML Tree being directly used here while it shouldn't be. + std::istringstream is(str); + double value; + // Check if input valid + if ((is >> value)) { + glyph->setAttribute("horiz-adv-x", str); + SPDocument* doc = this->getDesktop()->getDocument(); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Set glyph advance")); + + update_glyphs(); + } 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()); + SPDocument* doc = this->getDesktop()->getDocument(); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Remove font")); + + update_fonts(); +} + +void SvgFontsDialog::remove_selected_glyph(){ + if(!_GlyphsList.get_selection()) return; + + Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected(); + if(!i) return; + + SPGlyph* glyph = (*i)[_GlyphsListColumns.glyph_node]; + + //XML Tree being directly used here while it shouldn't be. + sp_repr_unparent(glyph->getRepr()); + + SPDocument* doc = this->getDesktop()->getDocument(); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Remove glyph")); + + update_glyphs(); +} + +void SvgFontsDialog::remove_selected_kerning_pair(){ + if(!_KerningPairsList.get_selection()) return; + + Gtk::TreeModel::iterator i = _KerningPairsList.get_selection()->get_selected(); + if(!i) return; + + SPGlyphKerning* pair = (*i)[_KerningPairsListColumns.spnode]; + + //XML Tree being directly used here while it shouldn't be. + sp_repr_unparent(pair->getRepr()); + + SPDocument* doc = this->getDesktop()->getDocument(); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Remove kerning pair")); + + update_glyphs(); +} + +Gtk::VBox* SvgFontsDialog::glyphs_tab(){ + _GlyphsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::glyphs_list_button_release)); + create_glyphs_popup_menu(_GlyphsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_glyph)); + + Gtk::HBox* missing_glyph_hbox = Gtk::manage(new Gtk::HBox(false, 4)); + Gtk::Label* missing_glyph_label = Gtk::manage(new Gtk::Label(_("Missing Glyph:"))); + missing_glyph_hbox->set_hexpand(false); + missing_glyph_hbox->pack_start(*missing_glyph_label, false,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.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.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::reset_missing_glyph_description)); + + glyphs_vbox.set_border_width(4); + glyphs_vbox.set_spacing(4); + glyphs_vbox.pack_start(*missing_glyph_hbox, false,false); + + glyphs_vbox.add(_GlyphsListScroller); + _GlyphsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _GlyphsListScroller.set_size_request(-1, 290); + _GlyphsListScroller.add(_GlyphsList); + _GlyphsListStore = Gtk::ListStore::create(_GlyphsListColumns); + _GlyphsList.set_model(_GlyphsListStore); + _GlyphsList.append_column_editable(_("Glyph name"), _GlyphsListColumns.glyph_name); + _GlyphsList.append_column_editable(_("Matching string"), _GlyphsListColumns.unicode); + _GlyphsList.append_column_numeric_editable(_("Advance"), _GlyphsListColumns.advance, "%.2f"); + Gtk::HBox* hb = Gtk::manage(new Gtk::HBox(false, 4)); + add_glyph_button.set_label(_("Add Glyph")); + add_glyph_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_glyph)); + + hb->pack_start(add_glyph_button, false,false); + hb->pack_start(glyph_from_path_button, false,false); + + glyphs_vbox.pack_start(*hb, false, false); + glyph_from_path_button.set_label(_("Get curves from selection...")); + glyph_from_path_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::set_glyph_description_from_selected_path)); + + dynamic_cast<Gtk::CellRendererText*>( _GlyphsList.get_column_cell_renderer(0))->signal_edited().connect( + sigc::mem_fun(*this, &SvgFontsDialog::glyph_name_edit)); + + dynamic_cast<Gtk::CellRendererText*>( _GlyphsList.get_column_cell_renderer(1))->signal_edited().connect( + sigc::mem_fun(*this, &SvgFontsDialog::glyph_unicode_edit)); + + dynamic_cast<Gtk::CellRendererText*>( _GlyphsList.get_column_cell_renderer(2))->signal_edited().connect( + sigc::mem_fun(*this, &SvgFontsDialog::glyph_advance_edit)); + + _glyphs_observer.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::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 + + SPDocument* document = this->getDesktop()->getDocument(); + Inkscape::XML::Document *xml_doc = document->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( document->getObjectByRepr(repr) ); + + DocumentUndo::done(document, SP_VERB_DIALOG_SVG_FONTS, _("Add kerning pair")); +} + +Gtk::VBox* 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::HBox* kerning_selector = Gtk::manage(new Gtk::HBox()); + kerning_selector->pack_start(*Gtk::manage(new Gtk::Label(_("1st Glyph:"))), false, false); + kerning_selector->pack_start(first_glyph, true, true, 4); + kerning_selector->pack_start(*Gtk::manage(new Gtk::Label(_("2nd Glyph:"))), false, false); + kerning_selector->pack_start(second_glyph, true, true, 4); + kerning_selector->pack_start(add_kernpair_button, true, true); + 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); + _KerningPairsListStore = Gtk::ListStore::create(_KerningPairsListColumns); + _KerningPairsList.set_model(_KerningPairsListStore); + _KerningPairsList.append_column(_("First Unicode range"), _KerningPairsListColumns.first_glyph); + _KerningPairsList.append_column(_("Second Unicode range"), _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::HBox* kerning_amount_hbox = Gtk::manage(new Gtk::HBox(false, 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(300 + 20, 150 + 20); + _font_da.set_size(300 + 50 + 20, 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 1024 units + repr->setAttribute("horiz-adv-x", "1024"); + + // Append the new font node to defs + defs->getRepr()->appendChild(repr); + + //create a missing glyph + Inkscape::XML::Node *fontface; + fontface = xml_doc->createElement("svg:font-face"); + fontface->setAttribute("units-per-em", "1024"); + repr->appendChild(fontface); + + //create a missing glyph + Inkscape::XML::Node *mg; + mg = xml_doc->createElement("svg:missing-glyph"); + mg->setAttribute("d", "M0,0h1000v1024h-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, SP_VERB_DIALOG_SVG_FONTS, _("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(); +// select_font(font); + + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Add font")); +} + +SvgFontsDialog::SvgFontsDialog() + : UI::Widget::Panel("/dialogs/svgfonts", SP_VERB_DIALOG_SVG_FONTS), + _add(_("_New"), true) +{ + kerning_slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL)); + _add.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_font)); + + Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox()); + Gtk::VBox* vbox = Gtk::manage(new Gtk::VBox()); + + vbox->pack_start(_FontsList); + vbox->pack_start(_add, false, false); + hbox->add(*vbox); + hbox->add(_font_settings); + _getContents()->add(*hbox); + +//List of SVGFonts declared in a document: + _model = Gtk::ListStore::create(_columns); + _FontsList.set_model(_model); + _FontsList.append_column_editable(_("_Fonts"), _columns.label); + _FontsList.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_font_selection_changed)); + + this->update_fonts(); + + 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); + + _font_settings.add(*tabs); + +//Text Preview: + _preview_entry.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_preview_text_changed)); + _getContents()->pack_start((Gtk::Widget&) _font_da, false, false); + _preview_entry.set_text(_("Sample Text")); + _font_da.set_text(_("Sample Text")); + + Gtk::HBox* preview_entry_hbox = Gtk::manage(new Gtk::HBox(false, 4)); + _getContents()->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); + + _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)); + + _defs_observer.set(this->getDesktop()->getDocument()->getDefs()); + _defs_observer.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::update_fonts)); + + _getContents()->show_all(); +} + +SvgFontsDialog::~SvgFontsDialog()= default; + +} // 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..ae07d7f --- /dev/null +++ b/src/ui/dialog/svg-fonts-dialog.h @@ -0,0 +1,283 @@ +// 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 "ui/widget/panel.h" +#include <2geom/pathvector.h> +#include "ui/widget/spinbutton.h" + +#include <gtkmm/box.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/drawingarea.h> +#include <gtkmm/entry.h> +#include <gtkmm/liststore.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/treeview.h> + +#include "attributes.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*); +}; + +class SvgFontsDialog : public UI::Widget::Panel { +public: + SvgFontsDialog(); + ~SvgFontsDialog() override; + + static SvgFontsDialog &getInstance() { return *new SvgFontsDialog(); } + + void update_fonts(); + 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(); + Geom::PathVector flip_coordinate_system(Geom::PathVector pathv); + bool updating; + + // Used for font-family + class AttrEntry : public Gtk::HBox + { + public: + AttrEntry(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttributeEnum attr); + void set_text(char*); + private: + SvgFontsDialog* dialog; + void on_attr_changed(); + Gtk::Entry entry; + SPAttributeEnum attr; + }; + + class AttrSpin : public Gtk::HBox + { + public: + AttrSpin(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttributeEnum attr); + void set_value(double v); + void set_range(double low, double high); + Inkscape::UI::Widget::SpinButton* getSpin() { return &spin; } + private: + SvgFontsDialog* dialog; + void on_attr_changed(); + Inkscape::UI::Widget::SpinButton spin; + SPAttributeEnum attr; + }; + +private: + void update_glyphs(); + 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 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); + + Inkscape::XML::SignalObserver _defs_observer; //in order to update fonts + Inkscape::XML::SignalObserver _glyphs_observer; + + Gtk::HBox* AttrCombo(gchar* lbl, const SPAttributeEnum attr); +// Gtk::HBox* AttrSpin(gchar* lbl, const SPAttributeEnum attr); + Gtk::VBox* 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::VBox* kerning_tab(); + Gtk::VBox* glyphs_tab(); + Gtk::Button _add; + Gtk::Button add_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; + + class GlyphsColumns : public Gtk::TreeModel::ColumnRecord + { + public: + GlyphsColumns() + { + add(glyph_node); + add(glyph_name); + add(unicode); + add(advance); + } + + Gtk::TreeModelColumn<SPGlyph*> glyph_node; + Gtk::TreeModelColumn<Glib::ustring> glyph_name; + Gtk::TreeModelColumn<Glib::ustring> unicode; + Gtk::TreeModelColumn<double> advance; + }; + GlyphsColumns _GlyphsListColumns; + Glib::RefPtr<Gtk::ListStore> _GlyphsListStore; + Gtk::TreeView _GlyphsList; + Gtk::ScrolledWindow _GlyphsListScroller; + + 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::VBox _font_settings; + Gtk::VBox global_vbox; + Gtk::VBox glyphs_vbox; + Gtk::VBox kerning_vbox; + Gtk::Entry _preview_entry; + + 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::HBox + { + public: + EntryWidget() + { + 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..0e83ab8 --- /dev/null +++ b/src/ui/dialog/svg-preview.cpp @@ -0,0 +1,476 @@ +// 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 { + + std::string 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() + : 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..8263e15 --- /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::VBox +{ +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..c79b660 --- /dev/null +++ b/src/ui/dialog/swatches.cpp @@ -0,0 +1,1418 @@ +// 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 <map> +#include <algorithm> +#include <iomanip> +#include <set> + +#include "swatches.h" +#include <gtkmm/radiomenuitem.h> + +#include <gtkmm/menu.h> +#include <gtkmm/checkmenuitem.h> +#include <gtkmm/radiomenuitem.h> +#include <gtkmm/separatormenuitem.h> +#include <gtkmm/menubutton.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.h" + +#include "desktop-style.h" +#include "document.h" +#include "document-undo.h" +#include "extension/db.h" +#include "inkscape.h" +#include "io/sys.h" +#include "io/resource.h" +#include "message-context.h" +#include "path-prefix.h" + +#include "ui/previewholder.h" +#include "widgets/desktop-widget.h" +#include "widgets/gradient-vector.h" +#include "display/cairo-utils.h" + +#include "object/sp-defs.h" +#include "object/sp-gradient-reference.h" + +#include "dialog-manager.h" +#include "verbs.h" +#include "gradient-chemistry.h" +#include "helper/action.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 + +void _loadPaletteFile( gchar const *filename, gboolean user=FALSE ); + +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 ) +{ + if ( gr ) { + bool shown = false; + if ( desktop && desktop->doc() ) { + 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->_dlg_mgr->showDialog("FillAndStroke"); + shown = true; + } + } + } + } + } + } + + if (!shown) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/dialogs/gradienteditor/showlegacy", false)) { + // Legacy gradient dialog + GtkWidget *dialog = sp_gradient_vector_editor_new( gr ); + gtk_widget_show( dialog ); + } else { + // Invoke the gradient tool + Inkscape::Verb *verb = Inkscape::Verb::get( SP_VERB_CONTEXT_GRADIENT ); + if ( verb ) { + SPAction *action = verb->get_action( Inkscape::ActionContext( ( Inkscape::UI::View::View * ) SP_ACTIVE_DESKTOP ) ); + if ( action ) { + sp_action_perform( action, nullptr ); + } + } + } + } + } +} + +static void 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, SP_VERB_CONTEXT_GRADIENT, + _("Add gradient stop")); + 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; + GtkWidget *wdgt = gtk_widget_get_ancestor(GTK_WIDGET(preview->gobj()), SP_TYPE_DESKTOP_WIDGET); + 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; +} + + +void _loadPaletteFile(Glib::ustring path, gboolean user/*=FALSE*/) +{ + Glib::ustring filename = Glib::path_get_basename(path); + 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); +#if ENABLE_MAGIC_COLORS + ColorItem::_wireMagicColors( onceMore ); +#endif // ENABLE_MAGIC_COLORS + } 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(); +} + + +/** + * Constructor + */ +SwatchesPanel::SwatchesPanel(gchar const* prefsPath) : + Inkscape::UI::Widget::Panel(prefsPath, SP_VERB_DIALOG_SWATCHES), + _menu(nullptr), + _holder(nullptr), + _clear(nullptr), + _remove(nullptr), + _currentIndex(0), + _currentDesktop(nullptr), + _currentDocument(nullptr) +{ + _holder = new PreviewHolder(); + + _build_menu(); + + auto menu_button = Gtk::manage(new Gtk::MenuButton()); + menu_button->set_halign(Gtk::ALIGN_END); + menu_button->set_relief(Gtk::RELIEF_NONE); + menu_button->set_image_from_icon_name("pan-start-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR); + menu_button->set_popup(*_menu); + + auto box = Gtk::manage(new Gtk::Box()); + + if (_prefs_path == "/dialogs/swatches") { + box->set_orientation(Gtk::ORIENTATION_VERTICAL); + box->pack_start(*menu_button, Gtk::PACK_SHRINK); + } else { + box->set_orientation(Gtk::ORIENTATION_HORIZONTAL); + box->pack_end(*menu_button, Gtk::PACK_SHRINK); + _updateSettings(SWATCHES_SETTINGS_MODE, 1); + _holder->setOrientation(SP_ANCHOR_SOUTH); + } + + box->pack_start(*_holder, Gtk::PACK_EXPAND_WIDGET); + _getContents()->pack_start(*box); + + load_palettes(); + + Gtk::RadioMenuItem* hotItem = nullptr; + _clear = new ColorItem( ege::PaintDef::CLEAR ); + _remove = new ColorItem( ege::PaintDef::NONE ); + + if (docPalettes.empty()) { + SwatchPage *docPalette = new SwatchPage(); + + docPalette->_name = "Auto"; + 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 == "Auto") { + first = docPalettes[nullptr]; + } else { + std::vector<SwatchPage*> pages = _getSwatchSets(); + for (auto & page : pages) { + if ( page->_name == targetName ) { + first = page; + break; + } + index++; + } + } + } + } + + if ( !first ) { + first = docPalettes[nullptr]; + _currentIndex = 0; + } else { + _currentIndex = index; + } + + _rebuild(); + + Gtk::RadioMenuItem::Group groupOne; + + int i = 0; + std::vector<SwatchPage*> swatchSets = _getSwatchSets(); + for (auto curr : swatchSets) { + Gtk::RadioMenuItem* single = Gtk::manage(new Gtk::RadioMenuItem(groupOne, curr->_name)); + if ( curr == first ) { + hotItem = single; + } + _regItem(single, i); + + i++; + } + } + + if ( hotItem ) { + hotItem->set_active(); + } + + show_all_children(); +} + +SwatchesPanel::~SwatchesPanel() +{ + _trackDocument( this, nullptr ); + + _documentConnection.disconnect(); + _selChanged.disconnect(); + + if ( _clear ) { + delete _clear; + } + if ( _remove ) { + delete _remove; + } + if ( _holder ) { + delete _holder; + } + + delete _menu; +} + +void SwatchesPanel::_build_menu() +{ + guint panel_size = 0, panel_mode = 0, panel_ratio = 100, panel_border = 0; + bool panel_wrap = false; + if (!_prefs_path.empty()) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + panel_wrap = prefs->getBool(_prefs_path + "/panel_wrap"); + panel_size = prefs->getIntLimited(_prefs_path + "/panel_size", 1, 0, UI::Widget::PREVIEW_SIZE_HUGE); + panel_mode = prefs->getIntLimited(_prefs_path + "/panel_mode", 1, 0, 10); + panel_ratio = prefs->getIntLimited(_prefs_path + "/panel_ratio", 100, 0, 500 ); + panel_border = prefs->getIntLimited(_prefs_path + "/panel_border", UI::Widget::BORDER_NONE, 0, 2 ); + } + + _menu = new Gtk::Menu(); + + if (_prefs_path == "/dialogs/swatches") { + Gtk::RadioMenuItem::Group group; + Glib::ustring list_label(_("List")); + Glib::ustring grid_label(_("Grid")); + Gtk::RadioMenuItem *list_item = Gtk::manage(new Gtk::RadioMenuItem(group, list_label)); + Gtk::RadioMenuItem *grid_item = Gtk::manage(new Gtk::RadioMenuItem(group, grid_label)); + + if (panel_mode == 0) { + list_item->set_active(true); + } else if (panel_mode == 1) { + grid_item->set_active(true); + } + + _menu->append(*list_item); + _menu->append(*grid_item); + _menu->append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + list_item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_MODE, 0)); + grid_item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_MODE, 1)); + } + + { + Glib::ustring heightItemLabel(C_("Swatches", "Size")); + + //TRANSLATORS: Indicates size of colour swatches + const gchar *heightLabels[] = { + NC_("Swatches height", "Tiny"), + NC_("Swatches height", "Small"), + NC_("Swatches height", "Medium"), + NC_("Swatches height", "Large"), + NC_("Swatches height", "Huge") + }; + + Gtk::MenuItem *sizeItem = Gtk::manage(new Gtk::MenuItem(heightItemLabel)); + Gtk::Menu *sizeMenu = Gtk::manage(new Gtk::Menu()); + sizeItem->set_submenu(*sizeMenu); + + Gtk::RadioMenuItem::Group heightGroup; + for (unsigned int i = 0; i < G_N_ELEMENTS(heightLabels); i++) { + Glib::ustring _label(g_dpgettext2(nullptr, "Swatches height", heightLabels[i])); + Gtk::RadioMenuItem* _item = Gtk::manage(new Gtk::RadioMenuItem(heightGroup, _label)); + sizeMenu->append(*_item); + if (i == panel_size) { + _item->set_active(true); + } + _item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_SIZE, i)); + } + + _menu->append(*sizeItem); + } + + { + Glib::ustring widthItemLabel(C_("Swatches", "Width")); + + //TRANSLATORS: Indicates width of colour swatches + const gchar *widthLabels[] = { + NC_("Swatches width", "Narrower"), + NC_("Swatches width", "Narrow"), + NC_("Swatches width", "Medium"), + NC_("Swatches width", "Wide"), + NC_("Swatches width", "Wider") + }; + + Gtk::MenuItem *item = Gtk::manage( new Gtk::MenuItem(widthItemLabel)); + Gtk::Menu *type_menu = Gtk::manage(new Gtk::Menu()); + item->set_submenu(*type_menu); + _menu->append(*item); + + Gtk::RadioMenuItem::Group widthGroup; + + guint values[] = {0, 25, 50, 100, 200, 400}; + guint hot_index = 3; + for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) { + // Assume all values are in increasing order + if ( values[i] <= panel_ratio ) { + hot_index = i; + } + } + for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) { + Glib::ustring _label(g_dpgettext2(nullptr, "Swatches width", widthLabels[i])); + Gtk::RadioMenuItem *_item = Gtk::manage(new Gtk::RadioMenuItem(widthGroup, _label)); + type_menu->append(*_item); + if ( i <= hot_index ) { + _item->set_active(true); + } + _item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_SHAPE, values[i])); + } + } + + { + Glib::ustring widthItemLabel(C_("Swatches", "Border")); + + //TRANSLATORS: Indicates border of colour swatches + const gchar *widthLabels[] = { + NC_("Swatches border", "None"), + NC_("Swatches border", "Solid"), + NC_("Swatches border", "Wide"), + }; + + Gtk::MenuItem *item = Gtk::manage( new Gtk::MenuItem(widthItemLabel)); + Gtk::Menu *type_menu = Gtk::manage(new Gtk::Menu()); + item->set_submenu(*type_menu); + _menu->append(*item); + + Gtk::RadioMenuItem::Group widthGroup; + + guint values[] = {0, 1, 2}; + guint hot_index = 0; + for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) { + // Assume all values are in increasing order + if ( values[i] <= panel_border ) { + hot_index = i; + } + } + for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) { + Glib::ustring _label(g_dpgettext2(nullptr, "Swatches border", widthLabels[i])); + Gtk::RadioMenuItem *_item = Gtk::manage(new Gtk::RadioMenuItem(widthGroup, _label)); + type_menu->append(*_item); + if ( i <= hot_index ) { + _item->set_active(true); + } + _item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_BORDER, values[i])); + } + } + + if (_prefs_path == "/embedded/swatches") { + //TRANSLATORS: "Wrap" indicates how colour swatches are displayed + Glib::ustring wrap_label(C_("Swatches","Wrap")); + Gtk::CheckMenuItem *check = Gtk::manage(new Gtk::CheckMenuItem(wrap_label)); + check->set_active(panel_wrap); + _menu->append(*check); + + check->signal_toggled().connect(sigc::bind<Gtk::CheckMenuItem*>(sigc::mem_fun(*this, &SwatchesPanel::_wrapToggled), check)); + } + + _menu->append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _menu->show_all(); + + _updateSettings(SWATCHES_SETTINGS_SIZE, panel_size); + _updateSettings(SWATCHES_SETTINGS_MODE, panel_mode); + _updateSettings(SWATCHES_SETTINGS_SHAPE, panel_ratio); + _updateSettings(SWATCHES_SETTINGS_WRAP, panel_wrap); + _updateSettings(SWATCHES_SETTINGS_BORDER, panel_border); +} + +void SwatchesPanel::setDesktop( SPDesktop* desktop ) +{ + if ( desktop != _currentDesktop ) { + if ( _currentDesktop ) { + _documentConnection.disconnect(); + _selChanged.disconnect(); + } + + _currentDesktop = desktop; + + if ( desktop ) { + _currentDesktop->selection->connectChanged( + sigc::hide(sigc::mem_fun(*this, &SwatchesPanel::_updateFromSelection))); + + _currentDesktop->selection->connectModified( + sigc::hide(sigc::hide(sigc::mem_fun(*this, &SwatchesPanel::_updateFromSelection)))); + + _currentDesktop->connectToolSubselectionChanged( + sigc::hide(sigc::mem_fun(*this, &SwatchesPanel::_updateFromSelection))); + + sigc::bound_mem_functor1<void, SwatchesPanel, SPDocument*> first = sigc::mem_fun(*this, &SwatchesPanel::_setDocument); + sigc::slot<void, SPDocument*> base2 = first; + sigc::slot<void,SPDesktop*, SPDocument*> slot2 = sigc::hide<0>( base2 ); + _documentConnection = desktop->connectDocumentReplaced( slot2 ); + + _setDocument( desktop->doc() ); + } else { + _setDocument(nullptr); + } + } +} + + +void SwatchesPanel::_updateSettings(int settings, int value) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + switch (settings) { + case SWATCHES_SETTINGS_SIZE: { + prefs->setInt(_prefs_path + "/panel_size", value); + + auto curr_type = _holder->getPreviewType(); + guint curr_ratio = _holder->getPreviewRatio(); + auto curr_border = _holder->getPreviewBorder(); + + switch (value) { + case 0: + _holder->setStyle(UI::Widget::PREVIEW_SIZE_TINY, curr_type, curr_ratio, curr_border); + break; + case 1: + _holder->setStyle(UI::Widget::PREVIEW_SIZE_SMALL, curr_type, curr_ratio, curr_border); + break; + case 2: + _holder->setStyle(UI::Widget::PREVIEW_SIZE_MEDIUM, curr_type, curr_ratio, curr_border); + break; + case 3: + _holder->setStyle(UI::Widget::PREVIEW_SIZE_BIG, curr_type, curr_ratio, curr_border); + break; + case 4: + _holder->setStyle(UI::Widget::PREVIEW_SIZE_HUGE, curr_type, curr_ratio, curr_border); + break; + default: + break; + } + + break; + } + case SWATCHES_SETTINGS_MODE: { + prefs->setInt(_prefs_path + "/panel_mode", value); + + auto curr_size = _holder->getPreviewSize(); + guint curr_ratio = _holder->getPreviewRatio(); + auto curr_border = _holder->getPreviewBorder(); + switch (value) { + case 0: + _holder->setStyle(curr_size, UI::Widget::VIEW_TYPE_LIST, curr_ratio, curr_border); + break; + case 1: + _holder->setStyle(curr_size, UI::Widget::VIEW_TYPE_GRID, curr_ratio, curr_border); + break; + default: + break; + } + break; + } + case SWATCHES_SETTINGS_SHAPE: { + prefs->setInt(_prefs_path + "/panel_ratio", value); + + auto curr_type = _holder->getPreviewType(); + auto curr_size = _holder->getPreviewSize(); + auto curr_border = _holder->getPreviewBorder(); + + _holder->setStyle(curr_size, curr_type, value, curr_border); + break; + } + case SWATCHES_SETTINGS_BORDER: { + prefs->setInt(_prefs_path + "/panel_border", value); + + auto curr_size = _holder->getPreviewSize(); + auto curr_type = _holder->getPreviewType(); + guint curr_ratio = _holder->getPreviewRatio(); + + switch (value) { + case 0: + _holder->setStyle(curr_size, curr_type, curr_ratio, UI::Widget::BORDER_NONE); + break; + case 1: + _holder->setStyle(curr_size, curr_type, curr_ratio, UI::Widget::BORDER_SOLID); + break; + case 2: + _holder->setStyle(curr_size, curr_type, curr_ratio, UI::Widget::BORDER_WIDE); + break; + default: + break; + } + break; + } + case SWATCHES_SETTINGS_WRAP: { + prefs->setBool(_prefs_path + "/panel_wrap", value); + _holder->setWrap(value); + break; + } + case SWATCHES_SETTINGS_PALETTE: { + std::vector<SwatchPage*> pages = _getSwatchSets(); + if (value >= 0 && value < static_cast<int>(pages.size()) ) { + _currentIndex = value; + + prefs->setString(_prefs_path + "/palette", pages[_currentIndex]->_name); + + _rebuild(); + } + } + default: + break; + } +} + +void SwatchesPanel::_wrapToggled(Gtk::CheckMenuItem* toggler) +{ + if (toggler) { + _updateSettings(SWATCHES_SETTINGS_WRAP, toggler->get_active() ? 1 : 0); + } +} + +void SwatchesPanel::_regItem(Gtk::MenuItem* item, int id) +{ + _menu->append(*item); + item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_PALETTE, id)); + item->show(); +} + + +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(); + doc->doUnref(); + doc = nullptr; + } + } + + 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; + + 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); + } + } + + return true; +} + +bool DocTrack::queueUpdateIfNeeded( SPDocument *doc ) +{ + bool deferProcessing = false; + for (auto track : docTrackings) { + if ( track->doc == 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 == 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; + } + } + } + } +} + +void SwatchesPanel::_setDocument( SPDocument *document ) +{ + if ( document != _currentDocument ) { + _trackDocument(this, document); + _currentDocument = document; + 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); + + // Figure out which SwatchesPanel instances are affected and update them. + + 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); + } + } +} + + +std::vector<SwatchPage*> SwatchesPanel::_getSwatchSets() const +{ + std::vector<SwatchPage*> tmp; + if (docPalettes.find(_currentDocument) != docPalettes.end()) { + tmp.push_back(docPalettes[_currentDocument]); + } + + tmp.insert(tmp.end(), userSwatchPages.begin(), userSwatchPages.end()); + tmp.insert(tmp.end(), systemSwatchPages.begin(), systemSwatchPages.end()); + + return tmp; +} + +void SwatchesPanel::_updateFromSelection() +{ + SwatchPage *docPalette = (docPalettes.find(_currentDocument) != docPalettes.end()) ? docPalettes[_currentDocument] : nullptr; + if ( docPalette ) { + Glib::ustring fillId; + Glib::ustring strokeId; + + SPStyle tmpStyle(_currentDesktop->getDocument()); + int result = sp_desktop_query_style( _currentDesktop, &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( _currentDesktop, &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]; + _holder->clear(); + + if ( curr->_prefWidth > 0 ) { + _holder->setColumnPref( curr->_prefWidth ); + } + _holder->freezeUpdates(); + // TODO restore once 'clear' works _holder->addPreview(_clear); + _holder->addPreview(_remove); + for (auto & _color : curr->_colors) { + _holder->addPreview(&_color); + } + _holder->thawUpdates(); +} + +} //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..da10911 --- /dev/null +++ b/src/ui/dialog/swatches.h @@ -0,0 +1,110 @@ +// 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/widget/panel.h" + +namespace Gtk { + class Menu; + class MenuItem; + class CheckMenuItem; +} + +namespace Inkscape { +namespace UI { + +class PreviewHolder; + +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 Inkscape::UI::Widget::Panel +{ +public: + SwatchesPanel(gchar const* prefsPath = "/dialogs/swatches"); + ~SwatchesPanel() override; + + static SwatchesPanel& getInstance(); + + void setDesktop( SPDesktop* desktop ) override; + virtual SPDesktop* getDesktop() {return _currentDesktop;} + + virtual int getSelectedIndex() {return _currentIndex;} // temporary + +protected: + static void handleGradientsChange(SPDocument *document); + + virtual void _updateFromSelection(); + virtual void _setDocument( 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(); + + static void _trackDocument( SwatchesPanel *panel, SPDocument *document ); + static void handleDefsModified(SPDocument *document); + + PreviewHolder* _holder; + ColorItem* _clear; + ColorItem* _remove; + int _currentIndex; + SPDesktop* _currentDesktop; + SPDocument* _currentDocument; + + void _regItem(Gtk::MenuItem* item, int id); + + void _updateSettings(int settings, int value); + + void _wrapToggled(Gtk::CheckMenuItem *toggler); + + Gtk::Menu *_menu; + + sigc::connection _documentConnection; + sigc::connection _selChanged; + + 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..b03bd5d --- /dev/null +++ b/src/ui/dialog/symbols.cpp @@ -0,0 +1,1403 @@ +// 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 <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 "desktop.h" +#include "document.h" +#include "inkscape.h" +#include "path-prefix.h" +#include "selection.h" +#include "symbols.h" +#include "verbs.h" + +#include "display/cairo-utils.h" +#include "helper/action.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" + +#ifdef WITH_LIBVISIO + #include <libvisio/libvisio.h> + + // TODO: Drop this check when librevenge is widespread. + #if WITH_LIBVISIO01 + #include <librevenge-stream/librevenge-stream.h> + + using librevenge::RVNGFileStream; + using librevenge::RVNGString; + using librevenge::RVNGStringVector; + using librevenge::RVNGPropertyList; + using librevenge::RVNGSVGDrawingGenerator; + #else + #include <libwpd-stream/libwpd-stream.h> + + typedef WPXFileStream RVNGFileStream; + typedef libvisio::VSDStringVector RVNGStringVector; + #endif +#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 ) : + UI::Widget::Panel(prefsPath, SP_VERB_DIALOG_SYMBOLS), + store(Gtk::ListStore::create(*getColumns())), + all_docs_processed(false), + icon_view(nullptr), + current_desktop(nullptr), + desk_track(), + current_document(nullptr), + preview_document(nullptr), + instanceConns(), + 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 + _getContents()->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 Gtk::ComboBoxText(); // Fill in later + symbol_set->append(CURRENTDOC); + symbol_set->append(ALLDOCS); + symbol_set->set_active_text(CURRENTDOC); + symbol_set->set_hexpand(); + + table->attach(*Gtk::manage(symbol_set),1,row,1,1); + sigc::connection connSet = symbol_set->signal_changed().connect( + sigc::mem_fun(*this, &SymbolsDialog::rebuild)); + instanceConns.push_back(connSet); + + ++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(_("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 ); + + 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); + icon_view->signal_drag_data_get().connect( + sigc::mem_fun(*this, &SymbolsDialog::iconDragDataGet)); + + sigc::connection connIconChanged; + connIconChanged = icon_view->signal_selection_changed().connect( + sigc::mem_fun(*this, &SymbolsDialog::iconChanged)); + instanceConns.push_back(connIconChanged); + + scroller = new Gtk::ScrolledWindow(); + scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS); + scroller->add(*Gtk::manage(icon_view)); + scroller->set_hexpand(); + scroller->set_vexpand(); + + 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, 250); + 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"); + + // 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(45); + + 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(155); + + overlay_desc = new Gtk::Label(); + overlay_desc->set_halign(Gtk::ALIGN_CENTER); + overlay_desc->set_valign(Gtk::ALIGN_START); + overlay_desc->set_margin_top(180); + overlay_desc->set_justify(Gtk::JUSTIFY_CENTER); + + 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::HBox(); + 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::HBox(); + + //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; + + current_desktop = SP_ACTIVE_DESKTOP; + current_document = current_desktop->getDocument(); + 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 )); + + // This might need to be a global variable so setTargetDesktop can modify it + SPDefs *defs = current_document->getDefs(); + sigc::connection defsModifiedConn = defs->connectModified(sigc::mem_fun(*this, &SymbolsDialog::defsModified)); + instanceConns.push_back(defsModifiedConn); + + sigc::connection selectionChangedConn = current_desktop->selection->connectChanged( + sigc::mem_fun(*this, &SymbolsDialog::selectionChanged)); + instanceConns.push_back(selectionChangedConn); + + sigc::connection documentReplacedConn = current_desktop->connectDocumentReplaced( + sigc::mem_fun(*this, &SymbolsDialog::documentReplaced)); + instanceConns.push_back(documentReplacedConn); + getSymbolsTitle(); + icons_found = false; + + addSymbolsInDoc(current_document); /* Defaults to current document */ + sigc::connection desktopChangeConn = + desk_track.connectDesktopChanged( sigc::mem_fun(*this, &SymbolsDialog::setTargetDesktop) ); + instanceConns.push_back( desktopChangeConn ); + desk_track.connect(GTK_WIDGET(gobj())); +} + +SymbolsDialog::~SymbolsDialog() +{ + for (auto & instanceConn : instanceConns) { + instanceConn.disconnect(); + } + idleconn.disconnect(); + instanceConns.clear(); + desk_track.disconnect(); +} + +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(_("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 results 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 results 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 results 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() { + Inkscape::Verb *verb = Inkscape::Verb::get( SP_VERB_EDIT_SYMBOL ); + SPAction *action = verb->get_action(Inkscape::ActionContext( (Inkscape::UI::View::View *) current_desktop) ); + sp_action_perform (action, nullptr); +} + +void SymbolsDialog::revertSymbol() { + Inkscape::Verb *verb = Inkscape::Verb::get( SP_VERB_EDIT_UNSYMBOL ); + SPAction *action = verb->get_action(Inkscape::ActionContext( (Inkscape::UI::View::View *) current_desktop ) ); + sp_action_perform (action, nullptr); +} + +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(SPDesktop *desktop, SPDocument *document) +{ + current_desktop = desktop; + current_document = document; + 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 = current_document; + 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 ) { + if( symbol_document == current_document ) { + // Select the symbol on the canvas so it can be manipulated + current_desktop->selection->set( symbol, false ); + } + // 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 == current_document ) { + style = styleFromUse( symbol_id.c_str(), current_document ); + } else { + style = symbol_document->getReprRoot()->attribute("style"); + } + } + + ClipboardManager *cm = ClipboardManager::get(); + cm->copySymbol(symbol->getRepr(), style, symbol_document == current_document); + } + } +} + +#ifdef WITH_LIBVISIO + +#if WITH_LIBVISIO01 +// 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; +}; +#endif + +// 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; +#if WITH_LIBVISIO01 + RVNGSVGDrawingGenerator_WithTitle generator(output, titles, "svg"); + + if (!libvisio::VisioDocument::parseStencils(&input, &generator)) { +#else + if (!libvisio::VisioDocument::generateSVGStencils(&input, output)) { +#endif + 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 WITH_LIBVISIO01 + if (titles.size() == output.size() && titles[i] != "") { + tmpSVGOutput += " <title>" + Glib::ustring(RVNGString::escapeXML(titles[i].cstr()).cstr()) + "</title>\n"; + } +#endif + + 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); + if (pos != 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); + if (pos != 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); + } + + // 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 == current_document ) { + gchar const *id = symbol->getRepr()->attribute("id"); + style = styleFromUse( id, symbol->document ); + } else { + style = symbol->document->getReprRoot()->attribute("style"); + } + } + // Last ditch effort to provide some default styling + if( !style ) style = "fill:#bbbbbb;stroke:#808080"; + + // This is for display in Symbols dialog only + if( style ) repr->setAttribute( "style", style ); + + // BUG: Symbols don't work if defined outside of <defs>. Causes Inkscape + // crash when trying to read in such a file. + root->appendChild(repr); + //defsrepr->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" ); + preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + preview_document->ensureUpToDate(); + + 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); +} + +void SymbolsDialog::setTargetDesktop(SPDesktop *desktop) +{ + if (this->current_desktop != desktop) { + this->current_desktop = desktop; + if( !symbol_sets[symbol_set->get_active_text()] ) { + // Symbol set is from Current document, update + rebuild(); + } + } +} + +} //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..382d310 --- /dev/null +++ b/src/ui/dialog/symbols.h @@ -0,0 +1,183 @@ +// 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 <vector> + +#include <gtkmm.h> + +#include "display/drawing.h" +#include "include/gtkmm_version.h" +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/panel.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 UI::Widget::Panel { + +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(); + + 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); + void selectionChanged(Inkscape::Selection *selection); + void documentReplaced(SPDesktop *desktop, SPDocument *document); + 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::HBox* 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::HBox* 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; + void setTargetDesktop(SPDesktop *desktop); + SPDesktop* current_desktop; + DesktopTracker desk_track; + SPDocument* current_document; + 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> instanceConns; +}; + +} //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/tags.cpp b/src/ui/dialog/tags.cpp new file mode 100644 index 0000000..35400ab --- /dev/null +++ b/src/ui/dialog/tags.cpp @@ -0,0 +1,1125 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple panel for tags + * + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tags.h" +#include <gtkmm/icontheme.h> +#include <gtkmm/imagemenuitem.h> +#include <glibmm/main.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "layer-fns.h" +#include "layer-manager.h" +#include "verbs.h" + +#include "helper/action.h" +#include "include/gtkmm_version.h" +#include "object/sp-defs.h" +#include "object/sp-item.h" +#include "object/sp-object-group.h" +#include "svg/css-ostringstream.h" +#include "ui/icon-loader.h" +#include "ui/tools/tool-base.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/iconrenderer.h" +#include "ui/widget/layertypeicon.h" +#include "xml/node-observer.h" + +//#define DUMP_LAYERS 1 + +namespace Inkscape { +namespace UI { +namespace Dialog { + +using Inkscape::XML::Node; + +TagsPanel& TagsPanel::getInstance() +{ + return *new TagsPanel(); +} + +enum { + COL_ADD = 1 +}; + +enum { + BUTTON_NEW = 0, + BUTTON_TOP, + BUTTON_BOTTOM, + BUTTON_UP, + BUTTON_DOWN, + BUTTON_DELETE, + DRAGNDROP +}; + +class TagsPanel::ObjectWatcher : public Inkscape::XML::NodeObserver { +public: + ObjectWatcher(TagsPanel* pnl, SPObject* obj, Inkscape::XML::Node * repr) : + _pnl(pnl), + _obj(obj), + _repr(repr), + _labelAttr(g_quark_from_string("inkscape:label")) + {} + + ObjectWatcher(TagsPanel* pnl, SPObject* obj) : + _pnl(pnl), + _obj(obj), + _repr(obj->getRepr()), + _labelAttr(g_quark_from_string("inkscape:label")) + {} + + void notifyChildAdded( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChanged( _obj ); + } + } + void notifyChildRemoved( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChanged( _obj ); + } + } + void notifyChildOrderChanged( Node &/*node*/, Node &/*child*/, Node */*old_prev*/, Node */*new_prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChanged( _obj ); + } + } + void notifyContentChanged( Node &/*node*/, Util::ptr_shared /*old_content*/, Util::ptr_shared /*new_content*/ ) override {} + void notifyAttributeChanged( Node &/*node*/, GQuark name, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ ) override { + + static GQuark const _labelID = g_quark_from_string("id"); + + if ( _pnl && _obj ) { + if ( name == _labelAttr || name == _labelID ) { + _pnl->_updateObject( _obj); + } + } + } + + TagsPanel* _pnl; + SPObject* _obj; + Inkscape::XML::Node* _repr; + GQuark _labelAttr; +}; + +class TagsPanel::InternalUIBounce +{ +public: + int _actionCode; +}; + +void TagsPanel::_styleButton(Gtk::Button& btn, char const* iconName, char const* tooltip) +{ + 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); +} + + +Gtk::MenuItem& TagsPanel::_addPopupItem( SPDesktop *desktop, unsigned int code, char const* iconName, char const* fallback, int id ) +{ + GtkWidget* iconWidget = nullptr; + const char* label = nullptr; + + if ( iconName ) { + iconWidget = sp_get_icon_image(iconName, GTK_ICON_SIZE_MENU); + } + + if ( desktop ) { + Verb *verb = Verb::get( code ); + if ( verb ) { + SPAction *action = verb->get_action(desktop); + if ( !iconWidget && action && action->image ) { + iconWidget = sp_get_icon_image(action->image, GTK_ICON_SIZE_MENU); + } + + if ( action ) { + // label = action->name; + } + } + } + + if ( !label && fallback ) { + label = fallback; + } + + Gtk::Widget* wrapped = nullptr; + if ( iconWidget ) { + wrapped = Gtk::manage(Glib::wrap(iconWidget)); + wrapped->show(); + } + + + Gtk::MenuItem* item = nullptr; + + if (wrapped) { + item = Gtk::manage(new Gtk::ImageMenuItem(*wrapped, label, true)); + } else { + item = Gtk::manage(new Gtk::MenuItem(label, true)); + } + + item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &TagsPanel::_takeAction), id)); + _popupMenu.append(*item); + + return *item; +} + +void TagsPanel::_fireAction( unsigned int code ) +{ + if ( _desktop ) { + Verb *verb = Verb::get( code ); + if ( verb ) { + SPAction *action = verb->get_action(_desktop); + if ( action ) { + sp_action_perform( action, nullptr ); + } + } + } +} + +void TagsPanel::_takeAction( int val ) +{ + if ( !_pending ) { + _pending = new InternalUIBounce(); + _pending->_actionCode = val; + Glib::signal_timeout().connect( sigc::mem_fun(*this, &TagsPanel::_executeAction), 0 ); + } +} + +bool TagsPanel::_executeAction() +{ + // Make sure selected layer hasn't changed since the action was triggered + if ( _pending) + { + int val = _pending->_actionCode; +// SPObject* target = _pending->_target; + bool empty = _desktop->selection->isEmpty(); + + switch ( val ) { + case BUTTON_NEW: + { + _fireAction( SP_VERB_TAG_NEW ); + } + break; + case BUTTON_TOP: + { + _fireAction( empty ? SP_VERB_LAYER_TO_TOP : SP_VERB_SELECTION_TO_FRONT); + } + break; + case BUTTON_BOTTOM: + { + _fireAction( empty ? SP_VERB_LAYER_TO_BOTTOM : SP_VERB_SELECTION_TO_BACK ); + } + break; + case BUTTON_UP: + { + _fireAction( empty ? SP_VERB_LAYER_RAISE : SP_VERB_SELECTION_RAISE ); + } + break; + case BUTTON_DOWN: + { + _fireAction( empty ? SP_VERB_LAYER_LOWER : SP_VERB_SELECTION_LOWER ); + } + break; + case BUTTON_DELETE: + { + std::vector<SPObject *> todelete; + _tree.get_selection()->selected_foreach_iter(sigc::bind<std::vector<SPObject *>*>(sigc::mem_fun(*this, &TagsPanel::_checkForDeleted), &todelete)); + for (auto obj : todelete) { + if (obj && obj->parent && obj->getRepr() && obj->parent->getRepr()) { + //obj->parent->getRepr()->removeChild(obj->getRepr()); + obj->deleteObject(true, true); + } + } + DocumentUndo::done(_document, SP_VERB_DIALOG_TAGS, _("Remove from selection set")); + } + break; + case DRAGNDROP: + { + _doTreeMove( ); + } + break; + } + + delete _pending; + _pending = nullptr; + } + + return false; +} + + +class TagsPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + + ModelColumns() + { + add(_colParentObject); + add(_colObject); + add(_colLabel); + add(_colAddRemove); + add(_colAllowAddRemove); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<SPObject*> _colParentObject; + Gtk::TreeModelColumn<SPObject*> _colObject; + Gtk::TreeModelColumn<Glib::ustring> _colLabel; + Gtk::TreeModelColumn<int> _colAddRemove; + Gtk::TreeModelColumn<bool> _colAllowAddRemove; +}; + +void TagsPanel::_checkForDeleted(const Gtk::TreeIter& iter, std::vector<SPObject *>* todelete) +{ + Gtk::TreeRow row = *iter; + SPObject * obj = row[_model->_colObject]; + if (obj && obj->parent) { + todelete->push_back(obj); + } +} + +void TagsPanel::_updateObject( SPObject *obj ) { + _store->foreach( sigc::bind<SPObject*>(sigc::mem_fun(*this, &TagsPanel::_checkForUpdated), obj) ); +} + +bool TagsPanel::_checkForUpdated(const Gtk::TreePath &/*path*/, const Gtk::TreeIter& iter, SPObject* obj) +{ + Gtk::TreeModel::Row row = *iter; + if ( obj == row[_model->_colObject] ) + { + /* + * We get notified of layer update here (from layer->setLabel()) before layer->label() is set + * with the correct value (sp-object bug?). So use the inkscape:label attribute instead which + * has the correct value (bug #168351) + */ + //row[_model->_colLabel] = layer->label() ? layer->label() : layer->getId(); + gchar const *label; + SPTagUse * use = SP_IS_TAG_USE(obj) ? SP_TAG_USE(obj) : nullptr; + if (use && use->ref->isAttached()) { + label = use->ref->getObject()->getAttribute("inkscape:label"); + if (!label || !label[0]) { + label = use->ref->getObject()->getId(); + } + } else { + label = obj->getAttribute("inkscape:label"); + } + row[_model->_colLabel] = label ? label : obj->getId(); + row[_model->_colAddRemove] = SP_IS_TAG(obj); + } + + return false; +} + +void TagsPanel::_objectsSelected( Selection *sel ) { + + _selectedConnection.block(); + _tree.get_selection()->unselect_all(); + auto tmp = sel->objects(); + for(auto i = tmp.begin(); i != tmp.end(); ++i) + { + SPObject *obj = *i; + _store->foreach(sigc::bind<SPObject *>( sigc::mem_fun(*this, &TagsPanel::_checkForSelected), obj)); + } + _selectedConnection.unblock(); + _checkTreeSelection(); +} + +bool TagsPanel::_checkForSelected(const Gtk::TreePath &/*path*/, const Gtk::TreeIter& iter, SPObject* obj) +{ + Gtk::TreeModel::Row row = *iter; + SPObject * it = row[_model->_colObject]; + if ( it && SP_IS_TAG_USE(it) && SP_TAG_USE(it)->ref->getObject() == obj ) + { + Glib::RefPtr<Gtk::TreeSelection> select = _tree.get_selection(); + + select->select(iter); + } + return false; +} + +// TODO does not look good that we're ignoring the passed in root. Investigate. +void TagsPanel::_objectsChanged(SPObject* root) +{ + while (!_objectWatchers.empty()) + { + TagsPanel::ObjectWatcher *w = _objectWatchers.back(); + w->_repr->removeObserver(*w); + _objectWatchers.pop_back(); + delete w; + } + + if (_desktop) { + SPDocument* document = _desktop->doc(); + SPDefs* root = document->getDefs(); + if ( root ) { + _selectedConnection.block(); + _store->clear(); + _addObject( document, root, nullptr ); + _selectedConnection.unblock(); + _objectsSelected(_desktop->selection); + _checkTreeSelection(); + } + } +} + +void TagsPanel::_addObject( SPDocument* doc, SPObject* obj, Gtk::TreeModel::Row* parentRow ) +{ + if ( _desktop && obj ) { + for (auto& child: obj->children) { + if (SP_IS_TAG(&child)) + { + Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend(); + Gtk::TreeModel::Row row = *iter; + row[_model->_colObject] = &child; + row[_model->_colParentObject] = NULL; + row[_model->_colLabel] = child.label() ? child.label() : child.getId(); + row[_model->_colAddRemove] = 1; + row[_model->_colAllowAddRemove] = true; + + _tree.expand_to_path( _store->get_path(iter) ); + + TagsPanel::ObjectWatcher *w = new TagsPanel::ObjectWatcher(this, &child); + child.getRepr()->addObserver(*w); + _objectWatchers.push_back(w); + _addObject( doc, &child, &row ); + } + } + if (SP_IS_TAG(obj) && obj->firstChild()) + { + Gtk::TreeModel::iterator iteritems = parentRow ? _store->append(parentRow->children()) : _store->prepend(); + Gtk::TreeModel::Row rowitems = *iteritems; + rowitems[_model->_colObject] = NULL; + rowitems[_model->_colParentObject] = obj; + rowitems[_model->_colLabel] = _("Items"); + rowitems[_model->_colAddRemove] = 0; + rowitems[_model->_colAllowAddRemove] = false; + + _tree.expand_to_path( _store->get_path(iteritems) ); + + for (auto& child: obj->children) { + if (SP_IS_TAG_USE(&child)) + { + SPItem *item = SP_TAG_USE(&child)->ref->getObject(); + Gtk::TreeModel::iterator iter = _store->prepend(rowitems->children()); + Gtk::TreeModel::Row row = *iter; + row[_model->_colObject] = &child; + row[_model->_colParentObject] = NULL; + row[_model->_colLabel] = item ? (item->label() ? item->label() : item->getId()) : SP_TAG_USE(&child)->href; + row[_model->_colAddRemove] = 0; + row[_model->_colAllowAddRemove] = true; + + if (SP_TAG(obj)->expanded()) { + _tree.expand_to_path( _store->get_path(iter) ); + } + + if (item) { + TagsPanel::ObjectWatcher *w = new TagsPanel::ObjectWatcher(this, &child, item->getRepr()); + item->getRepr()->addObserver(*w); + _objectWatchers.push_back(w); + } + } + } + } + } +} + +void TagsPanel::_select_tag( SPTag * tag ) +{ + for (auto& child: tag->children) { + if (SP_IS_TAG(&child)) { + _select_tag(SP_TAG(&child)); + } else if (SP_IS_TAG_USE(&child)) { + SPObject * obj = SP_TAG_USE(&child)->ref->getObject(); + if (obj) { + if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(obj->parent); + _desktop->selection->add(obj); + } + } + } +} + +void TagsPanel::_selected_row_callback( const Gtk::TreeModel::iterator& iter ) +{ + if (iter) { + Gtk::TreeModel::Row row = *iter; + SPObject *obj = row[_model->_colObject]; + if (obj) { + if (SP_IS_TAG(obj)) { + _select_tag(SP_TAG(obj)); + } else if (SP_IS_TAG_USE(obj)) { + SPObject * item = SP_TAG_USE(obj)->ref->getObject(); + if (item) { + if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(item->parent); + _desktop->selection->add(item); + } + } + } + } +} + +void TagsPanel::_pushTreeSelectionToCurrent() +{ + _selectionChangedConnection.block(); + // TODO hunt down the possible API abuse in getting NULL + if ( _desktop && _desktop->currentRoot() ) { + _desktop->selection->clear(); + _tree.get_selection()->selected_foreach_iter( sigc::mem_fun(*this, &TagsPanel::_selected_row_callback)); + } + _selectionChangedConnection.unblock(); + + _checkTreeSelection(); +} + +void TagsPanel::_checkTreeSelection() +{ + bool sensitive = _tree.get_selection()->count_selected_rows() > 0; + bool sensitiveNonTop = true; + bool sensitiveNonBottom = true; +// if ( _tree.get_selection()->count_selected_rows() > 0 ) { +// sensitive = true; +// +// SPObject* inTree = _selectedLayer(); +// if ( inTree ) { +// +// sensitiveNonTop = (Inkscape::Nex(inTree->parent, inTree) != 0); +// sensitiveNonBottom = (Inkscape::previous_layer(inTree->parent, inTree) != 0); +// +// } +// } + + + for (auto & it : _watching) { + it->set_sensitive( sensitive ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( sensitiveNonTop ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( sensitiveNonBottom ); + } +} + +bool TagsPanel::_handleKeyEvent(GdkEventKey *event) +{ + + switch (Inkscape::UI::Tools::get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + case GDK_KEY_F2: { + Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected(); + if (iter && !_text_renderer->property_editable()) { + Gtk::TreeRow row = *iter; + SPObject * obj = row[_model->_colObject]; + if (obj && SP_IS_TAG(obj)) { + Gtk::TreeModel::Path *path = new Gtk::TreeModel::Path(iter); + // Edit the layer label + _text_renderer->property_editable() = true; + _tree.set_cursor(*path, *_name_column, true); + grab_focus(); + return true; + } + } + } + case GDK_KEY_Delete: { + std::vector<SPObject *> todelete; + _tree.get_selection()->selected_foreach_iter(sigc::bind<std::vector<SPObject *>*>(sigc::mem_fun(*this, &TagsPanel::_checkForDeleted), &todelete)); + if (!todelete.empty()) { + for (auto obj : todelete) { + if (obj && obj->parent && obj->getRepr() && obj->parent->getRepr()) { + //obj->parent->getRepr()->removeChild(obj->getRepr()); + obj->deleteObject(true, true); + } + } + DocumentUndo::done(_document, SP_VERB_DIALOG_TAGS, _("Remove from selection set")); + } + return true; + } + break; + } + return false; +} + +bool TagsPanel::_handleButtonEvent(GdkEventButton* event) +{ + static unsigned doubleclick = 0; + + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 3) ) { + // TODO - fix to a better is-popup function + Gtk::TreeModel::Path path; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + if ( _tree.get_path_at_pos( x, y, path ) ) { + _checkTreeSelection(); +#if GTKMM_CHECK_VERSION(3,22,0) + _popupMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); +#else + _popupMenu.popup(event->button, event->time); +#endif + if (_tree.get_selection()->is_selected(path)) { + return true; + } + } + } + + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 1)) { + // Alt left click on the visible/lock columns - eat this event to keep row selection + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (col == _tree.get_column(COL_ADD-1)) { + down_at_add = true; + return true; + } else if ( !(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) & _tree.get_selection()->is_selected(path) ) { + _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &TagsPanel::_noSelection)); + _defer_target = path; + } else { + down_at_add = false; + } + } else { + down_at_add = false; + } + } + + if ( event->type == GDK_BUTTON_RELEASE) { + _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &TagsPanel::_rowSelectFunction)); + } + + // TODO - ImageToggler doesn't seem to handle Shift/Alt clicks - so we deal with them here. + if ( (event->type == GDK_BUTTON_RELEASE) && (event->button == 1)) { + + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (_defer_target) { + if (_defer_target == path && !(event->x == 0 && event->y == 0)) + { + _tree.set_cursor(path, *col, false); + } + _defer_target = Gtk::TreeModel::Path(); + } else { + Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + SPObject* obj = row[_model->_colObject]; + + if (obj) { + if (col == _tree.get_column(COL_ADD - 1) && down_at_add) { + if (SP_IS_TAG(obj)) { + bool wasadded = false; + auto items= _desktop->selection->items(); + for(auto i=items.begin();i!=items.end();++i){ + SPObject *newobj = *i; + bool addchild = true; + for (auto& child: obj->children) { + if (SP_IS_TAG_USE(&child) && SP_TAG_USE(&child)->ref->getObject() == newobj) { + addchild = false; + } + } + if (addchild) { + Inkscape::XML::Node *clone = _document->getReprDoc()->createElement("inkscape:tagref"); + clone->setAttribute("xlink:href", g_strdup_printf("#%s", newobj->getRepr()->attribute("id")), false); + obj->appendChild(clone); + wasadded = true; + } + } + if (wasadded) { + DocumentUndo::done(_document, SP_VERB_DIALOG_TAGS, _("Add selection to set")); + } + } else { + std::vector<SPObject *> todelete; + // FIXME unnecessary use of XML tree + _tree.get_selection()->selected_foreach_iter(sigc::bind<std::vector<SPObject *>*>(sigc::mem_fun(*this, &TagsPanel::_checkForDeleted), &todelete)); + if (!todelete.empty()) { + for (auto tobj : todelete) { + if (tobj && tobj->parent && tobj->getRepr() && tobj->parent->getRepr()) { + //tobj->parent->getRepr()->removeChild(tobj->getRepr()); + tobj->deleteObject(true, true); + } + } + } else if (obj && obj->parent && obj->getRepr() && obj->parent->getRepr()) { + obj->parent->getRepr()->removeChild(obj->getRepr()); + } + DocumentUndo::done(_document, SP_VERB_DIALOG_TAGS, _("Remove from selection set")); + } + } + } + } + } + } + + + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + doubleclick = 1; + } + + if ( event->type == GDK_BUTTON_RELEASE && doubleclick) { + doubleclick = 0; + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) && col == _name_column) { + Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + SPObject* obj = row[_model->_colObject]; + if (obj && (SP_IS_TAG(obj) || (SP_IS_TAG_USE(obj) && SP_TAG_USE(obj)->ref->getObject()))) { + // Double click on the Layer name, enable editing + _text_renderer->property_editable() = true; + _tree.set_cursor (path, *_name_column, true); + grab_focus(); + } + } + } + + return false; +} + +void TagsPanel::_storeDragSource(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + SPObject* obj = row[_model->_colObject]; + SPTag* item = ( obj && SP_IS_TAG(obj) ) ? SP_TAG(obj) : nullptr; + if (item) + { + _dnd_source.push_back(item); + } +} + +/* + * Drap and drop within the tree + * Save the drag source and drop target SPObjects and if its a drag between layers or into (sublayer) a layer + */ +bool TagsPanel::_handleDragDrop(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int x, int y, guint /*time*/) +{ + int cell_x = 0, cell_y = 0; + Gtk::TreeModel::Path target_path; + Gtk::TreeView::Column *target_column; + + _dnd_into = true; + _dnd_target = _document->getDefs(); + _dnd_source.clear(); + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &TagsPanel::_storeDragSource)); + + if (_dnd_source.empty()) { + return true; + } + + if (_tree.get_path_at_pos (x, y, target_path, target_column, cell_x, cell_y)) { + // Are we before, inside or after the drop layer + Gdk::Rectangle rect; + _tree.get_background_area (target_path, *target_column, rect); + int cell_height = rect.get_height(); + _dnd_into = (cell_y > (int)(cell_height * 1/3) && cell_y <= (int)(cell_height * 2/3)); + if (cell_y > (int)(cell_height * 2/3)) { + Gtk::TreeModel::Path next_path = target_path; + next_path.next(); + if (_store->iter_is_valid(_store->get_iter(next_path))) { + target_path = next_path; + } else { + // Dragging to the "end" + Gtk::TreeModel::Path up_path = target_path; + up_path.up(); + if (_store->iter_is_valid(_store->get_iter(up_path))) { + // Drop into parent + target_path = up_path; + _dnd_into = true; + } else { + // Drop into the top level + _dnd_target = _document->getDefs(); + _dnd_into = true; + } + } + } + Gtk::TreeModel::iterator iter = _store->get_iter(target_path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + SPObject *obj = row[_model->_colObject]; + SPObject *pobj = row[_model->_colParentObject]; + if (obj) { + if (SP_IS_TAG(obj)) { + _dnd_target = SP_TAG(obj); + } else if (SP_IS_TAG(obj->parent)) { + _dnd_target = SP_TAG(obj->parent); + _dnd_into = true; + } + } else if (pobj && SP_IS_TAG(pobj)) { + _dnd_target = SP_TAG(pobj); + _dnd_into = true; + } else { + return true; + } + } + } + + _takeAction(DRAGNDROP); + + return false; +} + +/* + * Move a layer in response to a drag & drop action + */ +void TagsPanel::_doTreeMove( ) +{ + if (_dnd_target) { + for (auto src : _dnd_source) + { + if (src != _dnd_target) { + src->moveTo(_dnd_target, _dnd_into); + } + } + _desktop->selection->clear(); + // moveTo may have deleted src pointers, don't use them anymore + _dnd_source.clear(); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_TAGS, + _("Moved sets")); + } +} + + +void TagsPanel::_handleEdited(const Glib::ustring& path, const Glib::ustring& new_text) +{ + Gtk::TreeModel::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + _renameObject(row, new_text); + _text_renderer->property_editable() = false; +} + +void TagsPanel::_handleEditingCancelled() +{ + _text_renderer->property_editable() = false; +} + +void TagsPanel::_renameObject(Gtk::TreeModel::Row row, const Glib::ustring& name) +{ + if ( row && _desktop) { + SPObject* obj = row[_model->_colObject]; + if ( obj ) { + if (SP_IS_TAG(obj)) { + gchar const* oldLabel = obj->label(); + if ( !name.empty() && (!oldLabel || name != oldLabel) ) { + obj->setLabel(name.c_str()); + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Rename object")); + } + } else if (SP_IS_TAG_USE(obj) && (obj = SP_TAG_USE(obj)->ref->getObject())) { + gchar const* oldLabel = obj->label(); + if ( !name.empty() && (!oldLabel || name != oldLabel) ) { + obj->setLabel(name.c_str()); + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Rename object")); + } + } + } + } +} + +bool TagsPanel::_noSelection( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool /*currentlySelected*/ ) +{ + return false; +} + +bool TagsPanel::_rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool currentlySelected ) +{ + bool val = true; + if ( !currentlySelected && _toggleEvent ) + { + GdkEvent* event = gtk_get_current_event(); + if ( event ) { + // (keep these checks separate, so we know when to call gdk_event_free() + if ( event->type == GDK_BUTTON_PRESS ) { + GdkEventButton const* target = reinterpret_cast<GdkEventButton const*>(_toggleEvent); + GdkEventButton const* evtb = reinterpret_cast<GdkEventButton const*>(event); + + if ( (evtb->window == target->window) + && (evtb->send_event == target->send_event) + && (evtb->time == target->time) + && (evtb->state == target->state) + ) + { + // Ooooh! It's a magic one + val = false; + } + } + gdk_event_free(event); + } + } + return val; +} + +void TagsPanel::_setExpanded(const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& /*path*/, bool isexpanded) +{ + Gtk::TreeModel::Row row = *iter; + + SPObject* obj = row[_model->_colParentObject]; + if (obj && SP_IS_TAG(obj)) + { + SP_TAG(obj)->setExpanded(isexpanded); + obj->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Constructor + */ +TagsPanel::TagsPanel() : + UI::Widget::Panel("/dialogs/tags", SP_VERB_DIALOG_TAGS), + _rootWatcher(nullptr), + deskTrack(), + _desktop(nullptr), + _document(nullptr), + _model(nullptr), + _pending(nullptr), + _toggleEvent(nullptr), + _defer_target(), + desktopChangeConn() +{ + //Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + ModelColumns *zoop = new ModelColumns(); + _model = zoop; + + _store = Gtk::TreeStore::create( *zoop ); + + _tree.set_model( _store ); + _tree.set_headers_visible(false); + _tree.set_reorderable(true); + _tree.enable_model_drag_dest (Gdk::ACTION_MOVE); + + // This string is constructed to use already translated strings. + // The tooltip applies to the whole tree area. It would be better + // if the tooltip was split into parts and only applied to the + // icons but doing that is quite complicated. + Glib::ustring tooltip_string = "'+': "; + tooltip_string += (_("Add selection to set")); + tooltip_string += "; '×': "; + tooltip_string += (_("Remove from selection set")); + _tree.set_tooltip_text( tooltip_string ); + + Inkscape::UI::Widget::IconRenderer * addRenderer = manage( new Inkscape::UI::Widget::IconRenderer()); + addRenderer->add_icon("edit-delete"); + addRenderer->add_icon("list-add"); + + int addColNum = _tree.append_column("type", *addRenderer) - 1; + Gtk::TreeViewColumn *col = _tree.get_column(addColNum); + if ( col ) { + col->add_attribute( addRenderer->property_icon(), _model->_colAddRemove ); + col->add_attribute( addRenderer->property_visible(), _model->_colAllowAddRemove ); + } + + _text_renderer = manage(new Gtk::CellRendererText()); + int nameColNum = _tree.append_column("Name", *_text_renderer) - 1; + _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.get_selection()->set_mode(Gtk::SELECTION_MULTIPLE); + _selectedConnection = _tree.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &TagsPanel::_pushTreeSelectionToCurrent) ); + _tree.get_selection()->set_select_function( sigc::mem_fun(*this, &TagsPanel::_rowSelectFunction) ); + + _tree.signal_drag_drop().connect( sigc::mem_fun(*this, &TagsPanel::_handleDragDrop), false); + _collapsedConnection = _tree.signal_row_collapsed().connect( sigc::bind<bool>(sigc::mem_fun(*this, &TagsPanel::_setExpanded), false)); + _expandedConnection = _tree.signal_row_expanded().connect( sigc::bind<bool>(sigc::mem_fun(*this, &TagsPanel::_setExpanded), true)); + + _text_renderer->signal_edited().connect( sigc::mem_fun(*this, &TagsPanel::_handleEdited) ); + _text_renderer->signal_editing_canceled().connect( sigc::mem_fun(*this, &TagsPanel::_handleEditingCancelled) ); + + _tree.signal_button_press_event().connect( sigc::mem_fun(*this, &TagsPanel::_handleButtonEvent), false ); + _tree.signal_button_release_event().connect( sigc::mem_fun(*this, &TagsPanel::_handleButtonEvent), false ); + _tree.signal_key_press_event().connect( sigc::mem_fun(*this, &TagsPanel::_handleKeyEvent), 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); + } + + _layersPage.pack_start( _scroller, Gtk::PACK_EXPAND_WIDGET ); + + _layersPage.pack_end(_buttonsRow, Gtk::PACK_SHRINK); + + _getContents()->pack_start(_layersPage, Gtk::PACK_EXPAND_WIDGET); + + SPDesktop* targetDesktop = getDesktop(); + + Gtk::Button* btn = manage( new Gtk::Button() ); + _styleButton(*btn, "list-add", _("Add a new selection set") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &TagsPanel::_takeAction), (int)BUTTON_NEW) ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + +// btn = manage( new Gtk::Button("Dup") ); +// btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_DUPLICATE) ); +// _buttonsRow.add( *btn ); + + btn = manage( new Gtk::Button() ); + _styleButton( *btn, "list-remove", _("Remove Item/Set") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &TagsPanel::_takeAction), (int)BUTTON_DELETE) ); + _watching.push_back( btn ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + _buttonsRow.pack_start(_buttonsSecondary, Gtk::PACK_EXPAND_WIDGET); + _buttonsRow.pack_end(_buttonsPrimary, Gtk::PACK_EXPAND_WIDGET); + + // ------------------------------------------------------- + { + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_TAG_NEW, nullptr, "Add a new selection set", (int)BUTTON_NEW ) ); + + _popupMenu.show_all_children(); + } + // ------------------------------------------------------- + + + + for (auto & it : _watching) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( false ); + } + + setDesktop( targetDesktop ); + + show_all_children(); + + // restorePanelPrefs(); + + // Connect this up last + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &TagsPanel::setDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +TagsPanel::~TagsPanel() +{ + + setDesktop(nullptr); + + if ( _model ) + { + delete _model; + _model = nullptr; + } + + if (_pending) { + delete _pending; + _pending = nullptr; + } + + if ( _toggleEvent ) + { + gdk_event_free( _toggleEvent ); + _toggleEvent = nullptr; + } + + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +void TagsPanel::setDocument(SPDesktop* /*desktop*/, SPDocument* document) +{ + while (!_objectWatchers.empty()) + { + TagsPanel::ObjectWatcher *w = _objectWatchers.back(); + w->_repr->removeObserver(*w); + _objectWatchers.pop_back(); + delete w; + } + + if (_rootWatcher) + { + _rootWatcher->_repr->removeObserver(*_rootWatcher); + delete _rootWatcher; + _rootWatcher = nullptr; + } + + _document = document; + + if (document && document->getDefs() && document->getDefs()->getRepr()) + { + _rootWatcher = new TagsPanel::ObjectWatcher(this, document->getDefs()); + document->getDefs()->getRepr()->addObserver(*_rootWatcher); + _objectsChanged(document->getDefs()); + } +} + +void TagsPanel::setDesktop( SPDesktop* desktop ) +{ + Panel::setDesktop(desktop); + + if ( desktop != _desktop ) { + _documentChangedConnection.disconnect(); + _selectionChangedConnection.disconnect(); + if ( _desktop ) { + _desktop = nullptr; + } + + _desktop = Panel::getDesktop(); + if ( _desktop ) { + //setLabel( _desktop->doc()->name ); + _documentChangedConnection = _desktop->connectDocumentReplaced( sigc::mem_fun(*this, &TagsPanel::setDocument)); + _selectionChangedConnection = _desktop->selection->connectChanged( sigc::mem_fun(*this, &TagsPanel::_objectsSelected)); + + setDocument(_desktop, _desktop->doc()); + } + } + deskTrack.setBase(desktop); +} + + + + + +} //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/tags.h b/src/ui/dialog/tags.h new file mode 100644 index 0000000..f8f1215 --- /dev/null +++ b/src/ui/dialog/tags.h @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple dialog for tags UI. + * + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_TAGS_PANEL_H +#define SEEN_TAGS_PANEL_H + +#include <gtkmm/box.h> +#include <gtkmm/treeview.h> +#include <gtkmm/treestore.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/dialog.h> +#include "ui/widget/spinbutton.h" +#include "ui/widget/panel.h" +#include "object/sp-tag.h" +#include "object/sp-tag-use.h" +#include "object/sp-tag-use-reference.h" +#include "desktop-tracker.h" +#include "selection.h" +#include "ui/widget/filter-effect-chooser.h" + +class SPObject; +class SPTag; +struct SPColorSelector; + +namespace Inkscape { + +namespace UI { +namespace Dialog { + + +/** + * A panel that displays layers. + */ +class TagsPanel : public UI::Widget::Panel +{ +public: + TagsPanel(); + ~TagsPanel() override; + + static TagsPanel& getInstance(); + + void setDesktop( SPDesktop* desktop ) override; + void setDocument( SPDesktop* desktop, SPDocument* document); + +protected: + friend void sp_highlight_picker_color_mod(SPColorSelector *csel, GObject *cp); +private: + class ModelColumns; + class InternalUIBounce; + class ObjectWatcher; + + TagsPanel(TagsPanel const &) = delete; // no copy + TagsPanel &operator=(TagsPanel const &) = delete; // no assign + + void _styleButton( Gtk::Button& btn, char const* iconName, char const* tooltip ); + void _fireAction( unsigned int code ); + Gtk::MenuItem& _addPopupItem( SPDesktop *desktop, unsigned int code, char const* iconName, char const* fallback, int id ); + + bool _handleButtonEvent(GdkEventButton *event); + bool _handleKeyEvent(GdkEventKey *event); + + void _storeDragSource(const Gtk::TreeModel::iterator& iter); + bool _handleDragDrop(const Glib::RefPtr<Gdk::DragContext>& context, int x, int y, guint time); + void _handleEdited(const Glib::ustring& path, const Glib::ustring& new_text); + void _handleEditingCancelled(); + + void _doTreeMove(); + void _renameObject(Gtk::TreeModel::Row row, const Glib::ustring& name); + + void _pushTreeSelectionToCurrent(); + void _selected_row_callback( const Gtk::TreeModel::iterator& iter ); + void _select_tag( SPTag * tag ); + + void _checkTreeSelection(); + + void _takeAction( int val ); + bool _executeAction(); + + void _setExpanded( const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& path, bool isexpanded ); + + bool _noSelection( Glib::RefPtr<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b ); + bool _rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b ); + + void _updateObject(SPObject *obj); + bool _checkForUpdated(const Gtk::TreePath &path, const Gtk::TreeIter& iter, SPObject* obj); + + void _objectsSelected(Selection *sel); + bool _checkForSelected(const Gtk::TreePath& path, const Gtk::TreeIter& iter, SPObject* layer); + + void _objectsChanged(SPObject *root); + void _addObject( SPDocument* doc, SPObject* obj, Gtk::TreeModel::Row* parentRow ); + + void _checkForDeleted(const Gtk::TreeIter& iter, std::vector<SPObject *>* todelete); + +// std::vector<sigc::connection> groupConnections; + TagsPanel::ObjectWatcher* _rootWatcher; + std::vector<TagsPanel::ObjectWatcher*> _objectWatchers; + + // Hooked to the layer manager: + sigc::connection _documentChangedConnection; + sigc::connection _selectionChangedConnection; + + sigc::connection _changedConnection; + sigc::connection _addedConnection; + sigc::connection _removedConnection; + + // Internal + sigc::connection _selectedConnection; + sigc::connection _expandedConnection; + sigc::connection _collapsedConnection; + + DesktopTracker deskTrack; + SPDesktop* _desktop; + SPDocument* _document; + ModelColumns* _model; + InternalUIBounce* _pending; + gboolean _dnd_into; + std::vector<SPTag*> _dnd_source; + SPObject* _dnd_target; + + GdkEvent* _toggleEvent; + bool down_at_add; + + Gtk::TreeModel::Path _defer_target; + + Glib::RefPtr<Gtk::TreeStore> _store; + 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::Box _buttonsRow; + Gtk::Box _buttonsPrimary; + Gtk::Box _buttonsSecondary; + Gtk::ScrolledWindow _scroller; + Gtk::Menu _popupMenu; + Inkscape::UI::Widget::SpinButton _spinBtn; + Gtk::VBox _layersPage; + + sigc::connection desktopChangeConn; + +}; + + + +} //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/template-load-tab.cpp b/src/ui/dialog/template-load-tab.cpp new file mode 100644 index 0000000..6f5c654 --- /dev/null +++ b/src/ui/dialog/template-load-tab.cpp @@ -0,0 +1,337 @@ +// 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) + : _current_keyword("") + , _keywords_combo(true) + , _current_search_type(ALL) + , _parent_widget(parent) +{ + 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); + 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..2b8f98f --- /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::HBox +{ + +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::VBox _tlist_box; + Gtk::HBox _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..c5c4522 --- /dev/null +++ b/src/ui/dialog/template-widget.cpp @@ -0,0 +1,152 @@ +// 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() + : _more_info_button(_("More info")) + , _short_description_label(" ") + , _template_name_label(_("no template selected")) + , _effect_prefs(nullptr) +{ + 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), _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..5d7023b --- /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::VBox +{ +public: + TemplateWidget (); + void create(); + void display(TemplateLoadTab::TemplateData); + void clear(); + +private: + TemplateLoadTab::TemplateData _current_template; + + Gtk::Button _more_info_button; + Gtk::HBox _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..652798a --- /dev/null +++ b/src/ui/dialog/text-edit.cpp @@ -0,0 +1,585 @@ +// 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> + +#ifdef WITH_GTKSPELL +extern "C" { +# include <gtkspell/gtkspell.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 "verbs.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 "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() + : UI::Widget::Panel("/dialogs/textandfont", SP_VERB_DIALOG_TEXT), + font_label(_("_Font"), true), + text_label(_("_Text"), true), + feat_label(_("_Features"), true), + setasdefault_button(_("Set as _default")), + close_button(_("_Close"), true), + apply_button(_("_Apply"), true), + desktop(nullptr), + deskTrack(), + 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?.;/()")) +{ + + /* Font tab -------------------------------- */ + + /* Font selector */ + // Do nothing. + + /* Font preview */ + preview_label.set_ellipsize (Pango::ELLIPSIZE_END); + preview_label.set_justify (Gtk::JUSTIFY_CENTER); + preview_label.set_line_wrap (false); + + font_vbox.set_border_width(4); + font_vbox.pack_start(font_selector, true, true); + font_vbox.pack_start(preview_label, false, false, 4); + + /* Features tab ---------------------------- */ + + /* Features preview */ + preview_label2.set_ellipsize (Pango::ELLIPSIZE_END); + preview_label2.set_justify (Gtk::JUSTIFY_CENTER); + preview_label2.set_line_wrap (false); + + feat_vbox.set_border_width(4); + feat_vbox.pack_start(font_features, true, true); + feat_vbox.pack_start(preview_label2, false, false, 4); + + /* Text tab -------------------------------- */ + scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + scroller.set_shadow_type(Gtk::SHADOW_IN); + + text_buffer = gtk_text_buffer_new (nullptr); + text_view = gtk_text_view_new_with_buffer (text_buffer); + gtk_text_view_set_wrap_mode ((GtkTextView *) text_view, GTK_WRAP_WORD); + +#ifdef WITH_GTKSPELL + /* + 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. + */ + GtkSpellChecker * speller = gtk_spell_checker_new(); + + if (! gtk_spell_checker_attach(speller, GTK_TEXT_VIEW(text_view))) { + g_print("gtkspell error:\n"); + } +#endif + + gtk_widget_set_size_request (text_view, -1, 64); + gtk_text_view_set_editable (GTK_TEXT_VIEW (text_view), TRUE); + scroller.add(*Gtk::manage(Glib::wrap(text_view))); + text_vbox.pack_start(scroller, true, true, 0); + + /* Notebook -----------------------------------*/ + notebook.set_name( "TextEdit Notebook" ); + notebook.append_page(font_vbox, font_label); + notebook.append_page(feat_vbox, feat_label); + notebook.append_page(text_vbox, text_label); + + /* Buttons (below notebook) ------------------ */ + setasdefault_button.set_use_underline(true); + apply_button.set_can_default(); + button_row.pack_start(setasdefault_button, false, false, 0); + button_row.pack_end(close_button, false, false, VB_MARGIN); + button_row.pack_end(apply_button, false, false, VB_MARGIN); + + Gtk::Box *contents = _getContents(); + contents->set_name("TextEdit Dialog Box"); + contents->set_spacing(4); + contents->pack_start(notebook, true, true); + contents->pack_start(button_row, false, false, VB_MARGIN); + + /* Signal handlers */ + g_signal_connect ( G_OBJECT (text_buffer), "changed", G_CALLBACK (onTextChange), this ); + setasdefault_button.signal_clicked().connect(sigc::mem_fun(*this, &TextEdit::onSetDefault)); + apply_button.signal_clicked().connect(sigc::mem_fun(*this, &TextEdit::onApply)); + close_button.signal_clicked().connect(sigc::bind(_signal_response.make_slot(), GTK_RESPONSE_CLOSE)); + 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)); + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &TextEdit::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + + font_selector.set_name ("TextEdit"); + + show_all_children(); +} + +TextEdit::~TextEdit() +{ + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.disconnect(); + fontChangedConn.disconnect(); + fontFeaturesChangedConn.disconnect(); +} + +void TextEdit::onSelectionModified(guint flags ) +{ + gboolean style, content; + + style = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG )) != 0 ); + + content = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_TEXT_CONTENT_MODIFIED_FLAG )) != 0 ); + + onReadSelection (style, content); +} + +void TextEdit::onReadSelection ( gboolean dostyle, gboolean /*docontent*/ ) +{ + if (blocked) + return; + + if (!desktop || SP_ACTIVE_DESKTOP != desktop) + { + return; + } + + blocked = true; + + SPItem *text = getSelectedTextItem (); + + Glib::ustring phrase = samplephrase; + + if (text) + { + guint items = getSelectedTextCount (); + if (items == 1) { + gtk_widget_set_sensitive (text_view, TRUE); + } else { + gtk_widget_set_sensitive (text_view, FALSE); + } + apply_button.set_sensitive ( false ); + setasdefault_button.set_sensitive ( true ); + + gchar *str; + str = sp_te_get_string_multiline (text); + if (str) { + if (items == 1) { + gtk_text_buffer_set_text (text_buffer, str, strlen (str)); + gtk_text_buffer_set_modified (text_buffer, FALSE); + } + phrase = str; + + } else { + gtk_text_buffer_set_text (text_buffer, "", 0); + } + + text->getRepr(); // was being called but result ignored. Check this. + } else { + gtk_widget_set_sensitive (text_view, FALSE); + apply_button.set_sensitive ( false ); + setasdefault_button.set_sensitive ( false ); + } + + if (dostyle) { + + // create temporary style + SPStyle query(SP_ACTIVE_DOCUMENT); + + // 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 (SP_ACTIVE_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 (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTVARIANTS); + int result_features = + sp_desktop_query_style (SP_ACTIVE_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 (!SP_ACTIVE_DESKTOP) + return nullptr; + + auto tmp= SP_ACTIVE_DESKTOP->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 (!SP_ACTIVE_DESKTOP) + return 0; + + unsigned int items = 0; + + auto tmp= SP_ACTIVE_DESKTOP->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::onSelectionChange() +{ + onReadSelection (TRUE, TRUE); +} + +void TextEdit::updateObjectText ( SPItem *text ) +{ + GtkTextIter start, end; + + // write text + if (gtk_text_buffer_get_modified (text_buffer)) { + gtk_text_buffer_get_bounds (text_buffer, &start, &end); + gchar *str = gtk_text_buffer_get_text (text_buffer, &start, &end, TRUE); + sp_te_set_repr_text_multiline (text, str); + g_free (str); + gtk_text_buffer_set_modified (text_buffer, 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 = SP_ACTIVE_DESKTOP; + + 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 = SP_ACTIVE_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(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Set text style")); + apply_button.set_sensitive ( false ); + + sp_repr_css_attr_unref (css); + + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + font_lister->update_font_list(SP_ACTIVE_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; + } + + GtkTextIter start; + GtkTextIter end; + gtk_text_buffer_get_bounds (text_buffer, &start, &end); + gchar *str = gtk_text_buffer_get_text(text_buffer, &start, &end, TRUE); + + Glib::ustring fontspec = font_selector.get_fontspec(); + Glib::ustring features = font_features.get_markup(); + const gchar *phrase = str && *str ? str : samplephrase.c_str(); + setPreviewText(fontspec, features, phrase); + g_free (str); + + SPItem *text = getSelectedTextItem(); + if (text) { + apply_button.set_sensitive ( true ); + } + + setasdefault_button.set_sensitive ( true); +} + +void TextEdit::onTextChange (GtkTextBuffer *text_buffer, TextEdit *self) +{ + self->onChange(); +} + +void TextEdit::onFontChange(Glib::ustring fontspec) +{ + // Is not necesary update open type features this done when user click on font features tab + onChange(); +} + +void TextEdit::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void TextEdit::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (this->desktop) { + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &TextEdit::onSelectionChange))); + subselChangedConn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &TextEdit::onSelectionChange))); + selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &TextEdit::onSelectionModified))); + } + //widget_setup(); + onReadSelection (TRUE, 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/text-edit.h b/src/ui/dialog/text-edit.h new file mode 100644 index 0000000..7a40c77 --- /dev/null +++ b/src/ui/dialog/text-edit.h @@ -0,0 +1,217 @@ +// 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 <gtkmm/box.h> +#include <gtkmm/notebook.h> +#include <gtkmm/button.h> +#include <gtkmm/frame.h> +#include <gtkmm/scrolledwindow.h> +#include "ui/widget/panel.h" +#include "ui/widget/frame.h" +#include "ui/dialog/desktop-tracker.h" + +#include "ui/widget/font-selector.h" +#include "ui/widget/font-variants.h" + +class SPItem; +struct SPFontSelector; +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 UI::Widget::Panel { +public: + TextEdit(); + ~TextEdit() 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 (); + void onSelectionChange (); + void onSelectionModified (guint flags); + + /** + * 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); + static void onTextChange (GtkTextBuffer *text_buffer, TextEdit *self); + + /** + * 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 (); + + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + + /** + * Is invoked by the desktop tracker when the desktop changes. + * + * @see DesktopTracker + */ + void setTargetDesktop(SPDesktop *desktop); + + + +private: + + /* + * All the dialogs widgets + */ + Gtk::Notebook notebook; + + // Tab 1: Font ---------------------- // + Gtk::VBox font_vbox; + Gtk::Label font_label; + + Inkscape::UI::Widget::FontSelector font_selector; + Inkscape::UI::Widget::FontVariations font_variations; + Gtk::Label preview_label; // Share with variants tab? + + // Tab 2: Text ---------------------- // + Gtk::VBox text_vbox; + Gtk::Label text_label; + + Gtk::ScrolledWindow scroller; + GtkWidget *text_view; // TODO - Convert this to a Gtk::TextView, but GtkSpell doesn't seem to work with it + GtkTextBuffer *text_buffer; + + // Tab 3: Features ----------------- // + Gtk::VBox feat_vbox; + Inkscape::UI::Widget::FontVariants font_features; + Gtk::Label feat_label; + Gtk::Label preview_label2; // Could reparent preview_label but having a second label is probably easier. + + // Shared ------- ------------------ // + Gtk::HBox button_row; + Gtk::Button setasdefault_button; + Gtk::Button close_button; + Gtk::Button apply_button; + + // Signals + SPDesktop *desktop; + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; + 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..ba980cb --- /dev/null +++ b/src/ui/dialog/tile.cpp @@ -0,0 +1,81 @@ +// 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 "ui/dialog/grid-arrange-tab.h" +#include "ui/dialog/polar-arrange-tab.h" + +#include <glibmm/i18n.h> + +#include "tile.h" +#include "verbs.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +ArrangeDialog::ArrangeDialog() + : UI::Widget::Panel("/dialogs/gridtiler", SP_VERB_SELECTION_ARRANGE), + _gridArrangeTab(new GridArrangeTab(this)), + _polarArrangeTab(new PolarArrangeTab(this)) +{ + Gtk::Box *contents = this->_getContents(); + + _notebook.append_page(*_gridArrangeTab, C_("Arrange dialog", "Rectangular grid")); + _notebook.append_page(*_polarArrangeTab, C_("Arrange dialog", "Polar Coordinates")); + _arrangeBox.pack_start(_notebook); + + _arrangeButton = this->addResponseButton(C_("Arrange dialog","_Arrange"), GTK_RESPONSE_APPLY); + _arrangeButton->set_use_underline(true); + _arrangeButton->set_tooltip_text(_("Arrange selected objects")); + contents->pack_start(_arrangeBox); + //show_all_children(); +} + + +void ArrangeDialog::on_show() +{ + UI::Widget::Panel::on_show(); + _polarArrangeTab->on_arrange_radio_changed(); +} + +void ArrangeDialog::_apply() +{ + switch(_notebook.get_current_page()) + { + case 0: + _gridArrangeTab->arrange(); + break; + case 1: + _polarArrangeTab->arrange(); + break; + } +} + +} //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..26836e1 --- /dev/null +++ b/src/ui/dialog/tile.h @@ -0,0 +1,80 @@ +// 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/notebook.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/radiobutton.h> + +#include "ui/widget/panel.h" + +namespace Gtk { +class Button; +class Grid; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ArrangeTab; +class GridArrangeTab; +class PolarArrangeTab; + +class ArrangeDialog : public UI::Widget::Panel { +private: + Gtk::VBox _arrangeBox; + Gtk::Notebook _notebook; + + GridArrangeTab *_gridArrangeTab; + PolarArrangeTab *_polarArrangeTab; + + Gtk::Button *_arrangeButton; + +public: + ArrangeDialog(); + ~ArrangeDialog() override = default;; + + /** + * Callback from Apply + */ + void _apply() override; + + void on_show() override; + + 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..00c461d --- /dev/null +++ b/src/ui/dialog/tracedialog.cpp @@ -0,0 +1,380 @@ +// 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 <gtkmm.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/notebook.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/spinbutton.h> +#include <gtkmm/stack.h> + + +#include <glibmm/i18n.h> + +#include "desktop-tracker.h" +#include "desktop.h" +#include "selection.h" + +#include "inkscape.h" +#include "io/resource.h" +#include "io/sys.h" +#include "trace/autotrace/inkscape-autotrace.h" +#include "trace/potrace/inkscape-potrace.h" +#include "trace/depixelize/inkscape-depixelize.h" + + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class TraceDialogImpl2 : public TraceDialog { + public: + TraceDialogImpl2(); + ~TraceDialogImpl2() override; + + private: + Inkscape::Trace::Tracer tracer; + void traceProcess(bool do_i_trace); + void abort(); + + void previewCallback(); + bool previewResize(const Cairo::RefPtr<Cairo::Context>&); + void traceCallback(); + void onSelectionModified(guint flags); + void onSetDefaults(); + + void setDesktop(SPDesktop *desktop) override; + void setTargetDesktop(SPDesktop *desktop); + + SPDesktop *desktop; + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; + sigc::connection selectChangedConn; + sigc::connection selectModifiedConn; + + 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_live,*/ *CB_SIOX; + Gtk::RadioButton *RB_PA_voronoi; + Gtk::Button *B_RESET, *B_STOP, *B_OK, *B_Update; + Gtk::Box *mainBox; + Gtk::Stack *choice_scan; + Gtk::Notebook *choice_tab; + Glib::RefPtr<Gdk::Pixbuf> scaledPreview; + Gtk::DrawingArea *previewArea; +}; + +void TraceDialogImpl2::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void TraceDialogImpl2::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (this->desktop) { + selectChangedConn.disconnect(); + selectModifiedConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + selectModifiedConn = desktop->selection->connectModified( + sigc::hide<0>(sigc::mem_fun(*this, &TraceDialogImpl2::onSelectionModified))); + } + } +} + +void TraceDialogImpl2::traceProcess(bool do_i_trace) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) + desktop->setWaitingCursor(); + + if (CB_SIOX->get_active()) + tracer.enableSiox(true); + else + tracer.enableSiox(false); + + Glib::ustring type = + choice_scan->get_visible_child_name() == "SingleScan" ? CBT_SS->get_active_text() : CBT_MS->get_active_text(); + + Inkscape::Trace::Potrace::TraceType potraceType; + // Inkscape::Trace::Autotrace::TraceType autotraceType; + + bool use_autotrace = false; + Inkscape::Trace::Autotrace::AutotraceTracingEngine ate; // TODO + + if (type == _("Brightness cutoff")) + potraceType = Inkscape::Trace::Potrace::TRACE_BRIGHTNESS; + else if (type == _("Edge detection")) + potraceType = Inkscape::Trace::Potrace::TRACE_CANNY; + else if (type == _("Color quantization")) + potraceType = Inkscape::Trace::Potrace::TRACE_QUANT; + else if (type == _("Autotrace")) + { + // autotraceType = Inkscape::Trace::Autotrace::TRACE_CENTERLINE + use_autotrace = true; + ate.opts->color_count = 2; + } + else if (type == _("Centerline tracing (autotrace)")) + { + // autotraceType = Inkscape::Trace::Autotrace::TRACE_CENTERLINE + use_autotrace = true; + ate.opts->color_count = 2; + ate.opts->centerline = true; + ate.opts->preserve_width = true; + } + else if (type == _("Brightness steps")) + potraceType = Inkscape::Trace::Potrace::TRACE_BRIGHTNESS_MULTI; + else if (type == _("Colors")) + potraceType = Inkscape::Trace::Potrace::TRACE_QUANT_COLOR; + else if (type == _("Grays")) + potraceType = Inkscape::Trace::Potrace::TRACE_QUANT_MONO; + else if (type == _("Autotrace (slower)")) + { + // autotraceType = Inkscape::Trace::Autotrace::TRACE_CENTERLINE + use_autotrace = true; + ate.opts->color_count = (int)MS_scans->get_value() + 1; + } + else + { + g_warning("Should not happen!"); + } + 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, 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()); + pte.potraceParams->opticurve = CB_optimize->get_active(); + pte.potraceParams->opttolerance = optimize->get_value(); + pte.potraceParams->alphamax = CB_smooth->get_active() ? smooth->get_value() : 0; + 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() ); + + + Glib::RefPtr<Gdk::Pixbuf> pixbuf = tracer.getSelectedImage(); + if (pixbuf) { + Glib::RefPtr<Gdk::Pixbuf> preview = use_autotrace ? ate.preview(pixbuf) : pte.preview(pixbuf); + if (preview) { + int width = preview->get_width(); + int height = preview->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); + scaledPreview = preview->scale_simple(newWidth, newHeight, Gdk::INTERP_NEAREST); + previewArea->queue_draw(); + } + } + if (do_i_trace){ + if (choice_tab->get_current_page() == 1){ + tracer.trace(&dte); + printf("dt\n"); + } else if (use_autotrace) { + tracer.trace(&ate); + printf("at\n"); + } else if (choice_tab->get_current_page() == 0) { + tracer.trace(&pte); + printf("pt\n"); + } + } + + if (desktop) + desktop->clearWaitingCursor(); +} + +bool TraceDialogImpl2::previewResize(const Cairo::RefPtr<Cairo::Context>& cr) +{ + if (!scaledPreview) return false; // return early + 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; + + Glib::RefPtr<Gdk::Pixbuf> temp = scaledPreview->scale_simple(newWidth, newHeight, Gdk::INTERP_NEAREST); + Gdk::Cairo::set_source_pixbuf(cr, temp, offsetX, offsetY); + cr->paint(); + return false; +} + +void TraceDialogImpl2::abort() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) + desktop->clearWaitingCursor(); + tracer.abort(); +} + +void TraceDialogImpl2::onSelectionModified(guint flags) +{ + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG)) { + previewCallback(); + } +} + +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_live->set_active(false); + CB_SIOX->set_active(false); +} + +void TraceDialogImpl2::previewCallback() { 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_live",*/ "CB_SIOX", "CBT_SS", "CBT_MS", "B_RESET", + "B_STOP", "B_OK", "mainBox", "choice_tab", "choice_scan", + "previewArea" }; + Glib::ustring gladefile = get_filename(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_live) + GET_W(CB_SIOX) + 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(choice_scan) + GET_W(previewArea) +#undef GET_W +#undef GET_O + _getContents()->add(*mainBox); + // show_all_children(); + desktopChangeConn = deskTrack.connectDesktopChanged(sigc::mem_fun(*this, &TraceDialogImpl2::setTargetDesktop)); + deskTrack.connect(GTK_WIDGET(gobj())); + + B_Update->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::previewCallback)); + 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)); +} + + +TraceDialogImpl2::~TraceDialogImpl2() +{ + selectChangedConn.disconnect(); + selectModifiedConn.disconnect(); + desktopChangeConn.disconnect(); +} + + + +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..d02e705 --- /dev/null +++ b/src/ui/dialog/tracedialog.h @@ -0,0 +1,69 @@ +// 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/widget/panel.h" +#include "verbs.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/** + * A dialog that displays log messages + */ +class TraceDialog : public UI::Widget::Panel +{ + +public: + + /** + * Constructor + */ + TraceDialog() : + UI::Widget::Panel("/dialogs/trace", SP_VERB_SELECTION_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..f212654 --- /dev/null +++ b/src/ui/dialog/transformation.cpp @@ -0,0 +1,1171 @@ +// 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 <gtkmm/dialog.h> + +#include <2geom/transforms.h> + +#include "align-and-distribute.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "transformation.h" +#include "verbs.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 { + +static void on_selection_changed(Inkscape::Selection *selection, Transformation *daad) +{ + int page = daad->getCurrentPage(); + daad->updateSelection((Inkscape::UI::Dialog::Transformation::PageType)page, selection); +} + +static void on_selection_modified(Inkscape::Selection *selection, Transformation *daad) +{ + int page = daad->getCurrentPage(); + daad->updateSelection((Inkscape::UI::Dialog::Transformation::PageType)page, selection); +} + +/*######################################################################## +# C O N S T R U C T O R +########################################################################*/ + +Transformation::Transformation() + : UI::Widget::Panel("/dialogs/transformation", SP_VERB_DIALOG_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 ("_A:", _("Transformation matrix element A")), + _scalar_transform_b ("_B:", _("Transformation matrix element B")), + _scalar_transform_c ("_C:", _("Transformation matrix element C")), + _scalar_transform_d ("_D:", _("Transformation matrix element D")), + _scalar_transform_e ("_E:", _("Transformation matrix element E")), + _scalar_transform_f ("_F:", _("Transformation matrix element F")), + + _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")); + Gtk::Box *contents = _getContents(); + + contents->set_spacing(0); + + // Notebook for individual transformations + contents->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(); + + _notebook.signal_switch_page().connect(sigc::mem_fun(*this, &Transformation::onSwitchPage)); + + // Apply separately + contents->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()))->set_activates_default(true); + ((Gtk::Entry *) (_scalar_move_vertical.getWidget()))->set_activates_default(true); + ((Gtk::Entry *) (_scalar_scale_horizontal.getWidget()))->set_activates_default(true); + ((Gtk::Entry *) (_scalar_scale_vertical.getWidget()))->set_activates_default(true); + ((Gtk::Entry *) (_scalar_rotate.getWidget()))->set_activates_default(true); + ((Gtk::Entry *) (_scalar_skew_horizontal.getWidget()))->set_activates_default(true); + ((Gtk::Entry *) (_scalar_skew_vertical.getWidget()))->set_activates_default(true); + + updateSelection(PAGE_MOVE, _getSelection()); + + resetButton = addResponseButton(_("_Clear"), 0); + if (resetButton) { + 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 = addResponseButton(_("_Apply"), Gtk::RESPONSE_APPLY); + if (applyButton) { + applyButton->set_tooltip_text(_("Apply transformation to selection")); + applyButton->set_sensitive(false); + } + + // Connect to the global selection changed & modified signals + _selChangeConn = INKSCAPE.signal_selection_changed.connect(sigc::bind(sigc::ptr_fun(&on_selection_changed), this)); + _selModifyConn = INKSCAPE.signal_selection_modified.connect(sigc::hide<1>(sigc::bind(sigc::ptr_fun(&on_selection_modified), this))); + + _desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &Transformation::setDesktop) ); + _deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); +} + +Transformation::~Transformation() +{ + _selModifyConn.disconnect(); + _selChangeConn.disconnect(); + _desktopChangeConn.disconnect(); + _deskTrack.disconnect(); +} + +void Transformation::setTargetDesktop(SPDesktop *desktop) +{ + if (_desktop != desktop) { + _desktop = desktop; + } +} + +/*######################################################################## +# U T I L I T Y +########################################################################*/ + +void Transformation::presentPage(Transformation::PageType page) +{ + _notebook.set_current_page(page); + show(); + present(); +} + + + + +/*######################################################################## +# S E T U P L A Y O U T +########################################################################*/ + + +void Transformation::layoutPageMove() +{ + _units_move.setUnitType(UNIT_TYPE_LINEAR); + + // Setting default unit to document unit + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->getNamedView(); + if (nv->display_units) { + _units_move.setUnit(nv->display_units->abbr); + } + + _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_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.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_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(); + + _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); + + _page_rotate.table().attach(_scalar_rotate, 0, 0, 2, 1); + _page_rotate.table().attach(_units_rotate, 2, 0, 1, 1); + _page_rotate.table().attach(_counterclockwise_rotate, 3, 0, 1, 1); + _page_rotate.table().attach(_clockwise_rotate, 4, 0, 1, 1); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/dialogs/transformation/rotateCounterClockwise", TRUE) != getDesktop()->is_yaxisdown()) { + _counterclockwise_rotate.set_active(); + onRotateCounterclockwiseClicked(); + } else { + _clockwise_rotate.set_active(); + onRotateClockwiseClicked(); + } + + _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_vertical.initScalar(-1e6, 1e6); + _scalar_skew_vertical.setDigits(3); + _scalar_skew_vertical.setIncrements(0.1, 1.0); + _scalar_skew_vertical.set_hexpand(); + + _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() +{ + _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(_scalar_transform_a, 0, 0, 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(_scalar_transform_b, 0, 1, 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(_scalar_transform_c, 1, 0, 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(_scalar_transform_d, 1, 1, 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(_scalar_transform_e, 2, 0, 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(_scalar_transform_f, 2, 1, 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, 2, 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) +{ + 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; + } + } + + setResponseSensitive(Gtk::RESPONSE_APPLY, + selection && !selection->isEmpty()); +} + +void Transformation::onSwitchPage(Gtk::Widget * /*page*/, guint pagenum) +{ + 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]); + _scalar_transform_f.setValue(new_displayed[5]); + } 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() +{ + Inkscape::Selection * const 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 + //setResponseSensitive(Gtk::RESPONSE_APPLY, 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() , SP_VERB_DIALOG_TRANSFORM, + _("Move")); +} + +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(), SP_VERB_DIALOG_TRANSFORM, + _("Scale")); +} + +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 { + boost::optional<Geom::Point> center = selection->center(); + if (center) { + selection->rotateRelative(*center, angle); + } + } + + DocumentUndo::done(selection->desktop()->getDocument(), SP_VERB_DIALOG_TRANSFORM, + _("Rotate")); +} + +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(); + boost::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(), SP_VERB_DIALOG_TRANSFORM, + _("Skew")); +} + + +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(); + double f = _scalar_transform_f.getValue(); + + 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(), SP_VERB_DIALOG_TRANSFORM, + _("Edit transformation matrix")); +} + + + + + +/*######################################################################## +# V A L U E - C H A N G E D C A L L B A C K S +########################################################################*/ + +void Transformation::onMoveValueChanged() +{ + setResponseSensitive(Gtk::RESPONSE_APPLY, true); +} + +void Transformation::onMoveRelativeToggled() +{ + Inkscape::Selection *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); + } + } + + setResponseSensitive(Gtk::RESPONSE_APPLY, true); +} + +void Transformation::onScaleXValueChanged() +{ + if (_scalar_scale_horizontal.setProgrammatically) { + _scalar_scale_horizontal.setProgrammatically = false; + return; + } + + setResponseSensitive(Gtk::RESPONSE_APPLY, 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; + } + + setResponseSensitive(Gtk::RESPONSE_APPLY, 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() +{ + setResponseSensitive(Gtk::RESPONSE_APPLY, 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() +{ + setResponseSensitive(Gtk::RESPONSE_APPLY, 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); + */ + + setResponseSensitive(Gtk::RESPONSE_APPLY, true); +} + +void Transformation::onReplaceMatrixToggled() +{ + Inkscape::Selection *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(); + double f = _scalar_transform_f.getValue(); + + 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]); + _scalar_transform_f.setValue(new_displayed[5]); +} + +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: { + Inkscape::Selection *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); + _scalar_transform_f.setValue(0); + break; + } + } +} + +void Transformation::onApplySeparatelyToggled() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/transformation/applyseparately", _check_apply_separately.get_active()); +} + + +} // 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..f50e214 --- /dev/null +++ b/src/ui/dialog/transformation.h @@ -0,0 +1,258 @@ +// 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 <gtkmm/notebook.h> +#include <glibmm/i18n.h> + +#include "ui/widget/panel.h" +#include "ui/widget/notebook-page.h" +#include "ui/widget/scalar-unit.h" +#include "ui/dialog/desktop-tracker.h" + + +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 UI::Widget::Panel +{ + +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 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::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::Scalar _scalar_transform_e; + UI::Widget::Scalar _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; + + SPDesktop *_desktop; + DesktopTracker _deskTrack; + sigc::connection _desktopChangeConn; + + /** + * Layout the GUI components, and prepare for use + */ + void layoutPageMove(); + void layoutPageScale(); + void layoutPageRotate(); + void layoutPageSkew(); + void layoutPageTransform(); + + void _apply() override; + 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 *); + + void setTargetDesktop(SPDesktop* desktop); + +private: + + /** + * Copy constructor + */ + Transformation(Transformation const &d) = delete; + + /** + * Assignment operator + */ + Transformation operator=(Transformation const &d) = delete; + + Gtk::Button *applyButton; + Gtk::Button *resetButton; + Gtk::Button *cancelButton; + + sigc::connection _selChangeConn; + sigc::connection _selModifyConn; +}; + + + + +} // 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..eae9ea6 --- /dev/null +++ b/src/ui/dialog/undo-history.cpp @@ -0,0 +1,406 @@ +// 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 "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "ui/icon-loader.h" +#include "util/signal-blocker.h" + +#include "desktop.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 this event type doesn't have an icon... + if ( !Inkscape::Verb::get(_property_event_type)->get_image() ) return; + + // if the icon isn't cached, render it to a pixbuf + if ( !_icon_cache[_property_event_type] ) { + + Glib::ustring image_name = Inkscape::Verb::get(_property_event_type)->get_image(); + Gtk::Image* icon = Gtk::manage(new Gtk::Image()); + icon = sp_get_icon_image(image_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(image_name, 16); + } else { + delete icon; + return; + } + + delete icon; + property_pixbuf() = _icon_cache[_property_event_type] = _property_icon.get_value(); + } + + } else { + property_pixbuf() = _icon_cache[_property_event_type]; + } + + 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() + : UI::Widget::Panel("/dialogs/undo-history", SP_VERB_DIALOG_UNDO_HISTORY), + _document_replaced_connection(), + _desktop(getDesktop()), + _document(_desktop ? _desktop->doc() : nullptr), + _event_log(_desktop ? _desktop->event_log : nullptr), + _columns(_event_log ? &_event_log->getColumns() : nullptr), + _scrolled_window(), + _event_list_store(), + _event_list_selection(_event_list_view.get_selection()), + _deskTrack(), + _desktopChangeConn(), + _callback_connections() +{ + if ( !_document || !_event_log || !_columns ) return; + + set_size_request(-1, 95); + + _getContents()->pack_start(_scrolled_window); + _scrolled_window.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + + // connect with the EventLog + _connectEventLog(); + + _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_event_type(), _columns->type); + + 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); + + // 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)); + + _desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &UndoHistory::setDesktop) ); + _deskTrack.connect(GTK_WIDGET(gobj())); + + // connect to be informed of document changes + signalDocumentReplaced().connect(sigc::mem_fun(*this, &UndoHistory::_handleDocumentReplaced)); + + show_all_children(); + + // scroll to the selected row + _event_list_view.set_cursor(_event_list_store->get_path(_event_log->getCurrEvent())); +} + +UndoHistory::~UndoHistory() +{ + _desktopChangeConn.disconnect(); +} + + +void UndoHistory::setDesktop(SPDesktop* desktop) +{ + Panel::setDesktop(desktop); + + EventLog *newEventLog = desktop ? desktop->event_log : nullptr; + if ((_desktop == desktop) && (_event_log == newEventLog)) { + // same desktop set + } + else + { + _connectDocument(desktop, desktop ? desktop->doc() : nullptr); + } +} + +void UndoHistory::_connectDocument(SPDesktop* desktop, SPDocument * /*document*/) +{ + // disconnect from prior + if (_event_log) { + _event_log->removeDialogConnection(&_event_list_view, &_callback_connections); + } + + SignalBlocker blocker(&_callback_connections[EventLog::CALLB_SELECTION_CHANGE]); + + _event_list_view.unset_model(); + + // connect to new EventLog/Desktop + _desktop = desktop; + _event_log = desktop ? desktop->event_log : nullptr; + _document = desktop ? desktop->doc() : nullptr; + _connectEventLog(); +} + +void UndoHistory::_connectEventLog() +{ + if (_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::_handleDocumentReplaced(SPDesktop* desktop, SPDocument *document) +{ + if ((desktop != _desktop) || (document != _document)) { + _connectDocument(desktop, document); + } +} + +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. + */ + 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(_document); + } + _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(_document); + + 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(_document); + + 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(); + } + +} + +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() ) { + 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(_document); + + for ( --last ; curr_event != last ; ++curr_event ) { + DocumentUndo::redo(_document); + } + _event_log->blockNotifications(false); + + _event_log->setCurrEvent(curr_event); + _event_log->setCurrEventParent(curr_event_parent); + _event_list_selection->select(curr_event_parent); + } +} + +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..244980b --- /dev/null +++ b/src/ui/dialog/undo-history.h @@ -0,0 +1,179 @@ +// 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 "ui/widget/panel.h" +#include <gtkmm/cellrendererpixbuf.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/treemodel.h> +#include <gtkmm/treeselection.h> +#include <glibmm/property.h> + +#include <functional> +#include <sstream> + +#include "event-log.h" + +#include "ui/dialog/desktop-tracker.h" + +class SPDesktop; + +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_event_type(*this, "event_type", 0) + { } + + Glib::PropertyProxy<unsigned int> + property_event_type() { return _property_event_type.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<unsigned int> _property_event_type; + std::map<const unsigned int, 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 Widget::Panel { +public: + ~UndoHistory() override; + + static UndoHistory &getInstance(); + void setDesktop(SPDesktop* desktop) override; + + sigc::connection _document_replaced_connection; + +protected: + + SPDesktop *_desktop; + SPDocument *_document; + EventLog *_event_log; + + + const EventLog::EventModelColumns *_columns; + + Gtk::ScrolledWindow _scrolled_window; + + Glib::RefPtr<Gtk::TreeModel> _event_list_store; + Gtk::TreeView _event_list_view; + Glib::RefPtr<Gtk::TreeSelection> _event_list_selection; + + DesktopTracker _deskTrack; + sigc::connection _desktopChangeConn; + + EventLog::CallbackMap _callback_connections; + + static void *_handleEventLogDestroyCB(void *data); + + void _connectDocument(SPDesktop* desktop, SPDocument *document); + void _connectEventLog(); + void _handleDocumentReplaced(SPDesktop* desktop, SPDocument *document); + 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..dcc5c55 --- /dev/null +++ b/src/ui/dialog/xml-tree.cpp @@ -0,0 +1,968 @@ +// 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 "message-context.h" +#include "message-stack.h" +#include "shortcuts.h" +#include "verbs.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 Inkscape { +namespace UI { +namespace Dialog { + +XmlTree::XmlTree() + : UI::Widget::Panel("/dialogs/xml/", SP_VERB_DIALOG_XML_EDITOR) + , blocked(0) + , _message_stack(nullptr) + , _message_context(nullptr) + , current_desktop(nullptr) + , current_document(nullptr) + , selected_attr(0) + , selected_repr(nullptr) + , tree(nullptr) + , status("") + , new_window(nullptr) + , _updating(false) +{ + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) { + return; + } + + Gtk::Box *root = _getContents(); + 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_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)))); + + 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_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL); + _paned.check_resize(); + _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_right(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); + contents->pack_start(*actionsbox, false, false, 0); + /* Signal handlers */ + GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(tree)); + g_signal_connect (G_OBJECT(selection), "changed", G_CALLBACK (on_tree_select_row), this); + 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)); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &XmlTree::set_tree_desktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + set_name("XMLAndAttributesDialog"); + set_spacing(0); + set_size_request(320, 260); + 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(); + root->pack_start(*Gtk::manage(contents), true, true); + g_assert(desktop != nullptr); + set_tree_desktop(desktop); + +} + +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_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL); + _paned.check_resize(); + 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(); + } +} + +void XmlTree::present() +{ + set_tree_select(get_dt_select()); + + UI::Widget::Panel::present(); + + if (!_attrswitch.property_active()) { + attributes->hide(); + } +} + +XmlTree::~XmlTree () +{ + set_tree_desktop(nullptr); + if (current_desktop) { + current_desktop->getDocument()->setXMLDialogSelectedObject(nullptr); + } + _message_changed_connection.disconnect(); + _message_context = nullptr; + _message_stack = nullptr; + _message_changed_connection.~connection(); +} + +void XmlTree::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +/** + * 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::set_tree_desktop(SPDesktop *desktop) +{ + if ( desktop == current_desktop ) { + return; + } + + if (current_desktop) { + sel_changed_connection.disconnect(); + document_replaced_connection.disconnect(); + } + current_desktop = desktop; + if (desktop) { + sel_changed_connection = desktop->getSelection()->connectChanged(sigc::hide(sigc::mem_fun(this, &XmlTree::on_desktop_selection_changed))); + document_replaced_connection = desktop->connectDocumentReplaced(sigc::mem_fun(this, &XmlTree::on_document_replaced)); + + set_tree_document(desktop->getDocument()); + } else { + set_tree_document(nullptr); + } + +} // end of set_tree_desktop() + + +void XmlTree::set_tree_document(SPDocument *document) +{ + if (document == current_document) { + return; + } + + if (current_document) { + document_uri_set_connection.disconnect(); + } + current_document = document; + if (current_document) { + + document_uri_set_connection = current_document->connectURISet(sigc::bind(sigc::ptr_fun(&on_document_uri_set), current_document)); + on_document_uri_set( current_document->getDocumentURI(), current_document ); + set_tree_repr(current_document->getReprRoot()); + } else { + set_tree_repr(nullptr); + } +} + + + +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 (current_desktop) { + current_desktop->getDocument()->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::ELEMENT_NODE || + repr->type() == Inkscape::XML::TEXT_NODE || + repr->type() == Inkscape::XML::COMMENT_NODE)) + { + attributes->setRepr(repr); + } else { + attributes->setRepr(nullptr); + } +} + + +Inkscape::XML::Node *XmlTree::get_dt_select() +{ + if (!current_desktop) { + return nullptr; + } + return current_desktop->getSelection()->singleRepr(); +} + + + +void XmlTree::set_dt_select(Inkscape::XML::Node *repr) +{ + if (!current_desktop) { + return; + } + + Inkscape::Selection *selection = current_desktop->getSelection(); + + SPObject *object; + if (repr) { + while ( ( repr->type() != Inkscape::XML::ELEMENT_NODE ) + && repr->parent() ) + { + repr = repr->parent(); + } // end of while loop + + object = current_desktop->getDocument()->getObjectByRepr(repr); + } else { + object = nullptr; + } + + blocked++; + if ( object && in_dt_coordsys(*object) + && !(SP_IS_STRING(object) || + current_desktop->isLayer(object) || + SP_IS_ROOT(object) ) ) + { + /* We cannot set selection to root or string - they are not items and selection is not + * equipped to deal with them */ + selection->set(SP_ITEM(object)); + } + + current_desktop->getDocument()->setXMLDialogSelectedObject(object); + + blocked--; + +} // end of set_dt_select() + + +void XmlTree::on_tree_select_row(GtkTreeSelection *selection, gpointer data) +{ + XmlTree *self = static_cast<XmlTree *>(data); + + if (self->blocked) { + 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->current_document, SP_VERB_DIALOG_XML_EDITOR, + Q_("Undo History / XML dialog|Drag XML subtree")); + } else { + //DocumentUndo::cancel(self->current_document); + /* + * There was a problem with drag & drop, + * data is probably not synchronized, so reload the tree + */ + SPDocument *document = self->current_document; + self->set_tree_document(nullptr); + self->set_tree_document(document); + } +} + +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::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::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_desktop_selection_changed() +{ + if (!blocked++) { + Inkscape::XML::Node *node = get_dt_select(); + set_tree_select(node); + } + blocked--; +} + +void XmlTree::on_document_replaced(SPDesktop *dt, SPDocument *doc) +{ + if (current_desktop) + sel_changed_connection.disconnect(); + + sel_changed_connection = dt->getSelection()->connectChanged(sigc::hide(sigc::mem_fun(this, &XmlTree::on_desktop_selection_changed))); + set_tree_document(doc); +} + +void XmlTree::on_document_uri_set(gchar const * /*uri*/, SPDocument * /*document*/) +{ +/* + * Seems to be no way to set the title on a docked dialog + gchar title[500]; + sp_ui_dialog_title_string(Inkscape::Verb::get(SP_VERB_DIALOG_XML_EDITOR), title); + gchar *t = g_strdup_printf("%s: %s", document->getName(), title); + //gtk_window_set_title(GTK_WINDOW(dlg), t); + g_free(t); +*/ +} + +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() +{ + 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 = current_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(current_document, SP_VERB_DIALOG_XML_EDITOR, + Q_("Undo History / XML dialog|Create new element node")); + } + } +} // end of cmd_new_element_node() + + +void XmlTree::cmd_new_text_node() +{ + g_assert(selected_repr != nullptr); + + Inkscape::XML::Document *xml_doc = current_document->getReprDoc(); + Inkscape::XML::Node *text = xml_doc->createTextNode(""); + selected_repr->appendChild(text); + + DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, + Q_("Undo History / XML dialog|Create new text node")); + + set_tree_select(text); + set_dt_select(text); +} + +void XmlTree::cmd_duplicate_node() +{ + 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(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Duplicate node")); + + 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() +{ + g_assert(selected_repr != nullptr); + + current_document->setXMLDialogSelectedObject(nullptr); + + Inkscape::XML::Node *parent = selected_repr->parent(); + + sp_repr_unparent(selected_repr); + + if (parent) { + auto parentobject = current_document->getObjectByRepr(parent); + if (parentobject) { + parentobject->requestDisplayUpdate(SP_OBJECT_CHILD_MODIFIED_FLAG); + } + } + + DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Delete node")); +} + +void XmlTree::cmd_raise_node() +{ + 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(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Raise node")); + + set_tree_select(selected_repr); + set_dt_select(selected_repr); +} + + + +void XmlTree::cmd_lower_node() +{ + 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(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Lower node")); + + set_tree_select(selected_repr); + set_dt_select(selected_repr); +} + +void XmlTree::cmd_indent_node() +{ + 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::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(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Indent node")); + set_tree_select(repr); + set_dt_select(repr); + +} // end of cmd_indent_node() + + + +void XmlTree::cmd_unindent_node() +{ + 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(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Unindent node")); + + 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; + g_return_val_if_fail(child != nullptr, false); + for(;;) { + if (!SP_IS_ITEM(child)) { + return false; + } + SPObject const * const parent = child->parent; + if (parent == nullptr) { + break; + } + child = parent; + } + g_assert(SP_IS_ROOT(child)); + /* Relevance: Otherwise, I'm not sure whether to return true or false. */ + return true; +} + +} +} +} + +/* + 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..b63417a --- /dev/null +++ b/src/ui/dialog/xml-tree.h @@ -0,0 +1,265 @@ +// 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 <memory> + +#include "ui/widget/panel.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 "message.h" + +#include "ui/dialog/attrdialog.h" +#include "ui/dialog/desktop-tracker.h" + + +class SPDesktop; +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 Widget::Panel { +public: + XmlTree (); + ~XmlTree () override; + + static XmlTree &getInstance() { return *new XmlTree(); } + +private: + + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void set_tree_desktop(SPDesktop *desktop); + + /** + * Is invoked when the document changes + */ + void set_tree_document(SPDocument *document); + + /** + * 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 + */ + void on_desktop_selection_changed(); + void on_document_replaced(SPDesktop *dt, SPDocument *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 present() override; + void _attrtoggler(); + void _toggleDirection(Gtk::RadioButton *vertical); + void _resized(); + bool in_dt_coordsys(SPObject const &item); + + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + + /** + * 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_replaced_connection; + sigc::connection document_uri_set_connection; + sigc::connection sel_changed_connection; + + /** + * Current document and desktop this dialog is attached to + */ + SPDesktop *current_desktop; + SPDocument *current_document; + + 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::VBox node_box; + Gtk::HBox 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; + + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; +}; + +} +} +} + +#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 : |