diff options
Diffstat (limited to 'src/ui/widget')
154 files changed, 40428 insertions, 0 deletions
diff --git a/src/ui/widget/alignment-selector.cpp b/src/ui/widget/alignment-selector.cpp new file mode 100644 index 0000000..11d1166 --- /dev/null +++ b/src/ui/widget/alignment-selector.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * anchor-selector.cpp + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/alignment-selector.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" + +#include <gtkmm/image.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +void AlignmentSelector::setupButton(const Glib::ustring& icon, Gtk::Button& button) { + Gtk::Image *buttonIcon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_SMALL_TOOLBAR)); + buttonIcon->show(); + + button.set_relief(Gtk::RELIEF_NONE); + button.show(); + button.add(*buttonIcon); + button.set_can_focus(false); +} + +AlignmentSelector::AlignmentSelector() + : _container() +{ + set_halign(Gtk::ALIGN_CENTER); + // clang-format off + setupButton(INKSCAPE_ICON("boundingbox_top_left"), _buttons[0]); + setupButton(INKSCAPE_ICON("boundingbox_top"), _buttons[1]); + setupButton(INKSCAPE_ICON("boundingbox_top_right"), _buttons[2]); + setupButton(INKSCAPE_ICON("boundingbox_left"), _buttons[3]); + setupButton(INKSCAPE_ICON("boundingbox_center"), _buttons[4]); + setupButton(INKSCAPE_ICON("boundingbox_right"), _buttons[5]); + setupButton(INKSCAPE_ICON("boundingbox_bottom_left"), _buttons[6]); + setupButton(INKSCAPE_ICON("boundingbox_bottom"), _buttons[7]); + setupButton(INKSCAPE_ICON("boundingbox_bottom_right"), _buttons[8]); + // clang-format on + + _container.set_row_homogeneous(); + _container.set_column_homogeneous(true); + + for(int i = 0; i < 9; ++i) { + _buttons[i].signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &AlignmentSelector::btn_activated), i)); + + _container.attach(_buttons[i], i % 3, i / 3, 1, 1); + } + + this->add(_container); +} + +AlignmentSelector::~AlignmentSelector() +{ + // TODO Auto-generated destructor stub +} + +void AlignmentSelector::btn_activated(int index) +{ + _alignmentClicked.emit(index); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/alignment-selector.h b/src/ui/widget/alignment-selector.h new file mode 100644 index 0000000..5bcf0fa --- /dev/null +++ b/src/ui/widget/alignment-selector.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * anchor-selector.h + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef ANCHOR_SELECTOR_H_ +#define ANCHOR_SELECTOR_H_ + +#include <gtkmm/bin.h> +#include <gtkmm/button.h> +#include <gtkmm/grid.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class AlignmentSelector : public Gtk::Bin +{ +private: + Gtk::Button _buttons[9]; + Gtk::Grid _container; + + sigc::signal<void, int> _alignmentClicked; + + void setupButton(const Glib::ustring &icon, Gtk::Button &button); + void btn_activated(int index); + +public: + + sigc::signal<void, int> &on_alignmentClicked() { return _alignmentClicked; } + + AlignmentSelector(); + ~AlignmentSelector() override; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif /* ANCHOR_SELECTOR_H_ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/anchor-selector.cpp b/src/ui/widget/anchor-selector.cpp new file mode 100644 index 0000000..b151a81 --- /dev/null +++ b/src/ui/widget/anchor-selector.cpp @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * anchor-selector.cpp + * + * Created on: Mar 22, 2012 + * Author: denis + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "ui/widget/anchor-selector.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" + +#include <gtkmm/image.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +void AnchorSelector::setupButton(const Glib::ustring& icon, Gtk::ToggleButton& button) { + Gtk::Image *buttonIcon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_SMALL_TOOLBAR)); + buttonIcon->show(); + + button.set_relief(Gtk::RELIEF_NONE); + button.show(); + button.add(*buttonIcon); + button.set_can_focus(false); +} + +AnchorSelector::AnchorSelector() + : _container() +{ + set_halign(Gtk::ALIGN_CENTER); + setupButton(INKSCAPE_ICON("boundingbox_top_left"), _buttons[0]); + setupButton(INKSCAPE_ICON("boundingbox_top"), _buttons[1]); + setupButton(INKSCAPE_ICON("boundingbox_top_right"), _buttons[2]); + setupButton(INKSCAPE_ICON("boundingbox_left"), _buttons[3]); + setupButton(INKSCAPE_ICON("boundingbox_center"), _buttons[4]); + setupButton(INKSCAPE_ICON("boundingbox_right"), _buttons[5]); + setupButton(INKSCAPE_ICON("boundingbox_bottom_left"), _buttons[6]); + setupButton(INKSCAPE_ICON("boundingbox_bottom"), _buttons[7]); + setupButton(INKSCAPE_ICON("boundingbox_bottom_right"), _buttons[8]); + + _container.set_row_homogeneous(); + _container.set_column_homogeneous(true); + + for (int i = 0; i < 9; ++i) { + _buttons[i].signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &AnchorSelector::btn_activated), i)); + + _container.attach(_buttons[i], i % 3, i / 3, 1, 1); + } + _selection = 4; + _buttons[4].set_active(); + + this->add(_container); +} + +AnchorSelector::~AnchorSelector() +{ + // TODO Auto-generated destructor stub +} + +void AnchorSelector::btn_activated(int index) +{ + if (_selection == index && _buttons[index].get_active() == false) { + _buttons[index].set_active(true); + } + else if (_selection != index && _buttons[index].get_active()) { + int old_selection = _selection; + _selection = index; + _buttons[old_selection].set_active(false); + _selectionChanged.emit(); + } +} + +void AnchorSelector::setAlignment(int horizontal, int vertical) +{ + int index = 3 * vertical + horizontal; + if (index >= 0 && index < 9) { + _buttons[index].set_active(!_buttons[index].get_active()); + } +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/anchor-selector.h b/src/ui/widget/anchor-selector.h new file mode 100644 index 0000000..49ce0b2 --- /dev/null +++ b/src/ui/widget/anchor-selector.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * anchor-selector.h + * + * Created on: Mar 22, 2012 + * Author: denis + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef ANCHOR_SELECTOR_H_ +#define ANCHOR_SELECTOR_H_ + +#include <gtkmm/bin.h> +#include <gtkmm/togglebutton.h> +#include <gtkmm/grid.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class AnchorSelector : public Gtk::Bin +{ +private: + Gtk::ToggleButton _buttons[9]; + int _selection; + Gtk::Grid _container; + + sigc::signal<void> _selectionChanged; + + void setupButton(const Glib::ustring &icon, Gtk::ToggleButton &button); + void btn_activated(int index); + +public: + + int getHorizontalAlignment() { return _selection % 3; } + int getVerticalAlignment() { return _selection / 3; } + + sigc::signal<void> &on_selectionChanged() { return _selectionChanged; } + + void setAlignment(int horizontal, int vertical); + + AnchorSelector(); + ~AnchorSelector() override; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif /* ANCHOR_SELECTOR_H_ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/attr-widget.h b/src/ui/widget/attr-widget.h new file mode 100644 index 0000000..ce5fe12 --- /dev/null +++ b/src/ui/widget/attr-widget.h @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Nicholas Bishop <nicholasbishop@gmail.com> + * Rodrigo Kumpera <kumpera@gmail.com> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_ATTR_WIDGET_H +#define INKSCAPE_UI_WIDGET_ATTR_WIDGET_H + +#include "attributes.h" +#include "object/sp-object.h" +#include "xml/node.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +enum DefaultValueType +{ + T_NONE, + T_DOUBLE, + T_VECT_DOUBLE, + T_BOOL, + T_UINT, + T_CHARPTR +}; + +/** + * Very basic interface for classes that control attributes. + */ +class DefaultValueHolder +{ + DefaultValueType type; + union { + double d_val; + std::vector<double>* vt_val; + bool b_val; + unsigned int uint_val; + char* cptr_val; + } value; + + //FIXME remove copy ctor and assignment operator as private to avoid double free of the vector +public: + DefaultValueHolder () { + type = T_NONE; + } + + DefaultValueHolder (double d) { + type = T_DOUBLE; + value.d_val = d; + } + + DefaultValueHolder (std::vector<double>* d) { + type = T_VECT_DOUBLE; + value.vt_val = d; + } + + DefaultValueHolder (char* c) { + type = T_CHARPTR; + value.cptr_val = c; + } + + DefaultValueHolder (bool d) { + type = T_BOOL; + value.b_val = d; + } + + DefaultValueHolder (unsigned int ui) { + type = T_UINT; + value.uint_val = ui; + } + + ~DefaultValueHolder() { + if (type == T_VECT_DOUBLE) + delete value.vt_val; + } + + unsigned int as_uint() { + g_assert (type == T_UINT); + return value.uint_val; + } + + bool as_bool() { + g_assert (type == T_BOOL); + return value.b_val; + } + + double as_double() { + g_assert (type == T_DOUBLE); + return value.d_val; + } + + std::vector<double>* as_vector() { + g_assert (type == T_VECT_DOUBLE); + return value.vt_val; + } + + char* as_charptr() { + g_assert (type == T_CHARPTR); + return value.cptr_val; + } +}; + +class AttrWidget +{ +public: + AttrWidget(const SPAttr a, unsigned int value) + : _attr(a), + _default(value) + {} + + AttrWidget(const SPAttr a, double value) + : _attr(a), + _default(value) + {} + + AttrWidget(const SPAttr a, bool value) + : _attr(a), + _default(value) + {} + + AttrWidget(const SPAttr a, char* value) + : _attr(a), + _default(value) + {} + + AttrWidget(const SPAttr a) + : _attr(a), + _default() + {} + + virtual ~AttrWidget() + = default; + + virtual Glib::ustring get_as_attribute() const = 0; + virtual void set_from_attribute(SPObject*) = 0; + + SPAttr get_attribute() const + { + return _attr; + } + + sigc::signal<void>& signal_attr_changed() + { + return _signal; + } +protected: + DefaultValueHolder* get_default() { return &_default; } + const gchar* attribute_value(SPObject* o) const + { + const gchar* name = (const gchar*)sp_attribute_name(_attr); + if(name && o) { + const gchar* val = o->getRepr()->attribute(name); + return val; + } + return nullptr; + } + +private: + const SPAttr _attr; + DefaultValueHolder _default; + sigc::signal<void> _signal; +}; + +} +} +} + +#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/widget/canvas-grid.cpp b/src/ui/widget/canvas-grid.cpp new file mode 100644 index 0000000..19b21d0 --- /dev/null +++ b/src/ui/widget/canvas-grid.cpp @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/* + * Author: + * Tavmjong Bah + * + * Rewrite of code originally in desktop-widget.cpp. + * + * Copyright (C) 2020 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +// The scrollbars, and canvas are tightly coupled so it makes sense to have a dedicated +// widget to handle their interactions. The buttons are along for the ride. I don't see +// how to add the buttons easily via a .ui file (which would allow the user to put any +// buttons they want in their place). + +#include <glibmm/i18n.h> +#include <gtkmm/enums.h> +#include <gtkmm/label.h> + +#include "canvas-grid.h" + +#include "desktop.h" // Hopefully temp. +#include "desktop-events.h" // Hopefully temp. + +#include "display/control/canvas-item-drawing.h" // sticky + +#include "ui/icon-loader.h" +#include "ui/widget/canvas.h" +#include "ui/widget/ink-ruler.h" + +#include "widgets/desktop-widget.h" // Hopefully temp. + + +namespace Inkscape { +namespace UI { +namespace Widget { + +CanvasGrid::CanvasGrid(SPDesktopWidget *dtw) +{ + _dtw = dtw; + set_name("CanvasGrid"); + + // Canvas + _canvas = Gtk::manage(new Inkscape::UI::Widget::Canvas()); + _canvas->set_hexpand(true); + _canvas->set_vexpand(true); + _canvas->set_can_focus(true); + _canvas->signal_event().connect(sigc::mem_fun(*this, &CanvasGrid::SignalEvent)); // TEMP + + _canvas_overlay.add(*_canvas); + _canvas_overlay.add_overlay(*_command_palette.get_base_widget()); + + // Horizontal Scrollbar + _hadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0); + _hadj->signal_value_changed().connect(sigc::mem_fun(_dtw, &SPDesktopWidget::on_adjustment_value_changed)); + _hscrollbar = Gtk::manage(new Gtk::Scrollbar(_hadj, Gtk::ORIENTATION_HORIZONTAL)); + _hscrollbar->set_name("CanvasScrollbar"); + _hscrollbar->set_hexpand(true); + _hscrollbar->set_no_show_all(); + + // Vertical Scrollbar + _vadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0); + _vadj->signal_value_changed().connect(sigc::mem_fun(_dtw, &SPDesktopWidget::on_adjustment_value_changed)); + _vscrollbar = Gtk::manage(new Gtk::Scrollbar(_vadj, Gtk::ORIENTATION_VERTICAL)); + _vscrollbar->set_name("CanvasScrollbar"); + _vscrollbar->set_vexpand(true); + _vscrollbar->set_no_show_all(); + + // Horizontal Ruler + _hruler = Gtk::manage(new Inkscape::UI::Widget::Ruler(Gtk::ORIENTATION_HORIZONTAL)); + _hruler->add_track_widget(*_canvas); + _hruler->set_hexpand(true); + // Tooltip/Unit set elsewhere. + + // For creating guides, etc. + _hruler->signal_button_press_event().connect( + sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_press_event), _hruler, true)); + _hruler->signal_button_release_event().connect( + sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_release_event), _hruler, true)); + _hruler->signal_motion_notify_event().connect( + sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), _hruler, true)); + + // Vertical Ruler + _vruler = Gtk::manage(new Inkscape::UI::Widget::Ruler(Gtk::ORIENTATION_VERTICAL)); + _vruler->add_track_widget(*_canvas); + _vruler->set_vexpand(true); + // Tooltip/Unit set elsewhere. + + // For creating guides, etc. + _vruler->signal_button_press_event().connect( + sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_press_event), _vruler, false)); + _vruler->signal_button_release_event().connect( + sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_release_event), _vruler, false)); + _vruler->signal_motion_notify_event().connect( + sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), _vruler, false)); + + // Guide Lock + auto image1 = Gtk::manage(new Gtk::Image("object-locked", Gtk::ICON_SIZE_MENU)); + _guide_lock = Gtk::manage(new Gtk::ToggleButton()); + _guide_lock->set_name("LockGuides"); + _guide_lock->add(*image1); + _guide_lock->set_no_show_all(); + // To be replaced by Gio::Action: + _guide_lock->signal_toggled().connect(sigc::mem_fun(_dtw, &SPDesktopWidget::update_guides_lock)); + _guide_lock->set_tooltip_text(_("Toggle lock of all guides in the document")); + + // CMS Adjust + auto image2 = Gtk::manage(new Gtk::Image("color-management", Gtk::ICON_SIZE_MENU)); + _cms_adjust = Gtk::manage(new Gtk::ToggleButton()); + _cms_adjust->set_name("CMS_Adjust"); + _cms_adjust->add(*image2); + // Can't access via C++ API, fixed in Gtk4. + gtk_actionable_set_action_name( GTK_ACTIONABLE(_cms_adjust->gobj()), "win.canvas-color-manage"); + _cms_adjust->set_tooltip_text(_("Toggle color-managed display for this document window")); + _cms_adjust->set_no_show_all(); + + // Sticky Zoom + auto image3 = Gtk::manage(sp_get_icon_image("zoom-original", Gtk::ICON_SIZE_MENU)); + _sticky_zoom = Gtk::manage(new Gtk::ToggleButton()); + _sticky_zoom->set_name("StickyZoom"); + _sticky_zoom->add(*image3); + // To be replaced by Gio::Action: + _sticky_zoom->signal_toggled().connect(sigc::mem_fun(_dtw, &SPDesktopWidget::sticky_zoom_toggled)); + _sticky_zoom->set_tooltip_text(_("Zoom drawing if window size changes")); + _sticky_zoom->set_no_show_all(); + + // Top row + attach(*_guide_lock, 0, 0, 1, 1); + attach(*_hruler, 1, 0, 1, 1); + attach(*_sticky_zoom, 2, 0, 1, 1); + + // Middle row + attach(*_vruler, 0, 1, 1, 1); + attach(_canvas_overlay, 1, 1, 1, 1); + attach(*_vscrollbar, 2, 1, 1, 1); + + // Bottom row + attach(*_hscrollbar, 1, 2, 1, 1); + attach(*_cms_adjust, 2, 2, 1, 1); + + // Update rulers on size change. + signal_size_allocate().connect(sigc::mem_fun(*this, &CanvasGrid::OnSizeAllocate)); + + show_all(); +} + +CanvasGrid::~CanvasGrid() { +} + +// _dt2r should be a member of _canvas. +// get_display_area should be a member of _canvas. +void +CanvasGrid::UpdateRulers() +{ + Geom::Rect viewbox = _dtw->desktop->get_display_area().bounds(); + // Use integer values of the canvas for calculating the display area, similar + // to the integer values used for positioning the grid lines. (see Canvas::scrollTo(), + // where ix and iy are rounded integer values; these values are stored in CanvasItemBuffer->rect, + // and used for drawing the grid). By using the integer values here too, the ruler ticks + // will be perfectly aligned to the grid + double _dt2r = _dtw->_dt2r; + Geom::Point _ruler_origin = _dtw->_ruler_origin; + + double lower_x = _dt2r * (viewbox.left() - _ruler_origin[Geom::X]); + double upper_x = _dt2r * (viewbox.right() - _ruler_origin[Geom::X]); + _hruler->set_range(lower_x, upper_x); + + double lower_y = _dt2r * (viewbox.bottom() - _ruler_origin[Geom::Y]); + double upper_y = _dt2r * (viewbox.top() - _ruler_origin[Geom::Y]); + if (_dtw->desktop->is_yaxisdown()) { + std::swap(lower_y, upper_y); + } + _vruler->set_range(lower_y, upper_y); +} + +void +CanvasGrid::ShowScrollbars(bool state) +{ + _show_scrollbars = state; + + if (_show_scrollbars) { + // Show scrollbars + _hscrollbar->show(); + _vscrollbar->show(); + _cms_adjust->show(); + _cms_adjust->show_all_children(); + _sticky_zoom->show(); + _sticky_zoom->show_all_children(); + } else { + // Hide scrollbars + _hscrollbar->hide(); + _vscrollbar->hide(); + _cms_adjust->hide(); + _sticky_zoom->hide(); + } +} + +void +CanvasGrid::ToggleScrollbars() +{ + _show_scrollbars = !_show_scrollbars; + ShowScrollbars(_show_scrollbars); + + // Will be replaced by actions + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/fullscreen/scrollbars/state", _show_scrollbars); + prefs->setBool("/window/scrollbars/state", _show_scrollbars); +} + +void +CanvasGrid::ShowRulers(bool state) +{ + // Sticky zoom button is always shown. We must adjust canvas when rulers are toggled or canvas + // won't expand fully. + _show_rulers = state; + + if (_show_rulers) { + // Show rulers + _hruler->show(); + _vruler->show(); + _guide_lock->show(); + _guide_lock->show_all_children(); + remove(_canvas_overlay); + attach(_canvas_overlay, 1, 1, 1, 1); + } else { + // Hide rulers + _hruler->hide(); + _vruler->hide(); + _guide_lock->hide(); + remove(_canvas_overlay); + attach(_canvas_overlay, 1, 0, 1, 2); + } +} + +void +CanvasGrid::ToggleRulers() +{ + _show_rulers = !_show_rulers; + ShowRulers(_show_rulers); + + // Will be replaced by actions + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/fullscreen/rulers/state", _show_rulers); + prefs->setBool("/window/rulers/state", _show_rulers); +} + +void +CanvasGrid::ToggleCommandPalette() { + _command_palette.toggle(); +} + +void +CanvasGrid::ShowCommandPalette(bool state) +{ + if (state) { + _command_palette.open(); + } + _command_palette.close(); +} + +// Update rulers on change of widget size, but only if allocation really changed. +void +CanvasGrid::OnSizeAllocate(Gtk::Allocation& allocation) +{ + if (!(_allocation == allocation)) { // No != function defined! + _allocation = allocation; + UpdateRulers(); + } +} + +// This belong in Canvas class +bool +CanvasGrid::SignalEvent(GdkEvent *event) +{ + if (event->type == GDK_BUTTON_PRESS) { + _canvas->grab_focus(); + _command_palette.close(); + } + + if (event->type == GDK_BUTTON_PRESS && event->button.button == 3) { + if (event->button.state & GDK_SHIFT_MASK) { + _dtw->desktop->getCanvasDrawing()->set_sticky(true); + } else { + _dtw->desktop->getCanvasDrawing()->set_sticky(false); + } + } + + // Pass keyboard events back to the desktop root handler so TextTool can work + if ((event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) && !_canvas->get_current_canvas_item()) { + return sp_desktop_root_handler(event, _dtw->desktop); + } + return false; +} + +// TODO Add actions so we can set shortcuts. +// * Sticky Zoom +// * CMS Adjust +// * Guide Lock + + +} // namespace Widget +} // 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/widget/canvas-grid.h b/src/ui/widget/canvas-grid.h new file mode 100644 index 0000000..17a986f --- /dev/null +++ b/src/ui/widget/canvas-grid.h @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef INKSCAPE_UI_WIDGET_CANVASGRID_H +#define INKSCAPE_UI_WIDGET_CANVASGRID_H +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> +#include <gtkmm/label.h> +#include <gtkmm/overlay.h> +#include "ui/dialog/command-palette.h" + +class SPCanvas; +class SPDesktopWidget; + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Canvas; +class Ruler; + +/** + * A Gtk::Grid widget that contains rulers, scrollbars, buttons, and, of course, the canvas. + * Canvas has an overlay to let us put stuff on the canvas. + */ +class CanvasGrid : public Gtk::Grid +{ +public: + + CanvasGrid(SPDesktopWidget *dtw); + ~CanvasGrid() override; + + void ShowScrollbars(bool state = true); + void ToggleScrollbars(); + + void ShowRulers(bool state = true); + void ToggleRulers(); + void UpdateRulers(); + + void ShowCommandPalette(bool state = true); + void ToggleCommandPalette(); + + Inkscape::UI::Widget::Canvas *GetCanvas() { return _canvas; }; + + // Hopefully temp. + Inkscape::UI::Widget::Ruler *GetHRuler() { return _vruler; }; + Inkscape::UI::Widget::Ruler *GetVRuler() { return _hruler; }; + Glib::RefPtr<Gtk::Adjustment> GetHAdj() { return _hadj; }; + Glib::RefPtr<Gtk::Adjustment> GetVAdj() { return _vadj; }; + Gtk::ToggleButton *GetGuideLock() { return _guide_lock; } + Gtk::ToggleButton *GetCmsAdjust() { return _cms_adjust; } + Gtk::ToggleButton *GetStickyZoom() { return _sticky_zoom; }; + +private: + + // Signal callbacks + void OnSizeAllocate(Gtk::Allocation& allocation); + bool SignalEvent(GdkEvent *event); + + // The Widgets + Inkscape::UI::Widget::Canvas *_canvas; + + Gtk::Overlay _canvas_overlay; + + Dialog::CommandPalette _command_palette; + + Glib::RefPtr<Gtk::Adjustment> _hadj; + Glib::RefPtr<Gtk::Adjustment> _vadj; + Gtk::Scrollbar *_hscrollbar; + Gtk::Scrollbar *_vscrollbar; + + Inkscape::UI::Widget::Ruler *_hruler; + Inkscape::UI::Widget::Ruler *_vruler; + + Gtk::ToggleButton *_guide_lock; + Gtk::ToggleButton *_cms_adjust; + Gtk::ToggleButton *_sticky_zoom; + + // To be replaced by stateful Gio::Actions + bool _show_scrollbars = true; + bool _show_rulers = true; + + // Hopefully temp + SPDesktopWidget *_dtw; + + // Store allocation so we don't redraw too often. + Gtk::Allocation _allocation; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + + +#endif // INKSCAPE_UI_WIDGET_CANVASGRID_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/widget/canvas.cpp b/src/ui/widget/canvas.cpp new file mode 100644 index 0000000..8e6de8c --- /dev/null +++ b/src/ui/widget/canvas.cpp @@ -0,0 +1,2797 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/* + * Authors: + * Tavmjong Bah + * PBS <pbs3141@gmail.com> + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iostream> // Logging +#include <algorithm> // Sort +#include <set> // Coarsener + +#include <glibmm/i18n.h> + +#include <2geom/rect.h> + +#include "canvas.h" +#include "canvas-grid.h" + +#include "color.h" // Background color +#include "cms-system.h" // Color correction +#include "desktop.h" +#include "preferences.h" + +#include "display/cairo-utils.h" // Checkerboard background +#include "display/drawing.h" +#include "display/control/canvas-item-group.h" +#include "display/control/snap-indicator.h" + +#include "ui/tools/tool-base.h" // Default cursor + +#include "framecheck.h" // For frame profiling +#define framecheck_whole_function(D) auto framecheckobj = D->prefs.debug_framecheck ? FrameCheck::Event(__func__) : FrameCheck::Event(); + +/* + * The canvas is responsible for rendering the SVG drawing with various "control" + * items below and on top of the drawing. Rendering is triggered by a call to one of: + * + * + * * redraw_all() Redraws the entire canvas by calling redraw_area() with the canvas area. + * + * * redraw_area() Redraws the indicated area. Use when there is a change that doesn't affect + * a CanvasItem's geometry or size. + * + * * request_update() Redraws after recalculating bounds for changed CanvasItems. Use if a + * CanvasItem's geometry or size has changed. + * + * The first three functions add a request to the Gtk's "idle" list via + * + * * add_idle() Which causes Gtk to call when resources are available: + * + * * on_idle() Which sets up the backing stores, divides the area of the canvas that has been marked + * unclean into rectangles that are small enough to render quickly, and renders them outwards + * from the mouse with a call to: + * + * * paint_rect_internal() Which paints the rectangle using paint_single_buffer(). It renders onto a Cairo + * surface "backing_store". After a piece is rendered there is a call to: + * + * * queue_draw_area() A Gtk function for marking areas of the window as needing a repaint, which when + * the time is right calls: + * + * * on_draw() Which blits the Cairo surface to the screen. + * + * The other responsibility of the canvas is to determine where to send GUI events. It does this + * by determining which CanvasItem is "picked" and then forwards the events to that item. Not all + * items can be picked. As a last resort, the "CatchAll" CanvasItem will be picked as it is the + * lowest CanvasItem in the stack (except for the "root" CanvasItem). With a small be of work, it + * should be possible to make the "root" CanvasItem a "CatchAll" eliminating the need for a + * dedicated "CatchAll" CanvasItem. There probably could be efficiency improvements as some + * items that are not pickable probably should be which would save having to effectively pick + * them "externally" (e.g. gradient CanvasItemCurves). + */ + +namespace Inkscape { +namespace UI { +namespace Widget { + +/* + * GDK event utilities + */ + +// GdkEvents can only be safely copied using gdk_event_copy. However, this function allocates. Therefore, we need the following smart pointer to wrap the result. +struct GdkEventFreer {void operator()(GdkEvent *ev) const {gdk_event_free(ev);}}; +using GdkEventUniqPtr = std::unique_ptr<GdkEvent, GdkEventFreer>; + +// Copies a GdkEvent, returning the result as a smart pointer. +auto make_unique_copy(const GdkEvent &ev) {return GdkEventUniqPtr(gdk_event_copy(&ev));} + +/* + * Preferences + */ + +template<typename T> +struct Pref {}; + +template<typename T> +struct PrefBase +{ + const char *path; + T t, def; + std::unique_ptr<Preferences::PreferencesObserver> obs; + std::function<void()> action; + operator T() const {return t;} + PrefBase(const char *path, T def) : path(path), def(def) {} + void act() {if (action) action();} + void enable() {t = static_cast<Pref<T>*>(this)->read(); act(); obs = Inkscape::Preferences::get()->createObserver(path, [this] (const Preferences::Entry &e) {t = static_cast<Pref<T>*>(this)->changed(e); act();});} + void disable() {t = def; act(); obs.reset();} + void set_enabled(bool enabled) {enabled ? enable() : disable();} +}; + +template<> +struct Pref<bool> : PrefBase<bool> +{ + Pref(const char *path, bool def = false) : PrefBase(path, def) {enable();} + bool read() {return Inkscape::Preferences::get()->getBool(path, def);} + bool changed(const Preferences::Entry &e) {return e.getBool(def);} +}; + +template<> +struct Pref<int> : PrefBase<int> +{ + int min, max; + Pref(const char *path, int def, int min, int max) : PrefBase(path, def), min(min), max(max) {enable();} + int read() {return Inkscape::Preferences::get()->getIntLimited(path, def, min, max);} + int changed(const Preferences::Entry &e) {return e.getIntLimited(def, min, max);} +}; + +template<> +struct Pref<double> : PrefBase<double> +{ + double min, max; + Pref(const char *path, double def, double min, double max) : PrefBase(path, def), min(min), max(max) {enable();} + double read() {return Inkscape::Preferences::get()->getDoubleLimited(path, def, min, max);} + double changed(const Preferences::Entry &e) {return e.getDoubleLimited(def, min, max);} +}; + +struct Prefs +{ + // Original parameters + Pref<int> tile_size = Pref<int> ("/options/rendering/tile-size", 16, 1, 10000); + Pref<int> tile_multiplier = Pref<int> ("/options/rendering/tile-multiplier", 16, 1, 512); + Pref<int> x_ray_radius = Pref<int> ("/options/rendering/xray-radius", 100, 1, 1500); + Pref<bool> from_display = Pref<bool> ("/options/displayprofile/from_display"); + Pref<int> grabsize = Pref<int> ("/options/grabsize/value", 3, 1, 15); + Pref<int> outline_overlay_opacity = Pref<int> ("/options/rendering/outline-overlay-opacity", 50, 1, 100); + + // New parameters + Pref<int> update_strategy = Pref<int> ("/options/rendering/update_strategy", 3, 1, 3); + Pref<int> render_time_limit = Pref<int> ("/options/rendering/render_time_limit", 1000, 100, 1000000); + Pref<bool> use_new_bisector = Pref<bool> ("/options/rendering/use_new_bisector", true); + Pref<int> new_bisector_size = Pref<int> ("/options/rendering/new_bisector_size", 500, 1, 10000); + Pref<double> max_affine_diff = Pref<double>("/options/rendering/max_affine_diff", 1.8, 0.0, 100.0); + Pref<int> pad = Pref<int> ("/options/rendering/pad", 200, 0, 1000); + Pref<int> coarsener_min_size = Pref<int> ("/options/rendering/coarsener_min_size", 200, 0, 1000); + Pref<int> coarsener_glue_size = Pref<int> ("/options/rendering/coarsener_glue_size", 80, 0, 1000); + Pref<double> coarsener_min_fullness = Pref<double>("/options/rendering/coarsener_min_fullness", 0.3, 0.0, 1.0); + + // Debug switches + Pref<bool> debug_framecheck = Pref<bool> ("/options/rendering/debug_framecheck"); + Pref<bool> debug_logging = Pref<bool> ("/options/rendering/debug_logging"); + Pref<bool> debug_slow_redraw = Pref<bool> ("/options/rendering/debug_slow_redraw"); + Pref<int> debug_slow_redraw_time = Pref<int> ("/options/rendering/debug_slow_redraw_time", 50, 0, 1000000); + Pref<bool> debug_show_redraw = Pref<bool> ("/options/rendering/debug_show_redraw"); + Pref<bool> debug_show_unclean = Pref<bool> ("/options/rendering/debug_show_unclean"); + Pref<bool> debug_show_snapshot = Pref<bool> ("/options/rendering/debug_show_snapshot"); + Pref<bool> debug_show_clean = Pref<bool> ("/options/rendering/debug_show_clean"); + Pref<bool> debug_disable_redraw = Pref<bool> ("/options/rendering/debug_disable_redraw"); + Pref<bool> debug_sticky_decoupled = Pref<bool> ("/options/rendering/debug_sticky_decoupled"); + + // Developer mode + Pref<bool> devmode = Pref<bool>("/options/rendering/devmode"); + void set_devmode(bool on); +}; + +void Prefs::set_devmode(bool on) +{ + tile_size.set_enabled(on); + render_time_limit.set_enabled(on); + use_new_bisector.set_enabled(on); + new_bisector_size.set_enabled(on); + max_affine_diff.set_enabled(on); + pad.set_enabled(on); + coarsener_min_size.set_enabled(on); + coarsener_glue_size.set_enabled(on); + coarsener_min_fullness.set_enabled(on); + debug_framecheck.set_enabled(on); + debug_logging.set_enabled(on); + debug_slow_redraw.set_enabled(on); + debug_slow_redraw_time.set_enabled(on); + debug_show_redraw.set_enabled(on); + debug_show_unclean.set_enabled(on); + debug_show_snapshot.set_enabled(on); + debug_show_clean.set_enabled(on); + debug_disable_redraw.set_enabled(on); + debug_sticky_decoupled.set_enabled(on); +} + +/* + * Conversion functions + */ + +auto geom_to_cairo(Geom::IntRect rect) +{ + return Cairo::RectangleInt{rect.left(), rect.top(), rect.width(), rect.height()}; +} + +auto cairo_to_geom(Cairo::RectangleInt rect) +{ + return Geom::IntRect::from_xywh(rect.x, rect.y, rect.width, rect.height); +} + +auto geom_to_cairo(Geom::Affine affine) +{ + return Cairo::Matrix(affine[0], affine[1], affine[2], affine[3], affine[4], affine[5]); +} + +auto geom_act(Geom::Affine a, Geom::IntPoint p) +{ + Geom::Point p2 = p; + p2 *= a; + return Geom::IntPoint(std::round(p2.x()), std::round(p2.y())); +} + +void region_to_path(const Cairo::RefPtr<Cairo::Context> &cr, const Cairo::RefPtr<Cairo::Region> ®) +{ + for (int i = 0; i < reg->get_num_rectangles(); i++) { + auto rect = reg->get_rectangle(i); + cr->rectangle(rect.x, rect.y, rect.width, rect.height); + } +} + +/* + * Update strategy + */ + +// A class hierarchy for controlling what order to update invalidated regions. +class Updater +{ +public: + // The subregion of the store with up-to-date content. + Cairo::RefPtr<Cairo::Region> clean_region; + + Updater(Cairo::RefPtr<Cairo::Region> clean_region) : clean_region(std::move(clean_region)) {} + + virtual void reset() {clean_region = Cairo::Region::create();} // Reset the clean region to empty. + virtual void intersect (const Geom::IntRect &rect) {clean_region->intersect(geom_to_cairo(rect));} // Called when the store changes position; clip everything to the new store rectangle. + virtual void mark_dirty(const Geom::IntRect &rect) {clean_region->subtract (geom_to_cairo(rect));} // Called on every invalidate event. + virtual void mark_clean(const Geom::IntRect &rect) {clean_region->do_union (geom_to_cairo(rect));} // Called on every rectangle redrawn. + + virtual Cairo::RefPtr<Cairo::Region> get_next_clean_region() {return clean_region;}; // Called by on_idle to determine what regions to consider clean for the current redraw. + virtual bool report_finished () {return false;} // Called in on_idle if the redraw has finished. Returns true to indicate that further redraws are required with a different clean region. + virtual void frame () {} // Called by on_draw to notify the updater of the display of the frame. + virtual ~Updater() = default; +}; + +// Responsive updater: As soon as a region is invalidated, redraw it. +using ResponsiveUpdater = Updater; + +// Full redraw updater: When a region is invalidated, delay redraw until after the current redraw is completed. +class FullredrawUpdater : public Updater +{ + // Whether we are currently in the middle of a redraw. + bool inprogress = false; + + // Contains a copy of the old clean region if damage events occurred during the current redraw, otherwise null. + Cairo::RefPtr<Cairo::Region> old_clean_region; + +public: + + FullredrawUpdater(Cairo::RefPtr<Cairo::Region> clean_region) : Updater(std::move(clean_region)) {} + + void reset() override + { + Updater::reset(); + inprogress = false; + old_clean_region.clear(); + } + + void intersect(const Geom::IntRect &rect) override + { + Updater::intersect(rect); + if (old_clean_region) old_clean_region->intersect(geom_to_cairo(rect)); + } + + void mark_dirty(const Geom::IntRect &rect) override + { + if (inprogress && !old_clean_region) old_clean_region = clean_region->copy(); + Updater::mark_dirty(rect); + } + + void mark_clean(const Geom::IntRect &rect) override + { + Updater::mark_clean(rect); + if (old_clean_region) old_clean_region->do_union(geom_to_cairo(rect)); + } + + Cairo::RefPtr<Cairo::Region> get_next_clean_region() override + { + inprogress = true; + if (!old_clean_region) { + return clean_region; + } else { + return old_clean_region; + } + } + + bool report_finished() override + { + assert(inprogress); + if (!old_clean_region) { + // Completed redraw without being damaged => finished. + inprogress = false; + return false; + } else { + // Completed redraw but damage events arrived => ask for another redraw, using the up-to-date clean region. + old_clean_region.clear(); + return true; + } + } +}; + +// Multiscale updater: Updates tiles near the mouse faster. Gives the best of both. +class MultiscaleUpdater : public Updater +{ + // Whether we are currently in the middle of a redraw. + bool inprogress = false; + + // Whether damage events occurred during the current redraw. + bool activated = false; + + int counter; // A steadily incrementing counter from which the current scale is derived. + int scale; // The current scale to process updates at. + int elapsed; // How much time has been spent at the current scale. + std::vector<Cairo::RefPtr<Cairo::Region>> blocked; // The region blocked from being updated at each scale. + +public: + + MultiscaleUpdater(Cairo::RefPtr<Cairo::Region> clean_region) : Updater(std::move(clean_region)) {} + + void reset() override + { + Updater::reset(); + inprogress = activated = false; + } + + void intersect(const Geom::IntRect &rect) override + { + Updater::intersect(rect); + if (activated) { + for (auto ® : blocked) { + reg->intersect(geom_to_cairo(rect)); + } + } + } + + void mark_dirty(const Geom::IntRect &rect) override + { + Updater::mark_dirty(rect); + if (inprogress && !activated) { + counter = scale = elapsed = 0; + blocked = {Cairo::Region::create()}; + activated = true; + } + } + + void mark_clean(const Geom::IntRect &rect) override + { + Updater::mark_clean(rect); + if (activated) blocked[scale]->do_union(geom_to_cairo(rect)); + } + + Cairo::RefPtr<Cairo::Region> get_next_clean_region() override + { + inprogress = true; + if (!activated) { + return clean_region; + } else { + auto result = clean_region->copy(); + result->do_union(blocked[scale]); + return result; + } + } + + bool report_finished() override + { + assert(inprogress); + if (!activated) { + // Completed redraw without damage => finished. + inprogress = false; + return false; + } else { + // Completed redraw but damage events arrived => begin updating any remaining damaged regions. + activated = false; + blocked.clear(); + return true; + } + } + + void frame() override + { + if (!activated) return; + + // Stay at the current scale for 2^scale frames. + elapsed++; + if (elapsed < (1 << scale)) return; + elapsed = 0; + + // Adjust the counter, which causes scale to hop around the values 0, 1, 2... spending half as much time at each subsequent scale. + counter++; + scale = 0; + for (int tmp = counter; tmp % 2 == 1; tmp /= 2) { + scale++; + } + + // Ensure sufficiently many blocked zones exist. + if (scale == blocked.size()) { + blocked.emplace_back(); + } + + // Recreate the current blocked zone as the union of the clean region and lower-scale blocked zones. + blocked[scale] = clean_region->copy(); + for (int i = 0; i < scale; i++) { + blocked[scale]->do_union(blocked[i]); + } + } +}; + +std::unique_ptr<Updater> +make_updater(int type, Cairo::RefPtr<Cairo::Region> clean_region = Cairo::Region::create()) +{ + switch (type) { + case 1: return std::make_unique<ResponsiveUpdater>(std::move(clean_region)); + case 2: return std::make_unique<FullredrawUpdater>(std::move(clean_region)); + default: + case 3: return std::make_unique<MultiscaleUpdater>(std::move(clean_region)); + } +} + +/* + * Implementation class + */ + +class CanvasPrivate +{ +public: + + friend class Canvas; + Canvas *q; + CanvasPrivate(Canvas *q) : q(q) {} + + // Lifecycle + bool active = false; + void update_active(); + void activate(); + void deactivate(); + + // Preferences + Prefs prefs; + + // Update strategy; tracks the unclean region and decides how to redraw it. + std::unique_ptr<Updater> updater; + + // Event processor. Events that interact with the Canvas are buffered here until the start of the next frame. They are processed by a separate object so that deleting the Canvas mid-event can be done safely. + struct EventProcessor + { + std::vector<GdkEventUniqPtr> events; + int pos; + GdkEvent *ignore = nullptr; + CanvasPrivate *canvasprivate; // Nulled on destruction. + bool in_processing = false; // For handling recursion due to nested GTK main loops. + void process(); + int gobble_key_events(guint keyval, guint mask); + void gobble_motion_events(guint mask); + }; + std::shared_ptr<EventProcessor> eventprocessor; // Usually held by CanvasPrivate, but temporarily also held by itself while processing so that it is not deleted mid-event. + bool add_to_bucket(GdkEvent*); + bool process_bucketed_event(const GdkEvent&); + bool pick_current_item(const GdkEvent&); + bool emit_event(const GdkEvent&); + Inkscape::CanvasItem *pre_scroll_grabbed_item; + + // State for determining when to run event processor. + bool pending_draw = false; + sigc::connection bucket_emptier; + std::optional<guint> bucket_emptier_tick_callback; + void schedule_bucket_emptier(); + + // Idle system. The high priority idle ensures at least one idle cycle between add_idle and on_draw. + void add_idle(); + sigc::connection hipri_idle; + sigc::connection lopri_idle; + bool on_hipri_idle(); + bool on_lopri_idle(); + bool idle_running = false; + + // Important global properties of all the stores. If these change, all the stores must be recreated. + int _device_scale = 1; + bool _store_solid_background; + + // The backing store. + Geom::IntRect _store_rect; + Geom::Affine _store_affine; + Cairo::RefPtr<Cairo::ImageSurface> _backing_store, _outline_store; + + // The snapshot store. Used to mask redraw delay on zoom/rotate. + Geom::IntRect _snapshot_rect; + Geom::Affine _snapshot_affine; + Geom::IntPoint _snapshot_static_offset = {0, 0}; + Cairo::RefPtr<Cairo::ImageSurface> _snapshot_store, _snapshot_outline_store; + Cairo::RefPtr<Cairo::Region> _snapshot_clean_region; + + Geom::Affine geom_affine; // The affine the geometry was last imbued with. + bool decoupled_mode = false; + + bool solid_background; // Whether the last background set is solid. + bool need_outline_store() const {return q->_split_mode != Inkscape::SplitMode::NORMAL || q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY;} + + // Drawing + bool on_idle(); + void paint_rect_internal(Geom::IntRect const &rect); + void paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr<Cairo::ImageSurface> const &store, bool is_backing_store, bool outline_overlay_pass); + std::optional<Geom::Dim2> old_bisector(const Geom::IntRect &rect); + std::optional<Geom::Dim2> new_bisector(const Geom::IntRect &rect); + + // Trivial overload of GtkWidget function. + void queue_draw_area(Geom::IntRect &rect); + + // For tracking the last known mouse position. (The function Gdk::Window::get_device_position cannot be used because of slow X11 round-trips. Remove this workaround when X11 dies.) + std::optional<Geom::Point> last_mouse; +}; + +/* + * Lifecycle + */ + +Canvas::Canvas() + : d(std::make_unique<CanvasPrivate>(this)) +{ + set_name("InkscapeCanvas"); + + // Events + add_events(Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::ENTER_NOTIFY_MASK | + Gdk::LEAVE_NOTIFY_MASK | + Gdk::FOCUS_CHANGE_MASK | + Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK | + Gdk::SCROLL_MASK | + Gdk::SMOOTH_SCROLL_MASK ); + + // Set up EventProcessor + d->eventprocessor = std::make_shared<CanvasPrivate::EventProcessor>(); + d->eventprocessor->canvasprivate = d.get(); + + // Updater + d->updater = make_updater(d->prefs.update_strategy); + + // Preferences + d->prefs.grabsize.action = [=] {_canvas_item_root->update_canvas_item_ctrl_sizes(d->prefs.grabsize);}; + d->prefs.debug_show_unclean.action = [=] {queue_draw();}; + d->prefs.debug_show_clean.action = [=] {queue_draw();}; + d->prefs.debug_disable_redraw.action = [=] {d->add_idle();}; + d->prefs.debug_sticky_decoupled.action = [=] {d->add_idle();}; + d->prefs.update_strategy.action = [=] {d->updater = make_updater(d->prefs.update_strategy, std::move(d->updater->clean_region));}; + d->prefs.outline_overlay_opacity.action = [=] {queue_draw();}; + + // Developer mode master switch + d->prefs.devmode.action = [=] {d->prefs.set_devmode(d->prefs.devmode);}; + d->prefs.devmode.action(); + + // Cavas item root + _canvas_item_root = new Inkscape::CanvasItemGroup(nullptr); + _canvas_item_root->set_name("CanvasItemGroup:Root"); + _canvas_item_root->set_canvas(this); + + // Background + _background = Cairo::SolidPattern::create_rgb(1.0, 1.0, 1.0); + d->solid_background = true; +} + +void CanvasPrivate::activate() +{ + // Event handling/item picking + q->_pick_event.type = GDK_LEAVE_NOTIFY; + q->_pick_event.crossing.x = 0; + q->_pick_event.crossing.y = 0; + + q->_in_repick = false; + q->_left_grabbed_item = false; + q->_all_enter_events = false; + q->_is_dragging = false; + q->_state = 0; + + q->_current_canvas_item = nullptr; + q->_current_canvas_item_new = nullptr; + q->_grabbed_canvas_item = nullptr; + q->_grabbed_event_mask = (Gdk::EventMask)0; + pre_scroll_grabbed_item = nullptr; + + // Drawing + q->_drawing_disabled = false; + q->_need_update = true; + + // Split view + q->_split_direction = Inkscape::SplitDirection::EAST; + q->_split_position = {-1, -1}; // initialize with off-canvas coordinates + q->_hover_direction = Inkscape::SplitDirection::NONE; + q->_split_dragging = false; + + add_idle(); +} + +void CanvasPrivate::deactivate() +{ + // Disconnect signals and timeouts. (Note: They will never be rescheduled while inactive.) + hipri_idle.disconnect(); + lopri_idle.disconnect(); + bucket_emptier.disconnect(); + if (bucket_emptier_tick_callback) q->remove_tick_callback(*bucket_emptier_tick_callback); +} + +Canvas::~Canvas() +{ + // Not necessary as GTK guarantees realization is always followed by unrealization. But just in case that invariant breaks, we deal with it. + if (d->active) { + std::cerr << "Canvas destructed while realized!" << std::endl; + d->deactivate(); + } + + // Disconnect from EventProcessor. + d->eventprocessor->canvasprivate = nullptr; + + // Remove entire CanvasItem tree. + delete _canvas_item_root; +} + +void CanvasPrivate::update_active() +{ + bool new_active = q->_drawing && q->get_realized(); + if (new_active != active) { + active = new_active; + active ? activate() : deactivate(); + } +} + +void Canvas::set_drawing(Drawing *drawing) +{ + _drawing = drawing; + d->update_active(); +} + +void +Canvas::on_realize() +{ + parent_type::on_realize(); + assert(get_realized()); + d->update_active(); +} + +void Canvas::on_unrealize() +{ + parent_type::on_unrealize(); + assert(!get_realized()); + d->update_active(); +} + +/* + * Events system + */ + +// The following protected functions of Canvas are where all incoming events initially arrive. +// Those that do not interact with the Canvas are processed instantaneously, while the rest are +// delayed by placing them into the bucket. + +bool +Canvas::on_scroll_event(GdkEventScroll *scroll_event) +{ + return d->add_to_bucket(reinterpret_cast<GdkEvent*>(scroll_event)); +} + +bool +Canvas::on_button_press_event(GdkEventButton *button_event) +{ + return on_button_event(button_event); +} + +bool +Canvas::on_button_release_event(GdkEventButton *button_event) +{ + return on_button_event(button_event); +} + +// Unified handler for press and release events. +bool +Canvas::on_button_event(GdkEventButton *button_event) +{ + // Sanity-check event type. + switch (button_event->type) { + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + break; // Good + default: + std::cerr << "Canvas::on_button_event: illegal event type!" << std::endl; + return false; + } + + // Drag the split view controller. + switch (button_event->type) { + case GDK_BUTTON_PRESS: + if (_hover_direction != Inkscape::SplitDirection::NONE) { + _split_dragging = true; + _split_drag_start = Geom::Point(button_event->x, button_event->y); + return true; + } + break; + case GDK_2BUTTON_PRESS: + if (_hover_direction != Inkscape::SplitDirection::NONE) { + _split_direction = _hover_direction; + _split_dragging = false; + queue_draw(); + return true; + } + break; + case GDK_BUTTON_RELEASE: + _split_dragging = false; + break; + } + + // Otherwise, handle as a delayed event. + return d->add_to_bucket(reinterpret_cast<GdkEvent*>(button_event)); +} + +bool +Canvas::on_enter_notify_event(GdkEventCrossing *crossing_event) +{ + if (crossing_event->window != get_window()->gobj()) { + return false; + } + return d->add_to_bucket(reinterpret_cast<GdkEvent*>(crossing_event)); +} + +bool +Canvas::on_leave_notify_event(GdkEventCrossing *crossing_event) +{ + if (crossing_event->window != get_window()->gobj()) { + return false; + } + d->last_mouse = {}; + return d->add_to_bucket(reinterpret_cast<GdkEvent*>(crossing_event)); +} + +bool +Canvas::on_focus_in_event(GdkEventFocus *focus_event) +{ + grab_focus(); + return false; +} + +bool +Canvas::on_key_press_event(GdkEventKey *key_event) +{ + return d->add_to_bucket(reinterpret_cast<GdkEvent*>(key_event)); +} + +bool +Canvas::on_key_release_event(GdkEventKey *key_event) +{ + return d->add_to_bucket(reinterpret_cast<GdkEvent*>(key_event)); +} + +bool +Canvas::on_motion_notify_event(GdkEventMotion *motion_event) +{ + // Record the last mouse position. + d->last_mouse = Geom::Point(motion_event->x, motion_event->y); + + // Handle interactions with the split view controller. + Geom::IntPoint cursor_position = Geom::IntPoint(motion_event->x, motion_event->y); + + // Check if we are near the edge. If so, revert to normal mode. + if (_split_mode == Inkscape::SplitMode::SPLIT && _split_dragging) { + if (cursor_position.x() < 5 || + cursor_position.y() < 5 || + cursor_position.x() - get_allocation().get_width() > -5 || + cursor_position.y() - get_allocation().get_height() > -5 ) { + + // Reset everything. + _split_mode = Inkscape::SplitMode::NORMAL; + _split_position = Geom::Point(-1, -1); + _hover_direction = Inkscape::SplitDirection::NONE; + set_cursor(); + queue_draw(); + + // Update action (turn into utility function?). + auto window = dynamic_cast<Gtk::ApplicationWindow *>(get_toplevel()); + if (!window) { + std::cerr << "Canvas::on_motion_notify_event: window missing!" << std::endl; + return true; + } + + auto action = window->lookup_action("canvas-split-mode"); + if (!action) { + std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' missing!" << std::endl; + return true; + } + + auto saction = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic(action); + if (!saction) { + std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' not SimpleAction!" << std::endl; + return true; + } + + saction->change_state((int)Inkscape::SplitMode::NORMAL); + + return true; + } + } + + if (_split_mode == Inkscape::SplitMode::XRAY) { + _split_position = cursor_position; + queue_draw(); + } + + if (_split_mode == Inkscape::SplitMode::SPLIT) { + Inkscape::SplitDirection hover_direction = Inkscape::SplitDirection::NONE; + Geom::Point difference(cursor_position - _split_position); + + // Move controller + if (_split_dragging) { + Geom::Point delta = cursor_position - _split_drag_start; // We don't use _split_position + if (_hover_direction == Inkscape::SplitDirection::HORIZONTAL) { + _split_position += Geom::Point(0, delta.y()); + } else if (_hover_direction == Inkscape::SplitDirection::VERTICAL) { + _split_position += Geom::Point(delta.x(), 0); + } else { + _split_position += delta; + } + _split_drag_start = cursor_position; + queue_draw(); + return true; + } + + if (Geom::distance(cursor_position, _split_position) < 20 * d->_device_scale) { + // We're hovering over circle, figure out which direction we are in. + if (difference.y() - difference.x() > 0) { + if (difference.y() + difference.x() > 0) { + hover_direction = Inkscape::SplitDirection::SOUTH; + } else { + hover_direction = Inkscape::SplitDirection::WEST; + } + } else { + if (difference.y() + difference.x() > 0) { + hover_direction = Inkscape::SplitDirection::EAST; + } else { + hover_direction = Inkscape::SplitDirection::NORTH; + } + } + } else if (_split_direction == Inkscape::SplitDirection::NORTH || + _split_direction == Inkscape::SplitDirection::SOUTH) { + if (std::abs(difference.y()) < 3 * d->_device_scale) { + // We're hovering over horizontal line + hover_direction = Inkscape::SplitDirection::HORIZONTAL; + } + } else { + if (std::abs(difference.x()) < 3 * d->_device_scale) { + // We're hovering over vertical line + hover_direction = Inkscape::SplitDirection::VERTICAL; + } + } + + if (_hover_direction != hover_direction) { + _hover_direction = hover_direction; + set_cursor(); + queue_draw(); + } + + if (_hover_direction != Inkscape::SplitDirection::NONE) { + // We're hovering, don't pick or emit event. + return true; + } + } + + // Otherwise, handle as a delayed event. + return d->add_to_bucket(reinterpret_cast<GdkEvent*>(motion_event)); +} + +// Most events end up here. We store them in the bucket, and process them as soon as possible after +// the next 'on_draw'. If 'on_draw' isn't pending, we use the 'tick_callback' signal to process them +// when 'on_draw' would have run anyway. If 'on_draw' later becomes pending, we remove this signal. + +// Add an event to the bucket and ensure it will be emptied in the near future. +bool +CanvasPrivate::add_to_bucket(GdkEvent *event) +{ + framecheck_whole_function(this) + + if (!active) { + std::cerr << "Canvas::add_to_bucket: Called while not active!" << std::endl; + return false; + } + + // Prevent re-fired events from going through again. + if (event == eventprocessor->ignore) { + return false; + } + + // If this is the first event, ensure event processing will run on the main loop as soon as possible after the next frame has started. + if (eventprocessor->events.empty() && !pending_draw) { +#ifndef NDEBUG + if (bucket_emptier_tick_callback) { + g_warning("bucket_emptier_tick_callback not empty"); + } +#endif + bucket_emptier_tick_callback = q->add_tick_callback([this] (const Glib::RefPtr<Gdk::FrameClock>&) { + assert(active); + bucket_emptier_tick_callback.reset(); + schedule_bucket_emptier(); + return false; + }); + } + + // Add a copy to the queue. + eventprocessor->events.emplace_back(gdk_event_copy(event)); + + // Tell GTK the event was handled. + return true; +} + +void CanvasPrivate::schedule_bucket_emptier() +{ + if (!active) { + std::cerr << "Canvas::schedule_bucket_emptier: Called while not active!" << std::endl; + return; + } + + if (!bucket_emptier.connected()) { + bucket_emptier = Glib::signal_idle().connect([this] { + assert(active); + eventprocessor->process(); + return false; + }, G_PRIORITY_HIGH_IDLE + 14); // before hipri_idle + } +} + +// The following functions run at the start of the next frame on the GTK main loop. +// (Note: It is crucial that it runs on the main loop and not in any frame clock tick callbacks. GTK does not allow widgets to be deleted in the latter; only the former.) + +// Process bucketed events. +void +CanvasPrivate::EventProcessor::process() +{ + framecheck_whole_function(canvasprivate) + + // Ensure the EventProcessor continues to live even if the Canvas is destroyed during event processing. + auto self = canvasprivate->eventprocessor; + + // Check if toplevel or recursive. (Recursive calls happen if processing an event starts its own nested GTK main loop.) + bool toplevel = !in_processing; + in_processing = true; + + // If toplevel, initialise the iteration index. It may be incremented externally by gobblers or recursive calls. + if (toplevel) { + pos = 0; + } + + while (pos < events.size()) { + // Extract next event. + auto event = std::move(events[pos]); + pos++; + + // Fire the event at the CanvasItems and see if it was handled. + bool handled = canvasprivate->process_bucketed_event(*event); + + if (!handled) { + // Re-fire the event at the window, and ignore it when it comes back here again. + ignore = event.get(); + canvasprivate->q->get_toplevel()->event(event.get()); + ignore = nullptr; + } + + // If the Canvas was destroyed or deactivated during event processing, exit now. + if (!canvasprivate || !canvasprivate->active) return; + } + + // Otherwise, clear the list of events that was just processed. + events.clear(); + + // Reset the variable to track recursive calls. + if (toplevel) { + in_processing = false; + } +} + +// Called during event processing by some tools to batch backlogs of key events that may have built up after a freeze. +int +Canvas::gobble_key_events(guint keyval, guint mask) +{ + return d->eventprocessor->gobble_key_events(keyval, mask); +} + +int +CanvasPrivate::EventProcessor::gobble_key_events(guint keyval, guint mask) +{ + int count = 0; + + while (pos < events.size()) { + auto &event = events[pos]; + if ((event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) && event->key.keyval == keyval && (!mask || (event->key.state & mask))) { + // Discard event and continue. + if (event->type == GDK_KEY_PRESS) count++; + pos++; + } + else { + // Stop discarding. + break; + } + } + + if (count > 0 && canvasprivate->prefs.debug_logging) std::cout << "Gobbled " << count << " key press(es)" << std::endl; + + return count; +} + +// Called during event processing by some tools to ignore backlogs of motion events that may have built up after a freeze. +void +Canvas::gobble_motion_events(guint mask) +{ + d->eventprocessor->gobble_motion_events(mask); +} + +void +CanvasPrivate::EventProcessor::gobble_motion_events(guint mask) +{ + int count = 0; + + while (pos < events.size()) { + auto &event = events[pos]; + if (event->type == GDK_MOTION_NOTIFY && (event->motion.state & mask)) { + // Discard event and continue. + count++; + pos++; + } + else { + // Stop discarding. + break; + } + } + + if (count > 0 && canvasprivate->prefs.debug_logging) std::cout << "Gobbled " << count << " motion event(s)" << std::endl; +} + +// From now on Inkscape's regular event processing logic takes place. The only thing to remember is that +// all of this happens at a slight delay after the original GTK events. Therefore, it's important to make +// sure that stateful variables like '_current_canvas_item' and friends are ONLY read/written within these +// functions, not during the earlier GTK event handlers. Otherwise state confusion will ensue. + +bool +CanvasPrivate::process_bucketed_event(const GdkEvent &event) +{ + auto calc_button_mask = [&] () -> int { + switch (event.button.button) { + case 1: return GDK_BUTTON1_MASK; break; + case 2: return GDK_BUTTON2_MASK; break; + case 3: return GDK_BUTTON3_MASK; break; + case 4: return GDK_BUTTON4_MASK; break; + case 5: return GDK_BUTTON5_MASK; break; + default: return 0; // Buttons can range at least to 9 but mask defined only to 5. + } + }; + + // Do event-specific processing. + switch (event.type) { + + case GDK_SCROLL: + { + // Save the current event-receiving item just before scrolling starts. It will continue to receive scroll events until the mouse is moved. + if (!pre_scroll_grabbed_item) { + pre_scroll_grabbed_item = q->_current_canvas_item; + if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) { + pre_scroll_grabbed_item = q->_grabbed_canvas_item; + } + } + + // Process the scroll event... + bool retval = emit_event(event); + + // ...then repick. + q->_state = event.scroll.state; + pick_current_item(event); + + return retval; + } + + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + { + pre_scroll_grabbed_item = nullptr; + + // Pick the current item as if the button were not pressed... + q->_state = event.button.state; + pick_current_item(event); + + // ...then process the event. + q->_state ^= calc_button_mask(); + bool retval = emit_event(event); + + return retval; + } + + case GDK_BUTTON_RELEASE: + { + pre_scroll_grabbed_item = nullptr; + + // Process the event as if the button were pressed... + q->_state = event.button.state; + bool retval = emit_event(event); + + // ...then repick after the button has been released. + auto event_copy = make_unique_copy(event); + event_copy->button.state ^= calc_button_mask(); + q->_state = event_copy->button.state; + pick_current_item(*event_copy); + + return retval; + } + + case GDK_ENTER_NOTIFY: + pre_scroll_grabbed_item = nullptr; + q->_state = event.crossing.state; + return pick_current_item(event); + + case GDK_LEAVE_NOTIFY: + pre_scroll_grabbed_item = nullptr; + q->_state = event.crossing.state; + // This is needed to remove alignment or distribution snap indicators. + if (q->_desktop) { + q->_desktop->snapindicator->remove_snaptarget(); + } + return pick_current_item(event); + + case GDK_KEY_PRESS: + case GDK_KEY_RELEASE: + return emit_event(event); + + case GDK_MOTION_NOTIFY: + pre_scroll_grabbed_item = nullptr; + q->_state = event.motion.state; + pick_current_item(event); + return emit_event(event); + + default: + return false; + } +} + +// This function is called by 'process_bucketed_event' to manipulate the state variables relating +// to the current object under the mouse, for example, to generate enter and leave events. +// (A more detailed explanation by Tavmjong follows.) +// -------- +// This routine reacts to events from the canvas. It's main purpose is to find the canvas item +// closest to the cursor where the event occurred and then send the event (sometimes modified) to +// that item. The event then bubbles up the canvas item tree until an object handles it. If the +// widget is redrawn, this routine may be called again for the same event. +// +// Canvas items register their interest by connecting to the "event" signal. +// Example in desktop.cpp: +// canvas_catchall->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), this)); +bool +CanvasPrivate::pick_current_item(const GdkEvent &event) +{ + // Ensure requested geometry updates are performed first. + if (q->_need_update) { + q->_canvas_item_root->update(geom_affine); + q->_need_update = false; + } + + int button_down = 0; + if (!q->_all_enter_events) { + // Only set true in connector-tool.cpp. + + // If a button is down, we'll perform enter and leave events on the + // current item, but not enter on any other item. This is more or + // less like X pointer grabbing for canvas items. + button_down = q->_state & (GDK_BUTTON1_MASK | + GDK_BUTTON2_MASK | + GDK_BUTTON3_MASK | + GDK_BUTTON4_MASK | + GDK_BUTTON5_MASK); + if (!button_down) q->_left_grabbed_item = false; + } + + // Save the event in the canvas. This is used to synthesize enter and + // leave events in case the current item changes. It is also used to + // re-pick the current item if the current one gets deleted. Also, + // synthesize an enter event. + if (&event != &q->_pick_event) { + if (event.type == GDK_MOTION_NOTIFY || event.type == GDK_SCROLL || event.type == GDK_BUTTON_RELEASE) { + // Convert to GDK_ENTER_NOTIFY + + // These fields have the same offsets in all types of events. + q->_pick_event.crossing.type = GDK_ENTER_NOTIFY; + q->_pick_event.crossing.window = event.motion.window; + q->_pick_event.crossing.send_event = event.motion.send_event; + q->_pick_event.crossing.subwindow = nullptr; + q->_pick_event.crossing.x = event.motion.x; + q->_pick_event.crossing.y = event.motion.y; + q->_pick_event.crossing.mode = GDK_CROSSING_NORMAL; + q->_pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR; + q->_pick_event.crossing.focus = false; + + // These fields don't have the same offsets in all types of events. + // (Todo: With C++20, can reduce the code repetition here using a templated lambda.) + switch (event.type) + { + case GDK_MOTION_NOTIFY: + q->_pick_event.crossing.state = event.motion.state; + q->_pick_event.crossing.x_root = event.motion.x_root; + q->_pick_event.crossing.y_root = event.motion.y_root; + break; + case GDK_SCROLL: + q->_pick_event.crossing.state = event.scroll.state; + q->_pick_event.crossing.x_root = event.scroll.x_root; + q->_pick_event.crossing.y_root = event.scroll.y_root; + break; + case GDK_BUTTON_RELEASE: + q->_pick_event.crossing.state = event.button.state; + q->_pick_event.crossing.x_root = event.button.x_root; + q->_pick_event.crossing.y_root = event.button.y_root; + break; + default: + assert(false); + } + + } else { + q->_pick_event = event; + } + } + + if (q->_in_repick) { + // Don't do anything else if this is a recursive call. + return false; + } + + // Find new item + q->_current_canvas_item_new = nullptr; + + if (q->_pick_event.type != GDK_LEAVE_NOTIFY && q->_canvas_item_root->is_visible()) { + // Leave notify means there is no current item. + // Find closest item. + double x = 0.0; + double y = 0.0; + + if (q->_pick_event.type == GDK_ENTER_NOTIFY) { + x = q->_pick_event.crossing.x; + y = q->_pick_event.crossing.y; + } else { + x = q->_pick_event.motion.x; + y = q->_pick_event.motion.y; + } + + // If in split mode, look at where cursor is to see if one should pick with outline mode. + if (q->_split_mode == Inkscape::SplitMode::SPLIT && q->_render_mode != Inkscape::RenderMode::OUTLINE_OVERLAY) { + if ((q->_split_direction == Inkscape::SplitDirection::NORTH && y > q->_split_position.y()) || + (q->_split_direction == Inkscape::SplitDirection::SOUTH && y < q->_split_position.y()) || + (q->_split_direction == Inkscape::SplitDirection::WEST && x > q->_split_position.x()) || + (q->_split_direction == Inkscape::SplitDirection::EAST && x < q->_split_position.x()) ) { + q->_drawing->setRenderMode(Inkscape::RenderMode::OUTLINE); + } + } + // Convert to world coordinates. + auto p = Geom::Point(x, y) + q->_pos; + if (decoupled_mode) { + p *= _store_affine * q->_affine.inverse(); + } + + q->_current_canvas_item_new = q->_canvas_item_root->pick_item(p); + // if (q->_current_canvas_item_new) { + // std::cout << " PICKING: FOUND ITEM: " << q->_current_canvas_item_new->get_name() << std::endl; + // } else { + // std::cout << " PICKING: DID NOT FIND ITEM" << std::endl; + // } + + // Reset the drawing back to the requested render mode. + q->_drawing->setRenderMode(q->_render_mode); + } + + if (q->_current_canvas_item_new == q->_current_canvas_item && !q->_left_grabbed_item) { + // Current item did not change! + return false; + } + + // Synthesize events for old and new current items. + bool retval = false; + if (q->_current_canvas_item_new != q->_current_canvas_item && + q->_current_canvas_item != nullptr && + !q->_left_grabbed_item ) { + + GdkEvent new_event; + new_event = q->_pick_event; + new_event.type = GDK_LEAVE_NOTIFY; + new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; + new_event.crossing.subwindow = nullptr; + q->_in_repick = true; + retval = emit_event(new_event); + q->_in_repick = false; + } + + if (q->_all_enter_events == false) { + // new_current_item may have been set to nullptr during the call to emitEvent() above. + if (q->_current_canvas_item_new != q->_current_canvas_item && button_down) { + q->_left_grabbed_item = true; + return retval; + } + } + + // Handle the rest of cases + q->_left_grabbed_item = false; + q->_current_canvas_item = q->_current_canvas_item_new; + + if (q->_current_canvas_item != nullptr) { + GdkEvent new_event; + new_event = q->_pick_event; + new_event.type = GDK_ENTER_NOTIFY; + new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; + new_event.crossing.subwindow = nullptr; + retval = emit_event(new_event); + } + + return retval; +} + +// Fires an event at the canvas, after a little pre-processing. Returns true if handled. +bool +CanvasPrivate::emit_event(const GdkEvent &event) +{ + // Handle grabbed items. + if (q->_grabbed_canvas_item) { + auto mask = (Gdk::EventMask)0; + + switch (event.type) { + case GDK_ENTER_NOTIFY: + mask = Gdk::ENTER_NOTIFY_MASK; + break; + case GDK_LEAVE_NOTIFY: + mask = Gdk::LEAVE_NOTIFY_MASK; + break; + case GDK_MOTION_NOTIFY: + mask = Gdk::POINTER_MOTION_MASK; + break; + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + mask = Gdk::BUTTON_PRESS_MASK; + break; + case GDK_BUTTON_RELEASE: + mask = Gdk::BUTTON_RELEASE_MASK; + break; + case GDK_KEY_PRESS: + mask = Gdk::KEY_PRESS_MASK; + break; + case GDK_KEY_RELEASE: + mask = Gdk::KEY_RELEASE_MASK; + break; + case GDK_SCROLL: + mask = Gdk::SCROLL_MASK; + mask |= Gdk::SMOOTH_SCROLL_MASK; + break; + default: + break; + } + + if (!(mask & q->_grabbed_event_mask)) { + return false; + } + } + + // Convert to world coordinates. We have two different cases due to different event structures. + auto conv = [&, this] (double &x, double &y) { + auto p = Geom::Point(x, y) + q->_pos; + if (decoupled_mode) { + p *= _store_affine * q->_affine.inverse(); + } + x = p.x(); + y = p.y(); + }; + + auto event_copy = make_unique_copy(event); + + switch (event.type) { + case GDK_ENTER_NOTIFY: + case GDK_LEAVE_NOTIFY: + conv(event_copy->crossing.x, event_copy->crossing.y); + break; + case GDK_MOTION_NOTIFY: + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + conv(event_copy->motion.x, event_copy->motion.y); + break; + default: + break; + } + + // Block undo/redo while anything is dragged. + if (event.type == GDK_BUTTON_PRESS && event.button.button == 1) { + q->_is_dragging = true; + } else if (event.type == GDK_BUTTON_RELEASE) { + q->_is_dragging = false; + } + + if (q->_current_canvas_item) { + // Choose where to send event. + auto item = q->_current_canvas_item; + + if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) { + item = q->_grabbed_canvas_item; + } + + if (pre_scroll_grabbed_item && event.type == GDK_SCROLL) { + item = pre_scroll_grabbed_item; + } + + // Propagate the event up the canvas item hierarchy until handled. + while (item) { + if (item->handle_event(event_copy.get())) return true; + item = item->get_parent(); + } + } + + return false; +} + +/* + * Protected functions + */ + +Geom::IntPoint +Canvas::get_dimensions() const +{ + Gtk::Allocation allocation = get_allocation(); + return {allocation.get_width(), allocation.get_height()}; +} + +/** + * Is world point inside canvas area? + */ +bool +Canvas::world_point_inside_canvas(Geom::Point const &world) const +{ + return get_area_world().contains(world.floor()); +} + +/** + * Translate point in canvas to world coordinates. + */ +Geom::Point +Canvas::canvas_to_world(Geom::Point const &point) const +{ + return point + _pos; +} + +/** + * Return the area shown in the canvas in world coordinates. + */ +Geom::IntRect +Canvas::get_area_world() const +{ + return Geom::IntRect(_pos, _pos + get_dimensions()); +} + +/** + * Set the affine for the canvas. + */ +void +Canvas::set_affine(Geom::Affine const &affine) +{ + if (_affine == affine) { + return; + } + + _affine = affine; + + d->add_idle(); + queue_draw(); +} + +void CanvasPrivate::queue_draw_area(Geom::IntRect &rect) +{ + q->queue_draw_area(rect.left(), rect.top(), rect.width(), rect.height()); +} + +/** + * Invalidate drawing and redraw during idle. + */ +void +Canvas::redraw_all() +{ + if (!d->active) { + // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed. + // We need to ignore their requests! + return; + } + d->updater->reset(); // Empty region (i.e. everything is dirty). + d->add_idle(); + if (d->prefs.debug_show_unclean) queue_draw(); +} + +/** + * Redraw the given area during idle. + */ +void +Canvas::redraw_area(int x0, int y0, int x1, int y1) +{ + if (!d->active) { + // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed. + // We need to ignore their requests! + return; + } + + // Clamp area to Cairo's technically supported max size (-2^30..+2^30-1). + // This ensures that the rectangle dimensions don't overflow and wrap around. + constexpr int min_coord = -(1 << 30); + constexpr int max_coord = (1 << 30) - 1; + + x0 = std::clamp(x0, min_coord, max_coord); + y0 = std::clamp(y0, min_coord, max_coord); + x1 = std::clamp(x1, min_coord, max_coord); + y1 = std::clamp(y1, min_coord, max_coord); + + if (x0 >= x1 || y0 >= y1) { + return; + } + + auto rect = Geom::IntRect::from_xywh(x0, y0, x1 - x0, y1 - y0); + d->updater->mark_dirty(rect); + d->add_idle(); + if (d->prefs.debug_show_unclean) queue_draw(); +} + +void +Canvas::redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1) +{ + // Handle overflow during conversion gracefully. + // Round outward to make sure integral coordinates cover the entire area. + constexpr Geom::Coord min_int = std::numeric_limits<int>::min(); + constexpr Geom::Coord max_int = std::numeric_limits<int>::max(); + + redraw_area( + (int)std::floor(std::clamp(x0, min_int, max_int)), + (int)std::floor(std::clamp(y0, min_int, max_int)), + (int)std::ceil (std::clamp(x1, min_int, max_int)), + (int)std::ceil (std::clamp(y1, min_int, max_int)) + ); +} + +void +Canvas::redraw_area(Geom::Rect &area) +{ + redraw_area(area.left(), area.top(), area.right(), area.bottom()); +} + +/** + * Redraw after changing canvas item geometry. + */ +void +Canvas::request_update() +{ + // Flag geometry as needing update. + _need_update = true; + + // Trigger the idle process to perform the update. + d->add_idle(); +} + +/** + * Scroll window so drawing point 'pos' is at upper left corner of canvas. + */ +void +Canvas::set_pos(Geom::IntPoint const &pos) +{ + if (pos == _pos) { + return; + } + + _pos = pos; + + d->add_idle(); + queue_draw(); + + if (auto grid = dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(get_parent())) { + grid->UpdateRulers(); + } +} + +/** + * Set canvas background color (display only). + */ +void +Canvas::set_background_color(guint32 rgba) +{ + double r = SP_RGBA32_R_F(rgba); + double g = SP_RGBA32_G_F(rgba); + double b = SP_RGBA32_B_F(rgba); + + _background = Cairo::SolidPattern::create_rgb(r, g, b); + d->solid_background = true; + + redraw_all(); +} + +/** + * Set canvas background to a checkerboard pattern. + */ +void +Canvas::set_background_checkerboard(guint32 rgba, bool use_alpha) +{ + auto pattern = ink_cairo_pattern_create_checkerboard(rgba, use_alpha); + _background = Cairo::RefPtr<Cairo::Pattern>(new Cairo::Pattern(pattern)); + d->solid_background = false; + redraw_all(); +} + +void Canvas::set_drawing_disabled(bool disable) +{ + _drawing_disabled = disable; + if (!disable) { + d->add_idle(); + } +} + +void +Canvas::set_render_mode(Inkscape::RenderMode mode) +{ + if (_render_mode != mode) { + _render_mode = mode; + _drawing->setRenderMode(_render_mode); + redraw_all(); + } + if (_desktop) { + _desktop->setWindowTitle(); // Mode is listed in title. + } +} + +void +Canvas::set_color_mode(Inkscape::ColorMode mode) +{ + if (_color_mode != mode) { + _color_mode = mode; + redraw_all(); + } + if (_desktop) { + _desktop->setWindowTitle(); // Mode is listed in title. + } +} + +void +Canvas::set_split_mode(Inkscape::SplitMode mode) +{ + if (_split_mode != mode) { + _split_mode = mode; + redraw_all(); + } +} + +Cairo::RefPtr<Cairo::ImageSurface> +Canvas::get_backing_store() const +{ + return d->_backing_store; +} + +/** + * Clear current and grabbed items. + */ +void +Canvas::canvas_item_destructed(Inkscape::CanvasItem* item) +{ + if (item == _current_canvas_item) { + _current_canvas_item = nullptr; + } + + if (item == _current_canvas_item_new) { + _current_canvas_item_new = nullptr; + } + + if (item == _grabbed_canvas_item) { + _grabbed_canvas_item = nullptr; + auto const display = Gdk::Display::get_default(); + auto const seat = display->get_default_seat(); + seat->ungrab(); + } + + if (item == d->pre_scroll_grabbed_item) { + d->pre_scroll_grabbed_item = nullptr; + } +} + +// Change cursor +void +Canvas::set_cursor() { + + if (!_desktop) { + return; + } + + auto display = Gdk::Display::get_default(); + + switch (_hover_direction) { + + case Inkscape::SplitDirection::NONE: + _desktop->event_context->use_tool_cursor(); + break; + + case Inkscape::SplitDirection::NORTH: + case Inkscape::SplitDirection::EAST: + case Inkscape::SplitDirection::SOUTH: + case Inkscape::SplitDirection::WEST: + { + auto cursor = Gdk::Cursor::create(display, "pointer"); + get_window()->set_cursor(cursor); + break; + } + + case Inkscape::SplitDirection::HORIZONTAL: + { + auto cursor = Gdk::Cursor::create(display, "ns-resize"); + get_window()->set_cursor(cursor); + break; + } + + case Inkscape::SplitDirection::VERTICAL: + { + auto cursor = Gdk::Cursor::create(display, "ew-resize"); + get_window()->set_cursor(cursor); + break; + } + + default: + // Shouldn't reach. + std::cerr << "Canvas::set_cursor: Unknown hover direction!" << std::endl; + } +} + +void +Canvas::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const +{ + minimum_width = natural_width = 256; +} + +void +Canvas::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const +{ + minimum_height = natural_height = 256; +} + +void +Canvas::on_size_allocate(Gtk::Allocation &allocation) +{ + parent_type::on_size_allocate(allocation); + assert(allocation == get_allocation()); + d->add_idle(); // Trigger the size update to be applied to the stores before the next call to on_draw. +} + +/* + * Drawing + */ + +/* + * The on_draw() function is called whenever Gtk wants to update the window. This function: + * + * 1. Ensures that if the idle process was started, at least one cycle has run. + * + * 2. Blits the store(s) onto the canvas, clipping the outline store as required. + * (Or composites them with the transformed snapshot store(s) in decoupled mode.) + * + * 3. Draws the "controller" in the 'split' split mode. + */ +bool +Canvas::on_draw(const Cairo::RefPtr<::Cairo::Context> &cr) +{ + auto f = FrameCheck::Event(); + + if (!d->active) { + std::cerr << "Canvas::on_draw: Called while not active!" << std::endl; + return true; + } + + // sp_canvas_item_recursive_print_tree(0, _root); + // canvas_item_print_tree(_canvas_item_root); + + assert(_drawing); + + // Although hipri_idle is scheduled at a priority higher than draw, and should therefore always be called first if asked, there are times when GTK simply decides to call on_draw anyway. + // Here we ensure that that call has taken place. This is problematic because if hipri_idle does rendering, enlarging the damage rect, then our drawing will still be clipped to the old + // damage rect. It was precisely this problem that lead to the introduction of hipri_idle. Fortunately, the following failsafe only seems to execute once during initialisation, and + // once on further resize events. Both these events seem to trigger a full damage, hence we are ok. + if (d->hipri_idle.connected()) { + d->hipri_idle.disconnect(); + d->on_hipri_idle(); + } + + // Blit background if not solid. (If solid, it is baked into the stores.) + if (!d->solid_background) { + if (d->prefs.debug_framecheck) f = FrameCheck::Event("background"); + cr->save(); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source(_background); + cr->paint(); + cr->restore(); + } + + auto draw_store = [&, this] (const Cairo::RefPtr<Cairo::ImageSurface> &store, const Cairo::RefPtr<Cairo::ImageSurface> &snapshot_store, bool is_backing_store) { + if (!d->decoupled_mode) { + // Blit store to screen. + if (d->prefs.debug_framecheck) f = FrameCheck::Event("draw"); + cr->save(); + cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); + cr->set_source(store, d->_store_rect.left() - _pos.x(), d->_store_rect.top() - _pos.y()); + cr->paint(); + cr->restore(); + } else { + // Turn off anti-aliasing for huge performance gains. Only applies to this compositing step. + cr->set_antialias(Cairo::ANTIALIAS_NONE); + + // Blit untransformed snapshot store to complement of transformed snapshot clean region. + if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 2); + cr->save(); + cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); + cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height()); + cr->translate(-_pos.x(), -_pos.y()); + cr->transform(geom_to_cairo(_affine * d->_snapshot_affine.inverse())); + region_to_path(cr, d->_snapshot_clean_region); + cr->clip(); + cr->transform(geom_to_cairo(d->_snapshot_affine * _affine.inverse())); + cr->translate(_pos.x(), _pos.y()); + cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); + cr->set_source(snapshot_store, d->_snapshot_static_offset.x(), d->_snapshot_static_offset.y()); + cr->paint(); + cr->restore(); + + // Draw transformed snapshot, clipped to its clean region and the complement of the store's clean region. + if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 1); + cr->save(); + cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); + cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height()); + cr->translate(-_pos.x(), -_pos.y()); + cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); + region_to_path(cr, d->updater->clean_region); + cr->clip(); + cr->transform(geom_to_cairo(d->_store_affine * d->_snapshot_affine.inverse())); + region_to_path(cr, d->_snapshot_clean_region); + cr->clip(); + cr->set_source(snapshot_store, d->_snapshot_rect.left(), d->_snapshot_rect.top()); + cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + cr->paint(); + if (d->prefs.debug_show_snapshot) { + cr->set_source_rgba(0, 0, 1, 0.2); + cr->set_operator(Cairo::OPERATOR_OVER); + cr->paint(); + } + cr->restore(); + + // Draw transformed store, clipped to clean region. + if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 0); + cr->save(); + cr->translate(-_pos.x(), -_pos.y()); + cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); + region_to_path(cr, d->updater->clean_region); + cr->clip(); + cr->set_source(store, d->_store_rect.left(), d->_store_rect.top()); + cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + cr->paint(); + cr->restore(); + } + }; + + // Draw the backing store. + draw_store(d->_backing_store, d->_snapshot_store, true); + + // Draw overlay if required. + if (_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) { + assert(d->_outline_store); + + double outline_overlay_opacity = 1.0 - d->prefs.outline_overlay_opacity / 100.0; + + // Partially obscure drawing by painting semi-transparent white. + cr->set_source_rgb(1.0, 1.0, 1.0); + cr->paint_with_alpha(outline_overlay_opacity); + + // Overlay outline. + draw_store(d->_outline_store, d->_snapshot_outline_store, false); + } + + // Draw split if required. + if (_split_mode != Inkscape::SplitMode::NORMAL) { + assert(d->_outline_store); + + // Move split position to center if not in canvas. + auto const rect = Geom::Rect(Geom::Point(), get_dimensions()); + if (!rect.contains(_split_position)) { + _split_position = rect.midpoint(); + } + + // Add clipping path and blit background. + cr->save(); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source(_background); + add_clippath(cr); + cr->paint(); + cr->restore(); + + // Add clipping path and draw outline store. + cr->save(); + add_clippath(cr); + draw_store(d->_outline_store, d->_snapshot_outline_store, false); + cr->restore(); + } + + // Paint unclean regions in red. + if (d->prefs.debug_show_unclean) { + if (d->prefs.debug_framecheck) f = FrameCheck::Event("paint_unclean"); + auto reg = Cairo::Region::create(geom_to_cairo(d->_store_rect)); + reg->subtract(d->updater->clean_region); + cr->save(); + cr->translate(-_pos.x(), -_pos.y()); + if (d->decoupled_mode) { + cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); + } + cr->set_source_rgba(1, 0, 0, 0.2); + region_to_path(cr, reg); + cr->fill(); + cr->restore(); + } + + // Paint internal edges of clean region in green. + if (d->prefs.debug_show_clean) { + if (d->prefs.debug_framecheck) f = FrameCheck::Event("paint_clean"); + cr->save(); + cr->translate(-_pos.x(), -_pos.y()); + if (d->decoupled_mode) { + cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); + } + cr->set_source_rgba(0, 0.7, 0, 0.4); + region_to_path(cr, d->updater->clean_region); + cr->stroke(); + cr->restore(); + } + + if (_split_mode == Inkscape::SplitMode::SPLIT) { + // Add dividing line. + cr->save(); + cr->set_source_rgb(0, 0, 0); + cr->set_line_width(1); + if (_split_direction == Inkscape::SplitDirection::EAST || + _split_direction == Inkscape::SplitDirection::WEST) { + cr->move_to((int)_split_position.x() + 0.5, 0); + cr->line_to((int)_split_position.x() + 0.5, get_dimensions().y()); + cr->stroke(); + } else { + cr->move_to( 0, (int)_split_position.y() + 0.5); + cr->line_to(get_dimensions().x(), (int)_split_position.y() + 0.5); + cr->stroke(); + } + cr->restore(); + + // Add controller image. + double a = _hover_direction == Inkscape::SplitDirection::NONE ? 0.5 : 1.0; + cr->save(); + cr->set_source_rgba(0.2, 0.2, 0.2, a); + cr->arc(_split_position.x(), _split_position.y(), 20 * d->_device_scale, 0, 2 * M_PI); + cr->fill(); + cr->restore(); + + cr->save(); + for (int i = 0; i < 4; ++i) { + // The four direction triangles. + cr->save(); + + // Position triangle. + cr->translate(_split_position.x(), _split_position.y()); + cr->rotate((i + 2) * M_PI / 2.0); + + // Draw triangle. + cr->move_to(-5 * d->_device_scale, 8 * d->_device_scale); + cr->line_to( 0, 18 * d->_device_scale); + cr->line_to( 5 * d->_device_scale, 8 * d->_device_scale); + cr->close_path(); + + double b = (int)_hover_direction == (i + 1) ? 0.9 : 0.7; + cr->set_source_rgba(b, b, b, a); + cr->fill(); + + cr->restore(); + } + cr->restore(); + } + + // Process bucketed events as soon as possible after draw. We cannot process them now, because we have + // a frame to get out as soon as possible, and processing events may take a while. Instead, we schedule + // it with a signal callback on the main loop that runs as soon as this function is completed. + if (!d->eventprocessor->events.empty()) d->schedule_bucket_emptier(); + + // Record the fact that a draw is no longer pending. + d->pending_draw = false; + + // Notify the update strategy that another frame has passed. + d->updater->frame(); + + // Just-for-1.2 flicker "prevention": save the last offset the store was drawn at outside of decoupled mode, + // so we can continue to draw a static snapshot upon next going into decoupled mode. + if (!d->decoupled_mode) { + d->_snapshot_static_offset = d->_store_rect.min() - _pos; + } + + return true; +} + +// Sets clip path for Split and X-Ray modes. +void +Canvas::add_clippath(const Cairo::RefPtr<Cairo::Context>& cr) +{ + double width = get_allocation().get_width(); + double height = get_allocation().get_height(); + double sx = _split_position.x(); + double sy = _split_position.y(); + + if (_split_mode == Inkscape::SplitMode::SPLIT) { + // We're clipping the outline region... so it's backwards. + switch (_split_direction) { + case Inkscape::SplitDirection::SOUTH: + cr->rectangle(0, 0, width, sy); + break; + case Inkscape::SplitDirection::NORTH: + cr->rectangle(0, sy, width, height - sy); + break; + case Inkscape::SplitDirection::EAST: + cr->rectangle(0, 0, sx, height ); + break; + case Inkscape::SplitDirection::WEST: + cr->rectangle(sx, 0, width - sx, height ); + break; + default: + // no clipping (for NONE, HORIZONTAL, VERTICAL) + break; + } + } else { + cr->arc(sx, sy, d->prefs.x_ray_radius, 0, 2 * M_PI); + } + + cr->clip(); +} + +void +CanvasPrivate::add_idle() +{ + framecheck_whole_function(this) + + if (!active) { + // We can safely discard events until active, because we will run add_idle on activation later in initialisation. + return; + } + + if (!hipri_idle.connected()) { + hipri_idle = Glib::signal_idle().connect(sigc::mem_fun(this, &CanvasPrivate::on_hipri_idle), G_PRIORITY_HIGH_IDLE + 15); // after resize, before draw + } + + if (!lopri_idle.connected()) { + lopri_idle = Glib::signal_idle().connect(sigc::mem_fun(this, &CanvasPrivate::on_lopri_idle), G_PRIORITY_DEFAULT_IDLE); + } + + idle_running = true; +} + +auto +distSq(const Geom::IntPoint pt, const Geom::IntRect &rect) +{ + auto v = rect.clamp(pt) - pt; + return v.x() * v.x() + v.y() * v.y(); +} + +auto +calc_affine_diff(const Geom::Affine &a, const Geom::Affine &b) { + auto c = a.inverse() * b; + return std::abs(c[0] - 1) + std::abs(c[1]) + std::abs(c[2]) + std::abs(c[3] - 1); +} + +// Replace a region with a larger region consisting of fewer, larger rectangles. (Allowed to slightly overlap.) +auto +coarsen(const Cairo::RefPtr<Cairo::Region> ®ion, int min_size, int glue_size, double min_fullness) +{ + // Sort the rects by minExtent. + struct Compare + { + bool operator()(const Geom::IntRect &a, const Geom::IntRect &b) const { + return a.minExtent() < b.minExtent(); + } + }; + std::multiset<Geom::IntRect, Compare> rects; + int nrects = region->get_num_rectangles(); + for (int i = 0; i < nrects; i++) { + rects.emplace(cairo_to_geom(region->get_rectangle(i))); + } + + // List of processed rectangles. + std::vector<Geom::IntRect> processed; + processed.reserve(nrects); + + // Removal lists. + std::vector<decltype(rects)::iterator> remove_rects; + std::vector<int> remove_processed; + + // Repeatedly expand small rectangles by absorbing their nearby small rectangles. + while (!rects.empty() && rects.begin()->minExtent() < min_size) { + // Extract the smallest unprocessed rectangle. + auto rect = *rects.begin(); + rects.erase(rects.begin()); + + // Initialise the effective glue size. + int effective_glue_size = glue_size; + + while (true) { + // Find the glue zone. + auto glue_zone = rect; + glue_zone.expandBy(effective_glue_size); + + // Absorb rectangles in the glue zone. We could do better algorithmically speaking, but in real life it's already plenty fast. + auto newrect = rect; + int absorbed_area = 0; + + remove_rects.clear(); + for (auto it = rects.begin(); it != rects.end(); ++it) { + if (glue_zone.contains(*it)) { + newrect.unionWith(*it); + absorbed_area += it->area(); + remove_rects.emplace_back(it); + } + } + + remove_processed.clear(); + for (int i = 0; i < processed.size(); i++) { + auto &r = processed[i]; + if (glue_zone.contains(r)) { + newrect.unionWith(r); + absorbed_area += r.area(); + remove_processed.emplace_back(i); + } + } + + // If the result was too empty, try again with a smaller glue size. + double fullness = (double)(rect.area() + absorbed_area) / newrect.area(); + if (fullness < min_fullness) { + effective_glue_size /= 2; + continue; + } + + // Commit the change. + rect = newrect; + + for (auto &it : remove_rects) { + rects.erase(it); + } + + for (int j = (int)remove_processed.size() - 1; j >= 0; j--) { + int i = remove_processed[j]; + processed[i] = processed.back(); + processed.pop_back(); + } + + // Stop growing if not changed or now big enough. + bool finished = absorbed_area == 0 || rect.minExtent() >= min_size; + if (finished) { + break; + } + + // Otherwise, continue normally. + effective_glue_size = glue_size; + } + + // Put the finished rectangle in processed. + processed.emplace_back(rect); + } + + // Put any remaining rectangles in processed. + for (auto &rect : rects) { + processed.emplace_back(rect); + } + + return processed; +} + +std::optional<Geom::Dim2> +CanvasPrivate::old_bisector(const Geom::IntRect &rect) +{ + int bw = rect.width(); + int bh = rect.height(); + + /* + * Determine redraw strategy: + * + * bw < bh (strips mode): Draw horizontal strips starting from cursor position. + * Seems to be faster for drawing many smaller objects zoomed out. + * + * bw > hb (chunks mode): Splits across the larger dimension of the rectangle, painting + * in almost square chunks (from the cursor. + * Seems to be faster for drawing a few blurred objects across the entire screen. + * Seems to be somewhat psychologically faster. + * + * Default is for strips mode. + */ + + int max_pixels; + if (q->_render_mode != Inkscape::RenderMode::OUTLINE) { + // Can't be too small or large gradient will be rerendered too many times! + max_pixels = 65536 * prefs.tile_multiplier; + } else { + // Paths only. 1M is catched buffer and we need four channels. + max_pixels = 262144; + } + + if (bw * bh > max_pixels) { + if (bw < bh || bh < 2 * prefs.tile_size) { + return Geom::X; + } else { + return Geom::Y; + } + } + + return {}; +} + +std::optional<Geom::Dim2> +CanvasPrivate::new_bisector(const Geom::IntRect &rect) +{ + int bw = rect.width(); + int bh = rect.height(); + + // Chop in half along the bigger dimension if the bigger dimension is too big. + if (bw > bh) { + if (bw > prefs.new_bisector_size) { + return Geom::X; + } + } else { + if (bh > prefs.new_bisector_size) { + return Geom::Y; + } + } + + return {}; +} + +bool +CanvasPrivate::on_hipri_idle() +{ + assert(active); + if (idle_running) { + idle_running = on_idle(); + } + return false; +} + +bool +CanvasPrivate::on_lopri_idle() +{ + assert(active); + if (idle_running) { + idle_running = on_idle(); + } + return idle_running; +} + +bool +CanvasPrivate::on_idle() +{ + framecheck_whole_function(this) + + assert(q->_canvas_item_root); + + // Quit idle process if not supposed to be drawing. + if (!q->_drawing || q->_drawing_disabled) { + return false; + } + + const Geom::IntPoint pad(prefs.pad, prefs.pad); + auto recreate_store = [&, this] { + // Recreate the store at the current affine so that it covers the visible region. + _store_rect = q->get_area_world(); + _store_rect.expandBy(pad); + Geom::IntRect expanded = _store_rect; + Geom::IntPoint expansion(expanded.width()/2, expanded.height()/2); + expanded.expandBy(expansion); + q->_drawing->setCacheLimit(expanded); + _store_affine = q->_affine; + int desired_width = _store_rect.width() * _device_scale; + int desired_height = _store_rect.height() * _device_scale; + if (!_backing_store || _backing_store->get_width() != desired_width || _backing_store->get_height() != desired_height) { + _backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height); + cairo_surface_set_device_scale(_backing_store->cobj(), _device_scale, _device_scale); // No C++ API! + } + auto cr = Cairo::Context::create(_backing_store); + if (solid_background) { + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source(q->_background); + } else { + cr->set_operator(Cairo::OPERATOR_CLEAR); + } + cr->paint(); + if (need_outline_store()) { + if (!_outline_store || _outline_store->get_width() != desired_width || _outline_store->get_height() != desired_height) { + _outline_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height); + cairo_surface_set_device_scale(_outline_store->cobj(), _device_scale, _device_scale); // No C++ API! + } + auto cr = Cairo::Context::create(_outline_store); + cr->set_operator(Cairo::OPERATOR_CLEAR); + cr->paint(); + } + updater->reset(); + if (prefs.debug_show_unclean) q->queue_draw(); + }; + + // Determine whether the rendering parameters have changed, and reset if so. + if (!_backing_store || (need_outline_store() && !_outline_store) || _device_scale != q->get_scale_factor() || _store_solid_background != solid_background) { + _device_scale = q->get_scale_factor(); + _store_solid_background = solid_background; + recreate_store(); + decoupled_mode = false; + if (prefs.debug_logging) std::cout << "Full reset" << std::endl; + } + + // Make sure to clear the outline store when not in use, so we don't accidentally re-use it when it is required again. + if (!need_outline_store()) { + _outline_store.clear(); + } + + auto shift_store = [&, this] { + // Recreate the store, but keep re-usable content from the old store. + auto store_rect = q->get_area_world(); + store_rect.expandBy(pad); + Geom::IntRect expanded = _store_rect; + Geom::IntPoint expansion(expanded.width()/2, expanded.height()/2); + expanded.expandBy(expansion); + q->_drawing->setCacheLimit(expanded); + auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale); + cairo_surface_set_device_scale(backing_store->cobj(), _device_scale, _device_scale); // No C++ API! + + // Determine the geometry of the shift. + auto shift = store_rect.min() - _store_rect.min(); + auto reuse_rect = store_rect & _store_rect; + assert(reuse_rect); // Should not be called if there is no overlap. + auto cr = Cairo::Context::create(backing_store); + + // Paint background into region not covered by next operation. + if (solid_background) { + auto reg = Cairo::Region::create(geom_to_cairo(store_rect)); + reg->subtract(geom_to_cairo(*reuse_rect)); + reg->translate(-store_rect.left(), -store_rect.top()); + cr->save(); + if (solid_background) { + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source(q->_background); + } + region_to_path(cr, reg); + cr->fill(); + cr->restore(); + } + + // Copy re-usuable contents of old store into new store, shifted. + cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height()); + cr->clip(); + cr->set_source(_backing_store, -shift.x(), -shift.y()); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->paint(); + + // Set the result as the new backing store. + _store_rect = store_rect; + assert(_store_affine == q->_affine); // Should not be called if the affine has changed. + _backing_store = std::move(backing_store); + + // Do the same for the outline store + if (_outline_store) { + auto outline_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale); + cairo_surface_set_device_scale(outline_store->cobj(), _device_scale, _device_scale); // No C++ API! + auto cr = Cairo::Context::create(outline_store); + cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height()); + cr->clip(); + cr->set_source(_outline_store, -shift.x(), -shift.y()); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->paint(); + _outline_store = std::move(outline_store); + } + + updater->intersect(_store_rect); + if (prefs.debug_show_unclean) q->queue_draw(); + }; + + auto take_snapshot = [&, this] { + // Copy the backing store to the snapshot, leaving us temporarily in an invalid state. + std::swap(_snapshot_store, _backing_store); // This will re-use the old snapshot store later if possible. + _snapshot_rect = _store_rect; + _snapshot_affine = _store_affine; + _snapshot_clean_region = updater->clean_region->copy(); + + // Do the same for the outline store + std::swap(_snapshot_outline_store, _outline_store); + + // Recreate the backing store, making the state valid again. + recreate_store(); + }; + + // Handle transitions and actions in response to viewport changes. + if (!decoupled_mode) { + // Enter decoupled mode if the affine has changed from what the backing store was drawn at. + if (q->_affine != _store_affine) { + // Snapshot and reset the backing store. + take_snapshot(); + + // Enter decoupled mode. + if (prefs.debug_logging) std::cout << "Entering decoupled mode" << std::endl; + decoupled_mode = true; + + // Note: If redrawing is fast enough to finish during the frame, then going into decoupled mode, drawing, and leaving + // it again performs exactly the same rendering operations as if we had not gone into it at all. Also, no extra copies + // or blits are performed, and the drawing operations done on the screen are the same. Hence this feature comes at zero cost. + } else { + // Get visible rectangle in canvas coordinates. + auto const visible = q->get_area_world(); + if (!_store_rect.intersects(visible)) { + // If the store has gone completely off-screen, recreate it. + recreate_store(); + if (prefs.debug_logging) std::cout << "Recreated store" << std::endl; + } else if (!_store_rect.contains(visible)) { + // If the store has gone partially off-screen, shift it. + shift_store(); + if (prefs.debug_logging) std::cout << "Shifted store" << std::endl; + } + // After these operations, the store should now be fully on-screen. + assert(_store_rect.contains(visible)); + } + } else { // if (decoupled_mode) + // Completely cancel the previous redraw and start again if the viewing parameters have changed too much. + if (!prefs.debug_sticky_decoupled) { + auto pl = Geom::Parallelogram(q->get_area_world()); + pl *= _store_affine * q->_affine.inverse(); + if (!pl.intersects(_store_rect)) { + // Store has gone off the screen. + recreate_store(); + if (prefs.debug_logging) std::cout << "Restarting redraw (store off-screen)" << std::endl; + } else { + auto diff = calc_affine_diff(q->_affine, _store_affine); + if (diff > prefs.max_affine_diff) { + // Affine has changed too much. + recreate_store(); + if (prefs.debug_logging) std::cout << "Restarting redraw (affine changed too much)" << std::endl; + } + } + } + } + + // Assert that _clean_region is a subregion of _store_rect. + #ifndef NDEBUG + auto tmp = updater->clean_region->copy(); + tmp->subtract(geom_to_cairo(_store_rect)); + assert(tmp->empty()); + #endif + + // Ensure the geometry is up-to-date and in the right place. + auto affine = decoupled_mode ? _store_affine : q->_affine; + if (q->_need_update || geom_affine != affine) { + q->_canvas_item_root->update(affine); + geom_affine = affine; + q->_need_update = false; + } + + // If asked to, don't paint anything and instead halt the idle process. + if (prefs.debug_disable_redraw) { + return false; + } + + // Get the subrectangle of store that is visible. + Geom::OptIntRect visible_rect; + if (!decoupled_mode) { + // By a previous assertion, this always lies within the store. + visible_rect = q->get_area_world(); + } else { + // Get the window rectangle transformed into canvas space. + auto pl = Geom::Parallelogram(q->get_area_world()); + pl *= _store_affine * q->_affine.inverse(); + + // Get its bounding box, rounded outwards. + auto b = pl.bounds(); + auto bi = Geom::IntRect(b.min().floor(), b.max().ceil()); + + // The visible rect is the intersection of this with the store + visible_rect = bi & _store_rect; + } + // The visible rectangle must be a subrectangle of store. + assert(_store_rect.contains(visible_rect)); + + // Get the mouse position in screen space. + Geom::IntPoint mouse_loc = (last_mouse ? *last_mouse : Geom::Point(q->get_dimensions()) / 2).round(); + + // Map the mouse to canvas space. + mouse_loc += q->_pos; + if (decoupled_mode) { + mouse_loc = geom_act(_store_affine * q->_affine.inverse(), mouse_loc); + } + + // Begin processing redraws. + auto start_time = g_get_monotonic_time(); + while (true) { + // Get the clean region for the next redraw as reported by the updater. + auto clean_region = updater->get_next_clean_region(); + + // Get the region to paint, which is the visible rectangle minus the clean region (both subregions of store). + Cairo::RefPtr<Cairo::Region> paint_region; + if (visible_rect) { + paint_region = Cairo::Region::create(geom_to_cairo(*visible_rect)); + paint_region->subtract(clean_region); + } else { + paint_region = Cairo::Region::create(); + } + + Geom::OptIntRect dragged = Geom::OptIntRect(); + if (q->_grabbed_canvas_item) { + dragged = q->_grabbed_canvas_item->get_bounds().roundOutwards(); + if (dragged) { + (*dragged).expandBy(prefs.pad); + dragged = dragged & visible_rect; + if (dragged) { + paint_region->subtract(geom_to_cairo(*dragged)); + } + } + } + // Get the list of rectangles to paint, coarsened to avoid fragmentation. + auto rects = coarsen(paint_region, + std::min<int>(prefs.coarsener_min_size, prefs.new_bisector_size / 2), + std::min<int>(prefs.coarsener_glue_size, prefs.new_bisector_size / 2), + prefs.coarsener_min_fullness); + if (dragged) { + // this become the first after look for cursor + rects.push_back(*dragged); + } + // Ensure that all the rectangles lie within the visible rect (and therefore within the store). + #ifndef NDEBUG + for (auto &rect : rects) { + assert(visible_rect.contains(rect)); + } + #endif + + // Put the rectangles into a heap sorted by distance from mouse. + auto cmp = [&] (const Geom::IntRect &a, const Geom::IntRect &b) { + return distSq(mouse_loc, a) > distSq(mouse_loc, b); + }; + std::make_heap(rects.begin(), rects.end(), cmp); + + // Process rectangles until none left or timed out. + bool start = true; + while (!rects.empty()) { + // Extract the closest rectangle to the mouse. + std::pop_heap(rects.begin(), rects.end(), cmp); + auto rect = rects.back(); + rects.pop_back(); + + // Cull empty rectangles. + if (rect.width() == 0 || rect.height() == 0) { + start = false; + continue; + } + + // Cull rectangles that lie entirely inside the clean region. + // (These can be generated by coarsening; they must be discarded to avoid getting stuck re-rendering the same rectangles.) + if (clean_region->contains_rectangle(geom_to_cairo(rect)) == Cairo::REGION_OVERLAP_IN) { + start = false; + continue; + } + + // Lambda to add a rectangle to the heap. + auto add_rect = [&] (const Geom::IntRect &rect) { + rects.emplace_back(rect); + std::push_heap(rects.begin(), rects.end(), cmp); + }; + + // If the rectangle needs bisecting, bisect it and put it back on the heap. + auto axis = prefs.use_new_bisector ? new_bisector(rect) : old_bisector(rect); + if (axis && !(dragged && start)) { + int mid = rect[*axis].middle(); + auto lo = rect; lo[*axis].setMax(mid); add_rect(lo); + auto hi = rect; hi[*axis].setMin(mid); add_rect(hi); + start = false; + continue; + } + start = false; + // Paint the rectangle. + paint_rect_internal(rect); + + // Check for timeout. + auto now = g_get_monotonic_time(); + auto elapsed = now - start_time; + bool fixchoping = false; + #ifdef _WIN32 + fixchoping = true; + #elif defined(__APPLE__) + fixchoping = true; + #endif + if (elapsed > ((fixchoping && prefs.render_time_limit == 1000) ? 80000 : prefs.render_time_limit)) { + // Timed out. Temporarily return to GTK main loop, and come back here when next idle. + if (prefs.debug_logging) std::cout << "Timed out: " << g_get_monotonic_time() - start_time << " us" << std::endl; + framecheckobj.subtype = 1; + return true; + } + } + + // Report the redraw as finished. Exit if there's no more redraws to process. + bool keep_going = updater->report_finished(); + if (!keep_going) break; + } + + // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to do a final redraw at the correct affine. + if (decoupled_mode) { + if (prefs.debug_sticky_decoupled) { + // Debug feature: quit idle process, but stay in decoupled mode. + return false; + } else if (_store_affine == q->_affine) { + // Content is rendered at the correct affine - exit decoupled mode and quit idle process. + if (prefs.debug_logging) std::cout << "Finished drawing - exiting decoupled mode" << std::endl; + // Exit decoupled mode. + decoupled_mode = false; + // Quit idle process. + return false; + } else { + // Content is rendered at the wrong affine - take a new snapshot and continue idle process to continue rendering at the new affine. + if (prefs.debug_logging) std::cout << "Scheduling final redraw" << std::endl; + // Snapshot and reset the backing store. + take_snapshot(); + // Continue idle process. + return true; + } + } else { + // All done, quit the idle process. + framecheckobj.subtype = 3; + return false; + } +} + +void +CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect) +{ + // Paint the rectangle. + q->_drawing->setColorMode(q->_color_mode); + paint_single_buffer(rect, _backing_store, true, false); + + if (_outline_store) { + q->_drawing->setRenderMode(Inkscape::RenderMode::OUTLINE); + paint_single_buffer(rect, _outline_store, false, q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY); + q->_drawing->setRenderMode(q->_render_mode); // Leave the drawing in the requested render mode. + } + + // Introduce an artificial delay for each rectangle. + if (prefs.debug_slow_redraw) g_usleep(prefs.debug_slow_redraw_time); + + // Mark the rectangle as clean. + updater->mark_clean(rect); + + // Mark the screen dirty. + if (!decoupled_mode) { + // Get rectangle needing repaint + auto repaint_rect = rect - q->_pos; + + // Assert that a repaint actually occurs (guaranteed because we are only asked to paint fully on-screen rectangles) + auto screen_rect = Geom::IntRect(0, 0, q->get_allocation().get_width(), q->get_allocation().get_height()); + assert(repaint_rect & screen_rect); + + // Schedule repaint + queue_draw_area(repaint_rect); // Guarantees on_draw will be called in the future. + if (bucket_emptier_tick_callback) {q->remove_tick_callback(*bucket_emptier_tick_callback); bucket_emptier_tick_callback.reset();} + pending_draw = true; + } else { + // Get rectangle needing repaint (transform into screen space, take bounding box, round outwards) + auto pl = Geom::Parallelogram(rect); + pl *= q->_affine * _store_affine.inverse(); + pl *= Geom::Translate(-q->_pos); + auto b = pl.bounds(); + auto repaint_rect = Geom::IntRect(b.min().floor(), b.max().ceil()); + + // Check if repaint is necessary - some rectangles could be entirely off-screen. + auto screen_rect = Geom::IntRect(0, 0, q->get_allocation().get_width(), q->get_allocation().get_height()); + if (repaint_rect & screen_rect) { + // Schedule repaint + queue_draw_area(repaint_rect); + if (bucket_emptier_tick_callback) {q->remove_tick_callback(*bucket_emptier_tick_callback); bucket_emptier_tick_callback.reset();} + pending_draw = true; + } + } +} + +void +CanvasPrivate::paint_single_buffer(Geom::IntRect const &paint_rect, const Cairo::RefPtr<Cairo::ImageSurface> &store, bool is_backing_store, bool outline_overlay_pass) +{ + // Make sure the following code does not go outside of store's data. + assert(store); + assert(store->get_format() == Cairo::FORMAT_ARGB32); + assert(_store_rect.contains(paint_rect)); // FIXME: Observed to fail once when hitting Ctrl+O while Canvas was busy. Haven't managed to reproduce it. Doesn't mean it's fixed. + + // Create temporary surface that draws directly to store. + store->flush(); + unsigned char *data = store->get_data(); + int stride = store->get_stride(); + + // Check we are using the correct device scale. + double x_scale = 1.0; + double y_scale = 1.0; + cairo_surface_get_device_scale(store->cobj(), &x_scale, &y_scale); // No C++ API! + assert (_device_scale == (int) x_scale); + assert (_device_scale == (int) y_scale); + + // Move to the correct row. + data += stride * (paint_rect.top() - _store_rect.top()) * (int)y_scale; + // Move to the correct column. + data += 4 * (paint_rect.left() - _store_rect.left()) * (int)x_scale; + auto imgs = Cairo::ImageSurface::create(data, Cairo::FORMAT_ARGB32, + paint_rect.width() * _device_scale, + paint_rect.height() * _device_scale, + stride); + + cairo_surface_set_device_scale(imgs->cobj(), _device_scale, _device_scale); // No C++ API! + + auto cr = Cairo::Context::create(imgs); + + // Clear background + cr->save(); + if (is_backing_store && solid_background) { + cr->set_source(q->_background); + cr->set_operator(Cairo::OPERATOR_SOURCE); + } else { + cr->set_operator(Cairo::OPERATOR_CLEAR); + } + cr->paint(); + cr->restore(); + + // Render drawing on top of background. + if (q->_canvas_item_root->is_visible()) { + auto buf = Inkscape::CanvasItemBuffer{ paint_rect, _device_scale, outline_overlay_pass, cr }; + q->_canvas_item_root->render(&buf); + } + + // Paint over newly drawn content with a translucent random colour. + if (prefs.debug_show_redraw) { + cr->set_source_rgba((rand() % 255) / 255.0, (rand() % 255) / 255.0, (rand() % 255) / 255.0, 0.2); + cr->set_operator(Cairo::OPERATOR_OVER); + cr->rectangle(0, 0, imgs->get_width(), imgs->get_height()); + cr->fill(); + } + + if (q->_cms_active) { + auto transf = prefs.from_display + ? Inkscape::CMSSystem::getDisplayPer(q->_cms_key) + : Inkscape::CMSSystem::getDisplayTransform(); + + if (transf) { + imgs->flush(); + auto px = imgs->get_data(); + int stride = imgs->get_stride(); + for (int i = 0; i < paint_rect.height(); i++) { + auto row = px + i * stride; + Inkscape::CMSSystem::doTransform(transf, row, row, paint_rect.width()); + } + imgs->mark_dirty(); + } + } + + store->mark_dirty(); +} + +} // namespace Widget +} // 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/widget/canvas.h b/src/ui/widget/canvas.h new file mode 100644 index 0000000..f2a434a --- /dev/null +++ b/src/ui/widget/canvas.h @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef INKSCAPE_UI_WIDGET_CANVAS_H +#define INKSCAPE_UI_WIDGET_CANVAS_H +/* + * Authors: + * Tavmjong Bah + * PBS <pbs3141@gmail.com> + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <memory> +#include <gtkmm.h> +#include <2geom/rect.h> +#include <2geom/int-rect.h> +#include "display/rendermode.h" + +class SPDesktop; + +namespace Inkscape { + +class CanvasItem; +class CanvasItemGroup; +class Drawing; + +namespace UI { +namespace Widget { + +class CanvasPrivate; + +/** + * A Gtk::DrawingArea widget for Inkscape's canvas. + */ +class Canvas : public Gtk::DrawingArea +{ + using parent_type = Gtk::DrawingArea; + +public: + + Canvas(); + ~Canvas() override; + + /* Configuration */ + + // Desktop (Todo: Remove.) + void set_desktop(SPDesktop *desktop) { _desktop = desktop; } + SPDesktop *get_desktop() const { return _desktop; } + + // Drawing + void set_drawing(Inkscape::Drawing *drawing); + + // Canvas item root + CanvasItemGroup *get_canvas_item_root() const { return _canvas_item_root; } + + // Geometry + void set_pos (const Geom::IntPoint &pos); + void set_pos (const Geom::Point &fpos) { set_pos(fpos.round()); } + void set_affine(const Geom::Affine &affine); + const Geom::IntPoint& get_pos () const {return _pos;} + const Geom::Affine& get_affine() const {return _affine;} + + // Background + void set_background_color(guint32 rgba); + void set_background_checkerboard(guint32 rgba = 0xC4C4C4FF, bool use_alpha = false); + + // Rendering modes + void set_render_mode(Inkscape::RenderMode mode); + void set_color_mode (Inkscape::ColorMode mode); + void set_split_mode (Inkscape::SplitMode mode); + Inkscape::RenderMode get_render_mode() const { return _render_mode; } + Inkscape::ColorMode get_color_mode() const { return _color_mode; } + Inkscape::SplitMode get_split_mode() const { return _split_mode; } + + // CMS + void set_cms_key(std::string key) { + _cms_key = std::move(key); + _cms_active = !_cms_key.empty(); + } + const std::string& get_cms_key() const { return _cms_key; } + void set_cms_active(bool active) { _cms_active = active; } + bool get_cms_active() const { return _cms_active; } + + /* Observers */ + + // Geometry + Geom::IntPoint get_dimensions() const; + bool world_point_inside_canvas(Geom::Point const &world) const; // desktop-events.cpp + Geom::Point canvas_to_world(Geom::Point const &window) const; + Geom::IntRect get_area_world() const; + + // State + bool is_dragging() const { return _is_dragging; } // selection-chemistry.cpp + + // Encapsulation leaks + Cairo::RefPtr<Cairo::ImageSurface> get_backing_store() const; // canvas-item-rotate.cpp + Cairo::RefPtr<Cairo::Pattern> get_background_pattern() const { return _background; } // canvas-item-rect.cpp, canvas-item-ctrl.cpp + + /* Methods */ + + // Invalidation + void redraw_all(); // Mark everything as having changed. + void redraw_area(Geom::Rect& area); // Mark a rectangle of world space as having changed. + void redraw_area(int x0, int y0, int x1, int y1); + void redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1); + void request_update(); // Mark geometry as needing recalculation. + + // Gobblers (tool-base.cpp) + int gobble_key_events(guint keyval, guint mask); + void gobble_motion_events(guint mask); + + // Callback run on destructor of any canvas item + void canvas_item_destructed(Inkscape::CanvasItem *item); + + // State + Inkscape::CanvasItem *get_current_canvas_item() const { return _current_canvas_item; } + void set_current_canvas_item(Inkscape::CanvasItem *item) { + _current_canvas_item = item; + } + Inkscape::CanvasItem *get_grabbed_canvas_item() const { return _grabbed_canvas_item; } + void set_grabbed_canvas_item(Inkscape::CanvasItem *item, Gdk::EventMask mask) { + _grabbed_canvas_item = item; + _grabbed_event_mask = mask; + } + void set_drawing_disabled(bool disable); // Disable during path ops, etc. + void set_all_enter_events(bool on) { _all_enter_events = on; } + +protected: + + void get_preferred_width_vfunc (int &minimum_width, int &natural_width ) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; + + // Event handlers + bool on_scroll_event (GdkEventScroll* ) override; + bool on_button_event (GdkEventButton* ); + bool on_button_press_event (GdkEventButton* ) override; + bool on_button_release_event(GdkEventButton* ) override; + bool on_enter_notify_event (GdkEventCrossing*) override; + bool on_leave_notify_event (GdkEventCrossing*) override; + bool on_focus_in_event (GdkEventFocus* ) override; + bool on_key_press_event (GdkEventKey* ) override; + bool on_key_release_event (GdkEventKey* ) override; + bool on_motion_notify_event (GdkEventMotion* ) override; + + void on_realize() override; + void on_unrealize() override; + void on_size_allocate(Gtk::Allocation&) override; + bool on_draw(const Cairo::RefPtr<Cairo::Context>&) override; + +private: + + /* Configuration */ + + // Desktop + SPDesktop *_desktop = nullptr; + + // Drawing + Inkscape::Drawing *_drawing = nullptr; + + // Canvas item root + CanvasItemGroup *_canvas_item_root = nullptr; + + // Geometry + Geom::IntPoint _pos; ///< Coordinates of top-left pixel of canvas view within canvas. + Geom::Affine _affine; ///< The affine that we have been requested to draw at. + + // Background + Cairo::RefPtr<Cairo::Pattern> _background; ///< The background of the widget. + + // Rendering modes + Inkscape::RenderMode _render_mode = Inkscape::RenderMode::NORMAL; + Inkscape::SplitMode _split_mode = Inkscape::SplitMode::NORMAL; + Inkscape::ColorMode _color_mode = Inkscape::ColorMode::NORMAL; + + // CMS + std::string _cms_key; + bool _cms_active = false; + + /* Internal state */ + + // Event handling/item picking + GdkEvent _pick_event; ///< Event used to find currently selected item. + bool _in_repick; ///< For tracking recursion of pick_current_item(). + bool _left_grabbed_item; ///< ? + bool _all_enter_events; ///< Keep all enter events. Only set true in connector-tool.cpp. + bool _is_dragging; ///< Used in selection-chemistry to block undo/redo. + int _state; ///< Last known modifier state (SHIFT, CTRL, etc.). + + Inkscape::CanvasItem *_current_canvas_item; ///< Item containing cursor, nullptr if none. + Inkscape::CanvasItem *_current_canvas_item_new; ///< Item to become _current_item, nullptr if none. + Inkscape::CanvasItem *_grabbed_canvas_item; ///< Item that holds a pointer grab; nullptr if none. + Gdk::EventMask _grabbed_event_mask; + + // Drawing + bool _drawing_disabled = false; ///< Disable drawing during critical operations + bool _need_update = true; // Set true so setting CanvasItem bounds are calculated at least once. + + // Split view + Inkscape::SplitDirection _split_direction; + Geom::Point _split_position; + Inkscape::SplitDirection _hover_direction; + bool _split_dragging; + Geom::Point _split_drag_start; + + void add_clippath(const Cairo::RefPtr<Cairo::Context>&); + void set_cursor(); + + // Opaque pointer to implementation + friend class CanvasPrivate; + std::unique_ptr<CanvasPrivate> d; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_CANVAS_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/widget/color-entry.cpp b/src/ui/widget/color-entry.cpp new file mode 100644 index 0000000..804350c --- /dev/null +++ b/src/ui/widget/color-entry.cpp @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Entry widget for typing color value in css form + *//* + * Authors: + * Tomasz Boczkowski <penginsbacon@gmail.com> + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <glibmm.h> +#include <glibmm/i18n.h> +#include <iomanip> + +#include "color-entry.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +ColorEntry::ColorEntry(SelectedColor &color) + : _color(color) + , _updating(false) + , _updatingrgba(false) + , _prevpos(0) + , _lastcolor(0) +{ + _color_changed_connection = color.signal_changed.connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged)); + _color_dragged_connection = color.signal_dragged.connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged)); + signal_activate().connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged)); + get_buffer()->signal_inserted_text().connect(sigc::mem_fun(this, &ColorEntry::_inputCheck)); + _onColorChanged(); + + // add extra character for pasting a hash, '#11223344' + set_max_length(9); + set_width_chars(8); + set_tooltip_text(_("Hexadecimal RGBA value of the color")); +} + +ColorEntry::~ColorEntry() +{ + _color_changed_connection.disconnect(); + _color_dragged_connection.disconnect(); +} + +void ColorEntry::_inputCheck(guint pos, const gchar * /*chars*/, guint n_chars) +{ + // remember position of last character, so we can remove it. + // we only overflow by 1 character at most. + _prevpos = pos + n_chars - 1; +} + +void ColorEntry::on_changed() +{ + if (_updating) { + return; + } + if (_updatingrgba) { + return; // Typing text into entry box + } + + Glib::ustring text = get_text(); + bool changed = false; + + // Coerce the value format to hexadecimal + for (auto it = text.begin(); it != text.end(); /*++it*/) { + if (!g_ascii_isxdigit(*it)) { + text.erase(it); + changed = true; + } else { + ++it; + } + } + + if (text.size() > 8) { + text.erase(_prevpos, 1); + changed = true; + } + + // autofill rules + gchar *str = g_strdup(text.c_str()); + gchar *end = nullptr; + guint64 rgba = g_ascii_strtoull(str, &end, 16); + ptrdiff_t len = end - str; + if (len < 8) { + if (len == 0) { + rgba = _lastcolor; + } else if (len <= 2) { + if (len == 1) { + rgba *= 17; + } + rgba = (rgba << 24) + (rgba << 16) + (rgba << 8); + } else if (len <= 4) { + // display as rrggbbaa + rgba = rgba << (4 * (4 - len)); + guint64 r = rgba & 0xf000; + guint64 g = rgba & 0x0f00; + guint64 b = rgba & 0x00f0; + guint64 a = rgba & 0x000f; + rgba = 17 * ((r << 12) + (g << 8) + (b << 4) + a); + } else { + rgba = rgba << (4 * (8 - len)); + } + + if (len == 7) { + rgba = (rgba & 0xfffffff0) + (_lastcolor & 0x00f); + } else if (len == 5) { + rgba = (rgba & 0xfffff000) + (_lastcolor & 0xfff); + } else if (len != 4 && len != 8) { + rgba = (rgba & 0xffffff00) + (_lastcolor & 0x0ff); + } + } + + _updatingrgba = true; + if (changed) { + set_text(str); + } + SPColor color(rgba); + _color.setColorAlpha(color, SP_RGBA32_A_F(rgba)); + _updatingrgba = false; + + g_free(str); +} + + +void ColorEntry::_onColorChanged() +{ + if (_updatingrgba) { + return; + } + + SPColor color = _color.color(); + gdouble alpha = _color.alpha(); + + _lastcolor = color.toRGBA32(alpha); + Glib::ustring text = Glib::ustring::format(std::hex, std::setw(8), std::setfill(L'0'), _lastcolor); + + Glib::ustring old_text = get_text(); + if (old_text != text) { + _updating = true; + set_text(text); + _updating = 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/widget/color-entry.h b/src/ui/widget/color-entry.h new file mode 100644 index 0000000..4df80de --- /dev/null +++ b/src/ui/widget/color-entry.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Entry widget for typing color value in css form + *//* + * Authors: + * Tomasz Boczkowski <penginsbacon@gmail.com> + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLOR_ENTRY_H +#define SEEN_COLOR_ENTRY_H + +#include <gtkmm/entry.h> +#include "ui/selected-color.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +class ColorEntry : public Gtk::Entry +{ +public: + ColorEntry(SelectedColor &color); + ~ColorEntry() override; + +protected: + void on_changed() override; + +private: + void _onColorChanged(); + void _inputCheck(guint pos, const gchar * /*chars*/, guint /*n_chars*/); + + SelectedColor &_color; + sigc::connection _color_changed_connection; + sigc::connection _color_dragged_connection; + bool _updating; + bool _updatingrgba; + guint32 _lastcolor; + int _prevpos; +}; + +} +} +} + +#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/widget/color-icc-selector.cpp b/src/ui/widget/color-icc-selector.cpp new file mode 100644 index 0000000..8c8456a --- /dev/null +++ b/src/ui/widget/color-icc-selector.cpp @@ -0,0 +1,1025 @@ +// 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. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <set> +#include <utility> + +#include <gtkmm/adjustment.h> +#include <gtkmm/combobox.h> +#include <gtkmm/spinbutton.h> +#include <glibmm/i18n.h> + +#include "colorspace.h" +#include "document.h" +#include "inkscape.h" +#include "profile-manager.h" + +#include "svg/svg-icc-color.h" + +#include "ui/dialog-events.h" +#include "ui/util.h" +#include "ui/widget/color-icc-selector.h" +#include "ui/widget/color-scales.h" +#include "ui/widget/color-slider.h" +#include "ui/widget/scrollprotected.h" + +#define noDEBUG_LCMS + +#include "object/color-profile.h" +#include "cms-system.h" +#include "color-profile-cms-fns.h" + +#ifdef DEBUG_LCMS +#include "preferences.h" +#endif // DEBUG_LCMS + +#ifdef DEBUG_LCMS +extern guint update_in_progress; +#define DEBUG_MESSAGE(key, ...) \ + { \ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); \ + bool dump = prefs->getBool("/options/scislac/" #key); \ + bool dumpD = prefs->getBool("/options/scislac/" #key "D"); \ + bool dumpD2 = prefs->getBool("/options/scislac/" #key "D2"); \ + dumpD && = ((update_in_progress == 0) || dumpD2); \ + if (dump) { \ + g_message(__VA_ARGS__); \ + } \ + if (dumpD) { \ + GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_INFO, \ + GTK_BUTTONS_OK, __VA_ARGS__); \ + g_signal_connect_swapped(dialog, "response", G_CALLBACK(gtk_widget_destroy), dialog); \ + gtk_widget_show_all(dialog); \ + } \ + } +#endif // DEBUG_LCMS + + +#define XPAD 4 +#define YPAD 1 + +namespace { + +GtkWidget *_scrollprotected_combo_box_new_with_model(GtkTreeModel *model) +{ + auto combobox = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBox>()); + gtk_combo_box_set_model(combobox->gobj(), model); + return GTK_WIDGET(combobox->gobj()); +} + +size_t maxColorspaceComponentCount = 0; + + +/** + * Internal variable to track all known colorspaces. + */ +std::set<cmsUInt32Number> knownColorspaces; + +/** + * Helper function to handle GTK2/GTK3 attachment #ifdef code. + */ +void attachToGridOrTable(GtkWidget *parent, GtkWidget *child, guint left, guint top, guint width, guint height, + bool hexpand = false, bool centered = false, guint xpadding = XPAD, guint ypadding = YPAD) +{ + gtk_widget_set_margin_start(child, xpadding); + gtk_widget_set_margin_end(child, xpadding); + gtk_widget_set_margin_top(child, ypadding); + gtk_widget_set_margin_bottom(child, ypadding); + + if (hexpand) { + gtk_widget_set_hexpand(child, TRUE); + } + + if (centered) { + gtk_widget_set_halign(child, GTK_ALIGN_CENTER); + gtk_widget_set_valign(child, GTK_ALIGN_CENTER); + } + + gtk_grid_attach(GTK_GRID(parent), child, left, top, width, height); +} + +} // namespace + +/* +icSigRgbData +icSigCmykData +icSigCmyData +*/ +#define SPACE_ID_RGB 0 +#define SPACE_ID_CMY 1 +#define SPACE_ID_CMYK 2 + + +colorspace::Component::Component() + : name() + , tip() + , scale(1) +{ +} + +colorspace::Component::Component(std::string name, std::string tip, guint scale) + : name(std::move(name)) + , tip(std::move(tip)) + , scale(scale) +{ +} + +static cmsUInt16Number *getScratch() +{ + // bytes per pixel * input channels * width + static cmsUInt16Number *scritch = static_cast<cmsUInt16Number *>(g_new(cmsUInt16Number, 4 * 1024)); + + return scritch; +} + +std::vector<colorspace::Component> colorspace::getColorSpaceInfo(uint32_t space) +{ + static std::map<cmsUInt32Number, std::vector<Component> > sets; + if (sets.empty()) { + sets[cmsSigXYZData].push_back(Component("_X", "X", 2)); // TYPE_XYZ_16 + sets[cmsSigXYZData].push_back(Component("_Y", "Y", 1)); + sets[cmsSigXYZData].push_back(Component("_Z", "Z", 2)); + + sets[cmsSigLabData].push_back(Component("_L", "L", 100)); // TYPE_Lab_16 + sets[cmsSigLabData].push_back(Component("_a", "a", 256)); + sets[cmsSigLabData].push_back(Component("_b", "b", 256)); + + // cmsSigLuvData + + sets[cmsSigYCbCrData].push_back(Component("_Y", "Y", 1)); // TYPE_YCbCr_16 + sets[cmsSigYCbCrData].push_back(Component("C_b", "Cb", 1)); + sets[cmsSigYCbCrData].push_back(Component("C_r", "Cr", 1)); + + sets[cmsSigYxyData].push_back(Component("_Y", "Y", 1)); // TYPE_Yxy_16 + sets[cmsSigYxyData].push_back(Component("_x", "x", 1)); + sets[cmsSigYxyData].push_back(Component("y", "y", 1)); + + sets[cmsSigRgbData].push_back(Component(_("_R:"), _("Red"), 1)); // TYPE_RGB_16 + sets[cmsSigRgbData].push_back(Component(_("_G:"), _("Green"), 1)); + sets[cmsSigRgbData].push_back(Component(_("_B:"), _("Blue"), 1)); + + sets[cmsSigGrayData].push_back(Component(_("G:"), _("Gray"), 1)); // TYPE_GRAY_16 + + sets[cmsSigHsvData].push_back(Component(_("_H:"), _("Hue"), 360)); // TYPE_HSV_16 + sets[cmsSigHsvData].push_back(Component(_("_S:"), _("Saturation"), 1)); + sets[cmsSigHsvData].push_back(Component("_V:", "Value", 1)); + + sets[cmsSigHlsData].push_back(Component(_("_H:"), _("Hue"), 360)); // TYPE_HLS_16 + sets[cmsSigHlsData].push_back(Component(_("_L:"), _("Lightness"), 1)); + sets[cmsSigHlsData].push_back(Component(_("_S:"), _("Saturation"), 1)); + + sets[cmsSigCmykData].push_back(Component(_("_C:"), _("Cyan"), 1)); // TYPE_CMYK_16 + sets[cmsSigCmykData].push_back(Component(_("_M:"), _("Magenta"), 1)); + sets[cmsSigCmykData].push_back(Component(_("_Y:"), _("Yellow"), 1)); + sets[cmsSigCmykData].push_back(Component(_("_K:"), _("Black"), 1)); + + sets[cmsSigCmyData].push_back(Component(_("_C:"), _("Cyan"), 1)); // TYPE_CMY_16 + sets[cmsSigCmyData].push_back(Component(_("_M:"), _("Magenta"), 1)); + sets[cmsSigCmyData].push_back(Component(_("_Y:"), _("Yellow"), 1)); + + for (auto & set : sets) { + knownColorspaces.insert(set.first); + maxColorspaceComponentCount = std::max(maxColorspaceComponentCount, set.second.size()); + } + } + + std::vector<Component> target; + + if (sets.find(space) != sets.end()) { + target = sets[space]; + } + return target; +} + + +std::vector<colorspace::Component> colorspace::getColorSpaceInfo(Inkscape::ColorProfile *prof) +{ + return getColorSpaceInfo(asICColorSpaceSig(prof->getColorSpace())); +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Class containing the parts for a single color component's UI presence. + */ +class ComponentUI { + public: + ComponentUI() + : _component() + , _adj(nullptr) + , _slider(nullptr) + , _btn(nullptr) + , _label(nullptr) + , _map(nullptr) + { + } + + ComponentUI(colorspace::Component component) + : _component(std::move(component)) + , _adj(nullptr) + , _slider(nullptr) + , _btn(nullptr) + , _label(nullptr) + , _map(nullptr) + { + } + + colorspace::Component _component; + Glib::RefPtr<Gtk::Adjustment> _adj; // Component adjustment + Inkscape::UI::Widget::ColorSlider *_slider; + GtkWidget *_btn; // spinbutton + GtkWidget *_label; // Label + guchar *_map; +}; + +/** + * Class that implements the internals of the selector. + */ +class ColorICCSelectorImpl { + public: + ColorICCSelectorImpl(ColorICCSelector *owner, SelectedColor &color); + + ~ColorICCSelectorImpl(); + + void _adjustmentChanged(Glib::RefPtr<Gtk::Adjustment> &adjustment); + + void _sliderGrabbed(); + void _sliderReleased(); + void _sliderChanged(); + + static void _profileSelected(GtkWidget *src, gpointer data); + static void _fixupHit(GtkWidget *src, gpointer data); + + void _setProfile(SVGICCColor *profile); + void _switchToProfile(gchar const *name); + + void _updateSliders(gint ignore); + void _profilesChanged(std::string const &name); + + ColorICCSelector *_owner; + SelectedColor &_color; + + gboolean _updating : 1; + gboolean _dragging : 1; + + guint32 _fixupNeeded; + GtkWidget *_fixupBtn; + GtkWidget *_profileSel; + + std::vector<ComponentUI> _compUI; + + Glib::RefPtr<Gtk::Adjustment> _adj; // Channel adjustment + Inkscape::UI::Widget::ColorSlider *_slider; + GtkWidget *_sbtn; // Spinbutton + GtkWidget *_label; // Label + + std::string _profileName; + Inkscape::ColorProfile *_prof; + guint _profChannelCount; + gulong _profChangedID; +}; + + + +const gchar *ColorICCSelector::MODE_NAME = N_("CMS"); + +ColorICCSelector::ColorICCSelector(SelectedColor &color) + : _impl(nullptr) +{ + _impl = new ColorICCSelectorImpl(this, color); + init(); + color.signal_changed.connect(sigc::mem_fun(this, &ColorICCSelector::_colorChanged)); + // color.signal_dragged.connect(sigc::mem_fun(this, &ColorICCSelector::_colorChanged)); +} + +ColorICCSelector::~ColorICCSelector() +{ + if (_impl) { + delete _impl; + _impl = nullptr; + } +} + + + +ColorICCSelectorImpl::ColorICCSelectorImpl(ColorICCSelector *owner, SelectedColor &color) + : _owner(owner) + , _color(color) + , _updating(FALSE) + , _dragging(FALSE) + , _fixupNeeded(0) + , _fixupBtn(nullptr) + , _profileSel(nullptr) + , _compUI() + , _adj(nullptr) + , _slider(nullptr) + , _sbtn(nullptr) + , _label(nullptr) + , _profileName() + , _prof(nullptr) + , _profChannelCount(0) + , _profChangedID(0) +{ +} + +ColorICCSelectorImpl::~ColorICCSelectorImpl() +{ + _sbtn = nullptr; + _label = nullptr; +} + +void ColorICCSelector::init() +{ + gint row = 0; + + _impl->_updating = FALSE; + _impl->_dragging = FALSE; + + GtkWidget *t = GTK_WIDGET(gobj()); + + _impl->_compUI.clear(); + + // Create components + row = 0; + + + _impl->_fixupBtn = gtk_button_new_with_label(_("Fix")); + g_signal_connect(G_OBJECT(_impl->_fixupBtn), "clicked", G_CALLBACK(ColorICCSelectorImpl::_fixupHit), + (gpointer)_impl); + gtk_widget_set_sensitive(_impl->_fixupBtn, FALSE); + gtk_widget_set_tooltip_text(_impl->_fixupBtn, _("Fix RGB fallback to match icc-color() value.")); + gtk_widget_show(_impl->_fixupBtn); + + attachToGridOrTable(t, _impl->_fixupBtn, 0, row, 1, 1); + + // Combobox and store with 2 columns : label (0) and full name (1) + GtkListStore *store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING); + _impl->_profileSel = _scrollprotected_combo_box_new_with_model(GTK_TREE_MODEL(store)); + + GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(_impl->_profileSel), renderer, TRUE); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(_impl->_profileSel), renderer, "text", 0, nullptr); + + GtkTreeIter iter; + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, _("<none>"), 1, _("<none>"), -1); + + gtk_widget_show(_impl->_profileSel); + gtk_combo_box_set_active(GTK_COMBO_BOX(_impl->_profileSel), 0); + + attachToGridOrTable(t, _impl->_profileSel, 1, row, 1, 1); + + _impl->_profChangedID = g_signal_connect(G_OBJECT(_impl->_profileSel), "changed", + G_CALLBACK(ColorICCSelectorImpl::_profileSelected), (gpointer)_impl); + + row++; + +// populate the data for colorspaces and channels: + std::vector<colorspace::Component> things = colorspace::getColorSpaceInfo(cmsSigRgbData); + + for (size_t i = 0; i < maxColorspaceComponentCount; i++) { + if (i < things.size()) { + _impl->_compUI.emplace_back(things[i]); + } + else { + _impl->_compUI.emplace_back(); + } + + std::string labelStr = (i < things.size()) ? things[i].name.c_str() : ""; + + _impl->_compUI[i]._label = gtk_label_new_with_mnemonic(labelStr.c_str()); + + gtk_widget_set_halign(_impl->_compUI[i]._label, GTK_ALIGN_END); + gtk_widget_show(_impl->_compUI[i]._label); + gtk_widget_set_no_show_all(_impl->_compUI[i]._label, TRUE); + + attachToGridOrTable(t, _impl->_compUI[i]._label, 0, row, 1, 1); + + // Adjustment + guint scaleValue = _impl->_compUI[i]._component.scale; + gdouble step = static_cast<gdouble>(scaleValue) / 100.0; + gdouble page = static_cast<gdouble>(scaleValue) / 10.0; + gint digits = (step > 0.9) ? 0 : 2; + _impl->_compUI[i]._adj = Gtk::Adjustment::create(0.0, 0.0, scaleValue, step, page, page); + + // Slider + _impl->_compUI[i]._slider = + Gtk::manage(new Inkscape::UI::Widget::ColorSlider(_impl->_compUI[i]._adj)); + _impl->_compUI[i]._slider->set_tooltip_text((i < things.size()) ? things[i].tip.c_str() : ""); + _impl->_compUI[i]._slider->show(); + _impl->_compUI[i]._slider->set_no_show_all(); + + attachToGridOrTable(t, _impl->_compUI[i]._slider->gobj(), 1, row, 1, 1, true); + + auto spinbutton = Gtk::manage(new ScrollProtected<Gtk::SpinButton>(_impl->_compUI[i]._adj, step, digits)); + _impl->_compUI[i]._btn = GTK_WIDGET(spinbutton->gobj()); + gtk_widget_set_tooltip_text(_impl->_compUI[i]._btn, (i < things.size()) ? things[i].tip.c_str() : ""); + sp_dialog_defocus_on_enter(_impl->_compUI[i]._btn); + gtk_label_set_mnemonic_widget(GTK_LABEL(_impl->_compUI[i]._label), _impl->_compUI[i]._btn); + gtk_widget_show(_impl->_compUI[i]._btn); + gtk_widget_set_no_show_all(_impl->_compUI[i]._btn, TRUE); + + attachToGridOrTable(t, _impl->_compUI[i]._btn, 2, row, 1, 1, false, true); + + _impl->_compUI[i]._map = g_new(guchar, 4 * 1024); + memset(_impl->_compUI[i]._map, 0x0ff, 1024 * 4); + + + // Signals + _impl->_compUI[i]._adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_adjustmentChanged), _impl->_compUI[i]._adj)); + + _impl->_compUI[i]._slider->signal_grabbed.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderGrabbed)); + _impl->_compUI[i]._slider->signal_released.connect( + sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderReleased)); + _impl->_compUI[i]._slider->signal_value_changed.connect( + sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderChanged)); + + row++; + } + + // Label + _impl->_label = gtk_label_new_with_mnemonic(_("_A:")); + + gtk_widget_set_halign(_impl->_label, GTK_ALIGN_END); + gtk_widget_show(_impl->_label); + + attachToGridOrTable(t, _impl->_label, 0, row, 1, 1); + + // Adjustment + _impl->_adj = Gtk::Adjustment::create(0.0, 0.0, 100.0, 1.0, 10.0, 10.0); + + // Slider + _impl->_slider = Gtk::manage(new Inkscape::UI::Widget::ColorSlider(_impl->_adj)); + _impl->_slider->set_tooltip_text(_("Alpha (opacity)")); + _impl->_slider->show(); + + attachToGridOrTable(t, _impl->_slider->gobj(), 1, row, 1, 1, true); + + _impl->_slider->setColors(SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.0), SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.5), + SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 1.0)); + + + // Spinbutton + auto spinbuttonalpha = Gtk::manage(new ScrollProtected<Gtk::SpinButton>(_impl->_adj, 1.0)); + _impl->_sbtn = GTK_WIDGET(spinbuttonalpha->gobj()); + gtk_widget_set_tooltip_text(_impl->_sbtn, _("Alpha (opacity)")); + sp_dialog_defocus_on_enter(_impl->_sbtn); + gtk_label_set_mnemonic_widget(GTK_LABEL(_impl->_label), _impl->_sbtn); + gtk_widget_show(_impl->_sbtn); + + attachToGridOrTable(t, _impl->_sbtn, 2, row, 1, 1, false, true); + + // Signals + _impl->_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_adjustmentChanged), _impl->_adj)); + + _impl->_slider->signal_grabbed.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderGrabbed)); + _impl->_slider->signal_released.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderReleased)); + _impl->_slider->signal_value_changed.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderChanged)); + + gtk_widget_show(t); +} + +void ColorICCSelectorImpl::_fixupHit(GtkWidget * /*src*/, gpointer data) +{ + ColorICCSelectorImpl *self = reinterpret_cast<ColorICCSelectorImpl *>(data); + gtk_widget_set_sensitive(self->_fixupBtn, FALSE); + self->_adjustmentChanged(self->_compUI[0]._adj); +} + +void ColorICCSelectorImpl::_profileSelected(GtkWidget * /*src*/, gpointer data) +{ + ColorICCSelectorImpl *self = reinterpret_cast<ColorICCSelectorImpl *>(data); + + GtkTreeIter iter; + if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(self->_profileSel), &iter)) { + GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(self->_profileSel)); + gchar *name = nullptr; + + gtk_tree_model_get(store, &iter, 1, &name, -1); + self->_switchToProfile(name); + gtk_widget_set_tooltip_text(self->_profileSel, name); + + g_free(name); + } +} + +void ColorICCSelectorImpl::_switchToProfile(gchar const *name) +{ + bool dirty = false; + SPColor tmp(_color.color()); + + if (name) { + if (tmp.icc && tmp.icc->colorProfile == name) { +#ifdef DEBUG_LCMS + g_message("Already at name [%s]", name); +#endif // DEBUG_LCMS + } + else { +#ifdef DEBUG_LCMS + g_message("Need to switch to profile [%s]", name); +#endif // DEBUG_LCMS + if (tmp.icc) { + tmp.icc->colors.clear(); + } + else { + tmp.icc = new SVGICCColor(); + } + tmp.icc->colorProfile = name; + Inkscape::ColorProfile *newProf = SP_ACTIVE_DOCUMENT->getProfileManager()->find(name); + if (newProf) { + cmsHTRANSFORM trans = newProf->getTransfFromSRGB8(); + if (trans) { + guint32 val = _color.color().toRGBA32(0); + guchar pre[4] = { + static_cast<guchar>(SP_RGBA32_R_U(val)), + static_cast<guchar>(SP_RGBA32_G_U(val)), + static_cast<guchar>(SP_RGBA32_B_U(val)), + 255}; +#ifdef DEBUG_LCMS + g_message("Shoving in [%02x] [%02x] [%02x]", pre[0], pre[1], pre[2]); +#endif // DEBUG_LCMS + cmsUInt16Number post[4] = { 0, 0, 0, 0 }; + cmsDoTransform(trans, pre, post, 1); +#ifdef DEBUG_LCMS + g_message("got on out [%04x] [%04x] [%04x] [%04x]", post[0], post[1], post[2], post[3]); +#endif // DEBUG_LCMS + guint count = cmsChannelsOf(asICColorSpaceSig(newProf->getColorSpace())); + + std::vector<colorspace::Component> things = + colorspace::getColorSpaceInfo(asICColorSpaceSig(newProf->getColorSpace())); + + for (guint i = 0; i < count; i++) { + gdouble val = + (((gdouble)post[i]) / 65535.0) * (gdouble)((i < things.size()) ? things[i].scale : 1); +#ifdef DEBUG_LCMS + g_message(" scaled %d by %d to be %f", i, ((i < things.size()) ? things[i].scale : 1), val); +#endif // DEBUG_LCMS + tmp.icc->colors.push_back(val); + } + cmsHTRANSFORM retrans = newProf->getTransfToSRGB8(); + if (retrans) { + cmsDoTransform(retrans, post, pre, 1); +#ifdef DEBUG_LCMS + g_message(" back out [%02x] [%02x] [%02x]", pre[0], pre[1], pre[2]); +#endif // DEBUG_LCMS + tmp.set(SP_RGBA32_U_COMPOSE(pre[0], pre[1], pre[2], 0xff)); + } + + dirty = true; + } + } + } + } + else { +#ifdef DEBUG_LCMS + g_message("NUKE THE ICC"); +#endif // DEBUG_LCMS + if (tmp.icc) { + delete tmp.icc; + tmp.icc = nullptr; + dirty = true; + _fixupHit(nullptr, this); + } + else { +#ifdef DEBUG_LCMS + g_message("No icc to nuke"); +#endif // DEBUG_LCMS + } + } + + if (dirty) { +#ifdef DEBUG_LCMS + g_message("+----------------"); + g_message("+ new color is [%s]", tmp.toString().c_str()); +#endif // DEBUG_LCMS + _setProfile(tmp.icc); + //_adjustmentChanged( _compUI[0]._adj, SP_COLOR_ICC_SELECTOR(_csel) ); + _color.setColor(tmp); +#ifdef DEBUG_LCMS + g_message("+_________________"); +#endif // DEBUG_LCMS + } +} + +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 ColorICCSelectorImpl::_profilesChanged(std::string const &name) +{ + GtkComboBox *combo = GTK_COMBO_BOX(_profileSel); + + g_signal_handler_block(G_OBJECT(_profileSel), _profChangedID); + + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(combo)); + gtk_list_store_clear(store); + + GtkTreeIter iter; + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, _("<none>"), 1, _("<none>"), -1); + + gtk_combo_box_set_active(combo, 0); + + int index = 1; + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList("iccprofile"); + + std::set<Inkscape::ColorProfile *> _current; + std::transform(current.begin(), + current.end(), + std::inserter(_current, _current.begin()), + static_caster<SPObject, Inkscape::ColorProfile>()); + + for (auto &it: _current) { + Inkscape::ColorProfile *prof = it; + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, ink_ellipsize_text(prof->name, 25).c_str(), 1, prof->name, -1); + + if (name == prof->name) { + gtk_combo_box_set_active(combo, index); + gtk_widget_set_tooltip_text(_profileSel, prof->name); + } + + index++; + } + + g_signal_handler_unblock(G_OBJECT(_profileSel), _profChangedID); +} + +void ColorICCSelector::on_show() +{ + Gtk::Grid::on_show(); + _colorChanged(); +} + +// Helpers for setting color value + +void ColorICCSelector::_colorChanged() +{ + _impl->_updating = TRUE; +// sp_color_icc_set_color( SP_COLOR_ICC( _icc ), &color ); + +#ifdef DEBUG_LCMS + g_message("/^^^^^^^^^ %p::_colorChanged(%08x:%s)", this, _impl->_color.color().toRGBA32(_impl->_color.alpha()), + ((_impl->_color.color().icc) ? _impl->_color.color().icc->colorProfile.c_str() : "<null>")); +#endif // DEBUG_LCMS + +#ifdef DEBUG_LCMS + g_message("FLIPPIES!!!! %p '%s'", _impl->_color.color().icc, + (_impl->_color.color().icc ? _impl->_color.color().icc->colorProfile.c_str() : "<null>")); +#endif // DEBUG_LCMS + + _impl->_profilesChanged((_impl->_color.color().icc) ? _impl->_color.color().icc->colorProfile : std::string("")); + ColorScales<>::setScaled(_impl->_adj, _impl->_color.alpha()); + + _impl->_setProfile(_impl->_color.color().icc); + _impl->_fixupNeeded = 0; + gtk_widget_set_sensitive(_impl->_fixupBtn, FALSE); + + if (_impl->_prof) { + if (_impl->_prof->getTransfToSRGB8()) { + cmsUInt16Number tmp[4]; + for (guint i = 0; i < _impl->_profChannelCount; i++) { + gdouble val = 0.0; + if (_impl->_color.color().icc->colors.size() > i) { + if (_impl->_compUI[i]._component.scale == 256) { + val = (_impl->_color.color().icc->colors[i] + 128.0) / + static_cast<gdouble>(_impl->_compUI[i]._component.scale); + } + else { + val = _impl->_color.color().icc->colors[i] / + static_cast<gdouble>(_impl->_compUI[i]._component.scale); + } + } + tmp[i] = val * 0x0ffff; + } + guchar post[4] = { 0, 0, 0, 0 }; + cmsHTRANSFORM trans = _impl->_prof->getTransfToSRGB8(); + if (trans) { + cmsDoTransform(trans, tmp, post, 1); + guint32 other = SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 255); + if (other != _impl->_color.color().toRGBA32(255)) { + _impl->_fixupNeeded = other; + gtk_widget_set_sensitive(_impl->_fixupBtn, TRUE); +#ifdef DEBUG_LCMS + g_message("Color needs to change 0x%06x to 0x%06x", _color.toRGBA32(255) >> 8, other >> 8); +#endif // DEBUG_LCMS + } + } + } + } + _impl->_updateSliders(-1); + + + _impl->_updating = FALSE; +#ifdef DEBUG_LCMS + g_message("\\_________ %p::_colorChanged()", this); +#endif // DEBUG_LCMS +} + +void ColorICCSelectorImpl::_setProfile(SVGICCColor *profile) +{ +#ifdef DEBUG_LCMS + g_message("/^^^^^^^^^ %p::_setProfile(%s)", this, ((profile) ? profile->colorProfile.c_str() : "<null>")); +#endif // DEBUG_LCMS + bool profChanged = false; + if (_prof && (!profile || (_profileName != profile->colorProfile))) { + // Need to clear out the prior one + profChanged = true; + _profileName.clear(); + _prof = nullptr; + _profChannelCount = 0; + } + else if (profile && !_prof) { + profChanged = true; + } + + for (auto & i : _compUI) { + gtk_widget_hide(i._label); + i._slider->hide(); + gtk_widget_hide(i._btn); + } + + if (profile) { + _prof = SP_ACTIVE_DOCUMENT->getProfileManager()->find(profile->colorProfile.c_str()); + if (_prof && (asICColorProfileClassSig(_prof->getProfileClass()) != cmsSigNamedColorClass)) { + _profChannelCount = cmsChannelsOf(asICColorSpaceSig(_prof->getColorSpace())); + + if (profChanged) { + std::vector<colorspace::Component> things = + colorspace::getColorSpaceInfo(asICColorSpaceSig(_prof->getColorSpace())); + for (size_t i = 0; (i < things.size()) && (i < _profChannelCount); ++i) { + _compUI[i]._component = things[i]; + } + + for (guint i = 0; i < _profChannelCount; i++) { + gtk_label_set_text_with_mnemonic(GTK_LABEL(_compUI[i]._label), + (i < things.size()) ? things[i].name.c_str() : ""); + + _compUI[i]._slider->set_tooltip_text((i < things.size()) ? things[i].tip.c_str() : ""); + gtk_widget_set_tooltip_text(_compUI[i]._btn, (i < things.size()) ? things[i].tip.c_str() : ""); + + _compUI[i]._slider->setColors(SPColor(0.0, 0.0, 0.0).toRGBA32(0xff), + SPColor(0.5, 0.5, 0.5).toRGBA32(0xff), + SPColor(1.0, 1.0, 1.0).toRGBA32(0xff)); + /* + _compUI[i]._adj = GTK_ADJUSTMENT( gtk_adjustment_new( val, 0.0, _fooScales[i], + step, page, page ) ); + g_signal_connect( G_OBJECT( _compUI[i]._adj ), "value_changed", G_CALLBACK( + _adjustmentChanged ), _csel ); + + sp_color_slider_set_adjustment( SP_COLOR_SLIDER(_compUI[i]._slider), + _compUI[i]._adj ); + gtk_spin_button_set_adjustment( GTK_SPIN_BUTTON(_compUI[i]._btn), + _compUI[i]._adj ); + gtk_spin_button_set_digits( GTK_SPIN_BUTTON(_compUI[i]._btn), digits ); + */ + gtk_widget_show(_compUI[i]._label); + _compUI[i]._slider->show(); + gtk_widget_show(_compUI[i]._btn); + // gtk_adjustment_set_value( _compUI[i]._adj, 0.0 ); + // gtk_adjustment_set_value( _compUI[i]._adj, val ); + } + for (size_t i = _profChannelCount; i < _compUI.size(); i++) { + gtk_widget_hide(_compUI[i]._label); + _compUI[i]._slider->hide(); + gtk_widget_hide(_compUI[i]._btn); + } + } + } + else { + // Give up for now on named colors + _prof = nullptr; + } + } + +#ifdef DEBUG_LCMS + g_message("\\_________ %p::_setProfile()", this); +#endif // DEBUG_LCMS +} + +void ColorICCSelectorImpl::_updateSliders(gint ignore) +{ + if (_color.color().icc) { + for (guint i = 0; i < _profChannelCount; i++) { + gdouble val = 0.0; + if (_color.color().icc->colors.size() > i) { + if (_compUI[i]._component.scale == 256) { + val = (_color.color().icc->colors[i] + 128.0) / static_cast<gdouble>(_compUI[i]._component.scale); + } + else { + val = _color.color().icc->colors[i] / static_cast<gdouble>(_compUI[i]._component.scale); + } + } + _compUI[i]._adj->set_value(val); + } + + if (_prof) { + if (_prof->getTransfToSRGB8()) { + for (guint i = 0; i < _profChannelCount; i++) { + if (static_cast<gint>(i) != ignore) { + cmsUInt16Number *scratch = getScratch(); + cmsUInt16Number filler[4] = { 0, 0, 0, 0 }; + for (guint j = 0; j < _profChannelCount; j++) { + filler[j] = 0x0ffff * ColorScales<>::getScaled(_compUI[j]._adj); + } + + cmsUInt16Number *p = scratch; + for (guint x = 0; x < 1024; x++) { + for (guint j = 0; j < _profChannelCount; j++) { + if (j == i) { + *p++ = x * 0x0ffff / 1024; + } + else { + *p++ = filler[j]; + } + } + } + + cmsHTRANSFORM trans = _prof->getTransfToSRGB8(); + if (trans) { + cmsDoTransform(trans, scratch, _compUI[i]._map, 1024); + if (_compUI[i]._slider) + { + _compUI[i]._slider->setMap(_compUI[i]._map); + } + } + } + } + } + } + } + + guint32 start = _color.color().toRGBA32(0x00); + guint32 mid = _color.color().toRGBA32(0x7f); + guint32 end = _color.color().toRGBA32(0xff); + + _slider->setColors(start, mid, end); +} + + +void ColorICCSelectorImpl::_adjustmentChanged(Glib::RefPtr<Gtk::Adjustment> &adjustment) +{ +#ifdef DEBUG_LCMS + g_message("/^^^^^^^^^ %p::_adjustmentChanged()", this); +#endif // DEBUG_LCMS + + ColorICCSelector *iccSelector = _owner; + if (iccSelector->_impl->_updating) { + return; + } + + iccSelector->_impl->_updating = TRUE; + + gint match = -1; + + SPColor newColor(iccSelector->_impl->_color.color()); + gfloat scaled = ColorScales<>::getScaled(iccSelector->_impl->_adj); + if (iccSelector->_impl->_adj == adjustment) { +#ifdef DEBUG_LCMS + g_message("ALPHA"); +#endif // DEBUG_LCMS + } + else { + for (size_t i = 0; i < iccSelector->_impl->_compUI.size(); i++) { + if (iccSelector->_impl->_compUI[i]._adj == adjustment) { + match = i; + break; + } + } + if (match >= 0) { +#ifdef DEBUG_LCMS + g_message(" channel %d", match); +#endif // DEBUG_LCMS + } + + + cmsUInt16Number tmp[4]; + for (guint i = 0; i < 4; i++) { + tmp[i] = ColorScales<>::getScaled(iccSelector->_impl->_compUI[i]._adj) * 0x0ffff; + } + guchar post[4] = { 0, 0, 0, 0 }; + + cmsHTRANSFORM trans = iccSelector->_impl->_prof->getTransfToSRGB8(); + if (trans) { + cmsDoTransform(trans, tmp, post, 1); + } + + SPColor other(SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 255)); + other.icc = new SVGICCColor(); + if (iccSelector->_impl->_color.color().icc) { + other.icc->colorProfile = iccSelector->_impl->_color.color().icc->colorProfile; + } + + guint32 prior = iccSelector->_impl->_color.color().toRGBA32(255); + guint32 newer = other.toRGBA32(255); + + if (prior != newer) { +#ifdef DEBUG_LCMS + g_message("Transformed color from 0x%08x to 0x%08x", prior, newer); + g_message(" ~~~~ FLIP"); +#endif // DEBUG_LCMS + newColor = other; + newColor.icc->colors.clear(); + for (guint i = 0; i < iccSelector->_impl->_profChannelCount; i++) { + gdouble val = ColorScales<>::getScaled(iccSelector->_impl->_compUI[i]._adj); + val *= iccSelector->_impl->_compUI[i]._component.scale; + if (iccSelector->_impl->_compUI[i]._component.scale == 256) { + val -= 128; + } + newColor.icc->colors.push_back(val); + } + } + } + iccSelector->_impl->_color.setColorAlpha(newColor, scaled); + // iccSelector->_updateInternals( newColor, scaled, iccSelector->_impl->_dragging ); + iccSelector->_impl->_updateSliders(match); + + iccSelector->_impl->_updating = FALSE; +#ifdef DEBUG_LCMS + g_message("\\_________ %p::_adjustmentChanged()", cs); +#endif // DEBUG_LCMS +} + +void ColorICCSelectorImpl::_sliderGrabbed() +{ + // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base); + // if (!iccSelector->_dragging) { + // iccSelector->_dragging = TRUE; + // iccSelector->_grabbed(); + // iccSelector->_updateInternals( iccSelector->_color, ColorScales<>::getScaled( iccSelector->_impl->_adj ), + // iccSelector->_dragging ); + // } +} + +void ColorICCSelectorImpl::_sliderReleased() +{ + // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base); + // if (iccSelector->_dragging) { + // iccSelector->_dragging = FALSE; + // iccSelector->_released(); + // iccSelector->_updateInternals( iccSelector->_color, ColorScales<>::getScaled( iccSelector->_adj ), + // iccSelector->_dragging ); + // } +} + +#ifdef DEBUG_LCMS +void ColorICCSelectorImpl::_sliderChanged(SPColorSlider *slider, SPColorICCSelector *cs) +#else +void ColorICCSelectorImpl::_sliderChanged() +#endif // DEBUG_LCMS +{ +#ifdef DEBUG_LCMS + g_message("Changed %p and %p", slider, cs); +#endif // DEBUG_LCMS + // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base); + + // iccSelector->_updateInternals( iccSelector->_color, ColorScales<>::getScaled( iccSelector->_adj ), + // iccSelector->_dragging ); +} + +Gtk::Widget *ColorICCSelectorFactory::createWidget(Inkscape::UI::SelectedColor &color) const +{ + Gtk::Widget *w = Gtk::manage(new ColorICCSelector(color)); + return w; +} + +Glib::ustring ColorICCSelectorFactory::modeName() const { return gettext(ColorICCSelector::MODE_NAME); } +} +} +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/color-icc-selector.h b/src/ui/widget/color-icc-selector.h new file mode 100644 index 0000000..2c5ec41 --- /dev/null +++ b/src/ui/widget/color-icc-selector.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_COLOR_ICC_SELECTOR_H +#define SEEN_SP_COLOR_ICC_SELECTOR_H + +#include <gtkmm/widget.h> +#include <gtkmm/grid.h> + +#include "ui/selected-color.h" + +namespace Inkscape { + +class ColorProfile; + +namespace UI { +namespace Widget { + +class ColorICCSelectorImpl; + +class ColorICCSelector + : public Gtk::Grid + { + public: + static const gchar *MODE_NAME; + + ColorICCSelector(SelectedColor &color); + ~ColorICCSelector() override; + + virtual void init(); + + protected: + void on_show() override; + + virtual void _colorChanged(); + + void _recalcColor(gboolean changing); + + private: + friend class ColorICCSelectorImpl; + + // By default, disallow copy constructor and assignment operator + ColorICCSelector(const ColorICCSelector &obj); + ColorICCSelector &operator=(const ColorICCSelector &obj); + + ColorICCSelectorImpl *_impl; +}; + + +class ColorICCSelectorFactory : public ColorSelectorFactory { + public: + Gtk::Widget *createWidget(SelectedColor &color) const override; + Glib::ustring modeName() const override; +}; +} +} +} +#endif // SEEN_SP_COLOR_ICC_SELECTOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/color-notebook.cpp b/src/ui/widget/color-notebook.cpp new file mode 100644 index 0000000..e0dcd16 --- /dev/null +++ b/src/ui/widget/color-notebook.cpp @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A notebook with RGB, CMYK, CMS, HSL, and Wheel pages + *//* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Tomasz Boczkowski <penginsbacon@gmail.com> (c++-sification) + * + * Copyright (C) 2001-2014 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 + +#undef SPCS_PREVIEW +#define noDUMP_CHANGE_INFO + +#include <glibmm/i18n.h> +#include <gtkmm/label.h> +#include <gtkmm/notebook.h> +#include <gtkmm/radiobutton.h> + +#include "cms-system.h" +#include "document.h" +#include "inkscape.h" +#include "preferences.h" +#include "profile-manager.h" + +#include "object/color-profile.h" +#include "ui/icon-loader.h" + +#include "svg/svg-icc-color.h" + +#include "ui/dialog-events.h" +#include "ui/tools/dropper-tool.h" +#include "ui/widget/color-entry.h" +#include "ui/widget/color-icc-selector.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/color-scales.h" +//#include "ui/widget/color-wheel-selector.h" +//#include "ui/widget/color-wheel-hsluv-selector.h" + +#include "widgets/spw-utilities.h" + +using Inkscape::CMSSystem; + +#define XPAD 2 +#define YPAD 1 + +namespace Inkscape { +namespace UI { +namespace Widget { + + +ColorNotebook::ColorNotebook(SelectedColor &color) + : Gtk::Grid() + , _selected_color(color) +{ + set_name("ColorNotebook"); + + _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::HSL>, "color-selector-hsx")); + _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::HSV>, "color-selector-hsx")); + _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::RGB>, "color-selector-rgb")); + _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::CMYK>, "color-selector-cmyk")); + _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::HSLUV>, "color-selector-hsluv")); + //_available_pages.push_back(new Page(new ColorWheelSelectorFactory, "color-selector-wheel")); + _available_pages.push_back(new Page(new ColorICCSelectorFactory, "color-selector-cms")); + + _initUI(); + + _selected_color.signal_changed.connect(sigc::mem_fun(this, &ColorNotebook::_onSelectedColorChanged)); + _selected_color.signal_dragged.connect(sigc::mem_fun(this, &ColorNotebook::_onSelectedColorChanged)); +} + +ColorNotebook::~ColorNotebook() +{ + if (_onetimepick) + _onetimepick.disconnect(); +} + +ColorNotebook::Page::Page(Inkscape::UI::ColorSelectorFactory *selector_factory, const char* icon) + : selector_factory(selector_factory), icon_name(icon) +{ +} + +void ColorNotebook::set_label(const Glib::ustring& label) { + _label->set_markup(label); +} + +void ColorNotebook::_initUI() +{ + guint row = 0; + + _book = Gtk::make_managed<Gtk::Stack>(); + _book->show(); + _book->set_transition_type(Gtk::STACK_TRANSITION_TYPE_CROSSFADE); + _book->set_transition_duration(130); + + // mode selection switcher widget shows all buttons for color mode selection, side by side + _switcher = Gtk::make_managed<Gtk::StackSwitcher>(); + _switcher->set_stack(*_book); + // cannot leave it homogeneous - in some themes switcher gets very wide + _switcher->set_homogeneous(false); + _switcher->set_halign(Gtk::ALIGN_CENTER); + _switcher->show(); + attach(*_switcher, 0, row++, 2); + + _buttonbox = Gtk::make_managed<Gtk::Box>(); + _buttonbox->show(); + + // combo mode selection is compact and only shows one entry (active) + _combo = Gtk::manage(new IconComboBox()); + _combo->set_can_focus(false); + _combo->set_visible(); + _combo->set_tooltip_text(_("Choose style of color selection")); + + for (auto&& page : _available_pages) { + _addPage(page); + } + + _label = Gtk::make_managed<Gtk::Label>(); + _label->set_visible(); + _buttonbox->pack_start(*_label, false, true); + _buttonbox->pack_end(*_combo, false, false); + _combo->signal_changed().connect([=](){ _setCurrentPage(_combo->get_active_row_id(), false); }); + + _buttonbox->set_margin_start(XPAD); + _buttonbox->set_margin_end(XPAD); + _buttonbox->set_margin_top(YPAD); + _buttonbox->set_margin_bottom(YPAD); + _buttonbox->set_hexpand(); + _buttonbox->set_valign(Gtk::ALIGN_START); + attach(*_buttonbox, 0, row, 2); + + row++; + + _book->set_margin_start(XPAD); + _book->set_margin_end(XPAD); + _book->set_margin_top(YPAD); + _book->set_margin_bottom(YPAD); + _book->set_hexpand(); + _book->set_vexpand(); + attach(*_book, 0, row, 2, 1); + + // restore the last active page + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _setCurrentPage(prefs->getInt("/colorselector/page", 0), true); + row++; + + auto switcher_path = Glib::ustring("/colorselector/switcher"); + auto choose_switch = [=](bool compact) { + if (compact) { + _switcher->hide(); + _buttonbox->show(); + } + else { + _buttonbox->hide(); + _switcher->show(); + } + }; + + _observer = prefs->createObserver(switcher_path, [=](const Preferences::Entry& new_value) { + choose_switch(new_value.getBool()); + }); + + choose_switch(prefs->getBool(switcher_path)); + + GtkWidget *rgbabox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + + /* Create color management icons */ + _box_colormanaged = gtk_event_box_new(); + GtkWidget *colormanaged = sp_get_icon_image("color-management", GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_container_add(GTK_CONTAINER(_box_colormanaged), colormanaged); + gtk_widget_set_tooltip_text(_box_colormanaged, _("Color Managed")); + gtk_widget_set_sensitive(_box_colormanaged, false); + gtk_box_pack_start(GTK_BOX(rgbabox), _box_colormanaged, FALSE, FALSE, 2); + + _box_outofgamut = gtk_event_box_new(); + GtkWidget *outofgamut = sp_get_icon_image("out-of-gamut-icon", GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_container_add(GTK_CONTAINER(_box_outofgamut), outofgamut); + gtk_widget_set_tooltip_text(_box_outofgamut, _("Out of gamut!")); + gtk_widget_set_sensitive(_box_outofgamut, false); + gtk_box_pack_start(GTK_BOX(rgbabox), _box_outofgamut, FALSE, FALSE, 2); + + _box_toomuchink = gtk_event_box_new(); + GtkWidget *toomuchink = sp_get_icon_image("too-much-ink-icon", GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_container_add(GTK_CONTAINER(_box_toomuchink), toomuchink); + gtk_widget_set_tooltip_text(_box_toomuchink, _("Too much ink!")); + gtk_widget_set_sensitive(_box_toomuchink, false); + gtk_box_pack_start(GTK_BOX(rgbabox), _box_toomuchink, FALSE, FALSE, 2); + + + /* Color picker */ + GtkWidget *picker = sp_get_icon_image("color-picker", GTK_ICON_SIZE_SMALL_TOOLBAR); + _btn_picker = gtk_button_new(); + gtk_button_set_relief(GTK_BUTTON(_btn_picker), GTK_RELIEF_NONE); + gtk_container_add(GTK_CONTAINER(_btn_picker), picker); + gtk_widget_set_tooltip_text(_btn_picker, _("Pick colors from image")); + gtk_box_pack_start(GTK_BOX(rgbabox), _btn_picker, FALSE, FALSE, 2); + g_signal_connect(G_OBJECT(_btn_picker), "clicked", G_CALLBACK(ColorNotebook::_onPickerClicked), this); + + /* Create RGBA entry and color preview */ + _rgbal = gtk_label_new_with_mnemonic(_("RGBA_:")); + gtk_widget_set_halign(_rgbal, GTK_ALIGN_END); + gtk_box_pack_start(GTK_BOX(rgbabox), _rgbal, TRUE, TRUE, 2); + + ColorEntry *rgba_entry = Gtk::manage(new ColorEntry(_selected_color)); + sp_dialog_defocus_on_enter(GTK_WIDGET(rgba_entry->gobj())); + gtk_box_pack_start(GTK_BOX(rgbabox), GTK_WIDGET(rgba_entry->gobj()), FALSE, FALSE, 0); + gtk_label_set_mnemonic_widget(GTK_LABEL(_rgbal), GTK_WIDGET(rgba_entry->gobj())); + + gtk_widget_show_all(rgbabox); + + // the "too much ink" icon is initially hidden + gtk_widget_hide(GTK_WIDGET(_box_toomuchink)); + + gtk_widget_set_margin_start(rgbabox, XPAD); + gtk_widget_set_margin_end(rgbabox, XPAD); + gtk_widget_set_margin_top(rgbabox, YPAD); + gtk_widget_set_margin_bottom(rgbabox, YPAD); + attach(*Glib::wrap(rgbabox), 0, row, 2, 1); + +#ifdef SPCS_PREVIEW + _p = sp_color_preview_new(0xffffffff); + gtk_widget_show(_p); + attach(*Glib::wrap(_p), 2, 3, row, row + 1, Gtk::FILL, Gtk::FILL, XPAD, YPAD); +#endif +} + +void ColorNotebook::_onPickerClicked(GtkWidget * /*widget*/, ColorNotebook *colorbook) +{ + // Set the dropper into a "one click" mode, so it reverts to the previous tool after a click + if (colorbook->_onetimepick) { + colorbook->_onetimepick.disconnect(); + } + else { + Inkscape::UI::Tools::sp_toggle_dropper(SP_ACTIVE_DESKTOP); + auto tool = dynamic_cast<Inkscape::UI::Tools::DropperTool *>(SP_ACTIVE_DESKTOP->event_context); + if (tool) { + colorbook->_onetimepick = tool->onetimepick_signal.connect(sigc::mem_fun(*colorbook, &ColorNotebook::_pickColor)); + } + } +} + +void ColorNotebook::_pickColor(ColorRGBA *color) { + // Set color to color notebook here. + _selected_color.setValue(color->getIntValue()); + _onSelectedColorChanged(); +} + +void ColorNotebook::_onSelectedColorChanged() { _updateICCButtons(); } + +void ColorNotebook::_onPageSwitched(int page_num) +{ + if (get_visible()) { + // remember the page we switched to + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/colorselector/page", page_num); + } +} + + +// TODO pass in param so as to avoid the need for SP_ACTIVE_DOCUMENT +void ColorNotebook::_updateICCButtons() +{ + SPColor color = _selected_color.color(); + gfloat alpha = _selected_color.alpha(); + + g_return_if_fail((0.0 <= alpha) && (alpha <= 1.0)); + + /* update color management icon*/ + gtk_widget_set_sensitive(_box_colormanaged, color.icc != nullptr); + + /* update out-of-gamut icon */ + gtk_widget_set_sensitive(_box_outofgamut, false); + if (color.icc) { + Inkscape::ColorProfile *target_profile = + SP_ACTIVE_DOCUMENT->getProfileManager()->find(color.icc->colorProfile.c_str()); + if (target_profile) + gtk_widget_set_sensitive(_box_outofgamut, target_profile->GamutCheck(color)); + } + + /* update too-much-ink icon */ + gtk_widget_set_sensitive(_box_toomuchink, false); + if (color.icc) { + Inkscape::ColorProfile *prof = SP_ACTIVE_DOCUMENT->getProfileManager()->find(color.icc->colorProfile.c_str()); + if (prof && CMSSystem::isPrintColorSpace(prof)) { + gtk_widget_show(GTK_WIDGET(_box_toomuchink)); + double ink_sum = 0; + for (double i : color.icc->colors) { + ink_sum += i; + } + + /* Some literature states that when the sum of paint values exceed 320%, it is considered to be a satured color, + which means the paper can get too wet due to an excessive amount of ink. This may lead to several issues + such as misalignment and poor quality of printing in general.*/ + if (ink_sum > 3.2) + gtk_widget_set_sensitive(_box_toomuchink, true); + } + else { + gtk_widget_hide(GTK_WIDGET(_box_toomuchink)); + } + } +} + +void ColorNotebook::_setCurrentPage(int i, bool sync_combo) +{ + const auto pages = _book->get_children(); + if (i >= 0 && i < pages.size()) { + _book->set_visible_child(*pages[i]); + if (sync_combo) { + _combo->set_active_by_id(i); + } + _onPageSwitched(i); + } +} + +void ColorNotebook::_addPage(Page &page) +{ + if (auto selector_widget = page.selector_factory->createWidget(_selected_color)) { + selector_widget->show(); + + Glib::ustring mode_name = page.selector_factory->modeName(); + _book->add(*selector_widget, mode_name, mode_name); + int page_num = _book->get_children().size() - 1; + + _combo->add_row(page.icon_name, mode_name, page_num); + } +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode: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/widget/color-notebook.h b/src/ui/widget/color-notebook.h new file mode 100644 index 0000000..938ab87 --- /dev/null +++ b/src/ui/widget/color-notebook.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A notebook with RGB, CMYK, CMS, HSL, and Wheel pages + *//* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Tomasz Boczkowski <penginsbacon@gmail.com> (c++-sification) + * + * Copyright (C) 2001-2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_COLOR_NOTEBOOK_H +#define SEEN_SP_COLOR_NOTEBOOK_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <boost/ptr_container/ptr_vector.hpp> +#include <gtkmm/grid.h> +#include <gtkmm/stack.h> +#include <gtkmm/stackswitcher.h> +#include <glib.h> + +#include "color.h" +#include "color-rgba.h" +#include "preferences.h" +#include "ui/selected-color.h" +#include "ui/widget/icon-combobox.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +class ColorNotebook + : public Gtk::Grid +{ +public: + ColorNotebook(SelectedColor &color); + ~ColorNotebook() override; + + void set_label(const Glib::ustring& label); + +protected: + struct Page { + Page(Inkscape::UI::ColorSelectorFactory *selector_factory, const char* icon); + + std::unique_ptr<Inkscape::UI::ColorSelectorFactory> selector_factory; + Glib::ustring icon_name; + }; + + virtual void _initUI(); + void _addPage(Page &page); + + void _pickColor(ColorRGBA *color); + static void _onPickerClicked(GtkWidget *widget, ColorNotebook *colorbook); + void _onPageSwitched(int page_num); + virtual void _onSelectedColorChanged(); + + void _updateICCButtons(); + void _setCurrentPage(int i, bool sync_combo); + + Inkscape::UI::SelectedColor &_selected_color; + gulong _entryId; + Gtk::Stack* _book; + Gtk::StackSwitcher* _switcher; + Gtk::Box* _buttonbox; + Gtk::Label* _label; + GtkWidget *_rgbal; /* RGBA entry */ + GtkWidget *_box_outofgamut, *_box_colormanaged, *_box_toomuchink; + GtkWidget *_btn_picker; + GtkWidget *_p; /* Color preview */ + boost::ptr_vector<Page> _available_pages; + sigc::connection _onetimepick; + IconComboBox* _combo = nullptr; + +private: + // By default, disallow copy constructor and assignment operator + ColorNotebook(const ColorNotebook &obj) = delete; + ColorNotebook &operator=(const ColorNotebook &obj) = delete; + + PrefObserver _observer; +}; + +} +} +} +#endif // SEEN_SP_COLOR_NOTEBOOK_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/ui/widget/color-palette.cpp b/src/ui/widget/color-palette.cpp new file mode 100644 index 0000000..a760f71 --- /dev/null +++ b/src/ui/widget/color-palette.cpp @@ -0,0 +1,618 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <gtkmm/adjustment.h> +#include <gtkmm/box.h> +#include <gtkmm/button.h> +#include <gtkmm/cssprovider.h> +#include <gtkmm/menu.h> +#include <gtkmm/menubutton.h> +#include <gtkmm/popover.h> +#include <gtkmm/radiomenuitem.h> +#include <gtkmm/scale.h> +#include <gtkmm/scrollbar.h> + +#include "color-palette.h" +#include "ui/builder-utils.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +ColorPalette::ColorPalette(): + _builder(create_builder("color-palette.glade")), + _flowbox(get_widget<Gtk::FlowBox>(_builder, "flow-box")), + _menu(get_widget<Gtk::Menu>(_builder, "menu")), + _scroll_btn(get_widget<Gtk::FlowBox>(_builder, "scroll-buttons")), + _scroll_left(get_widget<Gtk::Button>(_builder, "btn-left")), + _scroll_right(get_widget<Gtk::Button>(_builder, "btn-right")), + _scroll_up(get_widget<Gtk::Button>(_builder, "btn-up")), + _scroll_down(get_widget<Gtk::Button>(_builder, "btn-down")), + _scroll(get_widget<Gtk::ScrolledWindow>(_builder, "scroll-wnd")) + { + + auto& box = get_widget<Gtk::Box>(_builder, "palette-box"); + this->add(box); + + auto& config = get_widget<Gtk::MenuItem>(_builder, "config"); + auto& dlg = get_widget<Gtk::Popover>(_builder, "config-popup"); + config.signal_activate().connect([=,&dlg](){ + dlg.popup(); + }); + + auto& size = get_widget<Gtk::Scale>(_builder, "size-slider"); + size.signal_change_value().connect([=,&size](Gtk::ScrollType, double val) { + _set_tile_size(static_cast<int>(size.get_value())); + _signal_settings_changed.emit(); + return true; + }); + + auto& aspect = get_widget<Gtk::Scale>(_builder, "aspect-slider"); + aspect.signal_change_value().connect([=,&aspect](Gtk::ScrollType, double val) { + _set_aspect(aspect.get_value()); + _signal_settings_changed.emit(); + return true; + }); + + auto& border = get_widget<Gtk::Scale>(_builder, "border-slider"); + border.signal_change_value().connect([=,&border](Gtk::ScrollType, double val) { + _set_tile_border(static_cast<int>(border.get_value())); + _signal_settings_changed.emit(); + return true; + }); + + auto& rows = get_widget<Gtk::Scale>(_builder, "row-slider"); + rows.signal_change_value().connect([=,&rows](Gtk::ScrollType, double val) { + _set_rows(static_cast<int>(rows.get_value())); + _signal_settings_changed.emit(); + return true; + }); + + auto& sb = get_widget<Gtk::CheckButton>(_builder, "use-sb"); + sb.set_active(_force_scrollbar); + sb.signal_toggled().connect([=,&sb](){ + _enable_scrollbar(sb.get_active()); + _signal_settings_changed.emit(); + }); + update_checkbox(); + + auto& stretch = get_widget<Gtk::CheckButton>(_builder, "stretch"); + stretch.set_active(_force_scrollbar); + stretch.signal_toggled().connect([=,&stretch](){ + _enable_stretch(stretch.get_active()); + _signal_settings_changed.emit(); + }); + update_stretch(); + + _scroll.set_min_content_height(1); + + // set style for small buttons; we need them reasonably small, since they impact min height of color palette strip + { + auto css_provider = Gtk::CssProvider::create(); + css_provider->load_from_data( + ".small {" + " padding: 1px;" + " margin: 0;" + "}" + ); + + auto& btn_menu = get_widget<Gtk::MenuButton>(_builder, "btn-menu"); + Gtk::Widget* small_buttons[5] = {&_scroll_up, &_scroll_down, &_scroll_left, &_scroll_right, &btn_menu}; + for (auto button : small_buttons) { + button->get_style_context()->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + } + + _scroll_down.signal_clicked().connect([=](){ scroll(0, get_palette_height(), get_tile_height() + _border, true); }); + _scroll_up.signal_clicked().connect([=](){ scroll(0, -get_palette_height(), get_tile_height() + _border, true); }); + _scroll_left.signal_clicked().connect([=](){ scroll(-10 * (get_tile_width() + _border), 0, 0.0, false); }); + _scroll_right.signal_clicked().connect([=](){ scroll(10 * (get_tile_width() + _border), 0, 0.0, false); }); + + { + auto css_provider = Gtk::CssProvider::create(); + css_provider->load_from_data( + "flowbox, scrolledwindow {" + " padding: 0;" + " border: 0;" + " margin: 0;" + " min-width: 1px;" + " min-height: 1px;" + "}"); + _scroll.get_style_context()->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + _flowbox.get_style_context()->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + // remove padding/margins from FlowBoxChild widgets, so previews can be adjacent to each other + { + auto css_provider = Gtk::CssProvider::create(); + css_provider->load_from_data( + ".color-palette-main-box flowboxchild {" + " padding: 0;" + " border: 0;" + " margin: 0;" + " min-width: 1px;" + " min-height: 1px;" + "}"); + get_style_context()->add_provider_for_screen(this->get_screen(), css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + set_vexpand_set(true); + set_up_scrolling(); + + signal_size_allocate().connect([=](Gtk::Allocation& a){ set_up_scrolling(); }); +} + +ColorPalette::~ColorPalette() { + if (_active_timeout) { + g_source_remove(_active_timeout); + } +} + +void ColorPalette::do_scroll(int dx, int dy) { + if (auto vert = _scroll.get_vscrollbar()) { + vert->set_value(vert->get_value() + dy); + } + if (auto horz = _scroll.get_hscrollbar()) { + horz->set_value(horz->get_value() + dx); + } +} + +std::pair<double, double> get_range(Gtk::Scrollbar& sb) { + auto adj = sb.get_adjustment(); + return std::make_pair(adj->get_lower(), adj->get_upper() - adj->get_page_size()); +} + +gboolean ColorPalette::scroll_cb(gpointer self) { + auto ptr = static_cast<ColorPalette*>(self); + bool fire_again = false; + + if (auto vert = ptr->_scroll.get_vscrollbar()) { + auto value = vert->get_value(); + // is this the final adjustment step? + if (fabs(ptr->_scroll_final - value) < fabs(ptr->_scroll_step)) { + vert->set_value(ptr->_scroll_final); + fire_again = false; // cancel timer + } + else { + auto pos = value + ptr->_scroll_step; + vert->set_value(pos); + auto range = get_range(*vert); + if (pos > range.first && pos < range.second) { + // not yet done + fire_again = true; // fire this callback again + } + } + } + + if (!fire_again) { + ptr->_active_timeout = 0; + } + + return fire_again; +} + +void ColorPalette::scroll(int dx, int dy, double snap, bool smooth) { + if (auto vert = _scroll.get_vscrollbar()) { + if (smooth && dy != 0.0) { + _scroll_final = vert->get_value() + dy; + if (snap > 0) { + // round it to whole 'dy' increments + _scroll_final -= fmod(_scroll_final, snap); + } + auto range = get_range(*vert); + if (_scroll_final < range.first) { + _scroll_final = range.first; + } + else if (_scroll_final > range.second) { + _scroll_final = range.second; + } + _scroll_step = dy / 4.0; + if (!_active_timeout && vert->get_value() != _scroll_final) { + // limit refresh to 60 fps, in practice it will be slower + _active_timeout = g_timeout_add(1000 / 60, &ColorPalette::scroll_cb, this); + } + } + else { + vert->set_value(vert->get_value() + dy); + } + } + if (auto horz = _scroll.get_hscrollbar()) { + horz->set_value(horz->get_value() + dx); + } +} + +int ColorPalette::get_tile_size() const { + return _size; +} + +int ColorPalette::get_tile_border() const { + return _border; +} + +int ColorPalette::get_rows() const { + return _rows; +} + +double ColorPalette::get_aspect() const { + return _aspect; +} + +void ColorPalette::set_tile_border(int border) { + _set_tile_border(border); + auto& slider = get_widget<Gtk::Scale>(_builder, "border-slider"); + slider.set_value(border); +} + +void ColorPalette::_set_tile_border(int border) { + if (border == _border) return; + + if (border < 0 || border > 100) { + g_warning("Unexpected tile border size of color palette: %d", border); + return; + } + + _border = border; + set_up_scrolling(); +} + +void ColorPalette::set_tile_size(int size) { + _set_tile_size(size); + auto& slider = get_widget<Gtk::Scale>(_builder, "size-slider"); + slider.set_value(size); +} + +void ColorPalette::_set_tile_size(int size) { + if (size == _size) return; + + if (size < 1 || size > 1000) { + g_warning("Unexpected tile size for color palette: %d", size); + return; + } + + _size = size; + set_up_scrolling(); +} + +void ColorPalette::set_aspect(double aspect) { + _set_aspect(aspect); + auto& slider = get_widget<Gtk::Scale>(_builder, "aspect-slider"); + slider.set_value(aspect); +} + +void ColorPalette::_set_aspect(double aspect) { + if (aspect == _aspect) return; + + if (aspect < -2.0 || aspect > 2.0) { + g_warning("Unexpected aspect ratio for color palette: %f", aspect); + return; + } + + _aspect = aspect; + set_up_scrolling(); +} + +void ColorPalette::set_rows(int rows) { + _set_rows(rows); + auto& slider = get_widget<Gtk::Scale>(_builder, "row-slider"); + slider.set_value(rows); +} + +void ColorPalette::_set_rows(int rows) { + if (rows == _rows) return; + + if (rows < 1 || rows > 1000) { + g_warning("Unexpected number of rows for color palette: %d", rows); + return; + } + + _rows = rows; + update_checkbox(); + set_up_scrolling(); +} + +void ColorPalette::update_checkbox() { + auto& sb = get_widget<Gtk::CheckButton>(_builder, "use-sb"); + // scrollbar can only be applied to single-row layouts + sb.set_sensitive(_rows == 1); +} + +void ColorPalette::set_compact(bool compact) { + if (_compact != compact) { + _compact = compact; + set_up_scrolling(); + + get_widget<Gtk::Scale>(_builder, "row-slider").set_visible(compact); + get_widget<Gtk::Label>(_builder, "row-label").set_visible(compact); + } +} + +bool ColorPalette::is_scrollbar_enabled() const { + return _force_scrollbar; +} + +bool ColorPalette::is_stretch_enabled() const { + return _stretch_tiles; +} + +void ColorPalette::enable_stretch(bool enable) { + auto& stretch = get_widget<Gtk::CheckButton>(_builder, "stretch"); + stretch.set_active(enable); + _enable_stretch(enable); +} + +void ColorPalette::_enable_stretch(bool enable) { + if (_stretch_tiles == enable) return; + + _stretch_tiles = enable; + _flowbox.set_halign(enable ? Gtk::ALIGN_FILL : Gtk::ALIGN_START); + update_stretch(); + set_up_scrolling(); +} + +void ColorPalette::update_stretch() { + auto& aspect = get_widget<Gtk::Scale>(_builder, "aspect-slider"); + aspect.set_sensitive(!_stretch_tiles); + auto& label = get_widget<Gtk::Label>(_builder, "aspect-label"); + label.set_sensitive(!_stretch_tiles); +} + +void ColorPalette::enable_scrollbar(bool show) { + auto& sb = get_widget<Gtk::CheckButton>(_builder, "use-sb"); + sb.set_active(show); + _enable_scrollbar(show); +} + +void ColorPalette::_enable_scrollbar(bool show) { + if (_force_scrollbar == show) return; + + _force_scrollbar = show; + set_up_scrolling(); +} + +void ColorPalette::set_up_scrolling() { + auto& box = get_widget<Gtk::Box>(_builder, "palette-box"); + auto& btn_menu = get_widget<Gtk::MenuButton>(_builder, "btn-menu"); + + if (_compact) { + box.set_orientation(Gtk::ORIENTATION_HORIZONTAL); + btn_menu.set_margin_bottom(0); + btn_menu.set_margin_end(0); + // in compact mode scrollbars are hidden; they take up too much space + set_valign(Gtk::ALIGN_START); + set_vexpand(false); + + _scroll.set_valign(Gtk::ALIGN_END); + _flowbox.set_valign(Gtk::ALIGN_END); + + if (_rows == 1 && _force_scrollbar) { + // horizontal scrolling with single row + _flowbox.set_max_children_per_line(_count); + _flowbox.set_min_children_per_line(_count); + + _scroll_btn.hide(); + + if (_force_scrollbar) { + _scroll_left.hide(); + _scroll_right.hide(); + } + else { + _scroll_left.show(); + _scroll_right.show(); + } + + // ideally we should be able to use POLICY_AUTOMATIC, but on some themes this leads to a scrollbar + // that obscures color tiles (it overlaps them); thus resorting to manual scrollbar selection + _scroll.set_policy(_force_scrollbar ? Gtk::POLICY_ALWAYS : Gtk::POLICY_EXTERNAL, Gtk::POLICY_NEVER); + } + else { + // vertical scrolling with multiple rows + // 'external' allows scrollbar to shrink vertically + _scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_EXTERNAL); + _flowbox.set_min_children_per_line(1); + _flowbox.set_max_children_per_line(_count); + _scroll_left.hide(); + _scroll_right.hide(); + _scroll_btn.show(); + } + } + else { + box.set_orientation(Gtk::ORIENTATION_VERTICAL); + btn_menu.set_margin_bottom(2); + btn_menu.set_margin_end(2); + // in normal mode use regular full-size scrollbars + set_valign(Gtk::ALIGN_FILL); + set_vexpand(true); + + _scroll_left.hide(); + _scroll_right.hide(); + _scroll_btn.hide(); + + _flowbox.set_valign(Gtk::ALIGN_START); + _flowbox.set_min_children_per_line(1); + _flowbox.set_max_children_per_line(_count); + + _scroll.set_valign(Gtk::ALIGN_FILL); + // 'always' allocates space for scrollbar + _scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + } + + resize(); +} + +int ColorPalette::get_tile_size(bool horz) const { + if (_stretch_tiles) return _size; + + double aspect = horz ? _aspect : -_aspect; + + if (aspect > 0) { + return static_cast<int>(round((1.0 + aspect) * _size)); + } + else if (aspect < 0) { + return static_cast<int>(round((1.0 / (1.0 - aspect)) * _size)); + } + else { + return _size; + } +} + +int ColorPalette::get_tile_width() const { + return get_tile_size(true); +} + +int ColorPalette::get_tile_height() const { + return get_tile_size(false); +} + +int ColorPalette::get_palette_height() const { + return (get_tile_height() + _border) * _rows; +} + +void ColorPalette::resize() { + if (_rows == 1 && _force_scrollbar || !_compact) { + // auto size for single row to allocate space for scrollbar + _scroll.set_size_request(-1, -1); + } + else { + // exact size for multiple rows + int height = get_palette_height() - _border; + _scroll.set_size_request(1, height); + } + + _flowbox.set_column_spacing(_border); + _flowbox.set_row_spacing(_border); + + int width = get_tile_width(); + int height = get_tile_height(); + _flowbox.foreach([=](Gtk::Widget& w){ + w.set_size_request(width, height); + }); +} + +void ColorPalette::free() { + for (auto widget : _flowbox.get_children()) { + if (widget) { + _flowbox.remove(*widget); + delete widget; + } + } +} + +void ColorPalette::set_colors(const std::vector<Gtk::Widget*>& swatches) { + _flowbox.freeze_notify(); + _flowbox.freeze_child_notify(); + + free(); + + int count = 0; + for (auto widget : swatches) { + if (widget) { + _flowbox.add(*widget); + ++count; + } + } + + _flowbox.show_all(); + _count = std::max(1, count); + _flowbox.set_max_children_per_line(_count); + + // resize(); + set_up_scrolling(); + + _flowbox.thaw_child_notify(); + _flowbox.thaw_notify(); +} + +class CustomMenuItem : public Gtk::RadioMenuItem { +public: + CustomMenuItem(Gtk::RadioMenuItem::Group& group, const Glib::ustring& label, std::vector<ColorPalette::rgb_t> colors): + Gtk::RadioMenuItem(group, label), _colors(std::move(colors)) { + + set_margin_bottom(2); + } +private: + bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override; + std::vector<ColorPalette::rgb_t> _colors; +}; + +bool CustomMenuItem::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) { + RadioMenuItem::on_draw(cr); + if (_colors.empty()) return false; + + auto allocation = get_allocation(); + auto x = 0; + auto y = 0; + auto width = allocation.get_width(); + auto height = allocation.get_height(); + auto left = x + height; + auto right = x + width - height; + auto dx = 1; + auto dy = 2; + auto px = left; + auto py = y + height - dy; + auto w = right - left; + if (w <= 0) return false; + + for (int i = 0; i < w; ++i) { + if (px >= right) break; + + int index = i * _colors.size() / w; + auto& color = _colors.at(index); + + cr->set_source_rgb(color.r, color.g, color.b); + cr->rectangle(px, py, dx, dy); + cr->fill(); + + px += dx; + } + + return false; +} + +void ColorPalette::set_palettes(const std::vector<ColorPalette::palette_t>& palettes) { + auto items = _menu.get_children(); + auto count = items.size(); + + int index = 0; + while (count > 2) { + if (auto item = items[index++]) { + _menu.remove(*item); + delete item; + } + count--; + } + + Gtk::RadioMenuItem::Group group; + for (auto it = palettes.rbegin(); it != palettes.rend(); ++it) { + auto& name = it->name; + auto item = Gtk::manage(new CustomMenuItem(group, name, it->colors)); + item->signal_activate().connect([=](){ + if (!_in_update) { + _in_update = true; + _signal_palette_selected.emit(name); + _in_update = false; + } + }); + item->show(); + _menu.prepend(*item); + } +} + +sigc::signal<void, Glib::ustring>& ColorPalette::get_palette_selected_signal() { + return _signal_palette_selected; +} + +sigc::signal<void>& ColorPalette::get_settings_changed_signal() { + return _signal_settings_changed; +} + +void ColorPalette::set_selected(const Glib::ustring& name) { + auto items = _menu.get_children(); + _in_update = true; + for (auto item : items) { + if (auto radio = dynamic_cast<Gtk::RadioMenuItem*>(item)) { + radio->set_active(radio->get_label() == name); + } + } + _in_update = false; +} + +}}} // namespace diff --git a/src/ui/widget/color-palette.h b/src/ui/widget/color-palette.h new file mode 100644 index 0000000..bc4c47f --- /dev/null +++ b/src/ui/widget/color-palette.h @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Color palette widget + */ +/* Authors: + * Michael Kowalski + * + * Copyright (C) 2021 Michael Kowalski + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLOR_PALETTE_H +#define SEEN_COLOR_PALETTE_H + +#include <gtkmm/bin.h> +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/flowbox.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/menu.h> +#include <vector> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class ColorPalette : public Gtk::Bin { +public: + ColorPalette(); + ~ColorPalette() override; + + struct rgb_t { double r; double g; double b; }; + struct palette_t { Glib::ustring name; std::vector<rgb_t> colors; }; + + // set colors presented in a palette + void set_colors(const std::vector<Gtk::Widget*>& swatches); + // list of palettes to present in the menu + void set_palettes(const std::vector<palette_t>& palettes); + // enable compact mode (true) with mini-scroll buttons, or normal mode (false) with regular scrollbars + void set_compact(bool compact); + + void set_tile_size(int size_px); + void set_tile_border(int border_px); + void set_rows(int rows); + void set_aspect(double aspect); + // show horizontal scrollbar when only 1 row is set + void enable_scrollbar(bool show); + // allow tile stretching (horizontally) + void enable_stretch(bool enable); + + int get_tile_size() const; + int get_tile_border() const; + int get_rows() const; + double get_aspect() const; + bool is_scrollbar_enabled() const; + bool is_stretch_enabled() const; + + void set_selected(const Glib::ustring& name); + + sigc::signal<void, Glib::ustring>& get_palette_selected_signal(); + sigc::signal<void>& get_settings_changed_signal(); + +private: + void resize(); + void set_up_scrolling(); + void free(); + void scroll(int dx, int dy, double snap, bool smooth); + void do_scroll(int dx, int dy); + static gboolean scroll_cb(gpointer self); + void _set_tile_size(int size_px); + void _set_tile_border(int border_px); + void _set_rows(int rows); + void _set_aspect(double aspect); + void _enable_scrollbar(bool show); + void _enable_stretch(bool enable); + static gboolean check_scrollbar(gpointer self); + void update_checkbox(); + void update_stretch(); + int get_tile_size(bool horz) const; + int get_tile_width() const; + int get_tile_height() const; + int get_palette_height() const; + + Glib::RefPtr<Gtk::Builder> _builder; + Gtk::FlowBox& _flowbox; + Gtk::ScrolledWindow& _scroll; + Gtk::FlowBox& _scroll_btn; + Gtk::Button& _scroll_up; + Gtk::Button& _scroll_down; + Gtk::Button& _scroll_left; + Gtk::Button& _scroll_right; + Gtk::Menu& _menu; + int _size = 10; + int _border = 0; + int _rows = 1; + double _aspect = 0.0; + int _count = 1; + bool _compact = true; + sigc::signal<void, Glib::ustring> _signal_palette_selected; + sigc::signal<void> _signal_settings_changed; + bool _in_update = false; + guint _active_timeout = 0; + bool _force_scrollbar = false; + bool _stretch_tiles = false; + double _scroll_step = 0.0; // smooth scrolling step + double _scroll_final = 0.0; // smooth scroll final value +}; + +}}} // namespace + +#endif // SEEN_COLOR_PALETTE_H diff --git a/src/ui/widget/color-picker.cpp b/src/ui/widget/color-picker.cpp new file mode 100644 index 0000000..009ec8e --- /dev/null +++ b/src/ui/widget/color-picker.cpp @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Ralf Stephan <ralf@ark.in-berlin.de> + * Abhishek Sharma + * + * Copyright (C) Authors 2000-2005 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "color-picker.h" + +#include "inkscape.h" +#include "desktop.h" +#include "document.h" +#include "document-undo.h" + +#include "ui/dialog-events.h" +#include "ui/widget/color-notebook.h" + + +static bool _in_use = false; + +namespace Inkscape { +namespace UI { +namespace Widget { + +ColorPicker::ColorPicker (const Glib::ustring& title, const Glib::ustring& tip, + guint32 rgba, bool undo, Gtk::Button* external_button) + : _preview(new ColorPreview(rgba)) + , _title(title) + , _rgba(rgba) + , _undo(undo) + , _colorSelectorDialog("dialogs.colorpickerwindow") +{ + Gtk::Button* button = external_button ? external_button : this; + _color_selector = nullptr; + setupDialog(title); + _preview->show(); + button->add(*Gtk::manage(_preview)); + // set tooltip if given, otherwise leave original tooltip in place (from external button) + if (!tip.empty()) { + button->set_tooltip_text(tip); + } + _selected_color.signal_changed.connect(sigc::mem_fun(this, &ColorPicker::_onSelectedColorChanged)); + _selected_color.signal_dragged.connect(sigc::mem_fun(this, &ColorPicker::_onSelectedColorChanged)); + _selected_color.signal_released.connect(sigc::mem_fun(this, &ColorPicker::_onSelectedColorChanged)); + + if (external_button) { + external_button->signal_clicked().connect([=](){ on_clicked(); }); + } +} + +ColorPicker::~ColorPicker() +{ + closeWindow(); +} + +void ColorPicker::setupDialog(const Glib::ustring &title) +{ + GtkWidget *dlg = GTK_WIDGET(_colorSelectorDialog.gobj()); + sp_transientize(dlg); + + _colorSelectorDialog.hide(); + _colorSelectorDialog.set_title (title); + _colorSelectorDialog.set_border_width (4); +} + +void ColorPicker::setSensitive(bool sensitive) { set_sensitive(sensitive); } + +void ColorPicker::setRgba32 (guint32 rgba) +{ + if (_in_use) return; + + set_preview(rgba); + _rgba = rgba; + if (_color_selector) + { + _updating = true; + _selected_color.setValue(rgba); + _updating = false; + } +} + +void ColorPicker::closeWindow() +{ + _colorSelectorDialog.hide(); +} + +void ColorPicker::open() { + on_clicked(); +} + +void ColorPicker::on_clicked() +{ + if (!_color_selector) { + auto selector = Gtk::manage(new ColorNotebook(_selected_color)); + selector->set_label(_title); + _color_selector = selector; + _colorSelectorDialog.get_content_area()->pack_start(*_color_selector, true, true, 0); + _color_selector->show(); + } + + _updating = true; + _selected_color.setValue(_rgba); + _updating = false; + + _colorSelectorDialog.show(); + Glib::RefPtr<Gdk::Window> window = _colorSelectorDialog.get_parent_window(); + if (window) { + window->focus(1); + } +} + +void ColorPicker::on_changed (guint32) +{ +} + +void ColorPicker::_onSelectedColorChanged() { + if (_updating) { + return; + } + + if (_in_use) { + return; + } else { + _in_use = true; + } + + guint32 rgba = _selected_color.value(); + set_preview(rgba); + + if (_undo && SP_ACTIVE_DESKTOP) { + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), /* TODO: annotate */ "color-picker.cpp:129", ""); + } + + on_changed(rgba); + _in_use = false; + _changed_signal.emit(rgba); + _rgba = rgba; +} + +void ColorPicker::set_preview(guint32 rgba) { + _preview->setRgba32(_ignore_transparency ? rgba | 0xff : rgba); +} + +void ColorPicker::use_transparency(bool enable) { + _ignore_transparency = !enable; + set_preview(_rgba); +} + +}//namespace Widget +}//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/widget/color-picker.h b/src/ui/widget/color-picker.h new file mode 100644 index 0000000..c7147ae --- /dev/null +++ b/src/ui/widget/color-picker.h @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Color picker button and window. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) Authors 2000-2005 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __COLOR_PICKER_H__ +#define __COLOR_PICKER_H__ + +#include "labelled.h" + +#include <cstddef> + +#include "ui/selected-color.h" +#include "ui/widget/color-preview.h" +#include <gtkmm/button.h> +#include <gtkmm/dialog.h> +#include <gtkmm/window.h> +#include <sigc++/sigc++.h> + +struct SPColorSelector; + +namespace Inkscape +{ +namespace UI +{ +namespace Widget +{ + + +class ColorPicker : public Gtk::Button { +public: + + ColorPicker (const Glib::ustring& title, + const Glib::ustring& tip, + const guint32 rgba, + bool undo, + Gtk::Button* external_button = nullptr); + + ~ColorPicker() override; + + void setRgba32 (guint32 rgba); + void setSensitive(bool sensitive); + void open(); + void closeWindow(); + sigc::connection connectChanged (const sigc::slot<void,guint>& slot) + { return _changed_signal.connect (slot); } + void use_transparency(bool enable); + +protected: + + void _onSelectedColorChanged(); + void on_clicked() override; + virtual void on_changed (guint32); + + ColorPreview *_preview; + + /*const*/ Glib::ustring _title; + sigc::signal<void,guint32> _changed_signal; + guint32 _rgba; + bool _undo; + bool _updating; + + //Dialog + void setupDialog(const Glib::ustring &title); + //Inkscape::UI::Dialog::Dialog _colorSelectorDialog; + Gtk::Dialog _colorSelectorDialog; + SelectedColor _selected_color; + +private: + void set_preview(guint32 rgba); + + Gtk::Widget *_color_selector; + bool _ignore_transparency = false; +}; + + +class LabelledColorPicker : public Labelled { +public: + + LabelledColorPicker (const Glib::ustring& label, + const Glib::ustring& title, + const Glib::ustring& tip, + const guint32 rgba, + bool undo) : Labelled(label, tip, new ColorPicker(title, tip, rgba, undo)) {} + + void setRgba32 (guint32 rgba) + { static_cast<ColorPicker*>(_widget)->setRgba32 (rgba); } + + void closeWindow() + { static_cast<ColorPicker*>(_widget)->closeWindow (); } + + sigc::connection connectChanged (const sigc::slot<void,guint>& slot) + { return static_cast<ColorPicker*>(_widget)->connectChanged(slot); } +}; + +}//namespace Widget +}//namespace UI +}//namespace Inkscape + +#endif /* !__COLOR_PICKER_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/widget/color-preview.cpp b/src/ui/widget/color-preview.cpp new file mode 100644 index 0000000..ab81c60 --- /dev/null +++ b/src/ui/widget/color-preview.cpp @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2001-2005 Authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/color-preview.h" +#include "display/cairo-utils.h" +#include <cairo.h> + +#define SPCP_DEFAULT_WIDTH 32 +#define SPCP_DEFAULT_HEIGHT 12 + +namespace Inkscape { + namespace UI { + namespace Widget { + +ColorPreview::ColorPreview (guint32 rgba) +{ + _rgba = rgba; + set_has_window(false); + set_name("ColorPreview"); +} + +void +ColorPreview::on_size_allocate (Gtk::Allocation &all) +{ + set_allocation (all); + if (get_is_drawable()) + queue_draw(); +} + +void +ColorPreview::get_preferred_height_vfunc(int& minimum_height, int& natural_height) const +{ + minimum_height = natural_height = SPCP_DEFAULT_HEIGHT; +} + +void +ColorPreview::get_preferred_height_for_width_vfunc(int /* width */, int& minimum_height, int& natural_height) const +{ + minimum_height = natural_height = SPCP_DEFAULT_HEIGHT; +} + +void +ColorPreview::get_preferred_width_vfunc(int& minimum_width, int& natural_width) const +{ + minimum_width = natural_width = SPCP_DEFAULT_WIDTH; +} + +void +ColorPreview::get_preferred_width_for_height_vfunc(int /* height */, int& minimum_width, int& natural_width) const +{ + minimum_width = natural_width = SPCP_DEFAULT_WIDTH; +} + +void +ColorPreview::setRgba32 (guint32 rgba) +{ + _rgba = rgba; + + if (get_is_drawable()) + queue_draw(); +} + +bool +ColorPreview::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) +{ + double x, y, width, height; + const Gtk::Allocation& allocation = get_allocation(); + x = 0; + y = 0; + width = allocation.get_width()/2.0; + height = allocation.get_height() - 1; + + double radius = height / 7.5; + double degrees = M_PI / 180.0; + cairo_new_sub_path (cr->cobj()); + cairo_line_to(cr->cobj(), width, 0); + cairo_line_to(cr->cobj(), width, height); + cairo_arc (cr->cobj(), x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees); + cairo_arc (cr->cobj(), x + radius, y + radius, radius, 180 * degrees, 270 * degrees); + cairo_close_path (cr->cobj()); + + /* Transparent area */ + + cairo_pattern_t *checkers = ink_cairo_pattern_create_checkerboard(); + + cairo_set_source(cr->cobj(), checkers); + cr->fill_preserve(); + ink_cairo_set_source_rgba32(cr->cobj(), _rgba); + cr->fill(); + cairo_pattern_destroy(checkers); + + /* Solid area */ + + x = width; + + cairo_new_sub_path (cr->cobj()); + cairo_arc (cr->cobj(), x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees); + cairo_arc (cr->cobj(), x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees); + cairo_line_to(cr->cobj(), x, height); + cairo_line_to(cr->cobj(), x, y); + cairo_close_path (cr->cobj()); + ink_cairo_set_source_rgba32(cr->cobj(), _rgba | 0xff); + cr->fill(); + + return true; +} + +GdkPixbuf* +ColorPreview::toPixbuf (int width, int height) +{ + GdkRectangle carea; + gint w2; + w2 = width / 2; + + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *ct = cairo_create(s); + + /* Transparent area */ + carea.x = 0; + carea.y = 0; + carea.width = w2; + carea.height = height; + + cairo_pattern_t *checkers = ink_cairo_pattern_create_checkerboard(); + + // cairo_rectangle(ct, carea.x, carea.y, carea.width, carea.height); + cairo_arc(ct, carea.x + carea.width / 2, carea.y + carea.height / 2, carea.width / 2, 0, 2 * M_PI); + cairo_set_source(ct, checkers); + cairo_fill_preserve(ct); + ink_cairo_set_source_rgba32(ct, _rgba); + cairo_fill(ct); + + cairo_pattern_destroy(checkers); + + /* Solid area */ + carea.x = w2; + carea.y = 0; + carea.width = width - w2; + carea.height = height; + + cairo_rectangle(ct, carea.x, carea.y, carea.width, carea.height); + ink_cairo_set_source_rgba32(ct, _rgba | 0xff); + cairo_fill(ct); + + cairo_destroy(ct); + cairo_surface_flush(s); + + GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s); + return pixbuf; +} + +}}} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 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/widget/color-preview.h b/src/ui/widget/color-preview.h new file mode 100644 index 0000000..b789579 --- /dev/null +++ b/src/ui/widget/color-preview.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_COLOR_PREVIEW_H +#define SEEN_COLOR_PREVIEW_H +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2001-2005 Authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/widget.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A simple color preview widget, mainly used within a picker button. + */ +class ColorPreview : public Gtk::Widget { +public: + ColorPreview (guint32 rgba); + void setRgba32 (guint32 rgba); + GdkPixbuf* toPixbuf (int width, int height); + +protected: + void on_size_allocate (Gtk::Allocation &all) override; + + void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override; + void get_preferred_height_for_width_vfunc(int width, int& minimum_height, int& natural_height) const override; + void get_preferred_width_vfunc(int& minimum_width, int& natural_width) const override; + void get_preferred_width_for_height_vfunc(int height, int& minimum_width, int& natural_width) const override; + bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override; + + guint32 _rgba; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_COLOR_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 : diff --git a/src/ui/widget/color-scales.cpp b/src/ui/widget/color-scales.cpp new file mode 100644 index 0000000..5662656 --- /dev/null +++ b/src/ui/widget/color-scales.cpp @@ -0,0 +1,1062 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Color selector using sliders for each components, for multiple color modes + *//* + * Authors: + * see git history + * bulia byak <buliabyak@users.sf.net> + * Massinissa Derriche <massinissa.derriche@gmail.com> + * + * Copyright (C) 2018, 2021 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/adjustment.h> +#include <gtkmm/spinbutton.h> +#include <gtkmm/grid.h> +#include <glibmm/i18n.h> +#include <functional> + +#include "ui/dialog-events.h" +#include "ui/widget/color-scales.h" +#include "ui/widget/color-slider.h" +#include "ui/widget/scrollprotected.h" +#include "ui/icon-loader.h" +#include "preferences.h" + +#include "ui/widget/ink-color-wheel.h" + +static int const CSC_CHANNEL_R = (1 << 0); +static int const CSC_CHANNEL_G = (1 << 1); +static int const CSC_CHANNEL_B = (1 << 2); +static int const CSC_CHANNEL_A = (1 << 3); +static int const CSC_CHANNEL_H = (1 << 0); +static int const CSC_CHANNEL_S = (1 << 1); +static int const CSC_CHANNEL_V = (1 << 2); +static int const CSC_CHANNEL_C = (1 << 0); +static int const CSC_CHANNEL_M = (1 << 1); +static int const CSC_CHANNEL_Y = (1 << 2); +static int const CSC_CHANNEL_K = (1 << 3); +static int const CSC_CHANNEL_CMYKA = (1 << 4); + +static int const CSC_CHANNELS_ALL = 0; + +static int const XPAD = 2; +static int const YPAD = 2; + +namespace Inkscape { +namespace UI { +namespace Widget { + + +static guchar const *sp_color_scales_hue_map(); +static guchar const *sp_color_scales_hsluv_map(guchar *map, + std::function<void(float*, float)> callback); + + +template <SPColorScalesMode MODE> +gchar const *ColorScales<MODE>::SUBMODE_NAMES[] = { N_("None"), N_("RGB"), N_("HSL"), + N_("CMYK"), N_("HSV"), N_("HSLuv") }; + + +// Preference name for the saved state of toggle-able color wheel +template <> +gchar const * const ColorScales<SPColorScalesMode::HSL>::_pref_wheel_visibility = + "/wheel_vis_hsl"; + +template <> +gchar const * const ColorScales<SPColorScalesMode::HSV>::_pref_wheel_visibility = + "/wheel_vis_hsv"; + +template <> +gchar const * const ColorScales<SPColorScalesMode::HSLUV>::_pref_wheel_visibility = + "/wheel_vis_hsluv"; + + +template <SPColorScalesMode MODE> +ColorScales<MODE>::ColorScales(SelectedColor &color) + : Gtk::Box() + , _color(color) + , _range_limit(255.0) + , _updating(false) + , _dragging(false) + , _wheel(nullptr) +{ + for (gint i = 0; i < 5; i++) { + _l[i] = nullptr; + _s[i] = nullptr; + _b[i] = nullptr; + } + + _initUI(); + + _color_changed = _color.signal_changed.connect([this](){ _onColorChanged(); }); + _color_dragged = _color.signal_dragged.connect([this](){ _onColorChanged(); }); +} + +template <SPColorScalesMode MODE> +ColorScales<MODE>::~ColorScales() +{ + _color_changed.disconnect(); + _color_dragged.disconnect(); + + for (gint i = 0; i < 5; i++) { + _l[i] = nullptr; + _s[i] = nullptr; + _b[i] = nullptr; + } +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::_initUI() +{ + set_orientation(Gtk::ORIENTATION_VERTICAL); + + Gtk::Expander *wheel_frame = nullptr; + + if constexpr ( + MODE == SPColorScalesMode::HSL || + MODE == SPColorScalesMode::HSV || + MODE == SPColorScalesMode::HSLUV) + { + /* Create wheel */ + if constexpr (MODE == SPColorScalesMode::HSLUV) { + _wheel = Gtk::manage(new Inkscape::UI::Widget::ColorWheelHSLuv()); + } else { + _wheel = Gtk::manage(new Inkscape::UI::Widget::ColorWheelHSL()); + } + + _wheel->show(); + _wheel->set_halign(Gtk::ALIGN_FILL); + _wheel->set_valign(Gtk::ALIGN_FILL); + _wheel->set_hexpand(true); + _wheel->set_vexpand(true); + _wheel->set_name("ColorWheel"); + _wheel->set_size_request(-1, 130); // minimal size + + /* Signal */ + _wheel->signal_color_changed().connect([this](){ _wheelChanged(); }); + + /* Expander */ + // Label icon + Gtk::Image *expander_icon = Gtk::manage( + sp_get_icon_image("color-wheel", Gtk::ICON_SIZE_BUTTON) + ); + expander_icon->show(); + expander_icon->set_margin_start(2 * XPAD); + expander_icon->set_margin_end(3 * XPAD); + // Label + Gtk::Label *expander_label = Gtk::manage(new Gtk::Label(_("Color Wheel"))); + expander_label->show(); + // Content + Gtk::Box *expander_box = Gtk::manage(new Gtk::Box()); + expander_box->show(); + expander_box->pack_start(*expander_icon); + expander_box->pack_start(*expander_label); + expander_box->set_orientation(Gtk::ORIENTATION_HORIZONTAL); + // Expander + wheel_frame = Gtk::manage(new Gtk::Expander()); + wheel_frame->show(); + wheel_frame->set_margin_start(2 * XPAD); + wheel_frame->set_margin_end(XPAD); + wheel_frame->set_margin_top(2 * YPAD); + wheel_frame->set_margin_bottom(2 * YPAD); + wheel_frame->set_halign(Gtk::ALIGN_FILL); + wheel_frame->set_valign(Gtk::ALIGN_FILL); + wheel_frame->set_hexpand(true); + wheel_frame->set_vexpand(false); + wheel_frame->set_label_widget(*expander_box); + + // Signal + wheel_frame->property_expanded().signal_changed().connect([=](){ + bool visible = wheel_frame->get_expanded(); + wheel_frame->set_vexpand(visible); + + // Save wheel visibility + Inkscape::Preferences::get()->setBool(_prefs + _pref_wheel_visibility, visible); + }); + + wheel_frame->add(*_wheel); + add(*wheel_frame); + } + + /* Create sliders */ + Gtk::Grid *grid = Gtk::manage(new Gtk::Grid()); + grid->show(); + add(*grid); + + for (gint i = 0; i < 5; i++) { + /* Label */ + _l[i] = Gtk::manage(new Gtk::Label("", true)); + + _l[i]->set_halign(Gtk::ALIGN_START); + _l[i]->show(); + + _l[i]->set_margin_start(2 * XPAD); + _l[i]->set_margin_end(XPAD); + _l[i]->set_margin_top(YPAD); + _l[i]->set_margin_bottom(YPAD); + grid->attach(*_l[i], 0, i, 1, 1); + + /* Adjustment */ + _a.push_back(Gtk::Adjustment::create(0.0, 0.0, _range_limit, 1.0, 10.0, 10.0)); + /* Slider */ + _s[i] = Gtk::manage(new Inkscape::UI::Widget::ColorSlider(_a[i])); + _s[i]->show(); + + _s[i]->set_margin_start(XPAD); + _s[i]->set_margin_end(XPAD); + _s[i]->set_margin_top(YPAD); + _s[i]->set_margin_bottom(YPAD); + _s[i]->set_hexpand(true); + grid->attach(*_s[i], 1, i, 1, 1); + + /* Spinbutton */ + _b[i] = Gtk::manage(new ScrollProtected<Gtk::SpinButton>(_a[i], 1.0)); + sp_dialog_defocus_on_enter(_b[i]->gobj()); + _l[i]->set_mnemonic_widget(*_b[i]); + _b[i]->show(); + + _b[i]->set_margin_start(XPAD); + _b[i]->set_margin_end(XPAD); + _b[i]->set_margin_top(YPAD); + _b[i]->set_margin_bottom(YPAD); + _b[i]->set_halign(Gtk::ALIGN_END); + _b[i]->set_valign(Gtk::ALIGN_CENTER); + grid->attach(*_b[i], 2, i, 1, 1); + + /* Signals */ + _a[i]->signal_value_changed().connect([this, i](){ _adjustmentChanged(i); }); + _s[i]->signal_grabbed.connect([this](){ _sliderAnyGrabbed(); }); + _s[i]->signal_released.connect([this](){ _sliderAnyReleased(); }); + _s[i]->signal_value_changed.connect([this](){ _sliderAnyChanged(); }); + } + + // Prevent 5th bar from being shown by PanelDialog::show_all_children + _l[4]->set_no_show_all(true); + _s[4]->set_no_show_all(true); + _b[4]->set_no_show_all(true); + + setupMode(); + + if constexpr ( + MODE == SPColorScalesMode::HSL || + MODE == SPColorScalesMode::HSV || + MODE == SPColorScalesMode::HSLUV) + { + // Restore the visibility of the wheel + bool visible = Inkscape::Preferences::get()->getBool(_prefs + _pref_wheel_visibility, + false); + wheel_frame->set_expanded(visible); + wheel_frame->set_vexpand(visible); + } +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::_recalcColor() +{ + SPColor color; + gfloat alpha = 1.0; + gfloat c[5]; + + if constexpr ( + MODE == SPColorScalesMode::RGB || + MODE == SPColorScalesMode::HSL || + MODE == SPColorScalesMode::HSV || + MODE == SPColorScalesMode::HSLUV) + { + _getRgbaFloatv(c); + color.set(c[0], c[1], c[2]); + alpha = c[3]; + } else if constexpr (MODE == SPColorScalesMode::CMYK) { + _getCmykaFloatv(c); + + float rgb[3]; + SPColor::cmyk_to_rgb_floatv(rgb, c[0], c[1], c[2], c[3]); + color.set(rgb[0], rgb[1], rgb[2]); + alpha = c[4]; + } else { + g_warning("file %s: line %d: Illegal color selector mode NONE", __FILE__, __LINE__); + } + + _color.preserveICC(); + _color.setColorAlpha(color, alpha); +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::_updateDisplay(bool update_wheel) +{ +#ifdef DUMP_CHANGE_INFO + g_message("ColorScales::_onColorChanged( this=%p, %f, %f, %f, %f) %d", this, + _color.color().v.c[0], + _color.color().v.c[1], _color.color().v.c[2], _color.alpha(), int(update_wheel); +#endif + + gfloat tmp[3]; + gfloat c[5] = { 0.0, 0.0, 0.0, 0.0 }; + + SPColor color = _color.color(); + + if constexpr (MODE == SPColorScalesMode::RGB) { + color.get_rgb_floatv(c); + c[3] = _color.alpha(); + c[4] = 0.0; + } else if constexpr (MODE == SPColorScalesMode::HSL) { + color.get_rgb_floatv(tmp); + SPColor::rgb_to_hsl_floatv(c, tmp[0], tmp[1], tmp[2]); + c[3] = _color.alpha(); + c[4] = 0.0; + if (update_wheel) { _wheel->setRgb(tmp[0], tmp[1], tmp[2]); } + } else if constexpr (MODE == SPColorScalesMode::HSV) { + color.get_rgb_floatv(tmp); + SPColor::rgb_to_hsv_floatv(c, tmp[0], tmp[1], tmp[2]); + c[3] = _color.alpha(); + c[4] = 0.0; + if (update_wheel) { _wheel->setRgb(tmp[0], tmp[1], tmp[2]); } + } else if constexpr (MODE == SPColorScalesMode::CMYK) { + color.get_cmyk_floatv(c); + c[4] = _color.alpha(); + } else if constexpr (MODE == SPColorScalesMode::HSLUV) { + color.get_rgb_floatv(tmp); + SPColor::rgb_to_hsluv_floatv(c, tmp[0], tmp[1], tmp[2]); + c[3] = _color.alpha(); + c[4] = 0.0; + if (update_wheel) { _wheel->setRgb(tmp[0], tmp[1], tmp[2]); } + } else { + g_warning("file %s: line %d: Illegal color selector mode NONE", __FILE__, __LINE__); + } + + _updating = true; + setScaled(_a[0], c[0]); + setScaled(_a[1], c[1]); + setScaled(_a[2], c[2]); + setScaled(_a[3], c[3]); + setScaled(_a[4], c[4]); + _updateSliders(CSC_CHANNELS_ALL); + _updating = false; +} + +/* Helpers for setting color value */ +template <SPColorScalesMode MODE> +gfloat ColorScales<MODE>::getScaled(Glib::RefPtr<Gtk::Adjustment> const &a) +{ + gfloat val = a->get_value() / a->get_upper(); + return val; +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::setScaled(Glib::RefPtr<Gtk::Adjustment> &a, gfloat v, bool constrained) +{ + auto upper = a->get_upper(); + gfloat val = v * upper; + if (constrained) { + // TODO: do we want preferences for these? + if (upper == 255) { + val = round(val/16) * 16; + } else { + val = round(val/10) * 10; + } + } + a->set_value(val); +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::_setRangeLimit(gdouble upper) +{ + _range_limit = upper; + for (auto & i : _a) { + i->set_upper(upper); + } +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::_onColorChanged() +{ + if (!get_visible()) { return; } + + _updateDisplay(); +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::on_show() +{ + Gtk::Box::on_show(); + + _updateDisplay(); +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::_getRgbaFloatv(gfloat *rgba) +{ + g_return_if_fail(rgba != nullptr); + + if constexpr (MODE == SPColorScalesMode::RGB) { + rgba[0] = getScaled(_a[0]); + rgba[1] = getScaled(_a[1]); + rgba[2] = getScaled(_a[2]); + rgba[3] = getScaled(_a[3]); + } else if constexpr (MODE == SPColorScalesMode::HSL) { + SPColor::hsl_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2])); + rgba[3] = getScaled(_a[3]); + } else if constexpr (MODE == SPColorScalesMode::HSV) { + SPColor::hsv_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2])); + rgba[3] = getScaled(_a[3]); + } else if constexpr (MODE == SPColorScalesMode::CMYK) { + SPColor::cmyk_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), + getScaled(_a[2]), getScaled(_a[3])); + rgba[3] = getScaled(_a[4]); + } else if constexpr (MODE == SPColorScalesMode::HSLUV) { + SPColor::hsluv_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), + getScaled(_a[2])); + rgba[3] = getScaled(_a[3]); + } else { + g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); + } +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::_getCmykaFloatv(gfloat *cmyka) +{ + gfloat rgb[3]; + + g_return_if_fail(cmyka != nullptr); + + if constexpr (MODE == SPColorScalesMode::RGB) { + SPColor::rgb_to_cmyk_floatv(cmyka, getScaled(_a[0]), getScaled(_a[1]), + getScaled(_a[2])); + cmyka[4] = getScaled(_a[3]); + } else if constexpr (MODE == SPColorScalesMode::HSL) { + SPColor::hsl_to_rgb_floatv(rgb, getScaled(_a[0]), getScaled(_a[1]), + getScaled(_a[2])); + SPColor::rgb_to_cmyk_floatv(cmyka, rgb[0], rgb[1], rgb[2]); + cmyka[4] = getScaled(_a[3]); + } else if constexpr (MODE == SPColorScalesMode::HSLUV) { + SPColor::hsluv_to_rgb_floatv(rgb, getScaled(_a[0]), getScaled(_a[1]), + getScaled(_a[2])); + SPColor::rgb_to_cmyk_floatv(cmyka, rgb[0], rgb[1], rgb[2]); + cmyka[4] = getScaled(_a[3]); + } else if constexpr (MODE == SPColorScalesMode::CMYK) { + cmyka[0] = getScaled(_a[0]); + cmyka[1] = getScaled(_a[1]); + cmyka[2] = getScaled(_a[2]); + cmyka[3] = getScaled(_a[3]); + cmyka[4] = getScaled(_a[4]); + } else { + g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); + } +} + +template <SPColorScalesMode MODE> +guint32 ColorScales<MODE>::_getRgba32() +{ + gfloat c[4]; + guint32 rgba; + + _getRgbaFloatv(c); + + rgba = SP_RGBA32_F_COMPOSE(c[0], c[1], c[2], c[3]); + + return rgba; +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::setupMode() +{ + gfloat rgba[4]; + gfloat c[4]; + + if constexpr (MODE == SPColorScalesMode::NONE) { + rgba[0] = rgba[1] = rgba[2] = rgba[3] = 1.0; + } else { + _getRgbaFloatv(rgba); + } + + if constexpr (MODE == SPColorScalesMode::RGB) { + _setRangeLimit(255.0); + _a[3]->set_upper(100.0); + _l[0]->set_markup_with_mnemonic(_("_R:")); + _s[0]->set_tooltip_text(_("Red")); + _b[0]->set_tooltip_text(_("Red")); + _l[1]->set_markup_with_mnemonic(_("_G:")); + _s[1]->set_tooltip_text(_("Green")); + _b[1]->set_tooltip_text(_("Green")); + _l[2]->set_markup_with_mnemonic(_("_B:")); + _s[2]->set_tooltip_text(_("Blue")); + _b[2]->set_tooltip_text(_("Blue")); + _l[3]->set_markup_with_mnemonic(_("_A:")); + _s[3]->set_tooltip_text(_("Alpha (opacity)")); + _b[3]->set_tooltip_text(_("Alpha (opacity)")); + _s[0]->setMap(nullptr); + _l[4]->hide(); + _s[4]->hide(); + _b[4]->hide(); + _updating = true; + setScaled(_a[0], rgba[0]); + setScaled(_a[1], rgba[1]); + setScaled(_a[2], rgba[2]); + setScaled(_a[3], rgba[3]); + _updateSliders(CSC_CHANNELS_ALL); + _updating = false; + } else if constexpr (MODE == SPColorScalesMode::HSL) { + _setRangeLimit(100.0); + + _l[0]->set_markup_with_mnemonic(_("_H:")); + _s[0]->set_tooltip_text(_("Hue")); + _b[0]->set_tooltip_text(_("Hue")); + _a[0]->set_upper(360.0); + + _l[1]->set_markup_with_mnemonic(_("_S:")); + _s[1]->set_tooltip_text(_("Saturation")); + _b[1]->set_tooltip_text(_("Saturation")); + + _l[2]->set_markup_with_mnemonic(_("_L:")); + _s[2]->set_tooltip_text(_("Lightness")); + _b[2]->set_tooltip_text(_("Lightness")); + + _l[3]->set_markup_with_mnemonic(_("_A:")); + _s[3]->set_tooltip_text(_("Alpha (opacity)")); + _b[3]->set_tooltip_text(_("Alpha (opacity)")); + _s[0]->setMap(sp_color_scales_hue_map()); + _l[4]->hide(); + _s[4]->hide(); + _b[4]->hide(); + _updating = true; + c[0] = 0.0; + + SPColor::rgb_to_hsl_floatv(c, rgba[0], rgba[1], rgba[2]); + + setScaled(_a[0], c[0]); + setScaled(_a[1], c[1]); + setScaled(_a[2], c[2]); + setScaled(_a[3], rgba[3]); + + _updateSliders(CSC_CHANNELS_ALL); + _updating = false; + } else if constexpr (MODE == SPColorScalesMode::HSV) { + _setRangeLimit(100.0); + + _l[0]->set_markup_with_mnemonic(_("_H:")); + _s[0]->set_tooltip_text(_("Hue")); + _b[0]->set_tooltip_text(_("Hue")); + _a[0]->set_upper(360.0); + + _l[1]->set_markup_with_mnemonic(_("_S:")); + _s[1]->set_tooltip_text(_("Saturation")); + _b[1]->set_tooltip_text(_("Saturation")); + + _l[2]->set_markup_with_mnemonic(_("_V:")); + _s[2]->set_tooltip_text(_("Value")); + _b[2]->set_tooltip_text(_("Value")); + + _l[3]->set_markup_with_mnemonic(_("_A:")); + _s[3]->set_tooltip_text(_("Alpha (opacity)")); + _b[3]->set_tooltip_text(_("Alpha (opacity)")); + _s[0]->setMap(sp_color_scales_hue_map()); + _l[4]->hide(); + _s[4]->hide(); + _b[4]->hide(); + _updating = true; + c[0] = 0.0; + + SPColor::rgb_to_hsv_floatv(c, rgba[0], rgba[1], rgba[2]); + + setScaled(_a[0], c[0]); + setScaled(_a[1], c[1]); + setScaled(_a[2], c[2]); + setScaled(_a[3], rgba[3]); + + _updateSliders(CSC_CHANNELS_ALL); + _updating = false; + } else if constexpr (MODE == SPColorScalesMode::CMYK) { + _setRangeLimit(100.0); + _l[0]->set_markup_with_mnemonic(_("_C:")); + _s[0]->set_tooltip_text(_("Cyan")); + _b[0]->set_tooltip_text(_("Cyan")); + + _l[1]->set_markup_with_mnemonic(_("_M:")); + _s[1]->set_tooltip_text(_("Magenta")); + _b[1]->set_tooltip_text(_("Magenta")); + + _l[2]->set_markup_with_mnemonic(_("_Y:")); + _s[2]->set_tooltip_text(_("Yellow")); + _b[2]->set_tooltip_text(_("Yellow")); + + _l[3]->set_markup_with_mnemonic(_("_K:")); + _s[3]->set_tooltip_text(_("Black")); + _b[3]->set_tooltip_text(_("Black")); + + _l[4]->set_markup_with_mnemonic(_("_A:")); + _s[4]->set_tooltip_text(_("Alpha (opacity)")); + _b[4]->set_tooltip_text(_("Alpha (opacity)")); + + _s[0]->setMap(nullptr); + _l[4]->show(); + _s[4]->show(); + _b[4]->show(); + _updating = true; + + SPColor::rgb_to_cmyk_floatv(c, rgba[0], rgba[1], rgba[2]); + setScaled(_a[0], c[0]); + setScaled(_a[1], c[1]); + setScaled(_a[2], c[2]); + setScaled(_a[3], c[3]); + + setScaled(_a[4], rgba[3]); + _updateSliders(CSC_CHANNELS_ALL); + _updating = false; + } else if constexpr (MODE == SPColorScalesMode::HSLUV) { + _setRangeLimit(100.0); + + _l[0]->set_markup_with_mnemonic(_("_H*:")); + _s[0]->set_tooltip_text(_("Hue")); + _b[0]->set_tooltip_text(_("Hue")); + _a[0]->set_upper(360.0); + + _l[1]->set_markup_with_mnemonic(_("_S*:")); + _s[1]->set_tooltip_text(_("Saturation")); + _b[1]->set_tooltip_text(_("Saturation")); + + _l[2]->set_markup_with_mnemonic(_("_L*:")); + _s[2]->set_tooltip_text(_("Lightness")); + _b[2]->set_tooltip_text(_("Lightness")); + + _l[3]->set_markup_with_mnemonic(_("_A:")); + _s[3]->set_tooltip_text(_("Alpha (opacity)")); + _b[3]->set_tooltip_text(_("Alpha (opacity)")); + + _s[0]->setMap(hsluvHueMap(0.0f, 0.0f, &_sliders_maps[0])); + _s[1]->setMap(hsluvSaturationMap(0.0f, 0.0f, &_sliders_maps[1])); + _s[2]->setMap(hsluvLightnessMap(0.0f, 0.0f, &_sliders_maps[2])); + + _l[4]->hide(); + _s[4]->hide(); + _b[4]->hide(); + _updating = true; + c[0] = 0.0; + + SPColor::rgb_to_hsluv_floatv(c, rgba[0], rgba[1], rgba[2]); + + setScaled(_a[0], c[0]); + setScaled(_a[1], c[1]); + setScaled(_a[2], c[2]); + setScaled(_a[3], rgba[3]); + + _updateSliders(CSC_CHANNELS_ALL); + _updating = false; + } else { + g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); + } +} + +template <SPColorScalesMode MODE> +SPColorScalesMode ColorScales<MODE>::getMode() const { return MODE; } + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::_sliderAnyGrabbed() +{ + if (_updating) { return; } + + if (!_dragging) { + _dragging = true; + _color.setHeld(true); + } +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::_sliderAnyReleased() +{ + if (_updating) { return; } + + if (_dragging) { + _dragging = false; + _color.setHeld(false); + } +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::_sliderAnyChanged() +{ + if (_updating) { return; } + + _recalcColor(); +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::_adjustmentChanged(int channel) +{ + if (_updating) { return; } + + _updateSliders((1 << channel)); + _recalcColor(); +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::_wheelChanged() +{ + if constexpr ( + MODE == SPColorScalesMode::NONE || + MODE == SPColorScalesMode::RGB || + MODE == SPColorScalesMode::CMYK) + { + return; + } + + if (_updating) { return; } + + _updating = true; + + double rgb[3]; + _wheel->getRgbV(rgb); + SPColor color(rgb[0], rgb[1], rgb[2]); + + _color_changed.block(); + _color_dragged.block(); + + // Color + _color.preserveICC(); + _color.setHeld(_wheel->isAdjusting()); + _color.setColor(color); + + // Sliders + _updateDisplay(false); + + _color_changed.unblock(); + _color_dragged.unblock(); + + _updating = false; +} + +template <SPColorScalesMode MODE> +void ColorScales<MODE>::_updateSliders(guint channels) +{ + gfloat rgb0[3], rgbm[3], rgb1[3]; + +#ifdef SPCS_PREVIEW + guint32 rgba; +#endif + + std::array<gfloat, 4> const adj = [this]() -> std::array<gfloat, 4> { + if constexpr (MODE == SPColorScalesMode::CMYK) { + return { getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), getScaled(_a[3]) }; + } else { + return { getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 0.0 }; + } + }(); + + if constexpr (MODE == SPColorScalesMode::RGB) { + if ((channels != CSC_CHANNEL_R) && (channels != CSC_CHANNEL_A)) { + /* Update red */ + _s[0]->setColors(SP_RGBA32_F_COMPOSE(0.0, adj[1], adj[2], 1.0), + SP_RGBA32_F_COMPOSE(0.5, adj[1], adj[2], 1.0), + SP_RGBA32_F_COMPOSE(1.0, adj[1], adj[2], 1.0)); + } + if ((channels != CSC_CHANNEL_G) && (channels != CSC_CHANNEL_A)) { + /* Update green */ + _s[1]->setColors(SP_RGBA32_F_COMPOSE(adj[0], 0.0, adj[2], 1.0), + SP_RGBA32_F_COMPOSE(adj[0], 0.5, adj[2], 1.0), + SP_RGBA32_F_COMPOSE(adj[0], 1.0, adj[2], 1.0)); + } + if ((channels != CSC_CHANNEL_B) && (channels != CSC_CHANNEL_A)) { + /* Update blue */ + _s[2]->setColors(SP_RGBA32_F_COMPOSE(adj[0], adj[1], 0.0, 1.0), + SP_RGBA32_F_COMPOSE(adj[0], adj[1], 0.5, 1.0), + SP_RGBA32_F_COMPOSE(adj[0], adj[1], 1.0, 1.0)); + } + if (channels != CSC_CHANNEL_A) { + /* Update alpha */ + _s[3]->setColors(SP_RGBA32_F_COMPOSE(adj[0], adj[1], adj[2], 0.0), + SP_RGBA32_F_COMPOSE(adj[0], adj[1], adj[2], 0.5), + SP_RGBA32_F_COMPOSE(adj[0], adj[1], adj[2], 1.0)); + } + } else if constexpr (MODE == SPColorScalesMode::HSL) { + /* Hue is never updated */ + if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) { + /* Update saturation */ + SPColor::hsl_to_rgb_floatv(rgb0, adj[0], 0.0, adj[2]); + SPColor::hsl_to_rgb_floatv(rgbm, adj[0], 0.5, adj[2]); + SPColor::hsl_to_rgb_floatv(rgb1, adj[0], 1.0, adj[2]); + _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) { + /* Update value */ + SPColor::hsl_to_rgb_floatv(rgb0, adj[0], adj[1], 0.0); + SPColor::hsl_to_rgb_floatv(rgbm, adj[0], adj[1], 0.5); + SPColor::hsl_to_rgb_floatv(rgb1, adj[0], adj[1], 1.0); + _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if (channels != CSC_CHANNEL_A) { + /* Update alpha */ + SPColor::hsl_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2]); + _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0)); + } + } else if constexpr (MODE == SPColorScalesMode::HSV) { + /* Hue is never updated */ + if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) { + /* Update saturation */ + SPColor::hsv_to_rgb_floatv(rgb0, adj[0], 0.0, adj[2]); + SPColor::hsv_to_rgb_floatv(rgbm, adj[0], 0.5, adj[2]); + SPColor::hsv_to_rgb_floatv(rgb1, adj[0], 1.0, adj[2]); + _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) { + /* Update value */ + SPColor::hsv_to_rgb_floatv(rgb0, adj[0], adj[1], 0.0); + SPColor::hsv_to_rgb_floatv(rgbm, adj[0], adj[1], 0.5); + SPColor::hsv_to_rgb_floatv(rgb1, adj[0], adj[1], 1.0); + _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if (channels != CSC_CHANNEL_A) { + /* Update alpha */ + SPColor::hsv_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2]); + _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0)); + } + } else if constexpr (MODE == SPColorScalesMode::CMYK) { + if ((channels != CSC_CHANNEL_C) && (channels != CSC_CHANNEL_CMYKA)) { + /* Update C */ + SPColor::cmyk_to_rgb_floatv(rgb0, 0.0, adj[1], adj[2], adj[3]); + SPColor::cmyk_to_rgb_floatv(rgbm, 0.5, adj[1], adj[2], adj[3]); + SPColor::cmyk_to_rgb_floatv(rgb1, 1.0, adj[1], adj[2], adj[3]); + _s[0]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if ((channels != CSC_CHANNEL_M) && (channels != CSC_CHANNEL_CMYKA)) { + /* Update M */ + SPColor::cmyk_to_rgb_floatv(rgb0, adj[0], 0.0, adj[2], adj[3]); + SPColor::cmyk_to_rgb_floatv(rgbm, adj[0], 0.5, adj[2], adj[3]); + SPColor::cmyk_to_rgb_floatv(rgb1, adj[0], 1.0, adj[2], adj[3]); + _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if ((channels != CSC_CHANNEL_Y) && (channels != CSC_CHANNEL_CMYKA)) { + /* Update Y */ + SPColor::cmyk_to_rgb_floatv(rgb0, adj[0], adj[1], 0.0, adj[3]); + SPColor::cmyk_to_rgb_floatv(rgbm, adj[0], adj[1], 0.5, adj[3]); + SPColor::cmyk_to_rgb_floatv(rgb1, adj[0], adj[1], 1.0, adj[3]); + _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if ((channels != CSC_CHANNEL_K) && (channels != CSC_CHANNEL_CMYKA)) { + /* Update K */ + SPColor::cmyk_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2], 0.0); + SPColor::cmyk_to_rgb_floatv(rgbm, adj[0], adj[1], adj[2], 0.5); + SPColor::cmyk_to_rgb_floatv(rgb1, adj[0], adj[1], adj[2], 1.0); + _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if (channels != CSC_CHANNEL_CMYKA) { + /* Update alpha */ + SPColor::cmyk_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2], adj[3]); + _s[4]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0)); + } + } else if constexpr (MODE == SPColorScalesMode::HSLUV) { + if ((channels != CSC_CHANNEL_H) && (channels != CSC_CHANNEL_A)) { + /* Update hue */ + _s[0]->setMap(hsluvHueMap(adj[1], adj[2], &_sliders_maps[0])); + } + if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) { + /* Update saturation (scaled chroma) */ + _s[1]->setMap(hsluvSaturationMap(adj[0], adj[2], &_sliders_maps[1])); + } + if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) { + /* Update lightness */ + _s[2]->setMap(hsluvLightnessMap(adj[0], adj[1], &_sliders_maps[2])); + } + if (channels != CSC_CHANNEL_A) { + /* Update alpha */ + SPColor::hsluv_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2]); + _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0)); + } + } else { + g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); + } + +#ifdef SPCS_PREVIEW + rgba = sp_color_scales_get_rgba32(cs); + sp_color_preview_set_rgba32(SP_COLOR_PREVIEW(_p), rgba); +#endif +} + +static guchar const *sp_color_scales_hue_map() +{ + static std::array<guchar, 4 * 1024> const map = []() { + std::array<guchar, 4 * 1024> m; + + guchar *p; + p = m.data(); + for (gint h = 0; h < 1024; h++) { + gfloat rgb[3]; + SPColor::hsl_to_rgb_floatv(rgb, h / 1024.0, 1.0, 0.5); + *p++ = SP_COLOR_F_TO_U(rgb[0]); + *p++ = SP_COLOR_F_TO_U(rgb[1]); + *p++ = SP_COLOR_F_TO_U(rgb[2]); + *p++ = 0xFF; + } + + return m; + }(); + + return map.data(); +} + +static void sp_color_interp(guchar *out, gint steps, gfloat *start, gfloat *end) +{ + gfloat s[3] = { + (end[0] - start[0]) / steps, + (end[1] - start[1]) / steps, + (end[2] - start[2]) / steps + }; + + guchar *p = out; + for (int i = 0; i < steps; i++) { + *p++ = SP_COLOR_F_TO_U(start[0] + s[0] * i); + *p++ = SP_COLOR_F_TO_U(start[1] + s[1] * i); + *p++ = SP_COLOR_F_TO_U(start[2] + s[2] * i); + *p++ = 0xFF; + } +} + +template <typename T> +static std::vector<T> range (int const steps, T start, T end) +{ + T step = (end - start) / (steps - 1); + + std::vector<T> out; + out.reserve(steps); + + for (int i = 0; i < steps-1; i++) { + out.emplace_back(start + step * i); + } + out.emplace_back(end); + + return out; +} + +static guchar const *sp_color_scales_hsluv_map(guchar *map, + std::function<void(float*, float)> callback) +{ + // Only generate 21 colors and interpolate between them to get 1024 + static int const STEPS = 21; + static int const COLORS = (STEPS+1) * 3; + + std::vector<float> steps = range<float>(STEPS+1, 0.f, 1.f); + + // Generate color steps + gfloat colors[COLORS]; + for (int i = 0; i < STEPS+1; i++) { + callback(colors+(i*3), steps[i]); + } + + for (int i = 0; i < STEPS; i++) { + int a = steps[i] * 1023, + b = steps[i+1] * 1023; + sp_color_interp(map+(a * 4), b-a, colors+(i*3), colors+((i+1)*3)); + } + + return map; +} + +template <SPColorScalesMode MODE> +guchar const *ColorScales<MODE>::hsluvHueMap(gfloat s, gfloat l, + std::array<guchar, 4 * 1024> *map) +{ + return sp_color_scales_hsluv_map(map->data(), [s, l] (float *colors, float h) { + SPColor::hsluv_to_rgb_floatv(colors, h, s, l); + }); +} + +template <SPColorScalesMode MODE> +guchar const *ColorScales<MODE>::hsluvSaturationMap(gfloat h, gfloat l, + std::array<guchar, 4 * 1024> *map) +{ + return sp_color_scales_hsluv_map(map->data(), [h, l] (float *colors, float s) { + SPColor::hsluv_to_rgb_floatv(colors, h, s, l); + }); +} + +template <SPColorScalesMode MODE> +guchar const *ColorScales<MODE>::hsluvLightnessMap(gfloat h, gfloat s, + std::array<guchar, 4 * 1024> *map) +{ + return sp_color_scales_hsluv_map(map->data(), [h, s] (float *colors, float l) { + SPColor::hsluv_to_rgb_floatv(colors, h, s, l); + }); +} + +template <SPColorScalesMode MODE> +ColorScalesFactory<MODE>::ColorScalesFactory() +{} + +template <SPColorScalesMode MODE> +Gtk::Widget *ColorScalesFactory<MODE>::createWidget(Inkscape::UI::SelectedColor &color) const +{ + Gtk::Widget *w = Gtk::manage(new ColorScales<MODE>(color)); + return w; +} + +template <SPColorScalesMode MODE> +Glib::ustring ColorScalesFactory<MODE>::modeName() const +{ + if constexpr (MODE == SPColorScalesMode::RGB) { + return gettext(ColorScales<>::SUBMODE_NAMES[1]); + } else if constexpr (MODE == SPColorScalesMode::HSL) { + return gettext(ColorScales<>::SUBMODE_NAMES[2]); + } else if constexpr (MODE == SPColorScalesMode::CMYK) { + return gettext(ColorScales<>::SUBMODE_NAMES[3]); + } else if constexpr (MODE == SPColorScalesMode::HSV) { + return gettext(ColorScales<>::SUBMODE_NAMES[4]); + } else if constexpr (MODE == SPColorScalesMode::HSLUV) { + return gettext(ColorScales<>::SUBMODE_NAMES[5]); + } else { + return gettext(ColorScales<>::SUBMODE_NAMES[0]); + } +} + +// Explicit instantiations +template class ColorScales<SPColorScalesMode::NONE>; +template class ColorScales<SPColorScalesMode::RGB>; +template class ColorScales<SPColorScalesMode::HSL>; +template class ColorScales<SPColorScalesMode::CMYK>; +template class ColorScales<SPColorScalesMode::HSV>; +template class ColorScales<SPColorScalesMode::HSLUV>; + +template class ColorScalesFactory<SPColorScalesMode::NONE>; +template class ColorScalesFactory<SPColorScalesMode::RGB>; +template class ColorScalesFactory<SPColorScalesMode::HSL>; +template class ColorScalesFactory<SPColorScalesMode::CMYK>; +template class ColorScalesFactory<SPColorScalesMode::HSV>; +template class ColorScalesFactory<SPColorScalesMode::HSLUV>; + +} // namespace Widget +} // 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/widget/color-scales.h b/src/ui/widget/color-scales.h new file mode 100644 index 0000000..6e5b9a2 --- /dev/null +++ b/src/ui/widget/color-scales.h @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Color selector using sliders for each components, for multiple color modes + *//* + * Authors: + * see git history + * + * Copyright (C) 2018, 2021 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_COLOR_SCALES_H +#define SEEN_SP_COLOR_SCALES_H + +#include <gtkmm/box.h> +#include <array> + +#include "ui/selected-color.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +class ColorSlider; +class ColorWheel; + +enum class SPColorScalesMode { + NONE, + RGB, + HSL, + CMYK, + HSV, + HSLUV +}; + +template <SPColorScalesMode MODE = SPColorScalesMode::NONE> +class ColorScales + : public Gtk::Box +{ +public: + static gchar const *SUBMODE_NAMES[]; + + static gfloat getScaled(Glib::RefPtr<Gtk::Adjustment> const &a); + static void setScaled(Glib::RefPtr<Gtk::Adjustment> &a, gfloat v, bool constrained = false); + + ColorScales(SelectedColor &color); + ~ColorScales() override; + + void setupMode(); + SPColorScalesMode getMode() const; + + static guchar const *hsluvHueMap(gfloat s, gfloat l, + std::array<guchar, 4 * 1024> *map); + static guchar const *hsluvSaturationMap(gfloat h, gfloat l, + std::array<guchar, 4 * 1024> *map); + static guchar const *hsluvLightnessMap(gfloat h, gfloat s, + std::array<guchar, 4 * 1024> *map); + +protected: + void _onColorChanged(); + void on_show() override; + + virtual void _initUI(); + + void _sliderAnyGrabbed(); + void _sliderAnyReleased(); + void _sliderAnyChanged(); + void _adjustmentChanged(int channel); + void _wheelChanged(); + + void _getRgbaFloatv(gfloat *rgba); + void _getCmykaFloatv(gfloat *cmyka); + guint32 _getRgba32(); + void _updateSliders(guint channels); + void _recalcColor(); + void _updateDisplay(bool update_wheel = true); + + void _setRangeLimit(gdouble upper); + + SelectedColor &_color; + gdouble _range_limit; + gboolean _updating : 1; + gboolean _dragging : 1; + std::vector<Glib::RefPtr<Gtk::Adjustment>> _a; /* Channel adjustments */ + Inkscape::UI::Widget::ColorSlider *_s[5]; /* Channel sliders */ + Gtk::Widget *_b[5]; /* Spinbuttons */ + Gtk::Label *_l[5]; /* Labels */ + std::array<guchar, 4 * 1024> _sliders_maps[4]; + Inkscape::UI::Widget::ColorWheel *_wheel; + + const Glib::ustring _prefs = "/color_scales"; + static gchar const * const _pref_wheel_visibility; + + sigc::connection _color_changed; + sigc::connection _color_dragged; + +private: + // By default, disallow copy constructor and assignment operator + ColorScales(ColorScales const &obj) = delete; + ColorScales &operator=(ColorScales const &obj) = delete; +}; + +template <SPColorScalesMode MODE> +class ColorScalesFactory : public Inkscape::UI::ColorSelectorFactory +{ +public: + ColorScalesFactory(); + + Gtk::Widget *createWidget(Inkscape::UI::SelectedColor &color) const override; + Glib::ustring modeName() const override; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif /* !SEEN_SP_COLOR_SCALES_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/widget/color-slider.cpp b/src/ui/widget/color-slider.cpp new file mode 100644 index 0000000..6ffd266 --- /dev/null +++ b/src/ui/widget/color-slider.cpp @@ -0,0 +1,546 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A slider with colored background - implementation. + *//* + * Authors: + * see git history + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdkmm/cursor.h> +#include <gdkmm/general.h> +#include <gtkmm/adjustment.h> +#include <gtkmm/stylecontext.h> + +#include "ui/widget/color-scales.h" +#include "ui/widget/color-slider.h" +#include "preferences.h" + +static const gint SLIDER_WIDTH = 96; +static const gint SLIDER_HEIGHT = 8; +static const gint ARROW_SIZE = 8; + +static const guchar *sp_color_slider_render_gradient(gint x0, gint y0, gint width, gint height, gint c[], gint dc[], + guint b0, guint b1, guint mask); +static const guchar *sp_color_slider_render_map(gint x0, gint y0, gint width, gint height, guchar *map, gint start, + gint step, guint b0, guint b1, guint mask); + +namespace Inkscape { +namespace UI { +namespace Widget { + +ColorSlider::ColorSlider(Glib::RefPtr<Gtk::Adjustment> adjustment) + : _dragging(false) + , _value(0.0) + , _oldvalue(0.0) + , _map(nullptr) +{ + _c0[0] = 0x00; + _c0[1] = 0x00; + _c0[2] = 0x00; + _c0[3] = 0xff; + + _cm[0] = 0xff; + _cm[1] = 0x00; + _cm[2] = 0x00; + _cm[3] = 0xff; + + _c0[0] = 0xff; + _c0[1] = 0xff; + _c0[2] = 0xff; + _c0[3] = 0xff; + + _b0 = 0x5f; + _b1 = 0xa0; + _bmask = 0x08; + + setAdjustment(adjustment); +} + +ColorSlider::~ColorSlider() +{ + if (_adjustment) { + _adjustment_changed_connection.disconnect(); + _adjustment_value_changed_connection.disconnect(); + _adjustment.reset(); + } +} + +void ColorSlider::on_realize() +{ + set_realized(); + + if (!_gdk_window) { + GdkWindowAttr attributes; + gint attributes_mask; + Gtk::Allocation allocation = get_allocation(); + + memset(&attributes, 0, sizeof(attributes)); + attributes.x = allocation.get_x(); + attributes.y = allocation.get_y(); + attributes.width = allocation.get_width(); + attributes.height = allocation.get_height(); + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.visual = gdk_screen_get_system_visual(gdk_screen_get_default()); + attributes.event_mask = get_events(); + attributes.event_mask |= (Gdk::EXPOSURE_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK | Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK); + + attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL; + + _gdk_window = Gdk::Window::create(get_parent_window(), &attributes, attributes_mask); + set_window(_gdk_window); + _gdk_window->set_user_data(gobj()); + } +} + +void ColorSlider::on_unrealize() +{ + _gdk_window.reset(); + + Gtk::Widget::on_unrealize(); +} + +void ColorSlider::on_size_allocate(Gtk::Allocation &allocation) +{ + set_allocation(allocation); + + if (get_realized()) { + _gdk_window->move_resize(allocation.get_x(), allocation.get_y(), allocation.get_width(), + allocation.get_height()); + } +} + +void ColorSlider::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const +{ + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + Gtk::Border padding = style_context->get_padding(get_state_flags()); + int width = SLIDER_WIDTH + padding.get_left() + padding.get_right(); + minimum_width = natural_width = width; +} + +void ColorSlider::get_preferred_width_for_height_vfunc(int /*height*/, int &minimum_width, int &natural_width) const +{ + get_preferred_width(minimum_width, natural_width); +} + +void ColorSlider::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const +{ + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + Gtk::Border padding = style_context->get_padding(get_state_flags()); + int height = SLIDER_HEIGHT + padding.get_top() + padding.get_bottom(); + minimum_height = natural_height = height; +} + +void ColorSlider::get_preferred_height_for_width_vfunc(int /*width*/, int &minimum_height, int &natural_height) const +{ + get_preferred_height(minimum_height, natural_height); +} + +bool ColorSlider::on_button_press_event(GdkEventButton *event) +{ + if (event->button == 1) { + Gtk::Allocation allocation = get_allocation(); + gint cx, cw; + cx = get_style_context()->get_padding(get_state_flags()).get_left(); + cw = allocation.get_width() - 2 * cx; + signal_grabbed.emit(); + _dragging = true; + _oldvalue = _value; + gfloat value = CLAMP((gfloat)(event->x - cx) / cw, 0.0, 1.0); + bool constrained = event->state & GDK_CONTROL_MASK; + ColorScales<>::setScaled(_adjustment, value, constrained); + signal_dragged.emit(); + + auto window = _gdk_window->gobj(); + + auto seat = gdk_event_get_seat(reinterpret_cast<GdkEvent *>(event)); + gdk_seat_grab(seat, + window, + GDK_SEAT_CAPABILITY_ALL_POINTING, + FALSE, + nullptr, + reinterpret_cast<GdkEvent *>(event), + nullptr, + nullptr); + } + + return false; +} + +bool ColorSlider::on_button_release_event(GdkEventButton *event) +{ + if (event->button == 1) { + gdk_seat_ungrab(gdk_event_get_seat(reinterpret_cast<GdkEvent *>(event))); + _dragging = false; + signal_released.emit(); + if (_value != _oldvalue) { + signal_value_changed.emit(); + } + } + + return false; +} + +bool ColorSlider::on_motion_notify_event(GdkEventMotion *event) +{ + if (_dragging) { + gint cx, cw; + Gtk::Allocation allocation = get_allocation(); + cx = get_style_context()->get_padding(get_state_flags()).get_left(); + cw = allocation.get_width() - 2 * cx; + gfloat value = CLAMP((gfloat)(event->x - cx) / cw, 0.0, 1.0); + bool constrained = event->state & GDK_CONTROL_MASK; + ColorScales<>::setScaled(_adjustment, value, constrained); + signal_dragged.emit(); + } + + return false; +} + +void ColorSlider::setAdjustment(Glib::RefPtr<Gtk::Adjustment> adjustment) +{ + if (!adjustment) { + _adjustment = Gtk::Adjustment::create(0.0, 0.0, 1.0, 0.01, 0.0, 0.0); + } + else { + adjustment->set_page_increment(0.0); + adjustment->set_page_size(0.0); + } + + if (_adjustment != adjustment) { + if (_adjustment) { + _adjustment_changed_connection.disconnect(); + _adjustment_value_changed_connection.disconnect(); + } + + _adjustment = adjustment; + _adjustment_changed_connection = + _adjustment->signal_changed().connect(sigc::mem_fun(this, &ColorSlider::_onAdjustmentChanged)); + _adjustment_value_changed_connection = + _adjustment->signal_value_changed().connect(sigc::mem_fun(this, &ColorSlider::_onAdjustmentValueChanged)); + + _value = ColorScales<>::getScaled(_adjustment); + + _onAdjustmentChanged(); + } +} + +void ColorSlider::_onAdjustmentChanged() { queue_draw(); } + +void ColorSlider::_onAdjustmentValueChanged() +{ + if (_value != ColorScales<>::getScaled(_adjustment)) { + gint cx, cy, cw, ch; + auto style_context = get_style_context(); + auto allocation = get_allocation(); + auto padding = style_context->get_padding(get_state_flags()); + cx = padding.get_left(); + cy = padding.get_top(); + cw = allocation.get_width() - 2 * cx; + ch = allocation.get_height() - 2 * cy; + if ((gint)(ColorScales<>::getScaled(_adjustment) * cw) != (gint)(_value * cw)) { + gint ax, ay; + gfloat value; + value = _value; + _value = ColorScales<>::getScaled(_adjustment); + ax = (int)(cx + value * cw - ARROW_SIZE / 2 - 2); + ay = cy; + queue_draw_area(ax, ay, ARROW_SIZE + 4, ch); + ax = (int)(cx + _value * cw - ARROW_SIZE / 2 - 2); + ay = cy; + queue_draw_area(ax, ay, ARROW_SIZE + 4, ch); + } + else { + _value = ColorScales<>::getScaled(_adjustment); + } + } +} + +void ColorSlider::setColors(guint32 start, guint32 mid, guint32 end) +{ + // Remove any map, if set + _map = nullptr; + + _c0[0] = start >> 24; + _c0[1] = (start >> 16) & 0xff; + _c0[2] = (start >> 8) & 0xff; + _c0[3] = start & 0xff; + + _cm[0] = mid >> 24; + _cm[1] = (mid >> 16) & 0xff; + _cm[2] = (mid >> 8) & 0xff; + _cm[3] = mid & 0xff; + + _c1[0] = end >> 24; + _c1[1] = (end >> 16) & 0xff; + _c1[2] = (end >> 8) & 0xff; + _c1[3] = end & 0xff; + + queue_draw(); +} + +void ColorSlider::setMap(const guchar *map) +{ + _map = const_cast<guchar *>(map); + + queue_draw(); +} + +void ColorSlider::setBackground(guint dark, guint light, guint size) +{ + _b0 = dark; + _b1 = light; + _bmask = size; + + queue_draw(); +} + +bool ColorSlider::on_draw(const Cairo::RefPtr<Cairo::Context> &cr) +{ + gboolean colorsOnTop = Inkscape::Preferences::get()->getBool("/options/workarounds/colorsontop", false); + + auto allocation = get_allocation(); + auto style_context = get_style_context(); + + // Draw shadow + if (colorsOnTop) { + style_context->render_frame(cr, 0, 0, allocation.get_width(), allocation.get_height()); + } + + /* Paintable part of color gradient area */ + Gdk::Rectangle carea; + Gtk::Border padding; + + padding = style_context->get_padding(get_state_flags()); + + int scale = style_context->get_scale(); + carea.set_x(padding.get_left() * scale); + carea.set_y(padding.get_top() * scale); + + carea.set_width(allocation.get_width() * scale - 2 * carea.get_x()); + carea.set_height(allocation.get_height() * scale - 2 * carea.get_y()); + + cr->save(); + // changing scale to draw pixmap at display resolution + cr->scale(1.0 / scale, 1.0 / scale); + + if (_map) { + /* Render map pixelstore */ + gint d = (1024 << 16) / carea.get_width(); + gint s = 0; + + const guchar *b = + sp_color_slider_render_map(0, 0, carea.get_width(), carea.get_height(), _map, s, d, _b0, _b1, _bmask * scale); + + if (b != nullptr && carea.get_width() > 0) { + Glib::RefPtr<Gdk::Pixbuf> pb = Gdk::Pixbuf::create_from_data( + b, Gdk::COLORSPACE_RGB, false, 8, carea.get_width(), carea.get_height(), carea.get_width() * 3); + + Gdk::Cairo::set_source_pixbuf(cr, pb, carea.get_x(), carea.get_y()); + cr->paint(); + } + } + else { + gint c[4], dc[4]; + + /* Render gradient */ + + // part 1: from c0 to cm + if (carea.get_width() > 0) { + for (gint i = 0; i < 4; i++) { + c[i] = _c0[i] << 16; + dc[i] = ((_cm[i] << 16) - c[i]) / (carea.get_width() / 2); + } + guint wi = carea.get_width() / 2; + const guchar *b = sp_color_slider_render_gradient(0, 0, wi, carea.get_height(), c, dc, _b0, _b1, _bmask * scale); + + /* Draw pixelstore 1 */ + if (b != nullptr && wi > 0) { + Glib::RefPtr<Gdk::Pixbuf> pb = + Gdk::Pixbuf::create_from_data(b, Gdk::COLORSPACE_RGB, false, 8, wi, carea.get_height(), wi * 3); + + Gdk::Cairo::set_source_pixbuf(cr, pb, carea.get_x(), carea.get_y()); + cr->paint(); + } + } + + // part 2: from cm to c1 + if (carea.get_width() > 0) { + for (gint i = 0; i < 4; i++) { + c[i] = _cm[i] << 16; + dc[i] = ((_c1[i] << 16) - c[i]) / (carea.get_width() / 2); + } + guint wi = carea.get_width() / 2; + const guchar *b = sp_color_slider_render_gradient(carea.get_width() / 2, 0, wi, carea.get_height(), c, dc, + _b0, _b1, _bmask * scale); + + /* Draw pixelstore 2 */ + if (b != nullptr && wi > 0) { + Glib::RefPtr<Gdk::Pixbuf> pb = + Gdk::Pixbuf::create_from_data(b, Gdk::COLORSPACE_RGB, false, 8, wi, carea.get_height(), wi * 3); + + Gdk::Cairo::set_source_pixbuf(cr, pb, carea.get_width() / 2 + carea.get_x(), carea.get_y()); + cr->paint(); + } + } + } + + cr->restore(); + + /* Draw shadow */ + if (!colorsOnTop) { + style_context->render_frame(cr, 0, 0, allocation.get_width(), allocation.get_height()); + } + + /* Draw arrow */ + gint x = (int)(_value * (carea.get_width() / scale) - ARROW_SIZE / 2 + carea.get_x() / scale); + gint y1 = carea.get_y() / scale; + gint y2 = carea.get_y() / scale + carea.get_height() / scale - 1; + cr->set_line_width(2.0); + + // Define top arrow + cr->move_to(x - 0.5, y1 + 0.5); + cr->line_to(x + ARROW_SIZE - 0.5, y1 + 0.5); + cr->line_to(x + (ARROW_SIZE - 1) / 2.0, y1 + ARROW_SIZE / 2.0 + 0.5); + cr->close_path(); + + // Define bottom arrow + cr->move_to(x - 0.5, y2 + 0.5); + cr->line_to(x + ARROW_SIZE - 0.5, y2 + 0.5); + cr->line_to(x + (ARROW_SIZE - 1) / 2.0, y2 - ARROW_SIZE / 2.0 + 0.5); + cr->close_path(); + + // Render both arrows + cr->set_source_rgb(0.0, 0.0, 0.0); + cr->stroke_preserve(); + cr->set_source_rgb(1.0, 1.0, 1.0); + cr->fill(); + + return false; +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* Colors are << 16 */ + +inline bool checkerboard(gint x, gint y, guint size) { + return ((x / size) & 1) != ((y / size) & 1); +} + +static const guchar *sp_color_slider_render_gradient(gint x0, gint y0, gint width, gint height, gint c[], gint dc[], + guint b0, guint b1, guint mask) +{ + static guchar *buf = nullptr; + static gint bs = 0; + guchar *dp; + gint x, y; + guint r, g, b, a; + + if (buf && (bs < width * height)) { + g_free(buf); + buf = nullptr; + } + if (!buf) { + buf = g_new(guchar, width * height * 3); + bs = width * height; + } + + dp = buf; + r = c[0]; + g = c[1]; + b = c[2]; + a = c[3]; + for (x = x0; x < x0 + width; x++) { + gint cr, cg, cb, ca; + guchar *d; + cr = r >> 16; + cg = g >> 16; + cb = b >> 16; + ca = a >> 16; + d = dp; + for (y = y0; y < y0 + height; y++) { + guint bg, fc; + /* Background value */ + bg = checkerboard(x, y, mask) ? b0 : b1; + fc = (cr - bg) * ca; + d[0] = bg + ((fc + (fc >> 8) + 0x80) >> 8); + fc = (cg - bg) * ca; + d[1] = bg + ((fc + (fc >> 8) + 0x80) >> 8); + fc = (cb - bg) * ca; + d[2] = bg + ((fc + (fc >> 8) + 0x80) >> 8); + d += 3 * width; + } + r += dc[0]; + g += dc[1]; + b += dc[2]; + a += dc[3]; + dp += 3; + } + + return buf; +} + +/* Positions are << 16 */ + +static const guchar *sp_color_slider_render_map(gint x0, gint y0, gint width, gint height, guchar *map, gint start, + gint step, guint b0, guint b1, guint mask) +{ + static guchar *buf = nullptr; + static gint bs = 0; + guchar *dp; + gint x, y; + + if (buf && (bs < width * height)) { + g_free(buf); + buf = nullptr; + } + if (!buf) { + buf = g_new(guchar, width * height * 3); + bs = width * height; + } + + dp = buf; + for (x = x0; x < x0 + width; x++) { + gint cr, cg, cb, ca; + guchar *d = dp; + guchar *sp = map + 4 * (start >> 16); + cr = *sp++; + cg = *sp++; + cb = *sp++; + ca = *sp++; + for (y = y0; y < y0 + height; y++) { + guint bg, fc; + /* Background value */ + bg = checkerboard(x, y, mask) ? b0 : b1; + fc = (cr - bg) * ca; + d[0] = bg + ((fc + (fc >> 8) + 0x80) >> 8); + fc = (cg - bg) * ca; + d[1] = bg + ((fc + (fc >> 8) + 0x80) >> 8); + fc = (cb - bg) * ca; + d[2] = bg + ((fc + (fc >> 8) + 0x80) >> 8); + d += 3 * width; + } + dp += 3; + start += step; + } + + return buf; +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 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/widget/color-slider.h b/src/ui/widget/color-slider.h new file mode 100644 index 0000000..963e107 --- /dev/null +++ b/src/ui/widget/color-slider.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history +* Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLOR_SLIDER_H +#define SEEN_COLOR_SLIDER_H + +#include <gtkmm/widget.h> +#include <sigc++/signal.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/* + * A slider with colored background + */ +class ColorSlider : public Gtk::Widget { +public: + ColorSlider(Glib::RefPtr<Gtk::Adjustment> adjustment); + ~ColorSlider() override; + + void setAdjustment(Glib::RefPtr<Gtk::Adjustment> adjustment); + + void setColors(guint32 start, guint32 mid, guint32 end); + + void setMap(const guchar *map); + + void setBackground(guint dark, guint light, guint size); + + sigc::signal<void> signal_grabbed; + sigc::signal<void> signal_dragged; + sigc::signal<void> signal_released; + sigc::signal<void> signal_value_changed; + +protected: + void on_size_allocate(Gtk::Allocation &allocation) override; + void on_realize() override; + void on_unrealize() override; + bool on_button_press_event(GdkEventButton *event) override; + bool on_button_release_event(GdkEventButton *event) override; + bool on_motion_notify_event(GdkEventMotion *event) override; + bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override; + void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override; + void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; + void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override; + +private: + void _onAdjustmentChanged(); + void _onAdjustmentValueChanged(); + + bool _dragging; + + Glib::RefPtr<Gtk::Adjustment> _adjustment; + sigc::connection _adjustment_changed_connection; + sigc::connection _adjustment_value_changed_connection; + + gfloat _value; + gfloat _oldvalue; + guchar _c0[4], _cm[4], _c1[4]; + guchar _b0, _b1; + guchar _bmask; + + guchar *_map; + + Glib::RefPtr<Gdk::Window> _gdk_window; +}; + +} // namespace Widget +} // 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/widget/combo-box-entry-tool-item.cpp b/src/ui/widget/combo-box-entry-tool-item.cpp new file mode 100644 index 0000000..865d827 --- /dev/null +++ b/src/ui/widget/combo-box-entry-tool-item.cpp @@ -0,0 +1,707 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A class derived from Gtk::ToolItem that wraps a GtkComboBoxEntry. + * Features: + * Setting GtkEntryBox width in characters. + * Passing a function for formatting cells. + * Displaying a warning if entry text isn't in list. + * Check comma separated values in text against list. (Useful for font-family fallbacks.) + * Setting names for GtkComboBoxEntry and GtkEntry (actionName_combobox, actionName_entry) + * to allow setting resources. + * + * Author(s): + * Tavmjong Bah + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * We must provide for both a toolbar item and a menu item. + * As we don't know which widgets are used (or even constructed), + * we must keep track of things like active entry ourselves. + */ + +#include "combo-box-entry-tool-item.h" + +#include <cassert> +#include <iostream> +#include <cstring> +#include <glibmm/ustring.h> + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <gdkmm/display.h> + +#include "ui/icon-names.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +ComboBoxEntryToolItem::ComboBoxEntryToolItem(Glib::ustring name, + Glib::ustring label, + Glib::ustring tooltip, + GtkTreeModel *model, + gint entry_width, + gint extra_width, + void *cell_data_func, + void *separator_func, + GtkWidget *focusWidget) + : _label(std::move(label)), + _tooltip(std::move(tooltip)), + _model(model), + _entry_width(entry_width), + _extra_width(extra_width), + _cell_data_func(cell_data_func), + _separator_func(separator_func), + _focusWidget(focusWidget), + _active(-1), + _text(strdup("")), + _entry_completion(nullptr), + _popup(false), + _info(nullptr), + _info_cb(nullptr), + _info_cb_id(0), + _info_cb_blocked(false), + _warning(nullptr), + _warning_cb(nullptr), + _warning_cb_id(0), + _warning_cb_blocked(false), + _isload(true), + _markup(false) + +{ + set_name(name); + + gchar *action_name = g_strdup( get_name().c_str() ); + gchar *combobox_name = g_strjoin( nullptr, action_name, "_combobox", nullptr ); + gchar *entry_name = g_strjoin( nullptr, action_name, "_entry", nullptr ); + g_free( action_name ); + + GtkWidget* comboBoxEntry = gtk_combo_box_new_with_model_and_entry (_model); + gtk_combo_box_set_entry_text_column (GTK_COMBO_BOX (comboBoxEntry), 0); + + // Name it so we can muck with it using an RC file + gtk_widget_set_name( comboBoxEntry, combobox_name ); + g_free( combobox_name ); + + { + gtk_widget_set_halign(comboBoxEntry, GTK_ALIGN_START); + gtk_widget_set_hexpand(comboBoxEntry, FALSE); + gtk_widget_set_vexpand(comboBoxEntry, FALSE); + add(*Glib::wrap(comboBoxEntry)); + } + + _combobox = GTK_COMBO_BOX (comboBoxEntry); + + //gtk_combo_box_set_active( GTK_COMBO_BOX( comboBoxEntry ), ink_comboboxentry_action->active ); + gtk_combo_box_set_active( GTK_COMBO_BOX( comboBoxEntry ), 0 ); + + g_signal_connect( G_OBJECT(comboBoxEntry), "changed", G_CALLBACK(combo_box_changed_cb), this ); + + // Optionally add separator function... + if( _separator_func != nullptr ) { + gtk_combo_box_set_row_separator_func( _combobox, + GtkTreeViewRowSeparatorFunc (_separator_func), + nullptr, nullptr ); + } + + // Optionally add formatting... + if( _cell_data_func != nullptr ) { + gtk_combo_box_set_popup_fixed_width (GTK_COMBO_BOX(comboBoxEntry), false); + this->_cell = gtk_cell_renderer_text_new(); + int total = gtk_tree_model_iter_n_children (model, nullptr); + int height = 30; + if (total > 1000) { + height = 30000/total; + g_warning("You have a huge number of font families (%d), " + "and Cairo is limiting the size of widgets you can draw.\n" + "Your preview cell height is capped to %d.", + total, height); + } + gtk_cell_renderer_set_fixed_size(_cell, -1, height); + g_signal_connect(G_OBJECT(comboBoxEntry), "popup", G_CALLBACK(combo_box_popup_cb), this); + gtk_cell_layout_clear( GTK_CELL_LAYOUT( comboBoxEntry ) ); + gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( comboBoxEntry ), _cell, true ); + gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT( comboBoxEntry ), _cell, + GtkCellLayoutDataFunc (_cell_data_func), 0, nullptr ); + } + + // Optionally widen the combobox width... which widens the drop-down list in list mode. + if( _extra_width > 0 ) { + GtkRequisition req; + gtk_widget_get_preferred_size(GTK_WIDGET(_combobox), &req, nullptr); + gtk_widget_set_size_request( GTK_WIDGET( _combobox ), + req.width + _extra_width, -1 ); + } + + // Get reference to GtkEntry and fiddle a bit with it. + GtkWidget *child = gtk_bin_get_child( GTK_BIN(comboBoxEntry) ); + + // Name it so we can muck with it using an RC file + gtk_widget_set_name( child, entry_name ); + g_free( entry_name ); + + if( child && GTK_IS_ENTRY( child ) ) { + + _entry = GTK_ENTRY(child); + + // Change width + if( _entry_width > 0 ) { + gtk_entry_set_width_chars (GTK_ENTRY (child), _entry_width ); + } + + // Add pop-up entry completion if required + if( _popup ) { + popup_enable(); + } + + // Add signal for GtkEntry to check if finished typing. + g_signal_connect( G_OBJECT(child), "activate", G_CALLBACK(entry_activate_cb), this ); + g_signal_connect( G_OBJECT(child), "key-press-event", G_CALLBACK(keypress_cb), this ); + } + + set_tooltip(_tooltip.c_str()); + + show_all(); +} + +// Setters/Getters --------------------------------------------------- + +Glib::ustring +ComboBoxEntryToolItem::get_active_text() +{ + assert(_text); + return _text; +} + +/* + * For the font-family list we need to handle two cases: + * Text is in list store: + * In this case we use row number as the font-family list can have duplicate + * entries, one in the document font part and one in the system font part. In + * order that scrolling through the list works properly we must distinguish + * between the two. + * Text is not in the list store (i.e. default font-family is not on system): + * In this case we have a row number of -1, and the text must be set by hand. + */ +gboolean +ComboBoxEntryToolItem::set_active_text(const gchar* text, int row) +{ + if( strcmp( _text, text ) != 0 ) { + g_free( _text ); + _text = g_strdup( text ); + } + + // Get active row or -1 if none + if( row < 0 ) { + row = get_active_row_from_text(this, _text); + } + _active = row; + + // Set active row, check that combobox has been created. + if( _combobox ) { + gtk_combo_box_set_active( GTK_COMBO_BOX( _combobox ), _active ); + } + + // Fiddle with entry + if( _entry ) { + + // Explicitly set text in GtkEntry box (won't be set if text not in list). + gtk_entry_set_text( _entry, text ); + + // Show or hide warning -- this might be better moved to text-toolbox.cpp + if( _info_cb_id != 0 && + !_info_cb_blocked ) { + g_signal_handler_block (G_OBJECT(_entry), + _info_cb_id ); + _info_cb_blocked = true; + } + if( _warning_cb_id != 0 && + !_warning_cb_blocked ) { + g_signal_handler_block (G_OBJECT(_entry), + _warning_cb_id ); + _warning_cb_blocked = true; + } + + bool set = false; + if( _warning != nullptr ) { + Glib::ustring missing = check_comma_separated_text(); + if( !missing.empty() ) { + gtk_entry_set_icon_from_icon_name( _entry, + GTK_ENTRY_ICON_SECONDARY, + INKSCAPE_ICON("dialog-warning") ); + // Can't add tooltip until icon set + Glib::ustring warning = _warning; + warning += ": "; + warning += missing; + gtk_entry_set_icon_tooltip_text( _entry, + GTK_ENTRY_ICON_SECONDARY, + warning.c_str() ); + + if( _warning_cb ) { + + // Add callback if we haven't already + if( _warning_cb_id == 0 ) { + _warning_cb_id = + g_signal_connect( G_OBJECT(_entry), + "icon-press", + G_CALLBACK(_warning_cb), + this); + } + // Unblock signal + if( _warning_cb_blocked ) { + g_signal_handler_unblock (G_OBJECT(_entry), + _warning_cb_id ); + _warning_cb_blocked = false; + } + } + set = true; + } + } + + if( !set && _info != nullptr ) { + gtk_entry_set_icon_from_icon_name( GTK_ENTRY(_entry), + GTK_ENTRY_ICON_SECONDARY, + INKSCAPE_ICON("edit-select-all") ); + gtk_entry_set_icon_tooltip_text( _entry, + GTK_ENTRY_ICON_SECONDARY, + _info ); + + if( _info_cb ) { + // Add callback if we haven't already + if( _info_cb_id == 0 ) { + _info_cb_id = + g_signal_connect( G_OBJECT(_entry), + "icon-press", + G_CALLBACK(_info_cb), + this); + } + // Unblock signal + if( _info_cb_blocked ) { + g_signal_handler_unblock (G_OBJECT(_entry), + _info_cb_id ); + _info_cb_blocked = false; + } + } + set = true; + } + + if( !set ) { + gtk_entry_set_icon_from_icon_name( GTK_ENTRY(_entry), + GTK_ENTRY_ICON_SECONDARY, + nullptr ); + } + } + + // Return if active text in list + gboolean found = ( _active != -1 ); + return found; +} + +void +ComboBoxEntryToolItem::set_entry_width(gint entry_width) +{ + _entry_width = entry_width; + + // Clamp to limits + if(entry_width < -1) entry_width = -1; + if(entry_width > 100) entry_width = 100; + + // Widget may not have been created.... + if( _entry ) { + gtk_entry_set_width_chars( GTK_ENTRY(_entry), entry_width ); + } +} + +void +ComboBoxEntryToolItem::set_extra_width( gint extra_width ) +{ + _extra_width = extra_width; + + // Clamp to limits + if(extra_width < -1) extra_width = -1; + if(extra_width > 500) extra_width = 500; + + // Widget may not have been created.... + if( _combobox ) { + GtkRequisition req; + gtk_widget_get_preferred_size(GTK_WIDGET(_combobox), &req, nullptr); + gtk_widget_set_size_request( GTK_WIDGET( _combobox ), req.width + _extra_width, -1 ); + } +} + +void +ComboBoxEntryToolItem::focus_on_click( bool focus_on_click ) +{ + if (_combobox) { + gtk_widget_set_focus_on_click(GTK_WIDGET(_combobox), focus_on_click); + } +} + +void +ComboBoxEntryToolItem::popup_enable() +{ + _popup = true; + + // Widget may not have been created.... + if( _entry ) { + + // Check we don't already have a GtkEntryCompletion + if( _entry_completion ) return; + + _entry_completion = gtk_entry_completion_new(); + + gtk_entry_set_completion( _entry, _entry_completion ); + gtk_entry_completion_set_model( _entry_completion, _model ); + gtk_entry_completion_set_text_column( _entry_completion, 0 ); + gtk_entry_completion_set_popup_completion( _entry_completion, true ); + gtk_entry_completion_set_inline_completion( _entry_completion, false ); + gtk_entry_completion_set_inline_selection( _entry_completion, true ); + + g_signal_connect (G_OBJECT (_entry_completion), "match-selected", G_CALLBACK (match_selected_cb), this); + } +} + +void +ComboBoxEntryToolItem::popup_disable() +{ + _popup = false; + + if( _entry_completion ) { + gtk_widget_destroy(GTK_WIDGET(_entry_completion)); + _entry_completion = nullptr; + } +} + +void +ComboBoxEntryToolItem::set_tooltip(const gchar* tooltip) +{ + set_tooltip_text(tooltip); + gtk_widget_set_tooltip_text ( GTK_WIDGET(_combobox), tooltip); + + // Widget may not have been created.... + if( _entry ) { + gtk_widget_set_tooltip_text ( GTK_WIDGET(_entry), tooltip); + } +} + +void +ComboBoxEntryToolItem::set_info(const gchar* info) +{ + g_free( _info ); + _info = g_strdup( info ); + + // Widget may not have been created.... + if( _entry ) { + gtk_entry_set_icon_tooltip_text( GTK_ENTRY(_entry), + GTK_ENTRY_ICON_SECONDARY, + _info ); + } +} + +void +ComboBoxEntryToolItem::set_info_cb(gpointer info_cb) +{ + _info_cb = info_cb; +} + +void +ComboBoxEntryToolItem::set_warning(const gchar* warning) +{ + g_free( _warning ); + _warning = g_strdup( warning ); + + // Widget may not have been created.... + if( _entry ) { + gtk_entry_set_icon_tooltip_text( GTK_ENTRY(_entry), + GTK_ENTRY_ICON_SECONDARY, + _warning ); + } +} + +void +ComboBoxEntryToolItem::set_warning_cb(gpointer warning_cb) +{ + _warning_cb = warning_cb; +} + +// Internal --------------------------------------------------- + +// Return row of active text or -1 if not found. If exclude is true, +// use 3d column if available to exclude row from checking (useful to +// skip rows added for font-families included in doc and not on +// system) +gint +ComboBoxEntryToolItem::get_active_row_from_text(ComboBoxEntryToolItem *action, + const gchar *target_text, + gboolean exclude, + gboolean ignore_case ) +{ + // Check if text in list + gint row = 0; + gboolean found = false; + GtkTreeIter iter; + gboolean valid = gtk_tree_model_get_iter_first( action->_model, &iter ); + while ( valid ) { + + // See if we should exclude a row + gboolean check = true; // If true, font-family is on system. + if( exclude && gtk_tree_model_get_n_columns( action->_model ) > 2 ) { + gtk_tree_model_get( action->_model, &iter, 2, &check, -1 ); + } + + if( check ) { + // Get text from list entry + gchar* text = nullptr; + gtk_tree_model_get( action->_model, &iter, 0, &text, -1 ); // Column 0 + + if( !ignore_case ) { + // Case sensitive compare + if( strcmp( target_text, text ) == 0 ){ + found = true; + g_free(text); + break; + } + } else { + // Case insensitive compare + gchar* target_text_casefolded = g_utf8_casefold( target_text, -1 ); + gchar* text_casefolded = g_utf8_casefold( text, -1 ); + gboolean equal = (strcmp( target_text_casefolded, text_casefolded ) == 0 ); + g_free( text_casefolded ); + g_free( target_text_casefolded ); + if( equal ) { + found = true; + g_free(text); + break; + } + } + g_free(text); + } + + ++row; + valid = gtk_tree_model_iter_next( action->_model, &iter ); + } + + if( !found ) row = -1; + + return row; +} + +// Checks if all comma separated text fragments are in the list and +// returns a ustring with a list of missing fragments. +// This is useful for checking if all fonts in a font-family fallback +// list are available on the system. +// +// This routine could also create a Pango Markup string to show which +// fragments are invalid in the entry box itself. See: +// http://developer.gnome.org/pango/stable/PangoMarkupFormat.html +// However... it appears that while one can retrieve the PangoLayout +// for a GtkEntry box, it is only a copy and changing it has no effect. +// PangoLayout * pl = gtk_entry_get_layout( entry ); +// pango_layout_set_markup( pl, "NEW STRING", -1 ); // DOESN'T WORK +Glib::ustring +ComboBoxEntryToolItem::check_comma_separated_text() +{ + Glib::ustring missing; + + // Parse fallback_list using a comma as deliminator + gchar** tokens = g_strsplit( _text, ",", 0 ); + + gint i = 0; + while( tokens[i] != nullptr ) { + + // Remove any surrounding white space. + g_strstrip( tokens[i] ); + + if( get_active_row_from_text( this, tokens[i], true, true ) == -1 ) { + missing += tokens[i]; + missing += ", "; + } + ++i; + } + g_strfreev( tokens ); + + // Remove extra comma and space from end. + if( missing.size() >= 2 ) { + missing.resize( missing.size()-2 ); + } + return missing; +} + +// Callbacks --------------------------------------------------- + +void +ComboBoxEntryToolItem::combo_box_changed_cb( GtkComboBox* widget, gpointer data ) +{ + // Two things can happen to get here: + // An item is selected in the drop-down menu. + // Text is typed. + // We only react here if an item is selected. + + // Get action + auto action = reinterpret_cast<ComboBoxEntryToolItem *>( data ); + + // Check if item selected: + gint newActive = gtk_combo_box_get_active(widget); + if( newActive >= 0 && newActive != action->_active ) { + + action->_active = newActive; + + GtkTreeIter iter; + if( gtk_combo_box_get_active_iter( GTK_COMBO_BOX( action->_combobox ), &iter ) ) { + + gchar* text = nullptr; + gtk_tree_model_get( action->_model, &iter, 0, &text, -1 ); + gtk_entry_set_text( action->_entry, text ); + + g_free( action->_text ); + action->_text = text; + } + + // Now let the world know + action->_signal_changed.emit(); + } +} + +gboolean ComboBoxEntryToolItem::combo_box_popup_cb(ComboBoxEntryToolItem *widget, gpointer data) +{ + auto w = reinterpret_cast<ComboBoxEntryToolItem *>(data); + GtkComboBox *comboBoxEntry = GTK_COMBO_BOX(w->_combobox); + if (!w->_isload && !w->_markup && w->_cell_data_func) { + // first click is always displaying something wrong. + // Second loading of the screen should have preallocated space, and only has to render the text now + gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(comboBoxEntry), w->_cell, + GtkCellLayoutDataFunc(w->_cell_data_func), widget, nullptr); + w->_markup = true; + } + w->_isload = false; + return true; +} + +void +ComboBoxEntryToolItem::entry_activate_cb( GtkEntry *widget, + gpointer data ) +{ + // Get text from entry box.. check if it matches a menu entry. + + // Get action + auto action = reinterpret_cast<ComboBoxEntryToolItem*>( data ); + + // Get text + g_free( action->_text ); + action->_text = g_strdup( gtk_entry_get_text( widget ) ); + + // Get row + action->_active = + get_active_row_from_text( action, action->_text ); + + // Set active row + gtk_combo_box_set_active( GTK_COMBO_BOX( action->_combobox), action->_active ); + + // Now let the world know + action->_signal_changed.emit(); +} + +gboolean +ComboBoxEntryToolItem::match_selected_cb( GtkEntryCompletion* /*widget*/, GtkTreeModel* model, GtkTreeIter* iter, gpointer data ) +{ + // Get action + auto action = reinterpret_cast<ComboBoxEntryToolItem*>(data); + GtkEntry *entry = action->_entry; + + if( entry) { + gchar *family = nullptr; + gtk_tree_model_get(model, iter, 0, &family, -1); + + // Set text in GtkEntry + gtk_entry_set_text (GTK_ENTRY (entry), family ); + + // Set text in ToolItem + g_free( action->_text ); + action->_text = family; + + // Get row + action->_active = + get_active_row_from_text( action, action->_text ); + + // Set active row + gtk_combo_box_set_active( GTK_COMBO_BOX( action->_combobox), action->_active ); + + // Now let the world know + action->_signal_changed.emit(); + + return true; + } + return false; +} + +void +ComboBoxEntryToolItem::defocus() +{ + if ( _focusWidget ) { + gtk_widget_grab_focus( _focusWidget ); + } +} + +gboolean +ComboBoxEntryToolItem::keypress_cb( GtkWidget *entry, GdkEventKey *event, gpointer data ) +{ + gboolean wasConsumed = FALSE; /* default to report event not consumed */ + guint key = 0; + auto action = reinterpret_cast<ComboBoxEntryToolItem*>(data); + gdk_keymap_translate_keyboard_state( Gdk::Display::get_default()->get_keymap(), + event->hardware_keycode, (GdkModifierType)event->state, + 0, &key, nullptr, nullptr, nullptr ); + + switch ( key ) { + + case GDK_KEY_Escape: + { + //gtk_spin_button_set_value( GTK_SPIN_BUTTON(widget), action->private_data->lastVal ); + action->defocus(); + wasConsumed = TRUE; + } + break; + + case GDK_KEY_Tab: + { + // Fire activation similar to how Return does, but also return focus to text object + // itself + entry_activate_cb( GTK_ENTRY (entry), data ); + action->defocus(); + wasConsumed = TRUE; + } + break; + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + { + action->defocus(); + //wasConsumed = TRUE; + } + break; + + + } + + return wasConsumed; +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode: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/widget/combo-box-entry-tool-item.h b/src/ui/widget/combo-box-entry-tool-item.h new file mode 100644 index 0000000..978f2ef --- /dev/null +++ b/src/ui/widget/combo-box-entry-tool-item.h @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A class derived from Gtk::ToolItem that wraps a GtkComboBoxEntry. + * Features: + * Setting GtkEntryBox width in characters. + * Passing a function for formatting cells. + * Displaying a warning if entry text isn't in list. + * Check comma separated values in text against list. (Useful for font-family fallbacks.) + * Setting names for GtkComboBoxEntry and GtkEntry (actionName_combobox, actionName_entry) + * to allow setting resources. + * + * Author(s): + * Tavmjong Bah + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INK_COMBOBOXENTRY_ACTION +#define SEEN_INK_COMBOBOXENTRY_ACTION + +#include <gtkmm/toolitem.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Creates a Gtk::ToolItem subclass that wraps a Gtk::ComboBox object. + */ +class ComboBoxEntryToolItem : public Gtk::ToolItem { +private: + Glib::ustring _tooltip; + Glib::ustring _label; + GtkTreeModel *_model; ///< Tree Model + GtkComboBox *_combobox; + GtkEntry *_entry; + gint _entry_width;// Width of GtkEntry in characters. + gint _extra_width;// Extra Width of GtkComboBox.. to widen drop-down list in list mode. + gpointer _cell_data_func; // drop-down menu format + gpointer _separator_func; + gboolean _popup; // Do we pop-up an entry-completion dialog? + GtkEntryCompletion *_entry_completion; + GtkWidget *_focusWidget; ///< The widget to return focus to + GtkCellRenderer *_cell; + + gint _active; // Index of active menu item (-1 if not in list). + gchar *_text; // Text of active menu item or entry box. + gchar *_info; // Text for tooltip info about entry. + gpointer _info_cb; // Callback for clicking info icon. + gint _info_cb_id; + gboolean _info_cb_blocked; + gchar *_warning; // Text for tooltip warning that entry isn't in list. + gpointer _warning_cb; // Callback for clicking warning icon. + gint _warning_cb_id; + gboolean _warning_cb_blocked; + gboolean _isload; + gboolean _markup; + + // Signals + sigc::signal<void> _signal_changed; + + static gint get_active_row_from_text(ComboBoxEntryToolItem *action, + const gchar *target_text, + gboolean exclude = false, + gboolean ignore_case = false); + void defocus(); + + static void combo_box_changed_cb( GtkComboBox* widget, gpointer data ); + static gboolean combo_box_popup_cb( ComboBoxEntryToolItem* widget, gpointer data ); + static void entry_activate_cb( GtkEntry *widget, + gpointer data ); + static gboolean match_selected_cb( GtkEntryCompletion *widget, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer data); + static gboolean keypress_cb( GtkWidget *widget, + GdkEventKey *event, + gpointer data ); + + Glib::ustring check_comma_separated_text(); + +public: + ComboBoxEntryToolItem(const Glib::ustring name, + const Glib::ustring label, + const Glib::ustring tooltip, + GtkTreeModel *model, + gint entry_width = -1, + gint extra_width = -1, + gpointer cell_data_func = nullptr, + gpointer separator_func = nullptr, + GtkWidget* focusWidget = nullptr); + + Glib::ustring get_active_text(); + gboolean set_active_text(const gchar* text, int row=-1); + + void set_entry_width(gint entry_width); + void set_extra_width(gint extra_width); + + void popup_enable(); + void popup_disable(); + void focus_on_click( bool focus_on_click ); + + void set_info( const gchar* info ); + void set_info_cb( gpointer info_cb ); + void set_warning( const gchar* warning_cb ); + void set_warning_cb(gpointer warning ); + void set_tooltip( const gchar* tooltip ); + + // Accessor methods + decltype(_model) get_model() const {return _model;} + decltype(_combobox) get_combobox() const {return _combobox;} + decltype(_entry) get_entry() const {return _entry;} + decltype(_entry_width) get_entry_width() const {return _entry_width;} + decltype(_extra_width) get_extra_width() const {return _extra_width;} + decltype(_cell_data_func) get_cell_data_func() const {return _cell_data_func;} + decltype(_separator_func) get_separator_func() const {return _separator_func;} + decltype(_popup) get_popup() const {return _popup;} + decltype(_focusWidget) get_focus_widget() const {return _focusWidget;} + + decltype(_active) get_active() const {return _active;} + + decltype(_signal_changed) signal_changed() {return _signal_changed;} + + // Mutator methods + void set_model (decltype(_model) model) {_model = model;} + void set_combobox (decltype(_combobox) combobox) {_combobox = combobox;} + void set_entry (decltype(_entry) entry) {_entry = entry;} + void set_cell_data_func(decltype(_cell_data_func) cell_data_func) {_cell_data_func = cell_data_func;} + void set_separator_func(decltype(_separator_func) separator_func) {_separator_func = separator_func;} + void set_popup (decltype(_popup) popup) {_popup = popup;} + void set_focus_widget (decltype(_focusWidget) focus_widget) {_focusWidget = focus_widget;} + + // This doesn't seem right... surely we should set the active row in the Combobox too? + void set_active (decltype(_active) active) {_active = active;} +}; + +} +} +} +#endif /* SEEN_INK_COMBOBOXENTRY_ACTION */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/combo-enums.h b/src/ui/widget/combo-enums.h new file mode 100644 index 0000000..4647a00 --- /dev/null +++ b/src/ui/widget/combo-enums.h @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Nicholas Bishop <nicholasbishop@gmail.com> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_COMBO_ENUMS_H +#define INKSCAPE_UI_WIDGET_COMBO_ENUMS_H + +#include "ui/widget/labelled.h" +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> +#include "attr-widget.h" +#include "util/enums.h" +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Simplified management of enumerations in the UI as combobox. + */ +template<typename E> class ComboBoxEnum : public Gtk::ComboBox, public AttrWidget +{ +private: + int on_sort_compare( const Gtk::TreeModel::iterator & a, const Gtk::TreeModel::iterator & b) + { + Glib::ustring an=(*a)[_columns.label]; + Glib::ustring bn=(*b)[_columns.label]; + return an.compare(bn); + } + + bool _sort; + +public: + ComboBoxEnum(E default_value, const Util::EnumDataConverter<E>& c, const SPAttr a = SPAttr::INVALID, bool sort = true) + : AttrWidget(a, (unsigned int)default_value), setProgrammatically(false), _converter(c) + { + _sort = sort; + + signal_changed().connect(signal_attr_changed().make_slot()); + gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK); + signal_scroll_event().connect(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_scroll_event)); + _model = Gtk::ListStore::create(_columns); + set_model(_model); + + pack_start(_columns.label); + + // Initialize list + for(int i = 0; i < static_cast<int>(_converter._length); ++i) { + Gtk::TreeModel::Row row = *_model->append(); + const Util::EnumData<E>* data = &_converter.data(i); + row[_columns.data] = data; + row[_columns.label] = _( _converter.get_label(data->id).c_str() ); + } + set_active_by_id(default_value); + + // Sort the list + if (sort) { + _model->set_default_sort_func(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_sort_compare)); + _model->set_sort_column(_columns.label, Gtk::SORT_ASCENDING); + } + } + + ComboBoxEnum(const Util::EnumDataConverter<E>& c, const SPAttr a = SPAttr::INVALID, bool sort = true) + : AttrWidget(a, (unsigned int) 0), setProgrammatically(false), _converter(c) + { + _sort = sort; + + signal_changed().connect(signal_attr_changed().make_slot()); + gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK); + signal_scroll_event().connect(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_scroll_event)); + + _model = Gtk::ListStore::create(_columns); + set_model(_model); + + pack_start(_columns.label); + + // Initialize list + for(unsigned int i = 0; i < _converter._length; ++i) { + Gtk::TreeModel::Row row = *_model->append(); + const Util::EnumData<E>* data = &_converter.data(i); + row[_columns.data] = data; + row[_columns.label] = _( _converter.get_label(data->id).c_str() ); + } + set_active(0); + + // Sort the list + if (_sort) { + _model->set_default_sort_func(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_sort_compare)); + _model->set_sort_column(_columns.label, Gtk::SORT_ASCENDING); + } + } + + Glib::ustring get_as_attribute() const override + { + return get_active_data()->key; + } + + void set_from_attribute(SPObject* o) override + { + setProgrammatically = true; + const gchar* val = attribute_value(o); + if(val) + set_active_by_id(_converter.get_id_from_key(val)); + else + set_active(get_default()->as_uint()); + } + + const Util::EnumData<E>* get_active_data() const + { + Gtk::TreeModel::iterator i = this->get_active(); + if(i) + return (*i)[_columns.data]; + return nullptr; + } + + void add_row(const Glib::ustring& s) + { + Gtk::TreeModel::Row row = *_model->append(); + row[_columns.data] = 0; + row[_columns.label] = s; + } + + void remove_row(E id) { + Gtk::TreeModel::iterator i; + + for(i = _model->children().begin(); i != _model->children().end(); ++i) { + const Util::EnumData<E>* data = (*i)[_columns.data]; + + if(data->id == id) + break; + } + + if(i != _model->children().end()) + _model->erase(i); + } + + void set_active_by_id(E id) { + setProgrammatically = true; + for(Gtk::TreeModel::iterator i = _model->children().begin(); + i != _model->children().end(); ++i) + { + const Util::EnumData<E>* data = (*i)[_columns.data]; + if(data->id == id) { + set_active(i); + break; + } + } + }; + + bool on_scroll_event(GdkEventScroll *event) override { return false; } + + void set_active_by_key(const Glib::ustring& key) { + setProgrammatically = true; + set_active_by_id( _converter.get_id_from_key(key) ); + }; + + bool setProgrammatically; + +private: + class Columns : public Gtk::TreeModel::ColumnRecord + { + public: + Columns() + { + add(data); + add(label); + } + + Gtk::TreeModelColumn<const Util::EnumData<E>*> data; + Gtk::TreeModelColumn<Glib::ustring> label; + }; + + Columns _columns; + Glib::RefPtr<Gtk::ListStore> _model; + const Util::EnumDataConverter<E>& _converter; +}; + + +/** + * Simplified management of enumerations in the UI as combobox. + */ +template<typename E> class LabelledComboBoxEnum : public Labelled +{ +public: + LabelledComboBoxEnum( Glib::ustring const &label, + Glib::ustring const &tooltip, + const Util::EnumDataConverter<E>& c, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true, + bool sorted = true) + : Labelled(label, tooltip, new ComboBoxEnum<E>(c, SPAttr::INVALID, sorted), suffix, icon, mnemonic) + { + } + + ComboBoxEnum<E>* getCombobox() { + return static_cast< ComboBoxEnum<E>* > (_widget); + } +}; + +} +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/combo-tool-item.cpp b/src/ui/widget/combo-tool-item.cpp new file mode 100644 index 0000000..ffc7e75 --- /dev/null +++ b/src/ui/widget/combo-tool-item.cpp @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +/** \file + A combobox that can be displayed in a toolbar. +*/ + +#include "combo-tool-item.h" +#include "preferences.h" +#include <iostream> +#include <utility> +#include <gtkmm/toolitem.h> +#include <gtkmm/menuitem.h> +#include <gtkmm/radiomenuitem.h> +#include <gtkmm/combobox.h> +#include <gtkmm/menu.h> +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/image.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +ComboToolItem* +ComboToolItem::create(const Glib::ustring &group_label, + const Glib::ustring &tooltip, + const Glib::ustring &stock_id, + Glib::RefPtr<Gtk::ListStore> store, + bool has_entry) +{ + return new ComboToolItem(group_label, tooltip, stock_id, store, has_entry); +} + +ComboToolItem::ComboToolItem(Glib::ustring group_label, + Glib::ustring tooltip, + Glib::ustring stock_id, + Glib::RefPtr<Gtk::ListStore> store, + bool has_entry) : + _active(-1), + _group_label(std::move( group_label )), + _tooltip(std::move( tooltip )), + _stock_id(std::move( stock_id )), + _store (std::move(store)), + _use_label (true), + _use_icon (false), + _use_pixbuf (true), + _icon_size ( Gtk::ICON_SIZE_LARGE_TOOLBAR ), + _combobox (nullptr), + _group_label_widget(nullptr), + _container(Gtk::manage(new Gtk::Box())), + _menuitem (nullptr) +{ + add(*_container); + _container->set_spacing(3); + + // ": " is added to the group label later + if (!_group_label.empty()) { + // we don't expect trailing spaces + // g_assert(_group_label.raw()[_group_label.raw().size() - 1] != ' '); + + // strip space (note: raw() indexing is much cheaper on Glib::ustring) + if (_group_label.raw()[_group_label.raw().size() - 1] == ' ') { + _group_label.resize(_group_label.size() - 1); + } + } + if (!_group_label.empty()) { + // we don't expect a trailing colon + // g_assert(_group_label.raw()[_group_label.raw().size() - 1] != ':'); + + // strip colon (note: raw() indexing is much cheaper on Glib::ustring) + if (_group_label.raw()[_group_label.raw().size() - 1] == ':') { + _group_label.resize(_group_label.size() - 1); + } + } + + + // Create combobox + _combobox = Gtk::manage (new Gtk::ComboBox(has_entry)); + _combobox->set_model(_store); + + populate_combobox(); + + _combobox->signal_changed().connect( + sigc::mem_fun(*this, &ComboToolItem::on_changed_combobox)); + _container->pack_start(*_combobox); + + show_all(); +} + +void +ComboToolItem::focus_on_click( bool focus_on_click ) +{ + _combobox->set_focus_on_click(focus_on_click); +} + + +void +ComboToolItem::use_label(bool use_label) +{ + _use_label = use_label; + populate_combobox(); +} + +void +ComboToolItem::use_icon(bool use_icon) +{ + _use_icon = use_icon; + populate_combobox(); +} + +void +ComboToolItem::use_pixbuf(bool use_pixbuf) +{ + _use_pixbuf = use_pixbuf; + populate_combobox(); +} + +void +ComboToolItem::use_group_label(bool use_group_label) +{ + if (use_group_label == (_group_label_widget != nullptr)) { + return; + } + if (use_group_label) { + _container->remove(*_combobox); + _group_label_widget = Gtk::manage(new Gtk::Label(_group_label + ": ")); + _container->pack_start(*_group_label_widget); + _container->pack_start(*_combobox); + } else { + _container->remove(*_group_label_widget); + delete _group_label_widget; + _group_label_widget = nullptr; + } +} + +void +ComboToolItem::populate_combobox() +{ + _combobox->clear(); + + ComboToolItemColumns columns; + if (_use_icon) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/theme/symbolicIcons", false)) { + auto children = _store->children(); + for (auto row : children) { + Glib::ustring icon = row[columns.col_icon]; + gint pos = icon.find("-symbolic"); + if (pos == std::string::npos) { + icon += "-symbolic"; + } + row[columns.col_icon] = icon; + } + } + Gtk::CellRendererPixbuf *renderer = new Gtk::CellRendererPixbuf; + renderer->set_property ("stock_size", Gtk::ICON_SIZE_LARGE_TOOLBAR); + _combobox->pack_start (*Gtk::manage(renderer), false); + _combobox->add_attribute (*renderer, "icon_name", columns.col_icon ); + } else if (_use_pixbuf) { + Gtk::CellRendererPixbuf *renderer = new Gtk::CellRendererPixbuf; + //renderer->set_property ("stock_size", Gtk::ICON_SIZE_LARGE_TOOLBAR); + _combobox->pack_start (*Gtk::manage(renderer), false); + _combobox->add_attribute (*renderer, "pixbuf", columns.col_pixbuf ); + } + + if (_use_label) { + _combobox->pack_start(columns.col_label); + } + + std::vector<Gtk::CellRenderer*> cells = _combobox->get_cells(); + for (auto & cell : cells) { + _combobox->add_attribute (*cell, "sensitive", columns.col_sensitive); + } + + set_tooltip_text(_tooltip); + _combobox->set_tooltip_text(_tooltip); + _combobox->set_active (_active); +} + +void +ComboToolItem::set_active (gint active) { + if (_active != active) { + + _active = active; + + if (_combobox) { + _combobox->set_active (active); + } + + if (active < _radiomenuitems.size()) { + _radiomenuitems[ active ]->set_active(); + } + } +} + +Glib::ustring +ComboToolItem::get_active_text () { + Gtk::TreeModel::Row row = _store->children()[_active]; + ComboToolItemColumns columns; + Glib::ustring label = row[columns.col_label]; + return label; +} + +bool +ComboToolItem::on_create_menu_proxy() +{ + if (_menuitem == nullptr) { + + _menuitem = Gtk::manage (new Gtk::MenuItem(_group_label)); + Gtk::Menu *menu = Gtk::manage (new Gtk::Menu); + + Gtk::RadioButton::Group group; + int index = 0; + auto children = _store->children(); + for (auto row : children) { + ComboToolItemColumns columns; + Glib::ustring label = row[columns.col_label ]; + Glib::ustring icon = row[columns.col_icon ]; + Glib::ustring tooltip = row[columns.col_tooltip ]; + bool sensitive = row[columns.col_sensitive ]; + + Gtk::RadioMenuItem* button = Gtk::manage(new Gtk::RadioMenuItem(group)); + button->set_label (label); + button->set_tooltip_text( tooltip ); + button->set_sensitive( sensitive ); + + button->signal_toggled().connect( sigc::bind<0>( + sigc::mem_fun(*this, &ComboToolItem::on_toggled_radiomenu), index++) + ); + + menu->add (*button); + + _radiomenuitems.push_back( button ); + } + + if ( _active < _radiomenuitems.size()) { + _radiomenuitems[ _active ]->set_active(); + } + + _menuitem->set_submenu (*menu); + _menuitem->show_all(); + } + + set_proxy_menu_item(_group_label, *_menuitem); + return true; +} + +void +ComboToolItem::on_changed_combobox() { + + int row = _combobox->get_active_row_number(); + set_active( row ); + _changed.emit (_active); + _changed_after.emit (_active); +} + +void +ComboToolItem::on_toggled_radiomenu(int n) { + + // toggled emitted twice, first for button toggled off, second for button toggled on. + // We want to react only to the button turned on. + if ( n < _radiomenuitems.size() &&_radiomenuitems[ n ]->get_active()) { + set_active ( n ); + _changed.emit (_active); + _changed_after.emit (_active); + } +} + +} +} +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode: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/widget/combo-tool-item.h b/src/ui/widget/combo-tool-item.h new file mode 100644 index 0000000..1fc8b00 --- /dev/null +++ b/src/ui/widget/combo-tool-item.h @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_COMBO_TOOL_ITEM +#define SEEN_COMBO_TOOL_ITEM + +/* + * Authors: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/** + A combobox that can be displayed in a toolbar +*/ + +#include <gtkmm/toolitem.h> +#include <gtkmm/liststore.h> +#include <sigc++/sigc++.h> +#include <vector> + +namespace Gtk { +class Box; +class ComboBox; +class Label; +class MenuItem; +class RadioMenuItem; +} + +namespace Inkscape { +namespace UI { +namespace Widget { +class ComboToolItemColumns : public Gtk::TreeModel::ColumnRecord { +public: + ComboToolItemColumns() { + add (col_label); + add (col_value); + add (col_icon); + add (col_pixbuf); + add (col_data); // Used to store a pointer + add (col_tooltip); + add (col_sensitive); + } + Gtk::TreeModelColumn<Glib::ustring> col_label; + Gtk::TreeModelColumn<Glib::ustring> col_value; + Gtk::TreeModelColumn<Glib::ustring> col_icon; + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > col_pixbuf; + Gtk::TreeModelColumn<void *> col_data; + Gtk::TreeModelColumn<Glib::ustring> col_tooltip; + Gtk::TreeModelColumn<bool> col_sensitive; +}; + + +class ComboToolItem : public Gtk::ToolItem { + +public: + static ComboToolItem* create(const Glib::ustring &label, + const Glib::ustring &tooltip, + const Glib::ustring &stock_id, + Glib::RefPtr<Gtk::ListStore> store, + bool has_entry = false); + + /* Style of combobox */ + void use_label( bool use_label ); + void use_icon( bool use_icon ); + void focus_on_click( bool focus_on_click ); + void use_pixbuf( bool use_pixbuf ); + void use_group_label( bool use_group_label ); // Applies to tool item only + + gint get_active() { return _active; } + Glib::ustring get_active_text(); + void set_active( gint active ); + void set_icon_size( Gtk::BuiltinIconSize size ) { _icon_size = size; } + + Glib::RefPtr<Gtk::ListStore> get_store() { return _store; } + + sigc::signal<void, int> signal_changed() { return _changed; } + sigc::signal<void, int> signal_changed_after() { return _changed_after; } + +protected: + bool on_create_menu_proxy() override; + void populate_combobox(); + + /* Signals */ + sigc::signal<void, int> _changed; + sigc::signal<void, int> _changed_after; // Needed for unit tracker which eats _changed. + +private: + + Glib::ustring _group_label; + Glib::ustring _tooltip; + Glib::ustring _stock_id; + Glib::RefPtr<Gtk::ListStore> _store; + + gint _active; /* Active menu item/button */ + + /* Style */ + bool _use_label; + bool _use_icon; // Applies to menu item only + bool _use_pixbuf; + Gtk::BuiltinIconSize _icon_size; + + /* Combobox in tool */ + Gtk::ComboBox* _combobox; + Gtk::Label* _group_label_widget; + Gtk::Box* _container; + + Gtk::MenuItem* _menuitem; + std::vector<Gtk::RadioMenuItem*> _radiomenuitems; + + /* Internal Callbacks */ + void on_changed_combobox(); + void on_toggled_radiomenu(int n); + + ComboToolItem(Glib::ustring group_label, + Glib::ustring tooltip, + Glib::ustring stock_id, + Glib::RefPtr<Gtk::ListStore> store, + bool has_entry = false); +}; +} +} +} +#endif /* SEEN_COMBO_TOOL_ITEM */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/dash-selector.cpp b/src/ui/widget/dash-selector.cpp new file mode 100644 index 0000000..c3f40ab --- /dev/null +++ b/src/ui/widget/dash-selector.cpp @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Combobox for selecting dash patterns - implementation. + */ +/* Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Maximilian Albert <maximilian.albert@gmail.com> + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "dash-selector.h" + +#include <cstring> +#include <glibmm/i18n.h> +#include <2geom/coord.h> +#include <numeric> + +#include "preferences.h" +#include "display/cairo-utils.h" +#include "style.h" + +#include "ui/dialog-events.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +gchar const *const DashSelector::_prefs_path = "/palette/dashes"; + +static std::vector<std::vector<double>> s_dashes; + +DashSelector::DashSelector() + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4), + _preview_width(100), + _preview_height(16), + _preview_lineheight(2) +{ + // TODO: find something more sensible here!! + init_dashes(); + + _dash_store = Gtk::ListStore::create(dash_columns); + _dash_combo.set_model(_dash_store); + _dash_combo.pack_start(_image_renderer); + _dash_combo.set_cell_data_func(_image_renderer, sigc::mem_fun(*this, &DashSelector::prepareImageRenderer)); + _dash_combo.set_tooltip_text(_("Dash pattern")); + _dash_combo.show(); + _dash_combo.signal_changed().connect( sigc::mem_fun(*this, &DashSelector::on_selection) ); + // show dashes in two columns to eliminate or minimize scrolling + _dash_combo.set_wrap_width(2); + + this->pack_start(_dash_combo, true, true, 0); + + _offset = Gtk::Adjustment::create(0.0, 0.0, 1000.0, 0.1, 1.0, 0.0); + _offset->signal_value_changed().connect(sigc::mem_fun(*this, &DashSelector::offset_value_changed)); + _sb = new Inkscape::UI::Widget::SpinButton(_offset, 0.1, 2); + _sb->set_tooltip_text(_("Pattern offset")); + sp_dialog_defocus_on_enter_cpp(_sb); + _sb->set_width_chars(4); + _sb->show(); + + this->pack_start(*_sb, false, false, 0); + + for (std::size_t i = 0; i < s_dashes.size(); ++i) { + Gtk::TreeModel::Row row = *(_dash_store->append()); + row[dash_columns.dash] = i; + } + + _pattern = &s_dashes.front(); +} + +DashSelector::~DashSelector() { + // FIXME: for some reason this doesn't get called; does the call to manage() in + // sp_stroke_style_line_widget_new() not processed correctly? +} + +void DashSelector::prepareImageRenderer( Gtk::TreeModel::const_iterator const &row ) { + // dashes are rendered on the fly to adapt to current theme colors + std::size_t index = (*row)[dash_columns.dash]; + Cairo::RefPtr<Cairo::Surface> surface; + if (index == 1) { + // add the custom one as a second option; it'll show up at the top of second column + surface = sp_text_to_pixbuf((char *)"Custom"); + } + else if (index < s_dashes.size()) { + // add the dash to the combobox + surface = sp_dash_to_pixbuf(s_dashes[index]); + } + else { + surface = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1))); + g_warning("No surface in prepareImageRenderer."); + } + _image_renderer.property_surface() = surface; +} + +static std::vector<double> map_values(const std::vector<SPILength>& values) { + std::vector<double> out; + out.reserve(values.size()); + for (auto&& v : values) { + out.push_back(v.value); + } + return out; +} + +void DashSelector::init_dashes() { + if (s_dashes.empty()) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + std::vector<Glib::ustring> dash_prefs = prefs->getAllDirs(_prefs_path); + + if (!dash_prefs.empty()) { + SPStyle style; + s_dashes.reserve(dash_prefs.size() + 1); + + for (auto & dash_pref : dash_prefs) { + style.readFromPrefs( dash_pref ); + + if (!style.stroke_dasharray.values.empty()) { + s_dashes.emplace_back(map_values(style.stroke_dasharray.values)); + } else { + s_dashes.emplace_back(std::vector<double>()); + } + } + } else { + g_warning("Missing stock dash definitions. DashSelector::init_dashes."); + // This code may never execute - a new preferences.xml is created for a new user. Maybe if the user deletes dashes from preferences.xml? + s_dashes.emplace_back(std::vector<double>()); + } + + std::vector<double> custom {1, 2, 1, 4}; // 'custom' dashes second on the list, so they are at the top of the second column in a combo box + s_dashes.insert(s_dashes.begin() + 1, custom); + } +} + +void DashSelector::set_dash(const std::vector<double>& dash, double offset) { + int pos = -1; // Allows custom patterns to remain unscathed by this. + + double delta = std::accumulate(dash.begin(), dash.end(), 0.0) / (10000.0 * (dash.empty() ? 1 : dash.size())); + + int index = 0; + for (auto&& pattern : s_dashes) { + if (dash.size() == pattern.size() && + std::equal(dash.begin(), dash.end(), pattern.begin(), + [=](double a, double b) { return Geom::are_near(a, b, delta); })) { + pos = index; + break; + } + ++index; + } + + if (pos >= 0) { + _pattern = &s_dashes.at(pos); + _dash_combo.set_active(pos); + _offset->set_value(offset); + } + else { // Hit a custom pattern in the SVG, write it into the combobox. + pos = 1; // the one slot for custom patterns + _pattern = &s_dashes[pos]; + _pattern->assign(dash.begin(), dash.end()); + _dash_combo.set_active(pos); + _offset->set_value(offset); + } +} + +const std::vector<double>& DashSelector::get_dash(double* offset) const { + if (offset) *offset = _offset->get_value(); + return *_pattern; +} + +double DashSelector::get_offset() { + return _offset ? _offset->get_value() : 0.0; +} + +/** + * Fill a pixbuf with the dash pattern using standard cairo drawing + */ +Cairo::RefPtr<Cairo::Surface> DashSelector::sp_dash_to_pixbuf(const std::vector<double>& pattern) { + auto device_scale = get_scale_factor(); + + auto height = _preview_height * device_scale; + auto width = _preview_width * device_scale; + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *ct = cairo_create(s); + + auto context = get_style_context(); + Gdk::RGBA fg = context->get_color(get_state_flags()); + + cairo_set_line_width (ct, _preview_lineheight * device_scale); + cairo_scale (ct, _preview_lineheight * device_scale, 1); + cairo_move_to (ct, 0, height/2); + cairo_line_to (ct, width, height/2); + cairo_set_dash(ct, pattern.data(), pattern.size(), 0); + cairo_set_source_rgb(ct, fg.get_red(), fg.get_green(), fg.get_blue()); + cairo_stroke (ct); + + cairo_destroy(ct); + cairo_surface_flush(s); + + cairo_surface_set_device_scale(s, device_scale, device_scale); + return Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s)); +} + +/** + * Fill a pixbuf with a text label using standard cairo drawing + */ +Cairo::RefPtr<Cairo::Surface> DashSelector::sp_text_to_pixbuf(const char* text) { + auto device_scale = get_scale_factor(); + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, _preview_width * device_scale, _preview_height * device_scale); + cairo_t *ct = cairo_create(s); + + cairo_select_font_face (ct, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); + // todo: how to find default font face and size? + cairo_set_font_size (ct, 12 * device_scale); + auto context = get_style_context(); + Gdk::RGBA fg = context->get_color(get_state_flags()); + cairo_set_source_rgb(ct, fg.get_red(), fg.get_green(), fg.get_blue()); + cairo_move_to (ct, 16.0 * device_scale, 13.0 * device_scale); + cairo_show_text (ct, text); + + cairo_destroy(ct); + cairo_surface_flush(s); + + cairo_surface_set_device_scale(s, device_scale, device_scale); + return Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s)); +} + +void DashSelector::on_selection() +{ + _pattern = &s_dashes.at(_dash_combo.get_active()->get_value(dash_columns.dash)); + changed_signal.emit(); +} + +void DashSelector::offset_value_changed() +{ + Glib::ustring offset = _("Pattern offset"); + offset += " ("; + offset += Glib::ustring::format(_sb->get_value()); + offset += ")"; + _sb->set_tooltip_text(offset.c_str()); + changed_signal.emit(); +} + +} // namespace Widget +} // 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/widget/dash-selector.h b/src/ui/widget/dash-selector.h new file mode 100644 index 0000000..f98bb50 --- /dev/null +++ b/src/ui/widget/dash-selector.h @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_DASH_SELECTOR_NEW_H +#define SEEN_SP_DASH_SELECTOR_NEW_H + +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Maximilian Albert <maximilian.albert> (gtkmm-ification) + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/spinbutton.h" +#include <gtkmm/box.h> +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> + +#include <sigc++/signal.h> + +#include "scrollprotected.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Class that wraps a combobox and spinbutton for selecting dash patterns. + */ +class DashSelector : public Gtk::Box { +public: + DashSelector(); + ~DashSelector() override; + + /** + * Get and set methods for dashes + */ + void set_dash(const std::vector<double>& dash, double offset); + + const std::vector<double>& get_dash(double* offset) const; + + sigc::signal<void> changed_signal; + + double get_offset(); + +private: + + /** + * Initialize dashes list from preferences + */ + static void init_dashes(); + + /** + * Fill a pixbuf with the dash pattern using standard cairo drawing + */ + Cairo::RefPtr<Cairo::Surface> sp_dash_to_pixbuf(const std::vector<double>& pattern); + + /** + * Fill a pixbuf with text standard cairo drawing + */ + Cairo::RefPtr<Cairo::Surface> sp_text_to_pixbuf(const char* text); + + /** + * Callback for combobox image renderer + */ + void prepareImageRenderer( Gtk::TreeModel::const_iterator const &row ); + + /** + * Callback for offset adjustment changing + */ + void offset_value_changed(); + + /** + * Callback for combobox selection changing + */ + void on_selection(); + + /** + * Combobox columns + */ + class DashColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn<std::size_t> dash; + DashColumns() { + add(dash); + } + }; + DashColumns dash_columns; + Glib::RefPtr<Gtk::ListStore> _dash_store; + ScrollProtected<Gtk::ComboBox> _dash_combo; + Gtk::CellRendererPixbuf _image_renderer; + Glib::RefPtr<Gtk::Adjustment> _offset; + Inkscape::UI::Widget::SpinButton *_sb; + static gchar const *const _prefs_path; + int _preview_width; + int _preview_height; + int _preview_lineheight; + std::vector<double>* _pattern = nullptr; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_SP_DASH_SELECTOR_NEW_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/entity-entry.cpp b/src/ui/widget/entity-entry.cpp new file mode 100644 index 0000000..2e5c40b --- /dev/null +++ b/src/ui/widget/entity-entry.cpp @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * 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) + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2000 - 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "entity-entry.h" + +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/entry.h> + +#include "document-undo.h" +#include "inkscape.h" +#include "preferences.h" +#include "rdf.h" + +#include "object/sp-root.h" + +#include "ui/widget/registry.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +//=================================================== + +//--------------------------------------------------- + +EntityEntry* +EntityEntry::create (rdf_work_entity_t* ent, Registry& wr) +{ + g_assert (ent); + EntityEntry* obj = nullptr; + switch (ent->format) + { + case RDF_FORMAT_LINE: + obj = new EntityLineEntry (ent, wr); + break; + case RDF_FORMAT_MULTILINE: + obj = new EntityMultiLineEntry (ent, wr); + break; + default: + g_warning ("An unknown RDF format was requested."); + } + + g_assert (obj); + obj->_label.show(); + return obj; +} + +EntityEntry::EntityEntry (rdf_work_entity_t* ent, Registry& wr) + : _label(Glib::ustring(_(ent->title)), Gtk::ALIGN_END), + _packable(nullptr), + _entity(ent), _wr(&wr) +{ +} + +EntityEntry::~EntityEntry() +{ + _changed_connection.disconnect(); +} + +void EntityEntry::save_to_preferences(SPDocument *doc) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + const gchar *text = rdf_get_work_entity (doc, _entity); + prefs->setString(PREFS_METADATA + Glib::ustring(_entity->name), Glib::ustring(text ? text : "")); +} + +EntityLineEntry::EntityLineEntry (rdf_work_entity_t* ent, Registry& wr) +: EntityEntry (ent, wr) +{ + Gtk::Entry *e = new Gtk::Entry; + e->set_tooltip_text (_(ent->tip)); + _packable = e; + _changed_connection = e->signal_changed().connect (sigc::mem_fun (*this, &EntityLineEntry::on_changed)); +} + +EntityLineEntry::~EntityLineEntry() +{ + delete static_cast<Gtk::Entry*>(_packable); +} + +void EntityLineEntry::update(SPDocument *doc) +{ + const char *text = rdf_get_work_entity (doc, _entity); + // If RDF title is not set, get the document's <title> and set the RDF: + if ( !text && !strcmp(_entity->name, "title") && doc->getRoot() ) { + text = doc->getRoot()->title(); + rdf_set_work_entity(doc, _entity, text); + } + static_cast<Gtk::Entry*>(_packable)->set_text (text ? text : ""); +} + + +void EntityLineEntry::load_from_preferences() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring text = prefs->getString(PREFS_METADATA + Glib::ustring(_entity->name)); + if (text.length() > 0) { + static_cast<Gtk::Entry*>(_packable)->set_text (text.c_str()); + } +} + +void +EntityLineEntry::on_changed() +{ + if (_wr->isUpdating() || !_wr->desktop()) + return; + + _wr->setUpdating (true); + SPDocument *doc = _wr->desktop()->getDocument(); + Glib::ustring text = static_cast<Gtk::Entry*>(_packable)->get_text(); + if (rdf_set_work_entity (doc, _entity, text.c_str())) { + if (doc->isSensitive()) { + DocumentUndo::done(doc, "Document metadata updated", ""); + } + } + _wr->setUpdating (false); +} + +EntityMultiLineEntry::EntityMultiLineEntry (rdf_work_entity_t* ent, Registry& wr) +: EntityEntry (ent, wr) +{ + Gtk::ScrolledWindow *s = new Gtk::ScrolledWindow; + s->set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + s->set_shadow_type (Gtk::SHADOW_IN); + _packable = s; + _v.set_size_request (-1, 35); + _v.set_wrap_mode (Gtk::WRAP_WORD); + _v.set_accepts_tab (false); + s->add (_v); + _v.set_tooltip_text (_(ent->tip)); + _changed_connection = _v.get_buffer()->signal_changed().connect (sigc::mem_fun (*this, &EntityMultiLineEntry::on_changed)); +} + +EntityMultiLineEntry::~EntityMultiLineEntry() +{ + delete static_cast<Gtk::ScrolledWindow*>(_packable); +} + +void EntityMultiLineEntry::update(SPDocument *doc) +{ + const char *text = rdf_get_work_entity (doc, _entity); + // If RDF title is not set, get the document's <title> and set the RDF: + if ( !text && !strcmp(_entity->name, "title") && doc->getRoot() ) { + text = doc->getRoot()->title(); + rdf_set_work_entity(doc, _entity, text); + } + Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable); + Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child()); + tv->get_buffer()->set_text (text ? text : ""); +} + + +void EntityMultiLineEntry::load_from_preferences() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring text = prefs->getString(PREFS_METADATA + Glib::ustring(_entity->name)); + if (text.length() > 0) { + Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable); + Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child()); + tv->get_buffer()->set_text (text.c_str()); + } +} + + +void +EntityMultiLineEntry::on_changed() +{ + if (_wr->isUpdating() || !_wr->desktop()) + return; + + _wr->setUpdating (true); + SPDocument *doc = _wr->desktop()->getDocument(); + Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable); + Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child()); + Glib::ustring text = tv->get_buffer()->get_text(); + if (rdf_set_work_entity (doc, _entity, text.c_str())) { + DocumentUndo::done(doc, "Document metadata updated", ""); + } + _wr->setUpdating (false); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/entity-entry.h b/src/ui/widget/entity-entry.h new file mode 100644 index 0000000..3168e4c --- /dev/null +++ b/src/ui/widget/entity-entry.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_ENTITY_ENTRY__H +#define INKSCAPE_UI_WIDGET_ENTITY_ENTRY__H + +#include <gtkmm/textview.h> + +struct rdf_work_entity_t; +class SPDocument; + +namespace Gtk { +class TextBuffer; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Registry; + +class EntityEntry { +public: + static EntityEntry* create (rdf_work_entity_t* ent, Registry& wr); + virtual ~EntityEntry() = 0; + virtual void update (SPDocument *doc) = 0; + virtual void on_changed() = 0; + virtual void load_from_preferences() = 0; + void save_to_preferences(SPDocument *doc); + Gtk::Label _label; + Gtk::Widget *_packable; + +protected: + EntityEntry (rdf_work_entity_t* ent, Registry& wr); + sigc::connection _changed_connection; + rdf_work_entity_t *_entity; + Registry *_wr; +}; + +class EntityLineEntry : public EntityEntry { +public: + EntityLineEntry (rdf_work_entity_t* ent, Registry& wr); + ~EntityLineEntry() override; + void update (SPDocument *doc) override; + void load_from_preferences() override; + +protected: + void on_changed() override; +}; + +class EntityMultiLineEntry : public EntityEntry { +public: + EntityMultiLineEntry (rdf_work_entity_t* ent, Registry& wr); + ~EntityMultiLineEntry() override; + void update (SPDocument *doc) override; + void load_from_preferences() override; + +protected: + void on_changed() override; + Gtk::TextView _v; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_ENTITY_ENTRY__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/widget/entry.cpp b/src/ui/widget/entry.cpp new file mode 100644 index 0000000..e9a63c5 --- /dev/null +++ b/src/ui/widget/entry.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <goejendaagh@zonnet.nl> + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "entry.h" + +#include <gtkmm/entry.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +Entry::Entry( Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new Gtk::Entry(), suffix, icon, mnemonic) +{ +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + diff --git a/src/ui/widget/entry.h b/src/ui/widget/entry.h new file mode 100644 index 0000000..3674d51 --- /dev/null +++ b/src/ui/widget/entry.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <goejendaagh@zonnet.nl> + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_ENTRY__H +#define INKSCAPE_UI_WIDGET_ENTRY__H + +#include "labelled.h" + +namespace Gtk { +class Entry; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Helperclass for Gtk::Entry widgets. + */ +class Entry : public Labelled +{ +public: + Entry( Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + // TO DO: add methods to access Gtk::Entry widget + + Gtk::Entry* getEntry() {return (Gtk::Entry*)(_widget);}; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_ENTRY__H diff --git a/src/ui/widget/export-lists.cpp b/src/ui/widget/export-lists.cpp new file mode 100644 index 0000000..ba1da1a --- /dev/null +++ b/src/ui/widget/export-lists.cpp @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Anshudhar Kumar Singh <anshudhar2001@gmail.com> + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "export-lists.h" + +#include <glibmm/convert.h> +#include <glibmm/i18n.h> +#include <glibmm/miscutils.h> +#include <gtkmm.h> +#include <png.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "extension/db.h" +#include "extension/output.h" +#include "file.h" +#include "helper/png-write.h" +#include "inkscape-window.h" +#include "inkscape.h" +#include "io/resource.h" +#include "io/sys.h" +#include "message-stack.h" +#include "object/object-set.h" +#include "object/sp-namedview.h" +#include "object/sp-page.h" +#include "object/sp-root.h" +#include "page-manager.h" +#include "preferences.h" +#include "selection-chemistry.h" +#include "ui/dialog-events.h" +#include "ui/dialog/dialog-notebook.h" +#include "ui/dialog/filedialog.h" +#include "ui/icon-loader.h" +#include "ui/interface.h" +#include "ui/widget/scrollprotected.h" +#include "ui/widget/unit-menu.h" + +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +ExtensionList::ExtensionList() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _watch_pref = prefs->createObserver("/dialogs/export/show_all_extensions", [=]() { setup(); }); +} + +ExtensionList::ExtensionList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade) + : Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>(cobject, refGlade) +{ + // This duplication is silly, but needed because C++ can't + // both deligate the constructor, and construct for glade + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _watch_pref = prefs->createObserver("/dialogs/export/show_all_extensions", [=]() { setup(); }); +} + +void ExtensionList::setup() +{ + this->remove_all(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool export_all = prefs->getBool("/dialogs/export/show_all_extensions", false); + + Inkscape::Extension::DB::OutputList extensions; + Inkscape::Extension::db.get_output_list(extensions); + for (auto omod : extensions) { + auto oid = Glib::ustring(omod->get_id()); + if (!export_all && !omod->is_raster() && !omod->is_exported()) + continue; + // Comboboxes don't have a disabled row property + if (omod->deactivated()) + continue; + this->append(oid, omod->get_filetypename()); + // Record extensions map for filename-to-combo selections + auto ext = omod->get_extension(); + if (!ext_to_mod[ext]) { + // Some extensions have multiple of the same extension (for example PNG) + // we're going to pick the first in the found list to back-link to. + ext_to_mod[ext] = omod; + } + } + this->set_active_id(SP_MODULE_KEY_RASTER_PNG); +} + +/** + * Returns the Output extension currently selected in this dropdown. + */ +Inkscape::Extension::Output *ExtensionList::getExtension() +{ + return dynamic_cast<Inkscape::Extension::Output *>(Inkscape::Extension::db.get(this->get_active_id().c_str())); +} + +/** + * Returns the file extension (file ending) of the currently selected extension. + */ +Glib::ustring ExtensionList::getFileExtension() +{ + if (auto ext = getExtension()) { + return ext->get_extension(); + } + return ""; +} + +/** + * Removes the file extension, *if* it's one of the extensions in the list. + */ +void ExtensionList::removeExtension(Glib::ustring &filename) +{ + auto ext = Inkscape::IO::get_file_extension(filename); + if (ext_to_mod[ext]) { + filename.erase(filename.size()-ext.size()); + } +} + +void ExtensionList::setExtensionFromFilename(Glib::ustring const &filename) +{ + auto ext = Inkscape::IO::get_file_extension(filename); + if (auto omod = ext_to_mod[ext]) { + this->set_active_id(omod->get_id()); + } +} + +void ExportList::setup() +{ + if (_initialised) { + return; + } + _initialised = true; + prefs = Inkscape::Preferences::get(); + default_dpi = prefs->getDouble("/dialogs/export/defaultxdpi/value", DPI_BASE); + + Gtk::Button *add_button = Gtk::manage(new Gtk::Button()); + Glib::ustring label = _("Add Export"); + add_button->set_label(label); + this->attach(*add_button, 0, 0, 4, 1); + + this->insert_row(0); + + Gtk::Label *suffix_label = Gtk::manage(new Gtk::Label(_("Suffix"))); + this->attach(*suffix_label, _suffix_col, 0, 1, 1); + suffix_label->show(); + + Gtk::Label *extension_label = Gtk::manage(new Gtk::Label(_("Format"))); + this->attach(*extension_label, _extension_col, 0, 1, 1); + extension_label->show(); + + Gtk::Label *dpi_label = Gtk::manage(new Gtk::Label(_("DPI"))); + this->attach(*dpi_label, _dpi_col, 0, 1, 1); + dpi_label->show(); + + append_row(); + + add_button->signal_clicked().connect(sigc::mem_fun(*this, &ExportList::append_row)); + add_button->set_hexpand(true); + add_button->show(); + + this->set_row_spacing(5); + this->set_column_spacing(2); +} + +void ExportList::removeExtension(Glib::ustring &filename) +{ + ExtensionList *extension_cb = dynamic_cast<ExtensionList *>(this->get_child_at(_extension_col, 1)); + if (extension_cb) { + extension_cb->removeExtension(filename); + return; + } +} + +void ExportList::append_row() +{ + int current_row = _num_rows + 1; // because we have label row at top + this->insert_row(current_row); + + Gtk::Entry *suffix = Gtk::manage(new Gtk::Entry()); + this->attach(*suffix, _suffix_col, current_row, 1, 1); + suffix->set_width_chars(2); + suffix->set_hexpand(true); + suffix->set_placeholder_text(_("Suffix")); + suffix->show(); + + ExtensionList *extension = Gtk::manage(new ExtensionList()); + SpinButton *dpi_sb = Gtk::manage(new SpinButton()); + + extension->setup(); + extension->show(); + this->attach(*extension, _extension_col, current_row, 1, 1); + + // Disable DPI when not using a raster image output + extension->signal_changed().connect([=]() { + if (auto ext = extension->getExtension()) { + dpi_sb->set_sensitive(ext->is_raster()); + } + }); + + dpi_sb->set_digits(2); + dpi_sb->set_increments(0.1, 1.0); + dpi_sb->set_range(1.0, 100000.0); + dpi_sb->set_value(default_dpi); + dpi_sb->set_sensitive(true); + dpi_sb->set_width_chars(6); + dpi_sb->set_max_width_chars(6); + dpi_sb->show(); + this->attach(*dpi_sb, _dpi_col, current_row, 1, 1); + + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("window-close", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + Gtk::Button *delete_btn = Gtk::manage(new Gtk::Button()); + delete_btn->set_relief(Gtk::RELIEF_NONE); + delete_btn->add(*pIcon); + delete_btn->show_all(); + delete_btn->set_no_show_all(true); + this->attach(*delete_btn, _delete_col, current_row, 1, 1); + delete_btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &ExportList::delete_row), delete_btn)); + + _num_rows++; +} + +void ExportList::delete_row(Gtk::Widget *widget) +{ + if (widget == nullptr) { + return; + } + if (_num_rows <= 1) { + return; + } + int row = this->child_property_top_attach(*widget); + this->remove_row(row); + _num_rows--; + if (_num_rows <= 1) { + Gtk::Widget *d_button_0 = dynamic_cast<Gtk::Widget *>(this->get_child_at(_delete_col, 1)); + if (d_button_0) { + d_button_0->hide(); + } + } +} + +Glib::ustring ExportList::get_suffix(int row) +{ + Glib::ustring suffix = ""; + Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(this->get_child_at(_suffix_col, row + 1)); + if (entry == nullptr) { + return suffix; + } + suffix = entry->get_text(); + return suffix; +} +Inkscape::Extension::Output *ExportList::getExtension(int row) +{ + ExtensionList *extension_cb = dynamic_cast<ExtensionList *>(this->get_child_at(_extension_col, row + 1)); + return extension_cb ? extension_cb->getExtension() : nullptr; +} +double ExportList::get_dpi(int row) +{ + double dpi = default_dpi; + SpinButton *spin_sb = dynamic_cast<SpinButton *>(this->get_child_at(_dpi_col, row + 1)); + if (spin_sb == nullptr) { + return dpi; + } + dpi = spin_sb->get_value(); + return dpi; +} + + + +} // 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/widget/export-lists.h b/src/ui/widget/export-lists.h new file mode 100644 index 0000000..5bea6b3 --- /dev/null +++ b/src/ui/widget/export-lists.h @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Anshudhar Kumar Singh <anshudhar2001@gmail.com> + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_EXPORT_HELPER_H +#define SP_EXPORT_HELPER_H + +#include "2geom/rect.h" +#include "preferences.h" +#include "ui/widget/scrollprotected.h" + +class SPDocument; +class SPItem; +class SPPage; + +namespace Inkscape { + namespace Util { + class Unit; + } + namespace Extension { + class Output; + } +namespace UI { +namespace Dialog { + +#define EXPORT_COORD_PRECISION 3 +#define SP_EXPORT_MIN_SIZE 1.0 +#define DPI_BASE Inkscape::Util::Quantity::convert(1, "in", "px") + +// Class for storing and manipulating extensions +class ExtensionList : public Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText> +{ +public: + ExtensionList(); + ExtensionList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade); + ~ExtensionList() override {}; + +public: + void setup(); + Glib::ustring getFileExtension(); + void setExtensionFromFilename(Glib::ustring const &filename); + void removeExtension(Glib::ustring &filename); + void createList(); + Inkscape::Extension::Output *getExtension(); + +private: + PrefObserver _watch_pref; + std::map<std::string, Inkscape::Extension::Output *> ext_to_mod; +}; + +class ExportList : public Gtk::Grid +{ +public: + ExportList(){}; + ExportList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade) + : Gtk::Grid(cobject){}; + ~ExportList() override = default; + +public: + void setup(); + void append_row(); + void delete_row(Gtk::Widget *widget); + Glib::ustring get_suffix(int row); + Inkscape::Extension::Output *getExtension(int row); + void removeExtension(Glib::ustring &filename); + double get_dpi(int row); + int get_rows() { return _num_rows; } + +private: + typedef Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton> SpinButton; + Inkscape::Preferences *prefs = nullptr; + double default_dpi = 96.00; + +private: + bool _initialised = false; + int _num_rows = 0; + int _suffix_col = 0; + int _extension_col = 1; + int _dpi_col = 2; + int _delete_col = 3; +}; + +} // 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/widget/export-preview.cpp b/src/ui/widget/export-preview.cpp new file mode 100644 index 0000000..008caff --- /dev/null +++ b/src/ui/widget/export-preview.cpp @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Anshudhar Kumar Singh <anshudhar2001@gmail.com> + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "export-preview.h" + +#include <glibmm/i18n.h> +#include <glibmm/main.h> +#include <glibmm/timer.h> +#include <gtkmm.h> + +#include "display/cairo-utils.h" +#include "inkscape.h" +#include "object/sp-defs.h" +#include "object/sp-item.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" +#include "util/preview.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +void ExportPreview::resetPixels() +{ + clear(); + show(); +} + +ExportPreview::~ExportPreview() +{ + if (drawing) { + if (_document) { + _document->getRoot()->invoke_hide(visionkey); + } + delete drawing; + drawing = nullptr; + } + if (timer) { + timer->stop(); + delete timer; + timer = nullptr; + } + if (renderTimer) { + renderTimer->stop(); + delete renderTimer; + renderTimer = nullptr; + } + _item = nullptr; + _document = nullptr; +} + +void ExportPreview::setItem(SPItem *item) +{ + _item = item; + _dbox = Geom::OptRect(); +} +void ExportPreview::setDbox(double x0, double x1, double y0, double y1) +{ + if (!_document) { + return; + } + if ((x1 - x0 == 0) || (y1 - y0) == 0) { + return; + } + _item = nullptr; + _dbox = Geom::Rect(Geom::Point(x0, y0), Geom::Point(x1, y1)) * _document->dt2doc(); +} + +void ExportPreview::setDocument(SPDocument *document) +{ + if (drawing) { + if (_document) { + _document->getRoot()->invoke_hide(visionkey); + } + delete drawing; + drawing = nullptr; + } + _document = document; + if (_document) { + drawing = new Inkscape::Drawing(); + visionkey = SPItem::display_key_new(1); + DrawingItem *ai = _document->getRoot()->invoke_show(*drawing, visionkey, SP_ITEM_SHOW_DISPLAY); + if (ai) { + drawing->setRoot(ai); + } + } +} + +void ExportPreview::refreshHide(const std::vector<SPItem *> &list) +{ + _hidden_excluded = std::vector<SPItem *>(list.begin(), list.end()); + _hidden_requested = true; +} + +void ExportPreview::performHide(const std::vector<SPItem *> *list) +{ + if (_document) { + if (isLastHide) { + if (drawing) { + if (_document) { + _document->getRoot()->invoke_hide(visionkey); + } + delete drawing; + drawing = nullptr; + } + drawing = new Inkscape::Drawing(); + visionkey = SPItem::display_key_new(1); + DrawingItem *ai = _document->getRoot()->invoke_show(*drawing, visionkey, SP_ITEM_SHOW_DISPLAY); + if (ai) { + drawing->setRoot(ai); + } + isLastHide = false; + } + if (list && !list->empty()) { + hide_other_items_recursively(_document->getRoot(), *list); + isLastHide = true; + } + } +} + +void ExportPreview::hide_other_items_recursively(SPObject *o, const std::vector<SPItem *> &list) +{ + if (SP_IS_ITEM(o) && !SP_IS_DEFS(o) && !SP_IS_ROOT(o) && !SP_IS_GROUP(o) && + list.end() == find(list.begin(), list.end(), o)) { + SP_ITEM(o)->invoke_hide(visionkey); + } + + // recurse + if (list.end() == find(list.begin(), list.end(), o)) { + for (auto &child : o->children) { + hide_other_items_recursively(&child, list); + } + } +} + +void ExportPreview::queueRefresh() +{ + if (drawing == nullptr) { + return; + } + if (!pending) { + pending = true; + if (!timer) { + timer = new Glib::Timer(); + } + Glib::signal_idle().connect(sigc::mem_fun(this, &ExportPreview::refreshCB), Glib::PRIORITY_DEFAULT_IDLE); + } +} + +bool ExportPreview::refreshCB() +{ + bool callAgain = true; + if (!timer) { + timer = new Glib::Timer(); + } + if (timer->elapsed() > minDelay) { + callAgain = false; + refreshPreview(); + pending = false; + } + return callAgain; +} + +void ExportPreview::refreshPreview() +{ + auto document = _document; + if (!timer) { + timer = new Glib::Timer(); + } + if (timer->elapsed() < minDelay) { + // Do not refresh too quickly + queueRefresh(); + } else if (document) { + renderPreview(); + timer->reset(); + } +} + +/* +This is main function which finally render preview. Call this after setting document, item and dbox. +If dbox is given it will use it. +if item is given and not dbox then item is used +If both are not given then simply we do nothing. +*/ +void ExportPreview::renderPreview() +{ + if (!renderTimer) { + renderTimer = new Glib::Timer(); + } + renderTimer->reset(); + if (drawing == nullptr) { + return; + } + + if (_hidden_requested) { + this->performHide(&_hidden_excluded); + _hidden_requested = false; + } + if (_document) { + GdkPixbuf *pb = nullptr; + if (_item) { + pb = Inkscape::UI::PREVIEW::render_preview(_document, *drawing, _item, size, size); + } else if (_dbox) { + pb = Inkscape::UI::PREVIEW::render_preview(_document, *drawing, nullptr, size, size, &_dbox); + } + if (pb) { + set(Glib::wrap(pb)); + show(); + } + } + + renderTimer->stop(); + minDelay = std::max(0.1, renderTimer->elapsed() * 3.0); +} + +} // 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/widget/export-preview.h b/src/ui/widget/export-preview.h new file mode 100644 index 0000000..ad7f49c --- /dev/null +++ b/src/ui/widget/export-preview.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Anshudhar Kumar Singh <anshudhar2001@gmail.com> + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_EXPORT_PREVIEW_H +#define SP_EXPORT_PREVIEW_H + +#include <gtkmm.h> + +#include "desktop.h" +#include "document.h" + +class SPObject; +class SPItem; + +namespace Glib { +class Timer; +} + +namespace Inkscape { +class Drawing; +namespace UI { +namespace Dialog { + +class ExportPreview : public Gtk::Image +{ +public: + ExportPreview() {}; + ~ExportPreview() override; + + ExportPreview(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& refGlade):Gtk::Image(cobject){}; +private: + int size = 128; // size of preview image + bool isLastHide = false; + SPDocument *_document = nullptr; + SPItem *_item = nullptr; + Geom::OptRect _dbox; + + Drawing *drawing = nullptr; + unsigned int visionkey = 0; + Glib::Timer *timer = nullptr; + Glib::Timer *renderTimer = nullptr; + bool pending = false; + gdouble minDelay = 0.1; + + std::vector<SPItem *> _hidden_excluded; + bool _hidden_requested = false; +public: + void setDocument(SPDocument *document); + void refreshHide(const std::vector<SPItem *> &list = {}); + void hide_other_items_recursively(SPObject *o, const std::vector<SPItem *> &list); + void setItem(SPItem *item); + void setDbox(double x0, double x1, double y0, double y1); + void queueRefresh(); + void resetPixels(); + + void setSize(int newSize) + { + size = newSize; + resetPixels(); + } +private: + void refreshPreview(); + void renderPreview(); + bool refreshCB(); + void performHide(const std::vector<SPItem *> *list); +}; +} // 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/widget/fill-style.cpp b/src/ui/widget/fill-style.cpp new file mode 100644 index 0000000..b5b9d9a --- /dev/null +++ b/src/ui/widget/fill-style.cpp @@ -0,0 +1,714 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Fill style widget. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noSP_FS_VERBOSE + +#include <glibmm/i18n.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "fill-style.h" +#include "gradient-chemistry.h" +#include "inkscape.h" +#include "selection.h" + +#include "object/sp-defs.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-text.h" +#include "object/sp-stop.h" +#include "ui/dialog/dialog-base.h" +#include "style.h" + +#include "ui/icon-names.h" + +// These can be deleted once we sort out the libart dependence. + +#define ART_WIND_RULE_NONZERO 0 + +/* Fill */ + +namespace Inkscape { +namespace UI { +namespace Widget { + +FillNStroke::FillNStroke(FillOrStroke k) + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) + , kind(k) + , subselChangedConn() + , eventContextConn() +{ + // Add and connect up the paint selector widget: + _psel = Gtk::manage(new UI::Widget::PaintSelector(kind)); + _psel->show(); + add(*_psel); + _psel->signal_mode_changed().connect(sigc::mem_fun(*this, &FillNStroke::paintModeChangeCB)); + _psel->signal_dragged().connect(sigc::mem_fun(*this, &FillNStroke::dragFromPaint)); + _psel->signal_changed().connect(sigc::mem_fun(*this, &FillNStroke::paintChangedCB)); + _psel->signal_stop_selected().connect([=](SPStop* stop) { + if (_desktop) { _desktop->emit_gradient_stop_selected(this, stop); } + }); + + if (kind == FILL) { + _psel->signal_fillrule_changed().connect(sigc::mem_fun(*this, &FillNStroke::setFillrule)); + } + + performUpdate(); +} + +FillNStroke::~FillNStroke() +{ + if (_drag_id) { + g_source_remove(_drag_id); + _drag_id = 0; + } + + _psel = nullptr; + subselChangedConn.disconnect(); + eventContextConn.disconnect(); +} + +/** + * On signal modified, invokes an update of the fill or stroke style paint object. + */ +void FillNStroke::selectionModifiedCB(guint flags) +{ + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { +#ifdef SP_FS_VERBOSE + g_message("selectionModifiedCB(%d) on %p", flags, this); +#endif + performUpdate(); + } +} + +void FillNStroke::setDesktop(SPDesktop *desktop) +{ + if (_desktop != desktop) { + if (_drag_id) { + g_source_remove(_drag_id); + _drag_id = 0; + } + if (_desktop) { + subselChangedConn.disconnect(); + eventContextConn.disconnect(); + stop_selected_connection.disconnect(); + } + _desktop = desktop; + if (desktop && desktop->selection) { + // subselChangedConn = + // desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &FillNStroke::performUpdate))); + eventContextConn = desktop->connectEventContextChanged(sigc::hide(sigc::bind( + sigc::mem_fun(*this, &FillNStroke::eventContextCB), (Inkscape::UI::Tools::ToolBase *)nullptr))); + + stop_selected_connection = desktop->connect_gradient_stop_selected([=](void* sender, SPStop* stop){ + if (sender != this) { + performUpdate(); + } + }); + } + performUpdate(); + } +} + +/** + * Listen to this "change in tool" event, in case a subselection tool (such as Gradient or Node) selection + * is changed back to a selection tool - especially needed for selected gradient stops. + */ +void FillNStroke::eventContextCB(SPDesktop * /*desktop*/, Inkscape::UI::Tools::ToolBase * /*eventcontext*/) +{ + performUpdate(); +} + +/** + * Gets the active fill or stroke style property, then sets the appropriate + * color, alpha, gradient, pattern, etc. for the paint-selector. + * + * @param sel Selection to use, or NULL. + */ +void FillNStroke::performUpdate() +{ + if (_update || !_desktop) { + return; + } + auto *widg = get_parent()->get_parent()->get_parent()->get_parent(); + auto dialogbase = dynamic_cast<Inkscape::UI::Dialog::DialogBase*>(widg); + if (dialogbase && !dialogbase->getShowing()) { + return; + } + if (_drag_id) { + // local change; do nothing, but reset the flag + g_source_remove(_drag_id); + _drag_id = 0; + return; + } + + _update = true; + + // create temporary style + SPStyle query(_desktop->doc()); + + // query style from desktop into it. This returns a result flag and fills query with the style of subselection, if + // any, or selection + const int property = kind == FILL ? QUERY_STYLE_PROPERTY_FILL : QUERY_STYLE_PROPERTY_STROKE; + int result = sp_desktop_query_style(_desktop, &query, property); + SPIPaint& paint = *query.getFillOrStroke(kind == FILL); + auto stop = dynamic_cast<SPStop*>(paint.getTag()); + if (stop) { + // there's a stop selected, which is part of subselection, now query selection only to find selected gradient + if (_desktop->selection != nullptr) { + std::vector<SPItem*> vec(_desktop->selection->items().begin(), _desktop->selection->items().end()); + result = sp_desktop_query_style_from_list(vec, &query, property); + } + } + SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL); + SPIScale24 &targOpacity = *(kind == FILL ? query.fill_opacity.upcast() : query.stroke_opacity.upcast()); + + switch (result) { + case QUERY_STYLE_NOTHING: { + /* No paint at all */ + _psel->setMode(UI::Widget::PaintSelector::MODE_EMPTY); + break; + } + + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: // TODO: treat this slightly differently, e.g. display "averaged" somewhere + // in paint selector + case QUERY_STYLE_MULTIPLE_SAME: { + auto pselmode = UI::Widget::PaintSelector::getModeForStyle(query, kind); + _psel->setMode(pselmode); + + if (kind == FILL) { + _psel->setFillrule(query.fill_rule.computed == ART_WIND_RULE_NONZERO + ? UI::Widget::PaintSelector::FILLRULE_NONZERO + : UI::Widget::PaintSelector::FILLRULE_EVENODD); + } + + if (targPaint.set && targPaint.isColor()) { + _psel->setColorAlpha(targPaint.value.color, SP_SCALE24_TO_FLOAT(targOpacity.value)); + } else if (targPaint.set && targPaint.isPaintserver()) { + SPPaintServer* server = (kind == FILL) ? query.getFillPaintServer() : query.getStrokePaintServer(); + + if (server) { + if (SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()) { + SPGradient *vector = SP_GRADIENT(server)->getVector(); + _psel->setSwatch(vector); + } else if (SP_IS_LINEARGRADIENT(server)) { + SPGradient *vector = SP_GRADIENT(server)->getVector(); + SPLinearGradient *lg = SP_LINEARGRADIENT(server); + _psel->setGradientLinear(vector, lg, stop); + + _psel->setGradientProperties(lg->getUnits(), lg->getSpread()); + } else if (SP_IS_RADIALGRADIENT(server)) { + SPGradient *vector = SP_GRADIENT(server)->getVector(); + SPRadialGradient *rg = SP_RADIALGRADIENT(server); + _psel->setGradientRadial(vector, rg, stop); + + _psel->setGradientProperties(rg->getUnits(), rg->getSpread()); +#ifdef WITH_MESH + } else if (SP_IS_MESHGRADIENT(server)) { + SPGradient *array = SP_GRADIENT(server)->getArray(); + _psel->setGradientMesh(SP_MESHGRADIENT(array)); + _psel->updateMeshList(SP_MESHGRADIENT(array)); +#endif + } else if (SP_IS_PATTERN(server)) { + SPPattern *pat = SP_PATTERN(server)->rootPattern(); + _psel->updatePatternList(pat); + } + } + } + break; + } + + case QUERY_STYLE_MULTIPLE_DIFFERENT: { + _psel->setMode(UI::Widget::PaintSelector::MODE_MULTIPLE); + break; + } + } + + _update = false; +} + +/** + * When the mode is changed, invoke a regular changed handler. + */ +void FillNStroke::paintModeChangeCB(UI::Widget::PaintSelector::Mode /*mode*/, bool switch_style) +{ +#ifdef SP_FS_VERBOSE + g_message("paintModeChangeCB()"); +#endif + if (!_update) { + updateFromPaint(switch_style); + } +} + +void FillNStroke::setFillrule(UI::Widget::PaintSelector::FillRule mode) +{ + if (!_update && _desktop) { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-rule", + (mode == UI::Widget::PaintSelector::FILLRULE_EVENODD) ? "evenodd" : "nonzero"); + + sp_desktop_set_style(_desktop, css); + + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(_desktop->doc(), _("Change fill rule"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } +} + +static gchar const *undo_F_label_1 = "fill:flatcolor:1"; +static gchar const *undo_F_label_2 = "fill:flatcolor:2"; + +static gchar const *undo_S_label_1 = "stroke:flatcolor:1"; +static gchar const *undo_S_label_2 = "stroke:flatcolor:2"; + +static gchar const *undo_F_label = undo_F_label_1; +static gchar const *undo_S_label = undo_S_label_1; + +gboolean FillNStroke::dragDelayCB(gpointer data) +{ + gboolean keepGoing = TRUE; + if (data) { + FillNStroke *self = reinterpret_cast<FillNStroke *>(data); + if (!self->_update) { + if (self->_drag_id) { + g_source_remove(self->_drag_id); + self->_drag_id = 0; + + self->dragFromPaint(); + self->performUpdate(); + } + keepGoing = FALSE; + } + } else { + keepGoing = FALSE; + } + return keepGoing; +} + +/** + * This is called repeatedly while you are dragging a color slider, only for flat color + * modes. Previously it set the color in style but did not update the repr for efficiency, however + * this was flakey and didn't buy us almost anything. So now it does the same as _changed, except + * lumps all its changes for undo. + */ +void FillNStroke::dragFromPaint() +{ + if (!_desktop || _update) { + return; + } + + guint32 when = gtk_get_current_event_time(); + + // Don't attempt too many updates per second. + // Assume a base 15.625ms resolution on the timer. + if (!_drag_id && _last_drag && when && ((when - _last_drag) < 32)) { + // local change, do not update from selection + _drag_id = g_timeout_add_full(G_PRIORITY_DEFAULT, 33, dragDelayCB, this, nullptr); + } + + if (_drag_id) { + // previous local flag not cleared yet; + // this means dragged events come too fast, so we better skip this one to speed up display + // (it's safe to do this in any case) + return; + } + _last_drag = when; + + _update = true; + + switch (_psel->get_mode()) { + case UI::Widget::PaintSelector::MODE_SOLID_COLOR: { + // local change, do not update from selection + _drag_id = g_timeout_add_full(G_PRIORITY_DEFAULT, 100, dragDelayCB, this, nullptr); + _psel->setFlatColor(_desktop, + (kind == FILL) ? "fill" : "stroke", + (kind == FILL) ? "fill-opacity" : "stroke-opacity"); + DocumentUndo::maybeDone(_desktop->doc(), (kind == FILL) ? undo_F_label : undo_S_label, + (kind == FILL) ? _("Set fill color") : _("Set stroke color"), + INKSCAPE_ICON("dialog-fill-and-stroke")); + break; + } + + default: + g_warning("file %s: line %d: Paint %d should not emit 'dragged'", __FILE__, __LINE__, _psel->get_mode()); + break; + } + _update = false; +} + +/** +This is called (at least) when: +1 paint selector mode is switched (e.g. flat color -> gradient) +2 you finished dragging a gradient node and released mouse +3 you changed a gradient selector parameter (e.g. spread) +Must update repr. + */ +void FillNStroke::paintChangedCB() +{ +#ifdef SP_FS_VERBOSE + g_message("paintChangedCB()"); +#endif + if (!_update) { + updateFromPaint(); + } +} + +void FillNStroke::updateFromPaint(bool switch_style) +{ + if (!_desktop) { + return; + } + _update = true; + + auto document = _desktop->getDocument(); + auto selection = _desktop->getSelection(); + + std::vector<SPItem *> const items(selection->items().begin(), selection->items().end()); + + switch (_psel->get_mode()) { + case UI::Widget::PaintSelector::MODE_EMPTY: + // This should not happen. + g_warning("file %s: line %d: Paint %d should not emit 'changed'", __FILE__, __LINE__, _psel->get_mode()); + break; + case UI::Widget::PaintSelector::MODE_MULTIPLE: + // This happens when you switch multiple objects with different gradients to flat color; + // nothing to do here. + break; + + case UI::Widget::PaintSelector::MODE_NONE: { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, (kind == FILL) ? "fill" : "stroke", "none"); + + sp_desktop_set_style(_desktop, css, true, true, switch_style); + + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(document, (kind == FILL) ? _("Remove fill") : _("Remove stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); + break; + } + + case UI::Widget::PaintSelector::MODE_SOLID_COLOR: { + _psel->setFlatColor(_desktop, (kind == FILL) ? "fill" : "stroke", + (kind == FILL) ? "fill-opacity" : "stroke-opacity"); + DocumentUndo::maybeDone(_desktop->getDocument(), (kind == FILL) ? undo_F_label : undo_S_label, + (kind == FILL) ? _("Set fill color") : _("Set stroke color"), + INKSCAPE_ICON("dialog-fill-and-stroke")); + + // on release, toggle undo_label so that the next drag will not be lumped with this one + if (undo_F_label == undo_F_label_1) { + undo_F_label = undo_F_label_2; + undo_S_label = undo_S_label_2; + } else { + undo_F_label = undo_F_label_1; + undo_S_label = undo_S_label_1; + } + + break; + } + + case UI::Widget::PaintSelector::MODE_GRADIENT_LINEAR: + case UI::Widget::PaintSelector::MODE_GRADIENT_RADIAL: + case UI::Widget::PaintSelector::MODE_SWATCH: + if (!items.empty()) { + SPGradientType const gradient_type = + (_psel->get_mode() != UI::Widget::PaintSelector::MODE_GRADIENT_RADIAL ? SP_GRADIENT_TYPE_LINEAR + : SP_GRADIENT_TYPE_RADIAL); + bool createSwatch = (_psel->get_mode() == UI::Widget::PaintSelector::MODE_SWATCH); + + SPCSSAttr *css = nullptr; + if (kind == FILL) { + // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider + // for all tabs + css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-opacity", "1.0"); + } + + auto vector = _psel->getGradientVector(); + if (!vector) { + /* No vector in paint selector should mean that we just changed mode */ + + SPStyle query(_desktop->doc()); + int result = objects_query_fillstroke(items, &query, kind == FILL); + if (result == QUERY_STYLE_MULTIPLE_SAME) { + SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL); + SPColor common; + if (!targPaint.isColor()) { + common = sp_desktop_get_color(_desktop, kind == FILL); + } else { + common = targPaint.value.color; + } + vector = sp_document_default_gradient_vector(document, common, createSwatch); + if (vector && createSwatch) { + vector->setSwatch(); + } + } + + for (auto item : items) { + // FIXME: see above + if (kind == FILL) { + sp_repr_css_change_recursive(item->getRepr(), css, "style"); + } + + if (!vector) { + auto gr = sp_gradient_vector_for_object( + document, _desktop, item, + (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE, createSwatch); + if (gr && createSwatch) { + gr->setSwatch(); + } + sp_item_set_gradient(item, gr, gradient_type, + (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE); + } else { + sp_item_set_gradient(item, vector, gradient_type, + (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE); + } + } + } else { + // We have changed from another gradient type, or modified spread/units within + // this gradient type. + vector = sp_gradient_ensure_vector_normalized(vector); + for (auto item : items) { + // FIXME: see above + if (kind == FILL) { + sp_repr_css_change_recursive(item->getRepr(), css, "style"); + } + + SPGradient *gr = sp_item_set_gradient( + item, vector, gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE); + _psel->pushAttrsToGradient(gr); + } + } + + if (css) { + sp_repr_css_attr_unref(css); + css = nullptr; + } + + DocumentUndo::done(document, (kind == FILL) ? _("Set gradient on fill") : _("Set gradient on stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } + break; + +#ifdef WITH_MESH + case UI::Widget::PaintSelector::MODE_GRADIENT_MESH: + + if (!items.empty()) { + SPCSSAttr *css = nullptr; + if (kind == FILL) { + // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider + // for all tabs + css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-opacity", "1.0"); + } + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + SPDefs *defs = document->getDefs(); + + auto mesh = _psel->getMeshGradient(); + + for (auto item : items) { + + // FIXME: see above + if (kind == FILL) { + sp_repr_css_change_recursive(item->getRepr(), css, "style"); + } + + // Check if object already has mesh. + bool has_mesh = false; + SPStyle *style = item->style; + if (style) { + SPPaintServer *server = + (kind == FILL) ? style->getFillPaintServer() : style->getStrokePaintServer(); + if (server && SP_IS_MESHGRADIENT(server)) + has_mesh = true; + } + + if (!mesh || !has_mesh) { + // No mesh in document or object does not already have mesh -> + // Create new mesh. + + // Create mesh element + Inkscape::XML::Node *repr = xml_doc->createElement("svg:meshgradient"); + + // privates are garbage-collectable + repr->setAttribute("inkscape:collect", "always"); + + // Attach to document + defs->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + + // Get corresponding object + SPMeshGradient *mg = static_cast<SPMeshGradient *>(document->getObjectByRepr(repr)); + mg->array.create(mg, item, (kind == FILL) ? item->geometricBounds() : item->visualBounds()); + + bool isText = SP_IS_TEXT(item); + sp_style_set_property_url(item, ((kind == FILL) ? "fill" : "stroke"), mg, isText); + + // (*i)->requestModified(SP_OBJECT_MODIFIED_FLAG|SP_OBJECT_STYLE_MODIFIED_FLAG); + + } else { + // Using found mesh + + // Duplicate + Inkscape::XML::Node *mesh_repr = mesh->getRepr(); + Inkscape::XML::Node *copy_repr = mesh_repr->duplicate(xml_doc); + + // privates are garbage-collectable + copy_repr->setAttribute("inkscape:collect", "always"); + + // Attach to document + defs->getRepr()->appendChild(copy_repr); + Inkscape::GC::release(copy_repr); + + // Get corresponding object + SPMeshGradient *mg = static_cast<SPMeshGradient *>(document->getObjectByRepr(copy_repr)); + // std::cout << " " << (mg->getId()?mg->getId():"null") << std::endl; + mg->array.read(mg); + + Geom::OptRect item_bbox = (kind == FILL) ? item->geometricBounds() : item->visualBounds(); + mg->array.fill_box(item_bbox); + + bool isText = SP_IS_TEXT(item); + sp_style_set_property_url(item, ((kind == FILL) ? "fill" : "stroke"), mg, isText); + } + } + + if (css) { + sp_repr_css_attr_unref(css); + css = nullptr; + } + + DocumentUndo::done(document, (kind == FILL) ? _("Set mesh on fill") : _("Set mesh on stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } + break; +#endif + + case UI::Widget::PaintSelector::MODE_PATTERN: + + if (!items.empty()) { + + auto pattern = _psel->getPattern(); + if (!pattern) { + + /* No Pattern in paint selector should mean that we just + * changed mode - don't do jack. + */ + + } else { + Inkscape::XML::Node *patrepr = pattern->getRepr(); + SPCSSAttr *css = sp_repr_css_attr_new(); + gchar *urltext = g_strdup_printf("url(#%s)", patrepr->attribute("id")); + sp_repr_css_set_property(css, (kind == FILL) ? "fill" : "stroke", urltext); + + // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider + // for all tabs + if (kind == FILL) { + sp_repr_css_set_property(css, "fill-opacity", "1.0"); + } + + // cannot just call sp_desktop_set_style, because we don't want to touch those + // objects who already have the same root pattern but through a different href + // chain. FIXME: move this to a sp_item_set_pattern + for (auto item : items) { + Inkscape::XML::Node *selrepr = item->getRepr(); + if ((kind == STROKE) && !selrepr) { + continue; + } + SPObject *selobj = item; + + SPStyle *style = selobj->style; + if (style && ((kind == FILL) ? style->fill.isPaintserver() : style->stroke.isPaintserver())) { + SPPaintServer *server = (kind == FILL) ? selobj->style->getFillPaintServer() + : selobj->style->getStrokePaintServer(); + if (SP_IS_PATTERN(server) && SP_PATTERN(server)->rootPattern() == pattern) + // only if this object's pattern is not rooted in our selected pattern, apply + continue; + } + + if (kind == FILL) { + sp_desktop_apply_css_recursive(selobj, css, true); + } else { + sp_repr_css_change_recursive(selrepr, css, "style"); + } + } + + sp_repr_css_attr_unref(css); + css = nullptr; + g_free(urltext); + + } // end if + + DocumentUndo::done(document, (kind == FILL) ? _("Set pattern on fill") : _("Set pattern on stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } // end if + + break; + + case UI::Widget::PaintSelector::MODE_UNSET: + if (!items.empty()) { + SPCSSAttr *css = sp_repr_css_attr_new(); + if (kind == FILL) { + sp_repr_css_unset_property(css, "fill"); + } else { + sp_repr_css_unset_property(css, "stroke"); + sp_repr_css_unset_property(css, "stroke-opacity"); + sp_repr_css_unset_property(css, "stroke-width"); + sp_repr_css_unset_property(css, "stroke-miterlimit"); + sp_repr_css_unset_property(css, "stroke-linejoin"); + sp_repr_css_unset_property(css, "stroke-linecap"); + sp_repr_css_unset_property(css, "stroke-dashoffset"); + sp_repr_css_unset_property(css, "stroke-dasharray"); + } + + sp_desktop_set_style(_desktop, css); + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(document, (kind == FILL) ? _("Unset fill") : _("Unset stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } + break; + + default: + g_warning("file %s: line %d: Paint selector should not be in " + "mode %d", + __FILE__, __LINE__, _psel->get_mode()); + break; + } + + _update = false; +} + +} // namespace Widget +} // 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/widget/fill-style.h b/src/ui/widget/fill-style.h new file mode 100644 index 0000000..165aec7 --- /dev/null +++ b/src/ui/widget/fill-style.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Fill style configuration + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_SP_FILL_STYLE_H +#define SEEN_DIALOGS_SP_FILL_STYLE_H + +#include "ui/widget/paint-selector.h" + +#include <gtkmm/box.h> + +namespace Gtk { +class Widget; +} + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Tools { +class ToolBase; +} + +namespace Widget { + +class FillNStroke : public Gtk::Box { + private: + FillOrStroke kind; + SPDesktop *_desktop = nullptr; + PaintSelector *_psel = nullptr; + guint32 _last_drag = 0; + guint _drag_id = 0; + bool _update = false; + + sigc::connection subselChangedConn; + sigc::connection eventContextConn; + sigc::connection stop_selected_connection; + + void paintModeChangeCB(UI::Widget::PaintSelector::Mode mode, bool switch_style); + void paintChangedCB(); + static gboolean dragDelayCB(gpointer data); + + + void eventContextCB(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *eventcontext); + + void dragFromPaint(); + void updateFromPaint(bool switch_style = false); + + public: + FillNStroke(FillOrStroke k); + ~FillNStroke() override; + + void selectionModifiedCB(guint flags); + void performUpdate(); + + void setFillrule(PaintSelector::FillRule mode); + void setDesktop(SPDesktop *desktop); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_DIALOGS_SP_FILL_STYLE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/filter-effect-chooser.cpp b/src/ui/widget/filter-effect-chooser.cpp new file mode 100644 index 0000000..16c02a3 --- /dev/null +++ b/src/ui/widget/filter-effect-chooser.cpp @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Filter effect selection selection widget + * + * Author: + * Nicholas Bishop <nicholasbishop@gmail.com> + * Tavmjong Bah + * + * Copyright (C) 2007, 2017 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "filter-effect-chooser.h" + +#include "document.h" + +namespace Inkscape { + +const EnumData<SPBlendMode> SPBlendModeData[SP_CSS_BLEND_ENDMODE] = { + { SP_CSS_BLEND_NORMAL, _("Normal"), "normal" }, + { SP_CSS_BLEND_MULTIPLY, _("Multiply"), "multiply" }, + { SP_CSS_BLEND_SCREEN, _("Screen"), "screen" }, + { SP_CSS_BLEND_DARKEN, _("Darken"), "darken" }, + { SP_CSS_BLEND_LIGHTEN, _("Lighten"), "lighten" }, + // New in Compositing and Blending Level 1 + { SP_CSS_BLEND_OVERLAY, _("Overlay"), "overlay" }, + { SP_CSS_BLEND_COLORDODGE, _("Color Dodge"), "color-dodge" }, + { SP_CSS_BLEND_COLORBURN, _("Color Burn"), "color-burn" }, + { SP_CSS_BLEND_HARDLIGHT, _("Hard Light"), "hard-light" }, + { SP_CSS_BLEND_SOFTLIGHT, _("Soft Light"), "soft-light" }, + { SP_CSS_BLEND_DIFFERENCE, _("Difference"), "difference" }, + { SP_CSS_BLEND_EXCLUSION, _("Exclusion"), "exclusion" }, + { SP_CSS_BLEND_HUE, _("Hue"), "hue" }, + { SP_CSS_BLEND_SATURATION, _("Saturation"), "saturation" }, + { SP_CSS_BLEND_COLOR, _("Color"), "color" }, + { SP_CSS_BLEND_LUMINOSITY, _("Luminosity"), "luminosity" } +}; +const EnumDataConverter<SPBlendMode> SPBlendModeConverter(SPBlendModeData, SP_CSS_BLEND_ENDMODE); + + +namespace UI { +namespace Widget { + +SimpleFilterModifier::SimpleFilterModifier(int flags) + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) + , _flags(flags) + , _lb_blend(_("Blend mode:")) + , _lb_isolation("Isolate") // Translate for 1.1 + , _blend(SPBlendModeConverter, SPAttr::INVALID, false) + , _blur(_("Blur (%)"), 0, 0, 100, 1, 0.1, 1) + , _opacity(_("Opacity (%)"), 0, 0, 100, 1, 0.1, 1) + , _notify(true) + , _hb_blend(Gtk::ORIENTATION_HORIZONTAL) +{ + set_name("SimpleFilterModifier"); + + /* "More options" expander -------- + _extras.set_visible(); + _extras.set_label(_("More options")); + auto box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_VERTICAL); + _extras.add(*box); + if (flags & (BLEND | BLUR)) { + add(_extras); + } + */ + + _flags = flags; + + if (flags & BLEND) { + add(_hb_blend); + _lb_blend.set_use_underline(); + _hb_blend.set_halign(Gtk::ALIGN_END); + _hb_blend.set_valign(Gtk::ALIGN_CENTER); + _hb_blend.set_margin_top(0); + _hb_blend.set_margin_bottom(1); + _hb_blend.set_margin_end(2); + _lb_blend.set_mnemonic_widget(_blend); + _hb_blend.pack_start(_lb_blend, false, false, 0); + _hb_blend.pack_start(_blend, false, false, 0); + /* + * For best fit inkscape-browsers with no GUI to isolation we need all groups, + * clones, and symbols with isolation == isolate to not show to the Inkscape + * user "strange" behaviour from the designer point of view. + * It's strange because it only happens when object doesn't have: clip, mask, + * filter, blending, or opacity. + * Anyway the feature is a no-gui feature and renders as expected. + */ + /* if (flags & ISOLATION) { + _isolation.property_active() = false; + _hb_blend.pack_start(_isolation, false, false, 5); + _hb_blend.pack_start(_lb_isolation, false, false, 5); + _isolation.set_tooltip_text("Don't blend childrens with objects behind"); + _lb_isolation.set_tooltip_text("Don't blend childrens with objects behind"); + } */ + } + + if (flags & BLUR) { + add(_blur); + } + + if (flags & OPACITY) { + add(_opacity); + } + show_all_children(); + + _blend.signal_changed().connect(signal_blend_changed()); + _blur.signal_value_changed().connect(signal_blur_changed()); + _opacity.signal_value_changed().connect(signal_opacity_changed()); + _isolation.signal_toggled().connect(signal_isolation_changed()); +} + +sigc::signal<void> &SimpleFilterModifier::signal_isolation_changed() +{ + if (_notify) { + return _signal_isolation_changed; + } + _notify = true; + return _signal_null; +} + +sigc::signal<void>& SimpleFilterModifier::signal_blend_changed() +{ + if (_notify) { + return _signal_blend_changed; + } + _notify = true; + return _signal_null; +} + +sigc::signal<void>& SimpleFilterModifier::signal_blur_changed() +{ + // we dont use notifi to block use aberaje for multiple + return _signal_blur_changed; +} + +sigc::signal<void>& SimpleFilterModifier::signal_opacity_changed() +{ + // we dont use notifi to block use averaje for multiple + return _signal_opacity_changed; +} + +SPIsolation SimpleFilterModifier::get_isolation_mode() +{ + return _isolation.get_active() ? SP_CSS_ISOLATION_ISOLATE : SP_CSS_ISOLATION_AUTO; +} + +void SimpleFilterModifier::set_isolation_mode(const SPIsolation val, bool notify) +{ + _notify = notify; + _isolation.set_active(val == SP_CSS_ISOLATION_ISOLATE); +} + +SPBlendMode SimpleFilterModifier::get_blend_mode() +{ + const Util::EnumData<SPBlendMode> *d = _blend.get_active_data(); + if (d) { + return _blend.get_active_data()->id; + } else { + return SP_CSS_BLEND_NORMAL; + } +} + +void SimpleFilterModifier::set_blend_mode(const SPBlendMode val, bool notify) +{ + _notify = notify; + _blend.set_active(val); +} + +double SimpleFilterModifier::get_blur_value() const +{ + return _blur.get_value(); +} + +void SimpleFilterModifier::set_blur_value(const double val) +{ + _blur.set_value(val); +} + +double SimpleFilterModifier::get_opacity_value() const +{ + return _opacity.get_value(); +} + +void SimpleFilterModifier::set_opacity_value(const double val) +{ + _opacity.set_value(val); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode: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/widget/filter-effect-chooser.h b/src/ui/widget/filter-effect-chooser.h new file mode 100644 index 0000000..f0b07b3 --- /dev/null +++ b/src/ui/widget/filter-effect-chooser.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __FILTER_EFFECT_CHOOSER_H__ +#define __FILTER_EFFECT_CHOOSER_H__ + +/* + * Filter effect selection selection widget + * + * Author: + * Nicholas Bishop <nicholasbishop@gmail.com> + * Tavmjong Bah + * + * Copyright (C) 2007, 2017 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/combobox.h> +#include <gtkmm/separator.h> +#include <gtkmm/expander.h> + +#include "combo-enums.h" +#include "spin-scale.h" +#include "style-enums.h" + +using Inkscape::Util::EnumData; +using Inkscape::Util::EnumDataConverter; + +namespace Inkscape { +extern const Util::EnumDataConverter<SPBlendMode> SPBlendModeConverter; +namespace UI { +namespace Widget { + +/* Allows basic control over feBlend and feGaussianBlur effects as well as opacity. + * Common for Object, Layers, and Fill and Stroke dialogs. +*/ +class SimpleFilterModifier : public Gtk::Box +{ +public: + enum Flags { NONE = 0, BLUR = 1, OPACITY = 2, BLEND = 4, ISOLATION = 16 }; + + SimpleFilterModifier(int flags); + + sigc::signal<void> &signal_blend_changed(); + sigc::signal<void> &signal_blur_changed(); + sigc::signal<void> &signal_opacity_changed(); + sigc::signal<void> &signal_isolation_changed(); + + SPIsolation get_isolation_mode(); + void set_isolation_mode(const SPIsolation, bool notify); + + SPBlendMode get_blend_mode(); + void set_blend_mode(const SPBlendMode, bool notify); + + double get_blur_value() const; + void set_blur_value(const double); + + double get_opacity_value() const; + void set_opacity_value(const double); + +private: + int _flags; + bool _notify; + + Gtk::Expander _extras; + Gtk::Box _hb_blend; + Gtk::Label _lb_blend; + Gtk::Label _lb_isolation; + ComboBoxEnum<SPBlendMode> _blend; + SpinScale _blur; + SpinScale _opacity; + Gtk::CheckButton _isolation; + + sigc::signal<void> _signal_null; + sigc::signal<void> _signal_blend_changed; + sigc::signal<void> _signal_blur_changed; + sigc::signal<void> _signal_opacity_changed; + sigc::signal<void> _signal_isolation_changed; +}; + +} +} +} + +#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/widget/font-button.cpp b/src/ui/widget/font-button.cpp new file mode 100644 index 0000000..e0a140a --- /dev/null +++ b/src/ui/widget/font-button.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "font-button.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/fontbutton.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +FontButton::FontButton(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new Gtk::FontButton("Sans 10"), suffix, icon, mnemonic) +{ +} + +Glib::ustring FontButton::getValue() const +{ + g_assert(_widget != nullptr); + return static_cast<Gtk::FontButton*>(_widget)->get_font_name(); +} + + +void FontButton::setValue (Glib::ustring fontspec) +{ + g_assert(_widget != nullptr); + static_cast<Gtk::FontButton*>(_widget)->set_font_name(fontspec); +} + +Glib::SignalProxy0<void> FontButton::signal_font_value_changed() +{ + g_assert(_widget != nullptr); + return static_cast<Gtk::FontButton*>(_widget)->signal_font_set(); +} + + +} // namespace Widget +} // 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/widget/font-button.h b/src/ui/widget/font-button.h new file mode 100644 index 0000000..a53b7d6 --- /dev/null +++ b/src/ui/widget/font-button.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * + * Copyright (C) 2007 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_FONT_BUTTON_H +#define INKSCAPE_UI_WIDGET_FONT_BUTTON_H + +#include "labelled.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A labelled font button for entering font values + */ +class FontButton : public Labelled +{ +public: + /** + * Construct a FontButton Widget. + * + * @param label Label. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + FontButton( Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + Glib::ustring getValue() const; + void setValue (Glib::ustring fontspec); + /** + * Signal raised when the font button's value changes. + */ + Glib::SignalProxy0<void> signal_font_value_changed(); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_RANDOM_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/widget/font-selector-toolbar.cpp b/src/ui/widget/font-selector-toolbar.cpp new file mode 100644 index 0000000..68c5e79 --- /dev/null +++ b/src/ui/widget/font-selector-toolbar.cpp @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2018 Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <glibmm/regex.h> +#include <gdkmm/display.h> + +#include "font-selector-toolbar.h" + +#include "libnrtype/font-lister.h" +#include "libnrtype/font-instance.h" + +#include "ui/icon-names.h" + +// For updating from selection +#include "inkscape.h" +#include "desktop.h" +#include "object/sp-text.h" + +// TEMP TEMP TEMP +#include "ui/toolbar/text-toolbar.h" + +/* To do: + * Fix altx. The setToolboxFocusTo method now just searches for a named widget. + * We just need to do the following: + * * Set the name of the family_combo child widget + * * Change the setToolboxFocusTo() argument in tools/text-tool to point to that widget name + */ + +void family_cell_data_func(const Gtk::TreeModel::const_iterator iter, Gtk::CellRendererText* cell ) { + + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + Glib::ustring markup = font_lister->get_font_family_markup(iter); + // std::cout << "Markup: " << markup << std::endl; + + cell->set_property ("markup", markup); +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +FontSelectorToolbar::FontSelectorToolbar () + : Gtk::Grid () + , family_combo (true) // true => with text entry. + , style_combo (true) + , signal_block (false) +{ + + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + + // Font family + family_combo.set_model (font_lister->get_font_list()); + family_combo.set_entry_text_column (0); + family_combo.set_name ("FontSelectorToolBar: Family"); + family_combo.set_row_separator_func (&font_lister_separator_func); + + family_combo.clear(); // Clears all CellRenderer mappings. + family_combo.set_cell_data_func (family_cell, + sigc::bind(sigc::ptr_fun(family_cell_data_func), &family_cell)); + family_combo.pack_start (family_cell); + + + Gtk::Entry* entry = family_combo.get_entry(); + entry->signal_icon_press().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_icon_pressed)); + entry->signal_key_press_event().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_key_press_event), false); // false => connect first + + Glib::RefPtr<Gtk::EntryCompletion> completion = Gtk::EntryCompletion::create(); + completion->set_model (font_lister->get_font_list()); + completion->set_text_column (0); + completion->set_popup_completion (); + completion->set_inline_completion (false); + completion->set_inline_selection (); + // completion->signal_match_selected().connect(sigc::mem_fun(*this, &FontSelectorToolbar::on_match_selected), false); // false => connect before default handler. + entry->set_completion (completion); + + // Style + style_combo.set_model (font_lister->get_style_list()); + style_combo.set_name ("FontSelectorToolbar: Style"); + + // Grid + set_name ("FontSelectorToolbar: Grid"); + attach (family_combo, 0, 0, 1, 1); + attach (style_combo, 1, 0, 1, 1); + + // Add signals + family_combo.signal_changed().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_family_changed)); + style_combo.signal_changed().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_style_changed)); + + show_all_children(); + + // Initialize font family lists. (May already be done.) Should be done on document change. + font_lister->update_font_list(SP_ACTIVE_DESKTOP->getDocument()); + + // When FontLister is changed, update family and style shown in GUI. + font_lister->connectUpdate(sigc::mem_fun(*this, &FontSelectorToolbar::update_font)); +} + + +// Update GUI based on font-selector values. +void +FontSelectorToolbar::update_font () +{ + if (signal_block) return; + + signal_block = true; + + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + Gtk::TreeModel::Row row; + + // Set font family. + try { + row = font_lister->get_row_for_font (); + family_combo.set_active (row); + } catch (...) { + std::cerr << "FontSelectorToolbar::update_font: Couldn't find row for family: " + << font_lister->get_font_family() << std::endl; + } + + // Set style. + try { + row = font_lister->get_row_for_style (); + style_combo.set_active (row); + } catch (...) { + std::cerr << "FontSelectorToolbar::update_font: Couldn't find row for style: " + << font_lister->get_font_style() << std::endl; + } + + // Check for missing fonts. + Glib::ustring missing_fonts = get_missing_fonts(); + + // Add an icon to end of entry. + Gtk::Entry* entry = family_combo.get_entry(); + if (missing_fonts.empty()) { + // If no missing fonts, add icon for selecting all objects with this font-family. + entry->set_icon_from_icon_name (INKSCAPE_ICON("edit-select-all"), Gtk::ENTRY_ICON_SECONDARY); + entry->set_icon_tooltip_text (_("Select all text with this text family"), Gtk::ENTRY_ICON_SECONDARY); + } else { + // If missing fonts, add warning icon. + Glib::ustring warning = _("Font not found on system: ") + missing_fonts; + entry->set_icon_from_icon_name (INKSCAPE_ICON("dialog-warning"), Gtk::ENTRY_ICON_SECONDARY); + entry->set_icon_tooltip_text (warning, Gtk::ENTRY_ICON_SECONDARY); + } + + signal_block = false; +} + +// Get comma separated list of fonts in font-family that are not on system. +// To do, move to font-lister. +Glib::ustring +FontSelectorToolbar::get_missing_fonts () +{ + // Get font list in text entry which may be a font stack (with fallbacks). + Glib::ustring font_list = family_combo.get_entry_text(); + Glib::ustring missing_font_list; + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s*,\\s*", font_list); + + for (auto token: tokens) { + bool found = false; + Gtk::TreeModel::Children children = font_lister->get_font_list()->children(); + for (auto iter2: children) { + Gtk::TreeModel::Row row2 = *iter2; + Glib::ustring family2 = row2[font_lister->FontList.family]; + bool onSystem2 = row2[font_lister->FontList.onSystem]; + // CSS dictates that font family names are case insensitive. + // This should really implement full Unicode case unfolding. + if (onSystem2 && token.casefold().compare(family2.casefold()) == 0) { + found = true; + break; + } + } + + if (!found) { + missing_font_list += token; + missing_font_list += ", "; + } + } + + // Remove extra comma and space from end. + if (missing_font_list.size() >= 2) { + missing_font_list.resize(missing_font_list.size() - 2); + } + + return missing_font_list; +} + + +// Callbacks + +// Need to update style list +void +FontSelectorToolbar::on_family_changed() { + + if (signal_block) return; + signal_block = true; + + Glib::ustring family = family_combo.get_entry_text(); + + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + fontlister->set_font_family (family); + + signal_block = false; + + // Let world know + changed_emit(); +} + +void +FontSelectorToolbar::on_style_changed() { + + if (signal_block) return; + signal_block = true; + + Glib::ustring style = style_combo.get_entry_text(); + + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + fontlister->set_font_style (style); + + signal_block = false; + + // Let world know + changed_emit(); +} + +void +FontSelectorToolbar::on_icon_pressed (Gtk::EntryIconPosition icon_position, const GdkEventButton* event) { + std::cout << "FontSelectorToolbar::on_entry_icon_pressed" << std::endl; + std::cout << " .... Should select all items with same font-family. FIXME" << std::endl; + // Call equivalent of sp_text_toolbox_select_cb() in text-toolbar.cpp + // Should be action! (Maybe: select_all_fontfamily( Glib::ustring font_family );). + // Check how Find dialog works. +} + +// bool +// FontSelectorToolbar::on_match_selected (const Gtk::TreeModel::iterator& iter) +// { +// std::cout << "on_match_selected" << std::endl; +// std::cout << " FIXME" << std::endl; +// Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); +// Glib::ustring family = (*iter)[font_lister->FontList.family]; +// std::cout << " family: " << family << std::endl; +// return false; // Leave it to default handler to set entry text. +// } + +// Return focus to canvas. +bool +FontSelectorToolbar::on_key_press_event (GdkEventKey* key_event) +{ + bool consumed = false; + + unsigned int key = 0; + gdk_keymap_translate_keyboard_state( Gdk::Display::get_default()->get_keymap(), + key_event->hardware_keycode, + (GdkModifierType)key_event->state, + 0, &key, nullptr, nullptr, nullptr ); + + switch ( key ) { + + case GDK_KEY_Escape: + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + { + // Defocus + std::cerr << "FontSelectorToolbar::on_key_press_event: Defocus: FIXME" << std::endl; + consumed = true; + } + break; + } + + return consumed; // Leave it to default handler if false. +} + +void +FontSelectorToolbar::changed_emit() { + signal_block = true; + changed_signal.emit (); + signal_block = false; +} + +} // namespace Widget +} // 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 : diff --git a/src/ui/widget/font-selector-toolbar.h b/src/ui/widget/font-selector-toolbar.h new file mode 100644 index 0000000..53cdcea --- /dev/null +++ b/src/ui/widget/font-selector-toolbar.h @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2018 Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * + * The routines here create and manage a font selector widget with two parts, + * one each for font-family and font-style. + * + * This is essentially a toolbar version of the 'FontSelector' widget. Someday + * this may be merged with it. + * + * The main functions are: + * Create the font-selector toolbar widget. + * Update the lists when a new text selection is made. + * Update the Style list when a new font-family is selected, highlighting the + * best match to the original font style (as not all fonts have the same style options). + * Update the on-screen text. + * Provide the currently selected values. + */ + +#ifndef INKSCAPE_UI_WIDGET_FONT_SELECTOR_TOOLBAR_H +#define INKSCAPE_UI_WIDGET_FONT_SELECTOR_TOOLBAR_H + +#include <gtkmm/grid.h> +#include <gtkmm/treeview.h> +#include <gtkmm/comboboxtext.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A container of widgets for selecting font faces. + * + * It is used by Text tool toolbar. The FontSelectorToolbar class utilizes the + * FontLister class to obtain a list of font-families and their associated styles for fonts either + * on the system or in the document. The FontLister class is also used by the Text toolbar. Fonts + * are kept track of by their "fontspecs" which are the same as the strings that Pango generates. + * + * The main functions are: + * Create the font-selector widget. + * Update the child widgets when a new text selection is made. + * Update the Style list when a new font-family is selected, highlighting the + * best match to the original font style (as not all fonts have the same style options). + * Emit a signal when any change is made to a child widget. + */ +class FontSelectorToolbar : public Gtk::Grid +{ + +public: + + /** + * Constructor + */ + FontSelectorToolbar (); + +protected: + + // Font family + Gtk::ComboBox family_combo; + Gtk::CellRendererText family_cell; + + // Font style + Gtk::ComboBoxText style_combo; + Gtk::CellRendererText style_cell; + +private: + + // Make a list of missing fonts for tooltip and for warning icon. + Glib::ustring get_missing_fonts (); + + // Signal handlers + void on_family_changed(); + void on_style_changed(); + void on_icon_pressed (Gtk::EntryIconPosition icon_position, const GdkEventButton* event); + // bool on_match_selected (const Gtk::TreeModel::iterator& iter); + bool on_key_press_event (GdkEventKey* key_event) override; + + // Signals + sigc::signal<void> changed_signal; + void changed_emit(); + bool signal_block; + +public: + + /** + * Update GUI based on font-selector values. + */ + void update_font (); + + /** + * Let others know that user has changed GUI settings. + */ + sigc::connection connectChanged(sigc::slot<void> slot) { + return changed_signal.connect(slot); + } +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_FONT_SETTINGS_TOOLBAR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/font-selector.cpp b/src/ui/widget/font-selector.cpp new file mode 100644 index 0000000..0617e87 --- /dev/null +++ b/src/ui/widget/font-selector.cpp @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2018 Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <glibmm/markup.h> + +#include "font-selector.h" + +#include "libnrtype/font-lister.h" +#include "libnrtype/font-instance.h" + +// For updating from selection +#include "inkscape.h" +#include "desktop.h" +#include "object/sp-text.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +FontSelector::FontSelector (bool with_size, bool with_variations) + : Gtk::Grid () + , family_frame (_("Font family")) + , style_frame (C_("Font selector", "Style")) + , size_label (_("Font size")) + , size_combobox (true) // With entry + , signal_block (false) + , font_size (18) +{ + + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + Glib::RefPtr<Gtk::TreeModel> model = font_lister->get_font_list(); + int total = model->children().size(); + int height = 30; + if (total > 1000) { + height = 30000/total; + g_warning("You have a huge number of font families (%d), " + "and Cairo is limiting the size of widgets you can draw.\n" + "Your preview cell height is capped to %d.", + total, height); + // hope we dont need a forced height because now pango line height + // not add data outside parent rendered expanding it so no naturall cells become over 30 height + } + family_cell.set_fixed_size(-1, height); + // Font family + family_treecolumn.pack_start (family_cell, false); + family_treecolumn.set_fixed_width (120); // limit minimal width to keep entire dialog narrow; column can still grow + family_treecolumn.add_attribute (family_cell, "text", 0); + family_treecolumn.set_cell_data_func (family_cell, &font_lister_cell_data_func); + + family_treeview.set_row_separator_func (&font_lister_separator_func); + family_treeview.set_model (model); + family_treeview.set_name ("FontSelector: Family"); + family_treeview.set_headers_visible (false); + family_treeview.append_column (family_treecolumn); + + family_scroll.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + family_scroll.add (family_treeview); + + family_frame.set_hexpand (true); + family_frame.set_vexpand (true); + family_frame.add (family_scroll); + + // Style + style_treecolumn.pack_start (style_cell, false); + style_treecolumn.add_attribute (style_cell, "text", 0); + style_treecolumn.set_cell_data_func (style_cell, sigc::mem_fun(*this, &FontSelector::style_cell_data_func)); + style_treecolumn.set_title ("Face"); + style_treecolumn.set_resizable (true); + + style_treeview.set_model (font_lister->get_style_list()); + style_treeview.set_name ("FontSelectorStyle"); + style_treeview.append_column ("CSS", font_lister->FontStyleList.cssStyle); + style_treeview.append_column (style_treecolumn); + + style_treeview.get_column(0)->set_resizable (true); + + style_scroll.set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + style_scroll.add (style_treeview); + + style_frame.set_hexpand (true); + style_frame.set_vexpand (true); + style_frame.add (style_scroll); + + // Size + size_combobox.set_name ("FontSelectorSize"); + if (auto entry = size_combobox.get_entry()) { + // limit min size of the entry box to 6 chars, so it doesn't inflate entire dialog! + entry->set_width_chars(6); + } + set_sizes(); + size_combobox.set_active_text( "18" ); + + // Font Variations + font_variations.set_vexpand (true); + font_variations_scroll.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + font_variations_scroll.add (font_variations); + + // Grid + set_name ("FontSelectorGrid"); + set_row_spacing(4); + set_column_spacing(4); + // Add extra columns to the "family frame" to change space distribution + // by prioritizing font family over styles + const int extra = 4; + attach (family_frame, 0, 0, 1 + extra, 2); + attach (style_frame, 1 + extra, 0, 2, 1); + if (with_size) { // Glyph panel does not use size. + attach (size_label, 1 + extra, 1, 1, 1); + attach (size_combobox, 2 + extra, 1, 1, 1); + } + if (with_variations) { // Glyphs panel does not use variations. + attach (font_variations_scroll, 0, 2, 3 + extra, 1); + } + + // Add signals + family_treeview.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_family_changed)); + style_treeview.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_style_changed)); + size_combobox.signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_size_changed)); + font_variations.connectChanged(sigc::mem_fun(*this, &FontSelector::on_variations_changed)); + + show_all_children(); + + // Initialize font family lists. (May already be done.) Should be done on document change. + font_lister->update_font_list(SP_ACTIVE_DESKTOP->getDocument()); +} + +void +FontSelector::set_sizes () +{ + size_combobox.remove_all(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + + int sizes[] = { + 4, 6, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 28, + 32, 36, 40, 48, 56, 64, 72, 144 + }; + + // Array must be same length as SPCSSUnit in style-internal.h + // PX PT PC MM CM IN EM EX % + double ratios[] = {1, 1, 1, 10, 4, 40, 100, 16, 8, 0.16}; + + for (int i : sizes) + { + double size = i/ratios[unit]; + size_combobox.append( Glib::ustring::format(size) ); + } +} + +void +FontSelector::set_fontsize_tooltip() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + Glib::ustring tooltip = Glib::ustring::format(_("Font size"), " (", sp_style_get_css_unit_string(unit), ")"); + size_combobox.set_tooltip_text (tooltip); +} + +// Update GUI. +// We keep a private copy of the style list as the font-family in widget is only temporary +// until the "Apply" button is set so the style list can be different from that in +// FontLister. +void +FontSelector::update_font () +{ + signal_block = true; + + Inkscape::FontLister *font_lister = Inkscape::FontLister::get_instance(); + Gtk::TreePath path; + Glib::ustring family = font_lister->get_font_family(); + Glib::ustring style = font_lister->get_font_style(); + + // Set font family + try { + path = font_lister->get_row_for_font (family); + } catch (...) { + std::cerr << "FontSelector::update_font: Couldn't find row for font-family: " + << family << std::endl; + path.clear(); + path.push_back(0); + } + + Gtk::TreePath currentPath; + Gtk::TreeViewColumn *currentColumn; + family_treeview.get_cursor(currentPath, currentColumn); + if (currentPath.empty() || !font_lister->is_path_for_font(currentPath, family)) { + family_treeview.set_cursor (path); + family_treeview.scroll_to_row (path); + } + + // Get font-lister style list for selected family + Gtk::TreeModel::Row row = *(family_treeview.get_model()->get_iter (path)); + GList *styles; + row.get_value(1, styles); + + // Copy font-lister style list to private list store, searching for match. + Gtk::TreeModel::iterator match; + FontLister::FontStyleListClass FontStyleList; + Glib::RefPtr<Gtk::ListStore> local_style_list_store = Gtk::ListStore::create(FontStyleList); + for ( ; styles; styles = styles->next ) { + Gtk::TreeModel::iterator treeModelIter = local_style_list_store->append(); + (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)styles->data)->CssName; + (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)styles->data)->DisplayName; + if (style == ((StyleNames*)styles->data)->CssName) { + match = treeModelIter; + } + } + + // Attach store to tree view and select row. + style_treeview.set_model (local_style_list_store); + if (match) { + style_treeview.get_selection()->select (match); + } + + Glib::ustring fontspec = font_lister->get_fontspec(); + update_variations(fontspec); + + signal_block = false; +} + +void +FontSelector::update_size (double size) +{ + signal_block = true; + + // Set font size + std::stringstream ss; + ss << size; + size_combobox.get_entry()->set_text( ss.str() ); + font_size = size; // Store value + set_fontsize_tooltip(); + + signal_block = false; +} + + +// If use_variations is true (default), we get variation values from variations widget otherwise we +// get values from CSS widget (we need to be able to keep the two widgets synchronized both ways). +Glib::ustring +FontSelector::get_fontspec(bool use_variations) { + + // Build new fontspec from GUI settings + Glib::ustring family = "Sans"; // Default...family list may not have been constructed. + Gtk::TreeModel::iterator iter = family_treeview.get_selection()->get_selected(); + if (iter) { + (*iter).get_value(0, family); + } + + Glib::ustring style = "Normal"; + iter = style_treeview.get_selection()->get_selected(); + if (iter) { + (*iter).get_value(0, style); + } + + if (family.empty()) { + std::cerr << "FontSelector::get_fontspec: empty family!" << std::endl; + } + + if (style.empty()) { + std::cerr << "FontSelector::get_fontspec: empty style!" << std::endl; + } + + Glib::ustring fontspec = family + ", "; + + if (use_variations) { + // Clip any font_variation data in 'style' as we'll replace it. + auto pos = style.find('@'); + if (pos != Glib::ustring::npos) { + style.erase (pos, style.length()-1); + } + + Glib::ustring variations = font_variations.get_pango_string(); + + if (variations.empty()) { + fontspec += style; + } else { + fontspec += variations; + } + } else { + fontspec += style; + } + + return fontspec; +} + +void +FontSelector::style_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter) +{ + Glib::ustring family = "Sans"; // Default...family list may not have been constructed. + Gtk::TreeModel::iterator iter_family = family_treeview.get_selection()->get_selected(); + if (iter_family) { + (*iter_family).get_value(0, family); + } + + Glib::ustring style = "Normal"; + (*iter).get_value(1, style); + + Glib::ustring style_escaped = Glib::Markup::escape_text( style ); + Glib::ustring font_desc = Glib::Markup::escape_text( family + ", " + style ); + Glib::ustring markup; + + markup = "<span font='" + font_desc + "'>" + style_escaped + "</span>"; + + // std::cout << " markup: " << markup << " (" << name << ")" << std::endl; + + renderer->set_property("markup", markup); +} + + +// Callbacks + +// Need to update style list +void +FontSelector::on_family_changed() { + + if (signal_block) return; + signal_block = true; + + Glib::RefPtr<Gtk::TreeModel> model; + Gtk::TreeModel::iterator iter = family_treeview.get_selection()->get_selected(model); + + if (!iter) { + // This can happen just after the family list is recreated. + signal_block = false; + return; + } + + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + fontlister->ensureRowStyles(model, iter); + + Gtk::TreeModel::Row row = *iter; + + // Get family name + Glib::ustring family; + row.get_value(0, family); + + // Get style list (TO DO: Get rid of GList) + GList *styles; + row.get_value(1, styles); + + // Find best style match for selected family with current style (e.g. of selected text). + Glib::ustring style = fontlister->get_font_style(); + Glib::ustring best = fontlister->get_best_style_match (family, style); + + // Create are own store of styles for selected font-family (the font-family selected + // in the dialog may not be the same as stored in the font-lister class until the + // "Apply" button is triggered). + Gtk::TreeModel::iterator it_best; + FontLister::FontStyleListClass FontStyleList; + Glib::RefPtr<Gtk::ListStore> local_style_list_store = Gtk::ListStore::create(FontStyleList); + + // Build list and find best match. + for ( ; styles; styles = styles->next ) { + Gtk::TreeModel::iterator treeModelIter = local_style_list_store->append(); + (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)styles->data)->CssName; + (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)styles->data)->DisplayName; + if (best == ((StyleNames*)styles->data)->CssName) { + it_best = treeModelIter; + } + } + + // Attach store to tree view and select row. + style_treeview.set_model (local_style_list_store); + if (it_best) { + style_treeview.get_selection()->select (it_best); + } + + signal_block = false; + + // Let world know + changed_emit(); +} + +void +FontSelector::on_style_changed() { + if (signal_block) return; + + // Update variations widget if new style selected from style widget. + signal_block = true; + Glib::ustring fontspec = get_fontspec( false ); + update_variations(fontspec); + signal_block = false; + + // Let world know + changed_emit(); +} + +void +FontSelector::on_size_changed() { + + if (signal_block) return; + + double size; + Glib::ustring input = size_combobox.get_active_text(); + try { + size = std::stod (input); + } + catch (std::invalid_argument) { + std::cerr << "FontSelector::on_size_changed: Invalid input: " << input << std::endl; + size = -1; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // Arbitrary: Text and Font preview freezes with huge font sizes. + int max_size = prefs->getInt("/dialogs/textandfont/maxFontSize", 10000); + + if (size <= 0) { + return; + } + if (size > max_size) + size = max_size; + + if (fabs(font_size - size) > 0.001) { + font_size = size; + // Let world know + changed_emit(); + } +} + +void +FontSelector::on_variations_changed() { + + if (signal_block) return; + + // Let world know + changed_emit(); +} + +void +FontSelector::changed_emit() { + signal_block = true; + signal_changed.emit (get_fontspec()); + if (initial) { + initial = false; + family_treecolumn.unset_cell_data_func (family_cell); + family_treecolumn.set_cell_data_func (family_cell, &font_lister_cell_data_func_markup); + } + signal_block = false; +} + +void FontSelector::update_variations(const Glib::ustring& fontspec) { + font_variations.update(fontspec); + + // Check if there are any variations available; if not, don't expand font_variations_scroll + bool hasContent = font_variations.variations_present(); + font_variations_scroll.set_vexpand(hasContent); +} + +} // namespace Widget +} // 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 : diff --git a/src/ui/widget/font-selector.h b/src/ui/widget/font-selector.h new file mode 100644 index 0000000..e1d358e --- /dev/null +++ b/src/ui/widget/font-selector.h @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2018 Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * + * The routines here create and manage a font selector widget with three parts, + * one each for font-family, font-style, and font-size. + * + * It is used by the TextEdit and Glyphs panel dialogs. The FontLister class is used + * to access the list of font-families and their associated styles for fonts either + * on the system or in the document. The FontLister class is also used by the Text + * toolbar. Fonts are kept track of by their "fontspecs" which are the same as the + * strings that Pango generates. + * + * The main functions are: + * Create the font-seletor widget. + * Update the lists when a new text selection is made. + * Update the Style list when a new font-family is selected, highlighting the + * best match to the original font style (as not all fonts have the same style options). + * Emit a signal when any change is made so that the Text Preview can be updated. + * Provide the currently selected values. + */ + +#ifndef INKSCAPE_UI_WIDGET_FONT_SELECTOR_H +#define INKSCAPE_UI_WIDGET_FONT_SELECTOR_H + +#include <gtkmm/grid.h> +#include <gtkmm/frame.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/treeview.h> +#include <gtkmm/label.h> +#include <gtkmm/comboboxtext.h> + +#include "ui/widget/font-variations.h" +#include "ui/widget/scrollprotected.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A container of widgets for selecting font faces. + * + * It is used by the TextEdit and Glyphs panel dialogs. The FontSelector class utilizes the + * FontLister class to obtain a list of font-families and their associated styles for fonts either + * on the system or in the document. The FontLister class is also used by the Text toolbar. Fonts + * are kept track of by their "fontspecs" which are the same as the strings that Pango generates. + * + * The main functions are: + * Create the font-selector widget. + * Update the child widgets when a new text selection is made. + * Update the Style list when a new font-family is selected, highlighting the + * best match to the original font style (as not all fonts have the same style options). + * Emit a signal when any change is made to a child widget. + */ +class FontSelector : public Gtk::Grid +{ + +public: + + /** + * Constructor + */ + FontSelector (bool with_size = true, bool with_variations = true); + +protected: + + // Font family + Gtk::Frame family_frame; + Gtk::ScrolledWindow family_scroll; + Gtk::TreeView family_treeview; + Gtk::TreeViewColumn family_treecolumn; + Gtk::CellRendererText family_cell; + + // Font style + Gtk::Frame style_frame; + Gtk::ScrolledWindow style_scroll; + Gtk::TreeView style_treeview; + Gtk::TreeViewColumn style_treecolumn; + Gtk::CellRendererText style_cell; + + // Font size + Gtk::Label size_label; + ScrollProtected<Gtk::ComboBoxText> size_combobox; + + // Font variations + Gtk::ScrolledWindow font_variations_scroll; + FontVariations font_variations; + +private: + + // Set sizes in font size combobox. + void set_sizes(); + void set_fontsize_tooltip(); + + // Use font style when listing style names. + void style_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter); + + // Signal handlers + void on_family_changed(); + void on_style_changed(); + void on_size_changed(); + void on_variations_changed(); + + // Signals + sigc::signal<void, Glib::ustring> signal_changed; + void changed_emit(); + bool signal_block; + + // Variables + double font_size; + bool initial = true; + + // control font variations update and UI element size + void update_variations(const Glib::ustring& fontspec); + +public: + + /** + * Update GUI based on fontspec + */ + void update_font (); + void update_size (double size); + + /** + * Get fontspec based on current settings. (Does not handle size, yet.) + */ + Glib::ustring get_fontspec(bool use_variations = true); + + /** + * Get font size. Could be merged with fontspec. + */ + double get_fontsize() { return font_size; }; + + /** + * Let others know that user has changed GUI settings. + * (Used to enable 'Apply' and 'Default' buttons.) + */ + sigc::connection connectChanged(sigc::slot<void, Glib::ustring> slot) { + return signal_changed.connect(slot); + } +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_FONT_SETTINGS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/font-variants.cpp b/src/ui/widget/font-variants.cpp new file mode 100644 index 0000000..3ec77c8 --- /dev/null +++ b/src/ui/widget/font-variants.cpp @@ -0,0 +1,1459 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2015, 2018 Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> +#include <glibmm/i18n.h> + +#include <libnrtype/font-instance.h> + +#include "font-variants.h" + +// For updating from selection +#include "desktop.h" +#include "object/sp-text.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + + // A simple class to handle UI for one feature. We could of derived this from Gtk::HBox but by + // attaching widgets directly to Gtk::Grid, we keep columns lined up (which may or may not be a + // good thing). + class Feature + { + public: + Feature( const Glib::ustring& name, OTSubstitution& glyphs, int options, Glib::ustring family, Gtk::Grid& grid, int &row, FontVariants* parent) + : _name (name) + { + Gtk::Label* table_name = Gtk::manage (new Gtk::Label()); + table_name->set_markup ("\"" + name + "\" "); + + grid.attach (*table_name, 0, row, 1, 1); + + Gtk::FlowBox* flow_box = nullptr; + Gtk::ScrolledWindow* scrolled_window = nullptr; + if (options > 2) { + // If there are more than 2 option, pack them into a flowbox instead of directly putting them in the grid. + // Some fonts might have a table with many options (Bungee Hairline table 'ornm' has 113 entries). + flow_box = Gtk::manage (new Gtk::FlowBox()); + flow_box->set_selection_mode(); // Turn off selection + flow_box->set_homogeneous(); + flow_box->set_max_children_per_line (100); // Override default value + flow_box->set_min_children_per_line (10); // Override default value + + // We pack this into a scrollbar... otherwise the minimum height is set to what is required to fit all + // flow box children into the flow box when the flow box has minimum width. (Crazy if you ask me!) + scrolled_window = Gtk::manage (new Gtk::ScrolledWindow()); + scrolled_window->set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + scrolled_window->add(*flow_box); + } + + Gtk::RadioButton::Group group; + for (int i = 0; i < options; ++i) { + + // Create radio button and create or add to button group. + Gtk::RadioButton* button = Gtk::manage (new Gtk::RadioButton()); + if (i == 0) { + group = button->get_group(); + } else { + button->set_group (group); + } + button->signal_clicked().connect ( sigc::mem_fun(*parent, &FontVariants::feature_callback) ); + buttons.push_back (button); + + // Create label. + Gtk::Label* label = Gtk::manage (new Gtk::Label()); + + // Restrict label width (some fonts have lots of alternatives). + label->set_line_wrap( true ); + label->set_line_wrap_mode( Pango::WRAP_WORD_CHAR ); + label->set_ellipsize( Pango::ELLIPSIZE_END ); + label->set_lines(3); + label->set_hexpand(); + + Glib::ustring markup; + markup += "<span font_family='"; + markup += family; + markup += "' font_features='"; + markup += name; + markup += " "; + markup += std::to_string (i); + markup += "'>"; + markup += Glib::Markup::escape_text (glyphs.input); + markup += "</span>"; + label->set_markup (markup); + + // Add button and label to widget + if (!flow_box) { + // Attach directly to grid (keeps things aligned row-to-row). + grid.attach (*button, 2*i+1, row, 1, 1); + grid.attach (*label, 2*i+2, row, 1, 1); + } else { + // Pack into FlowBox + + // Pack button and label into a box so they stay together. + Gtk::Box* box = Gtk::manage (new Gtk::Box()); + box->add(*button); + box->add(*label); + + flow_box->add(*box); + } + } + + if (scrolled_window) { + grid.attach (*scrolled_window, 1, row, 4, 1); + } + } + + Glib::ustring + get_css() + { + int i = 0; + for (auto b: buttons) { + if (b->get_active()) { + if (i == 0) { + // Features are always off by default (for those handled here). + return ""; + } else if (i == 1) { + // Feature without value has implied value of 1. + return ("\"" + _name + "\", "); + } else { + // Feature with value greater than 1 must be explicitly set. + return ("\"" + _name + "\" " + std::to_string (i) + ", "); + } + } + ++i; + } + return ""; + } + + void + set_active(int i) + { + if (i < buttons.size()) { + buttons[i]->set_active(); + } + } + + private: + Glib::ustring _name; + std::vector <Gtk::RadioButton*> buttons; + }; + + FontVariants::FontVariants () : + Gtk::Box (Gtk::ORIENTATION_VERTICAL), + _ligatures_frame ( Glib::ustring(C_("Font feature", "Ligatures" )) ), + _ligatures_common ( Glib::ustring(C_("Font feature", "Common" )) ), + _ligatures_discretionary ( Glib::ustring(C_("Font feature", "Discretionary")) ), + _ligatures_historical ( Glib::ustring(C_("Font feature", "Historical" )) ), + _ligatures_contextual ( Glib::ustring(C_("Font feature", "Contextual" )) ), + + _position_frame ( Glib::ustring(C_("Font feature", "Position" )) ), + _position_normal ( Glib::ustring(C_("Font feature", "Normal" )) ), + _position_sub ( Glib::ustring(C_("Font feature", "Subscript" )) ), + _position_super ( Glib::ustring(C_("Font feature", "Superscript" )) ), + + _caps_frame ( Glib::ustring(C_("Font feature", "Capitals" )) ), + _caps_normal ( Glib::ustring(C_("Font feature", "Normal" )) ), + _caps_small ( Glib::ustring(C_("Font feature", "Small" )) ), + _caps_all_small ( Glib::ustring(C_("Font feature", "All small" )) ), + _caps_petite ( Glib::ustring(C_("Font feature", "Petite" )) ), + _caps_all_petite ( Glib::ustring(C_("Font feature", "All petite" )) ), + _caps_unicase ( Glib::ustring(C_("Font feature", "Unicase" )) ), + _caps_titling ( Glib::ustring(C_("Font feature", "Titling" )) ), + + _numeric_frame ( Glib::ustring(C_("Font feature", "Numeric" )) ), + _numeric_lining ( Glib::ustring(C_("Font feature", "Lining" )) ), + _numeric_old_style ( Glib::ustring(C_("Font feature", "Old Style" )) ), + _numeric_default_style ( Glib::ustring(C_("Font feature", "Default Style")) ), + _numeric_proportional ( Glib::ustring(C_("Font feature", "Proportional" )) ), + _numeric_tabular ( Glib::ustring(C_("Font feature", "Tabular" )) ), + _numeric_default_width ( Glib::ustring(C_("Font feature", "Default Width")) ), + _numeric_diagonal ( Glib::ustring(C_("Font feature", "Diagonal" )) ), + _numeric_stacked ( Glib::ustring(C_("Font feature", "Stacked" )) ), + _numeric_default_fractions( Glib::ustring(C_("Font feature", "Default Fractions")) ), + _numeric_ordinal ( Glib::ustring(C_("Font feature", "Ordinal" )) ), + _numeric_slashed_zero ( Glib::ustring(C_("Font feature", "Slashed Zero" )) ), + + _asian_frame ( Glib::ustring(C_("Font feature", "East Asian" )) ), + _asian_default_variant ( Glib::ustring(C_("Font feature", "Default" )) ), + _asian_jis78 ( Glib::ustring(C_("Font feature", "JIS78" )) ), + _asian_jis83 ( Glib::ustring(C_("Font feature", "JIS83" )) ), + _asian_jis90 ( Glib::ustring(C_("Font feature", "JIS90" )) ), + _asian_jis04 ( Glib::ustring(C_("Font feature", "JIS04" )) ), + _asian_simplified ( Glib::ustring(C_("Font feature", "Simplified" )) ), + _asian_traditional ( Glib::ustring(C_("Font feature", "Traditional" )) ), + _asian_default_width ( Glib::ustring(C_("Font feature", "Default" )) ), + _asian_full_width ( Glib::ustring(C_("Font feature", "Full Width" )) ), + _asian_proportional_width ( Glib::ustring(C_("Font feature", "Proportional" )) ), + _asian_ruby ( Glib::ustring(C_("Font feature", "Ruby" )) ), + + _feature_frame ( Glib::ustring(C_("Font feature", "Feature Settings")) ), + _feature_label ( Glib::ustring(C_("Font feature", "Selection has different Feature Settings!")) ), + + _ligatures_changed( false ), + _position_changed( false ), + _caps_changed( false ), + _numeric_changed( false ), + _asian_changed( false ), + _feature_vbox(Gtk::ORIENTATION_VERTICAL) + + { + + set_name ( "FontVariants" ); + + // Ligatures -------------------------- + + // Add tooltips + _ligatures_common.set_tooltip_text( + _("Common ligatures. On by default. OpenType tables: 'liga', 'clig'")); + _ligatures_discretionary.set_tooltip_text( + _("Discretionary ligatures. Off by default. OpenType table: 'dlig'")); + _ligatures_historical.set_tooltip_text( + _("Historical ligatures. Off by default. OpenType table: 'hlig'")); + _ligatures_contextual.set_tooltip_text( + _("Contextual forms. On by default. OpenType table: 'calt'")); + + // Add signals + _ligatures_common.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) ); + _ligatures_discretionary.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) ); + _ligatures_historical.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) ); + _ligatures_contextual.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) ); + + // Restrict label widths (some fonts have lots of ligatures). Must also set ellipsize mode. + Gtk::Label* labels[] = { + &_ligatures_label_common, + &_ligatures_label_discretionary, + &_ligatures_label_historical, + &_ligatures_label_contextual + }; + for (auto label : labels) { + // char limit - not really needed, since number of lines is restricted + label->set_max_width_chars(999); + // show ellipsis when text overflows + label->set_ellipsize(Pango::ELLIPSIZE_END); + // up to 5 lines + label->set_lines(5); + // multiline + label->set_line_wrap(); + // break it as needed + label->set_line_wrap_mode(Pango::WRAP_WORD_CHAR); + } + + // Allow user to select characters. Not useful as this selects the ligatures. + // _ligatures_label_common.set_selectable( true ); + // _ligatures_label_discretionary.set_selectable( true ); + // _ligatures_label_historical.set_selectable( true ); + // _ligatures_label_contextual.set_selectable( true ); + + // Add to frame + _ligatures_grid.attach( _ligatures_common, 0, 0, 1, 1); + _ligatures_grid.attach( _ligatures_discretionary, 0, 1, 1, 1); + _ligatures_grid.attach( _ligatures_historical, 0, 2, 1, 1); + _ligatures_grid.attach( _ligatures_contextual, 0, 3, 1, 1); + _ligatures_grid.attach( _ligatures_label_common, 1, 0, 1, 1); + _ligatures_grid.attach( _ligatures_label_discretionary, 1, 1, 1, 1); + _ligatures_grid.attach( _ligatures_label_historical, 1, 2, 1, 1); + _ligatures_grid.attach( _ligatures_label_contextual, 1, 3, 1, 1); + + _ligatures_grid.set_margin_start(15); + _ligatures_grid.set_margin_end(15); + + _ligatures_frame.add( _ligatures_grid ); + pack_start( _ligatures_frame, Gtk::PACK_SHRINK ); + + ligatures_init(); + + // Position ---------------------------------- + + // Add tooltips + _position_normal.set_tooltip_text( _("Normal position.")); + _position_sub.set_tooltip_text( _("Subscript. OpenType table: 'subs'") ); + _position_super.set_tooltip_text( _("Superscript. OpenType table: 'sups'") ); + + // Group buttons + Gtk::RadioButton::Group position_group = _position_normal.get_group(); + _position_sub.set_group(position_group); + _position_super.set_group(position_group); + + // Add signals + _position_normal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::position_callback) ); + _position_sub.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::position_callback) ); + _position_super.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::position_callback) ); + + // Add to frame + _position_grid.attach( _position_normal, 0, 0, 1, 1); + _position_grid.attach( _position_sub, 1, 0, 1, 1); + _position_grid.attach( _position_super, 2, 0, 1, 1); + + _position_grid.set_margin_start(15); + _position_grid.set_margin_end(15); + + _position_frame.add( _position_grid ); + pack_start( _position_frame, Gtk::PACK_SHRINK ); + + position_init(); + + // Caps ---------------------------------- + + // Add tooltips + _caps_normal.set_tooltip_text( _("Normal capitalization.")); + _caps_small.set_tooltip_text( _("Small-caps (lowercase). OpenType table: 'smcp'")); + _caps_all_small.set_tooltip_text( _("All small-caps (uppercase and lowercase). OpenType tables: 'c2sc' and 'smcp'")); + _caps_petite.set_tooltip_text( _("Petite-caps (lowercase). OpenType table: 'pcap'")); + _caps_all_petite.set_tooltip_text( _("All petite-caps (uppercase and lowercase). OpenType tables: 'c2sc' and 'pcap'")); + _caps_unicase.set_tooltip_text( _("Unicase (small caps for uppercase, normal for lowercase). OpenType table: 'unic'")); + _caps_titling.set_tooltip_text( _("Titling caps (lighter-weight uppercase for use in titles). OpenType table: 'titl'")); + + // Group buttons + Gtk::RadioButton::Group caps_group = _caps_normal.get_group(); + _caps_small.set_group(caps_group); + _caps_all_small.set_group(caps_group); + _caps_petite.set_group(caps_group); + _caps_all_petite.set_group(caps_group); + _caps_unicase.set_group(caps_group); + _caps_titling.set_group(caps_group); + + // Add signals + _caps_normal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + _caps_small.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + _caps_all_small.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + _caps_petite.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + _caps_all_petite.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + _caps_unicase.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + _caps_titling.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + + // Add to frame + _caps_grid.attach( _caps_normal, 0, 0, 1, 1); + _caps_grid.attach( _caps_unicase, 1, 0, 1, 1); + _caps_grid.attach( _caps_titling, 2, 0, 1, 1); + _caps_grid.attach( _caps_small, 0, 1, 1, 1); + _caps_grid.attach( _caps_all_small, 1, 1, 1, 1); + _caps_grid.attach( _caps_petite, 2, 1, 1, 1); + _caps_grid.attach( _caps_all_petite, 3, 1, 1, 1); + + _caps_grid.set_margin_start(15); + _caps_grid.set_margin_end(15); + + _caps_frame.add( _caps_grid ); + pack_start( _caps_frame, Gtk::PACK_SHRINK ); + + caps_init(); + + // Numeric ------------------------------ + + // Add tooltips + _numeric_default_style.set_tooltip_text( _("Normal style.")); + _numeric_lining.set_tooltip_text( _("Lining numerals. OpenType table: 'lnum'")); + _numeric_old_style.set_tooltip_text( _("Old style numerals. OpenType table: 'onum'")); + _numeric_default_width.set_tooltip_text( _("Normal widths.")); + _numeric_proportional.set_tooltip_text( _("Proportional width numerals. OpenType table: 'pnum'")); + _numeric_tabular.set_tooltip_text( _("Same width numerals. OpenType table: 'tnum'")); + _numeric_default_fractions.set_tooltip_text( _("Normal fractions.")); + _numeric_diagonal.set_tooltip_text( _("Diagonal fractions. OpenType table: 'frac'")); + _numeric_stacked.set_tooltip_text( _("Stacked fractions. OpenType table: 'afrc'")); + _numeric_ordinal.set_tooltip_text( _("Ordinals (raised 'th', etc.). OpenType table: 'ordn'")); + _numeric_slashed_zero.set_tooltip_text( _("Slashed zeros. OpenType table: 'zero'")); + + // Group buttons + Gtk::RadioButton::Group style_group = _numeric_default_style.get_group(); + _numeric_lining.set_group(style_group); + _numeric_old_style.set_group(style_group); + + Gtk::RadioButton::Group width_group = _numeric_default_width.get_group(); + _numeric_proportional.set_group(width_group); + _numeric_tabular.set_group(width_group); + + Gtk::RadioButton::Group fraction_group = _numeric_default_fractions.get_group(); + _numeric_diagonal.set_group(fraction_group); + _numeric_stacked.set_group(fraction_group); + + // Add signals + _numeric_default_style.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_lining.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_old_style.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_default_width.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_proportional.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_tabular.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_default_fractions.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_diagonal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_stacked.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_ordinal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_slashed_zero.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + + // Add to frame + _numeric_grid.attach (_numeric_default_style, 0, 0, 1, 1); + _numeric_grid.attach (_numeric_lining, 1, 0, 1, 1); + _numeric_grid.attach (_numeric_lining_label, 2, 0, 1, 1); + _numeric_grid.attach (_numeric_old_style, 3, 0, 1, 1); + _numeric_grid.attach (_numeric_old_style_label, 4, 0, 1, 1); + + _numeric_grid.attach (_numeric_default_width, 0, 1, 1, 1); + _numeric_grid.attach (_numeric_proportional, 1, 1, 1, 1); + _numeric_grid.attach (_numeric_proportional_label, 2, 1, 1, 1); + _numeric_grid.attach (_numeric_tabular, 3, 1, 1, 1); + _numeric_grid.attach (_numeric_tabular_label, 4, 1, 1, 1); + + _numeric_grid.attach (_numeric_default_fractions, 0, 2, 1, 1); + _numeric_grid.attach (_numeric_diagonal, 1, 2, 1, 1); + _numeric_grid.attach (_numeric_diagonal_label, 2, 2, 1, 1); + _numeric_grid.attach (_numeric_stacked, 3, 2, 1, 1); + _numeric_grid.attach (_numeric_stacked_label, 4, 2, 1, 1); + + _numeric_grid.attach (_numeric_ordinal, 0, 3, 1, 1); + _numeric_grid.attach (_numeric_ordinal_label, 1, 3, 4, 1); + + _numeric_grid.attach (_numeric_slashed_zero, 0, 4, 1, 1); + _numeric_grid.attach (_numeric_slashed_zero_label, 1, 4, 1, 1); + + _numeric_grid.set_margin_start(15); + _numeric_grid.set_margin_end(15); + + _numeric_frame.add( _numeric_grid ); + pack_start( _numeric_frame, Gtk::PACK_SHRINK ); + + // East Asian + + // Add tooltips + _asian_default_variant.set_tooltip_text ( _("Default variant.")); + _asian_jis78.set_tooltip_text( _("JIS78 forms. OpenType table: 'jp78'.")); + _asian_jis83.set_tooltip_text( _("JIS83 forms. OpenType table: 'jp83'.")); + _asian_jis90.set_tooltip_text( _("JIS90 forms. OpenType table: 'jp90'.")); + _asian_jis04.set_tooltip_text( _("JIS2004 forms. OpenType table: 'jp04'.")); + _asian_simplified.set_tooltip_text( _("Simplified forms. OpenType table: 'smpl'.")); + _asian_traditional.set_tooltip_text( _("Traditional forms. OpenType table: 'trad'.")); + _asian_default_width.set_tooltip_text ( _("Default width.")); + _asian_full_width.set_tooltip_text( _("Full width variants. OpenType table: 'fwid'.")); + _asian_proportional_width.set_tooltip_text(_("Proportional width variants. OpenType table: 'pwid'.")); + _asian_ruby.set_tooltip_text( _("Ruby variants. OpenType table: 'ruby'.")); + + // Add signals + _asian_default_variant.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_jis78.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_jis83.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_jis90.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_jis04.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_simplified.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_traditional.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_default_width.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_full_width.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_proportional_width.signal_clicked().connect (sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_ruby.signal_clicked().connect( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + + // Add to frame + _asian_grid.attach (_asian_default_variant, 0, 0, 1, 1); + _asian_grid.attach (_asian_jis78, 1, 0, 1, 1); + _asian_grid.attach (_asian_jis83, 2, 0, 1, 1); + _asian_grid.attach (_asian_jis90, 1, 1, 1, 1); + _asian_grid.attach (_asian_jis04, 2, 1, 1, 1); + _asian_grid.attach (_asian_simplified, 1, 2, 1, 1); + _asian_grid.attach (_asian_traditional, 2, 2, 1, 1); + _asian_grid.attach (_asian_default_width, 0, 3, 1, 1); + _asian_grid.attach (_asian_full_width, 1, 3, 1, 1); + _asian_grid.attach (_asian_proportional_width, 2, 3, 1, 1); + _asian_grid.attach (_asian_ruby, 0, 4, 1, 1); + + _asian_grid.set_margin_start(15); + _asian_grid.set_margin_end(15); + + _asian_frame.add( _asian_grid ); + pack_start( _asian_frame, Gtk::PACK_SHRINK ); + + // Group Buttons + Gtk::RadioButton::Group asian_variant_group = _asian_default_variant.get_group(); + _asian_jis78.set_group(asian_variant_group); + _asian_jis83.set_group(asian_variant_group); + _asian_jis90.set_group(asian_variant_group); + _asian_jis04.set_group(asian_variant_group); + _asian_simplified.set_group(asian_variant_group); + _asian_traditional.set_group(asian_variant_group); + + Gtk::RadioButton::Group asian_width_group = _asian_default_width.get_group(); + _asian_full_width.set_group (asian_width_group); + _asian_proportional_width.set_group (asian_width_group); + + // Feature settings --------------------- + + // Add tooltips + _feature_entry.set_tooltip_text( _("Feature settings in CSS form (e.g. \"wxyz\" or \"wxyz\" 3).")); + + _feature_substitutions.set_justify( Gtk::JUSTIFY_LEFT ); + _feature_substitutions.set_line_wrap( true ); + _feature_substitutions.set_line_wrap_mode( Pango::WRAP_WORD_CHAR ); + + _feature_list.set_justify( Gtk::JUSTIFY_LEFT ); + _feature_list.set_line_wrap( true ); + + // Add to frame + _feature_vbox.pack_start( _feature_grid, Gtk::PACK_SHRINK ); + _feature_vbox.pack_start( _feature_entry, Gtk::PACK_SHRINK ); + _feature_vbox.pack_start( _feature_label, Gtk::PACK_SHRINK ); + _feature_vbox.pack_start( _feature_substitutions, Gtk::PACK_SHRINK ); + _feature_vbox.pack_start( _feature_list, Gtk::PACK_SHRINK ); + + _feature_vbox.set_margin_start(15); + _feature_vbox.set_margin_end(15); + + _feature_frame.add( _feature_vbox ); + pack_start( _feature_frame, Gtk::PACK_SHRINK ); + + // Add signals + //_feature_entry.signal_key_press_event().connect ( sigc::mem_fun(*this, &FontVariants::feature_callback) ); + _feature_entry.signal_changed().connect( sigc::mem_fun(*this, &FontVariants::feature_callback) ); + + show_all_children(); + + } + + void + FontVariants::ligatures_init() { + // std::cout << "FontVariants::ligatures_init()" << std::endl; + } + + void + FontVariants::ligatures_callback() { + // std::cout << "FontVariants::ligatures_callback()" << std::endl; + _ligatures_changed = true; + _changed_signal.emit(); + } + + void + FontVariants::position_init() { + // std::cout << "FontVariants::position_init()" << std::endl; + } + + void + FontVariants::position_callback() { + // std::cout << "FontVariants::position_callback()" << std::endl; + _position_changed = true; + _changed_signal.emit(); + } + + void + FontVariants::caps_init() { + // std::cout << "FontVariants::caps_init()" << std::endl; + } + + void + FontVariants::caps_callback() { + // std::cout << "FontVariants::caps_callback()" << std::endl; + _caps_changed = true; + _changed_signal.emit(); + } + + void + FontVariants::numeric_init() { + // std::cout << "FontVariants::numeric_init()" << std::endl; + } + + void + FontVariants::numeric_callback() { + // std::cout << "FontVariants::numeric_callback()" << std::endl; + _numeric_changed = true; + _changed_signal.emit(); + } + + void + FontVariants::asian_init() { + // std::cout << "FontVariants::asian_init()" << std::endl; + } + + void + FontVariants::asian_callback() { + // std::cout << "FontVariants::asian_callback()" << std::endl; + _asian_changed = true; + _changed_signal.emit(); + } + + void + FontVariants::feature_init() { + // std::cout << "FontVariants::feature_init()" << std::endl; + } + + void + FontVariants::feature_callback() { + // std::cout << "FontVariants::feature_callback()" << std::endl; + _feature_changed = true; + _changed_signal.emit(); + } + + // Update GUI based on query. + void + FontVariants::update( SPStyle const *query, bool different_features, Glib::ustring& font_spec ) { + + update_opentype( font_spec ); + + _ligatures_all = query->font_variant_ligatures.computed; + _ligatures_mix = query->font_variant_ligatures.value; + + _ligatures_common.set_active( _ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_COMMON ); + _ligatures_discretionary.set_active(_ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_DISCRETIONARY ); + _ligatures_historical.set_active( _ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_HISTORICAL ); + _ligatures_contextual.set_active( _ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_CONTEXTUAL ); + + _ligatures_common.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_COMMON ); + _ligatures_discretionary.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_DISCRETIONARY ); + _ligatures_historical.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_HISTORICAL ); + _ligatures_contextual.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_CONTEXTUAL ); + + _position_all = query->font_variant_position.computed; + _position_mix = query->font_variant_position.value; + + _position_normal.set_active( _position_all & SP_CSS_FONT_VARIANT_POSITION_NORMAL ); + _position_sub.set_active( _position_all & SP_CSS_FONT_VARIANT_POSITION_SUB ); + _position_super.set_active( _position_all & SP_CSS_FONT_VARIANT_POSITION_SUPER ); + + _position_normal.set_inconsistent( _position_mix & SP_CSS_FONT_VARIANT_POSITION_NORMAL ); + _position_sub.set_inconsistent( _position_mix & SP_CSS_FONT_VARIANT_POSITION_SUB ); + _position_super.set_inconsistent( _position_mix & SP_CSS_FONT_VARIANT_POSITION_SUPER ); + + _caps_all = query->font_variant_caps.computed; + _caps_mix = query->font_variant_caps.value; + + _caps_normal.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_NORMAL ); + _caps_small.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_SMALL ); + _caps_all_small.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL ); + _caps_petite.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_PETITE ); + _caps_all_petite.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE ); + _caps_unicase.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_UNICASE ); + _caps_titling.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_TITLING ); + + _caps_normal.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_NORMAL ); + _caps_small.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_SMALL ); + _caps_all_small.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL ); + _caps_petite.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_PETITE ); + _caps_all_petite.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE ); + _caps_unicase.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_UNICASE ); + _caps_titling.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_TITLING ); + + _numeric_all = query->font_variant_numeric.computed; + _numeric_mix = query->font_variant_numeric.value; + + if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_LINING_NUMS) { + _numeric_lining.set_active(); + } else if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS) { + _numeric_old_style.set_active(); + } else { + _numeric_default_style.set_active(); + } + + if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS) { + _numeric_proportional.set_active(); + } else if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_TABULAR_NUMS) { + _numeric_tabular.set_active(); + } else { + _numeric_default_width.set_active(); + } + + if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS) { + _numeric_diagonal.set_active(); + } else if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS) { + _numeric_stacked.set_active(); + } else { + _numeric_default_fractions.set_active(); + } + + _numeric_ordinal.set_active( _numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_ORDINAL ); + _numeric_slashed_zero.set_active( _numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_SLASHED_ZERO ); + + + _numeric_lining.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_LINING_NUMS ); + _numeric_old_style.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS ); + _numeric_proportional.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS ); + _numeric_tabular.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_TABULAR_NUMS ); + _numeric_diagonal.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS ); + _numeric_stacked.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS ); + _numeric_ordinal.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_ORDINAL ); + _numeric_slashed_zero.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_SLASHED_ZERO ); + + _asian_all = query->font_variant_east_asian.computed; + _asian_mix = query->font_variant_east_asian.value; + + if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS78) { + _asian_jis78.set_active(); + } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS83) { + _asian_jis83.set_active(); + } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS90) { + _asian_jis90.set_active(); + } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS04) { + _asian_jis04.set_active(); + } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED) { + _asian_simplified.set_active(); + } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL) { + _asian_traditional.set_active(); + } else { + _asian_default_variant.set_active(); + } + + if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH) { + _asian_full_width.set_active(); + } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_PROPORTIONAL_WIDTH) { + _asian_proportional_width.set_active(); + } else { + _asian_default_width.set_active(); + } + + _asian_ruby.set_active ( _asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_RUBY ); + + _asian_jis78.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS78); + _asian_jis83.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS83); + _asian_jis90.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS90); + _asian_jis04.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS04); + _asian_simplified.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED); + _asian_traditional.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL); + _asian_full_width.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH); + _asian_proportional_width.set_inconsistent(_asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_PROPORTIONAL_WIDTH); + _asian_ruby.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_RUBY); + + // Fix me: Should match a space if second part matches. ---, + // : Add boundary to 'on' and 'off'. v + Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create("\"(\\w{4})\"\\s*([0-9]+|on|off|)"); + Glib::MatchInfo matchInfo; + std::string setting; + + // Set feature radiobutton (if it exists) or add to _feature_entry string. + char const *val = query->font_feature_settings.value(); + if (val) { + + std::vector<Glib::ustring> tokens = + Glib::Regex::split_simple("\\s*,\\s*", val); + + for (auto token: tokens) { + regex->match(token, matchInfo); + if (matchInfo.matches()) { + Glib::ustring table = matchInfo.fetch(1); + Glib::ustring value = matchInfo.fetch(2); + + if (_features.find(table) != _features.end()) { + int v = 0; + if (value == "0" || value == "off") v = 0; + else if (value == "1" || value == "on" || value.empty() ) v = 1; + else v = std::stoi(value); + _features[table]->set_active(v); + } else { + setting += token + ", "; + } + } + } + } + + // Remove final ", " + if (setting.length() > 1) { + setting.pop_back(); + setting.pop_back(); + } + + // Tables without radiobuttons. + _feature_entry.set_text( setting ); + + if( different_features ) { + _feature_label.show(); + } else { + _feature_label.hide(); + } + } + + // Update GUI based on OpenType tables of selected font (which may be changed in font selector tab). + void + FontVariants::update_opentype (Glib::ustring& font_spec) { + + // Disable/Enable based on available OpenType tables. + font_instance* res = font_factory::Default()->FaceFromFontSpecification( font_spec.c_str() ); + if( res ) { + + std::map<Glib::ustring, OTSubstitution>::iterator it; + + if((it = res->openTypeTables.find("liga"))!= res->openTypeTables.end() || + (it = res->openTypeTables.find("clig"))!= res->openTypeTables.end()) { + _ligatures_common.set_sensitive(); + } else { + _ligatures_common.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("dlig"))!= res->openTypeTables.end()) { + _ligatures_discretionary.set_sensitive(); + } else { + _ligatures_discretionary.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("hlig"))!= res->openTypeTables.end()) { + _ligatures_historical.set_sensitive(); + } else { + _ligatures_historical.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("calt"))!= res->openTypeTables.end()) { + _ligatures_contextual.set_sensitive(); + } else { + _ligatures_contextual.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("subs"))!= res->openTypeTables.end()) { + _position_sub.set_sensitive(); + } else { + _position_sub.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("sups"))!= res->openTypeTables.end()) { + _position_super.set_sensitive(); + } else { + _position_super.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("smcp"))!= res->openTypeTables.end()) { + _caps_small.set_sensitive(); + } else { + _caps_small.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("c2sc"))!= res->openTypeTables.end() && + (it = res->openTypeTables.find("smcp"))!= res->openTypeTables.end()) { + _caps_all_small.set_sensitive(); + } else { + _caps_all_small.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("pcap"))!= res->openTypeTables.end()) { + _caps_petite.set_sensitive(); + } else { + _caps_petite.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("c2sc"))!= res->openTypeTables.end() && + (it = res->openTypeTables.find("pcap"))!= res->openTypeTables.end()) { + _caps_all_petite.set_sensitive(); + } else { + _caps_all_petite.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("unic"))!= res->openTypeTables.end()) { + _caps_unicase.set_sensitive(); + } else { + _caps_unicase.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("titl"))!= res->openTypeTables.end()) { + _caps_titling.set_sensitive(); + } else { + _caps_titling.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("lnum"))!= res->openTypeTables.end()) { + _numeric_lining.set_sensitive(); + } else { + _numeric_lining.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("onum"))!= res->openTypeTables.end()) { + _numeric_old_style.set_sensitive(); + } else { + _numeric_old_style.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("pnum"))!= res->openTypeTables.end()) { + _numeric_proportional.set_sensitive(); + } else { + _numeric_proportional.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("tnum"))!= res->openTypeTables.end()) { + _numeric_tabular.set_sensitive(); + } else { + _numeric_tabular.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("frac"))!= res->openTypeTables.end()) { + _numeric_diagonal.set_sensitive(); + } else { + _numeric_diagonal.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("afrac"))!= res->openTypeTables.end()) { + _numeric_stacked.set_sensitive(); + } else { + _numeric_stacked.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("ordn"))!= res->openTypeTables.end()) { + _numeric_ordinal.set_sensitive(); + } else { + _numeric_ordinal.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("zero"))!= res->openTypeTables.end()) { + _numeric_slashed_zero.set_sensitive(); + } else { + _numeric_slashed_zero.set_sensitive( false ); + } + + // East-Asian + if((it = res->openTypeTables.find("jp78"))!= res->openTypeTables.end()) { + _asian_jis78.set_sensitive(); + } else { + _asian_jis78.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("jp83"))!= res->openTypeTables.end()) { + _asian_jis83.set_sensitive(); + } else { + _asian_jis83.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("jp90"))!= res->openTypeTables.end()) { + _asian_jis90.set_sensitive(); + } else { + _asian_jis90.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("jp04"))!= res->openTypeTables.end()) { + _asian_jis04.set_sensitive(); + } else { + _asian_jis04.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("smpl"))!= res->openTypeTables.end()) { + _asian_simplified.set_sensitive(); + } else { + _asian_simplified.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("trad"))!= res->openTypeTables.end()) { + _asian_traditional.set_sensitive(); + } else { + _asian_traditional.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("fwid"))!= res->openTypeTables.end()) { + _asian_full_width.set_sensitive(); + } else { + _asian_full_width.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("pwid"))!= res->openTypeTables.end()) { + _asian_proportional_width.set_sensitive(); + } else { + _asian_proportional_width.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("ruby"))!= res->openTypeTables.end()) { + _asian_ruby.set_sensitive(); + } else { + _asian_ruby.set_sensitive( false ); + } + + // List available ligatures + Glib::ustring markup_liga; + Glib::ustring markup_dlig; + Glib::ustring markup_hlig; + Glib::ustring markup_calt; + + for (auto table: res->openTypeTables) { + + if (table.first == "liga" || + table.first == "clig" || + table.first == "dlig" || + table.first == "hgli" || + table.first == "calt") { + + Glib::ustring markup; + markup += "<span font_family='"; + markup += sp_font_description_get_family(res->descr); + markup += "'>"; + markup += Glib::Markup::escape_text(table.second.output); + markup += "</span>"; + + if (table.first == "liga") markup_liga += markup; + if (table.first == "clig") markup_liga += markup; + if (table.first == "dlig") markup_dlig += markup; + if (table.first == "hlig") markup_hlig += markup; + if (table.first == "calt") markup_calt += markup; + } + } + + _ligatures_label_common.set_markup ( markup_liga.c_str() ); + _ligatures_label_discretionary.set_markup ( markup_dlig.c_str() ); + _ligatures_label_historical.set_markup ( markup_hlig.c_str() ); + _ligatures_label_contextual.set_markup ( markup_calt.c_str() ); + + // List available numeric variants + Glib::ustring markup_lnum; + Glib::ustring markup_onum; + Glib::ustring markup_pnum; + Glib::ustring markup_tnum; + Glib::ustring markup_frac; + Glib::ustring markup_afrc; + Glib::ustring markup_ordn; + Glib::ustring markup_zero; + + for (auto table: res->openTypeTables) { + + Glib::ustring markup; + markup += "<span font_family='"; + markup += sp_font_description_get_family(res->descr); + markup += "' font_features='"; + markup += table.first; + markup += "'>"; + if (table.first == "lnum" || + table.first == "onum" || + table.first == "pnum" || + table.first == "tnum") markup += "0123456789"; + if (table.first == "zero") markup += "0"; + if (table.first == "ordn") markup += "[" + table.second.before + "]" + table.second.output; + if (table.first == "frac" || + table.first == "afrc" ) markup += "1/2 2/3 3/4 4/5 5/6"; // Can we do better? + markup += "</span>"; + + if (table.first == "lnum") markup_lnum += markup; + if (table.first == "onum") markup_onum += markup; + if (table.first == "pnum") markup_pnum += markup; + if (table.first == "tnum") markup_tnum += markup; + if (table.first == "frac") markup_frac += markup; + if (table.first == "afrc") markup_afrc += markup; + if (table.first == "ordn") markup_ordn += markup; + if (table.first == "zero") markup_zero += markup; + } + + _numeric_lining_label.set_markup ( markup_lnum.c_str() ); + _numeric_old_style_label.set_markup ( markup_onum.c_str() ); + _numeric_proportional_label.set_markup ( markup_pnum.c_str() ); + _numeric_tabular_label.set_markup ( markup_tnum.c_str() ); + _numeric_diagonal_label.set_markup ( markup_frac.c_str() ); + _numeric_stacked_label.set_markup ( markup_afrc.c_str() ); + _numeric_ordinal_label.set_markup ( markup_ordn.c_str() ); + _numeric_slashed_zero_label.set_markup ( markup_zero.c_str() ); + + // Make list of tables not handled above. + std::map<Glib::ustring, OTSubstitution> table_copy = res->openTypeTables; + if( (it = table_copy.find("liga")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("clig")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("dlig")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("hlig")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("calt")) != table_copy.end() ) table_copy.erase( it ); + + if( (it = table_copy.find("subs")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("sups")) != table_copy.end() ) table_copy.erase( it ); + + if( (it = table_copy.find("smcp")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("c2sc")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("pcap")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("c2pc")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("unic")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("titl")) != table_copy.end() ) table_copy.erase( it ); + + if( (it = table_copy.find("lnum")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("onum")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("pnum")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("tnum")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("frac")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("afrc")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("ordn")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("zero")) != table_copy.end() ) table_copy.erase( it ); + + if( (it = table_copy.find("jp78")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("jp83")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("jp90")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("jp04")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("smpl")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("trad")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("fwid")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("pwid")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("ruby")) != table_copy.end() ) table_copy.erase( it ); + + // An incomplete list of tables that should not be exposed to the user: + if( (it = table_copy.find("abvf")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("abvs")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("akhn")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("blwf")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("blws")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("ccmp")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("cjct")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("dnom")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("dtls")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("fina")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("half")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("haln")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("init")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("isol")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("locl")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("medi")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("nukt")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("numr")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("pref")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("pres")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("pstf")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("psts")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("rlig")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("rkrf")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("rphf")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("rtlm")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("ssty")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("vatu")) != table_copy.end() ) table_copy.erase( it ); + + // Clear out old features + auto children = _feature_grid.get_children(); + for (auto child: children) { + _feature_grid.remove (*child); + } + _features.clear(); + + std::string markup; + int grid_row = 0; + + // GSUB lookup type 1 (1 to 1 mapping). + for (auto table: res->openTypeTables) { + if (table.first == "case" || + table.first == "hist" || + (table.first[0] == 's' && table.first[1] == 's' && !(table.first[2] == 't'))) { + + if( (it = table_copy.find(table.first)) != table_copy.end() ) table_copy.erase( it ); + + _features[table.first] = new Feature (table.first, table.second, 2, + sp_font_description_get_family(res->descr), + _feature_grid, grid_row, this); + grid_row++; + } + } + + // GSUB lookup type 3 (1 to many mapping). Optionally type 1. + for (auto table: res->openTypeTables) { + if (table.first == "salt" || + table.first == "swsh" || + table.first == "cwsh" || + table.first == "ornm" || + table.first == "nalt" || + (table.first[0] == 'c' && table.first[1] == 'v')) { + + if (table.second.input.length() == 0) { + // This can happen if a table is not in the 'DFLT' script and 'dflt' language. + // We should be using the 'lang' attribute to find the correct tables. + // std::cerr << "FontVariants::open_type_update: " + // << table.first << " has no entries!" << std::endl; + continue; + } + + if( (it = table_copy.find(table.first)) != table_copy.end() ) table_copy.erase( it ); + + // Our lame attempt at determining number of alternative glyphs for one glyph: + int number = table.second.output.length() / table.second.input.length(); + if (number < 1) { + number = 1; // Must have at least on/off, see comment above about 'lang' attribute. + // std::cout << table.first << " " + // << table.second.output.length() << "/" + // << table.second.input.length() << "=" + // << number << std::endl; + } + + _features[table.first] = new Feature (table.first, table.second, number+1, + sp_font_description_get_family(res->descr), + _feature_grid, grid_row, this); + grid_row++; + } + } + + _feature_grid.show_all(); + + _feature_substitutions.set_markup ( markup.c_str() ); + + std::string ott_list = "OpenType tables not included above: "; + for(it = table_copy.begin(); it != table_copy.end(); ++it) { + ott_list += it->first; + ott_list += ", "; + } + + if (table_copy.size() > 0) { + ott_list.pop_back(); + ott_list.pop_back(); + _feature_list.set_text( ott_list.c_str() ); + } else { + _feature_list.set_text( "" ); + } + + } else { + std::cerr << "FontVariants::update(): Couldn't find font_instance for: " + << font_spec << std::endl; + } + + _ligatures_changed = false; + _position_changed = false; + _caps_changed = false; + _numeric_changed = false; + _feature_changed = false; + } + + void + FontVariants::fill_css( SPCSSAttr *css ) { + + // Ligatures + bool common = _ligatures_common.get_active(); + bool discretionary = _ligatures_discretionary.get_active(); + bool historical = _ligatures_historical.get_active(); + bool contextual = _ligatures_contextual.get_active(); + + if( !common && !discretionary && !historical && !contextual ) { + sp_repr_css_set_property(css, "font-variant-ligatures", "none" ); + } else if ( common && !discretionary && !historical && contextual ) { + sp_repr_css_set_property(css, "font-variant-ligatures", "normal" ); + } else { + Glib::ustring css_string; + if ( !common ) + css_string += "no-common-ligatures "; + if ( discretionary ) + css_string += "discretionary-ligatures "; + if ( historical ) + css_string += "historical-ligatures "; + if ( !contextual ) + css_string += "no-contextual "; + sp_repr_css_set_property(css, "font-variant-ligatures", css_string.c_str() ); + } + + // Position + { + unsigned position_new = SP_CSS_FONT_VARIANT_POSITION_NORMAL; + Glib::ustring css_string; + if( _position_normal.get_active() ) { + css_string = "normal"; + } else if( _position_sub.get_active() ) { + css_string = "sub"; + position_new = SP_CSS_FONT_VARIANT_POSITION_SUB; + } else if( _position_super.get_active() ) { + css_string = "super"; + position_new = SP_CSS_FONT_VARIANT_POSITION_SUPER; + } + + // 'if' may not be necessary... need to test. + if( (_position_all != position_new) || ((_position_mix != 0) && _position_changed) ) { + sp_repr_css_set_property(css, "font-variant-position", css_string.c_str() ); + } + } + + // Caps + { + //unsigned caps_new; + Glib::ustring css_string; + if( _caps_normal.get_active() ) { + css_string = "normal"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_NORMAL; + } else if( _caps_small.get_active() ) { + css_string = "small-caps"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_SMALL; + } else if( _caps_all_small.get_active() ) { + css_string = "all-small-caps"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL; + } else if( _caps_petite.get_active() ) { + css_string = "petite"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_PETITE; + } else if( _caps_all_petite.get_active() ) { + css_string = "all-petite"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE; + } else if( _caps_unicase.get_active() ) { + css_string = "unicase"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_UNICASE; + } else if( _caps_titling.get_active() ) { + css_string = "titling"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_TITLING; + //} else { + // caps_new = SP_CSS_FONT_VARIANT_CAPS_NORMAL; + } + + // May not be necessary... need to test. + //if( (_caps_all != caps_new) || ((_caps_mix != 0) && _caps_changed) ) { + sp_repr_css_set_property(css, "font-variant-caps", css_string.c_str() ); + //} + } + + // Numeric + bool default_style = _numeric_default_style.get_active(); + bool lining = _numeric_lining.get_active(); + bool old_style = _numeric_old_style.get_active(); + + bool default_width = _numeric_default_width.get_active(); + bool proportional = _numeric_proportional.get_active(); + bool tabular = _numeric_tabular.get_active(); + + bool default_fractions = _numeric_default_fractions.get_active(); + bool diagonal = _numeric_diagonal.get_active(); + bool stacked = _numeric_stacked.get_active(); + + bool ordinal = _numeric_ordinal.get_active(); + bool slashed_zero = _numeric_slashed_zero.get_active(); + + if (default_style & default_width & default_fractions & !ordinal & !slashed_zero) { + sp_repr_css_set_property(css, "font-variant-numeric", "normal"); + } else { + Glib::ustring css_string; + if ( lining ) + css_string += "lining-nums "; + if ( old_style ) + css_string += "oldstyle-nums "; + if ( proportional ) + css_string += "proportional-nums "; + if ( tabular ) + css_string += "tabular-nums "; + if ( diagonal ) + css_string += "diagonal-fractions "; + if ( stacked ) + css_string += "stacked-fractions "; + if ( ordinal ) + css_string += "ordinal "; + if ( slashed_zero ) + css_string += "slashed-zero "; + sp_repr_css_set_property(css, "font-variant-numeric", css_string.c_str() ); + } + + // East Asian + bool jis78 = _asian_jis78.get_active(); + bool jis83 = _asian_jis83.get_active(); + bool jis90 = _asian_jis90.get_active(); + bool jis04 = _asian_jis04.get_active(); + bool simplified = _asian_simplified.get_active(); + bool traditional = _asian_traditional.get_active(); + bool asian_width = _asian_default_width.get_active(); + bool fwid = _asian_full_width.get_active(); + bool pwid = _asian_proportional_width.get_active(); + bool ruby = _asian_ruby.get_active(); + + if (default_style & asian_width & !ruby) { + sp_repr_css_set_property(css, "font-variant-east-asian", "normal"); + } else { + Glib::ustring css_string; + if (jis78) css_string += "jis78 "; + if (jis83) css_string += "jis83 "; + if (jis90) css_string += "jis90 "; + if (jis04) css_string += "jis04 "; + if (simplified) css_string += "simplfied "; + if (traditional) css_string += "traditional "; + + if (fwid) css_string += "fwid "; + if (pwid) css_string += "pwid "; + + if (ruby) css_string += "ruby "; + + sp_repr_css_set_property(css, "font-variant-east-asian", css_string.c_str() ); + } + + // Feature settings + Glib::ustring feature_string; + for (auto i: _features) { + feature_string += i.second->get_css(); + } + + feature_string += _feature_entry.get_text(); + // std::cout << "feature_string: " << feature_string << std::endl; + + if (!feature_string.empty()) { + sp_repr_css_set_property(css, "font-feature-settings", feature_string.c_str()); + } else { + sp_repr_css_unset_property(css, "font-feature-settings"); + } + } + + Glib::ustring + FontVariants::get_markup() { + + Glib::ustring markup; + + // Ligatures + bool common = _ligatures_common.get_active(); + bool discretionary = _ligatures_discretionary.get_active(); + bool historical = _ligatures_historical.get_active(); + bool contextual = _ligatures_contextual.get_active(); + + if (!common) markup += "liga=0,clig=0,"; // On by default. + if (discretionary) markup += "dlig=1,"; + if (historical) markup += "hlig=1,"; + if (contextual) markup += "calt=1,"; + + // Position + if ( _position_sub.get_active() ) markup += "subs=1,"; + else if ( _position_super.get_active() ) markup += "sups=1,"; + + // Caps + if ( _caps_small.get_active() ) markup += "smcp=1,"; + else if ( _caps_all_small.get_active() ) markup += "c2sc=1,smcp=1,"; + else if ( _caps_petite.get_active() ) markup += "pcap=1,"; + else if ( _caps_all_petite.get_active() ) markup += "c2pc=1,pcap=1,"; + else if ( _caps_unicase.get_active() ) markup += "unic=1,"; + else if ( _caps_titling.get_active() ) markup += "titl=1,"; + + // Numeric + bool lining = _numeric_lining.get_active(); + bool old_style = _numeric_old_style.get_active(); + + bool proportional = _numeric_proportional.get_active(); + bool tabular = _numeric_tabular.get_active(); + + bool diagonal = _numeric_diagonal.get_active(); + bool stacked = _numeric_stacked.get_active(); + + bool ordinal = _numeric_ordinal.get_active(); + bool slashed_zero = _numeric_slashed_zero.get_active(); + + if (lining) markup += "lnum=1,"; + if (old_style) markup += "onum=1,"; + if (proportional) markup += "pnum=1,"; + if (tabular) markup += "tnum=1,"; + if (diagonal) markup += "frac=1,"; + if (stacked) markup += "afrc=1,"; + if (ordinal) markup += "ordn=1,"; + if (slashed_zero) markup += "zero=1,"; + + // East Asian + bool jis78 = _asian_jis78.get_active(); + bool jis83 = _asian_jis83.get_active(); + bool jis90 = _asian_jis90.get_active(); + bool jis04 = _asian_jis04.get_active(); + bool simplified = _asian_simplified.get_active(); + bool traditional = _asian_traditional.get_active(); + //bool asian_width = _asian_default_width.get_active(); + bool fwid = _asian_full_width.get_active(); + bool pwid = _asian_proportional_width.get_active(); + bool ruby = _asian_ruby.get_active(); + + if (jis78 ) markup += "jp78=1,"; + if (jis83 ) markup += "jp83=1,"; + if (jis90 ) markup += "jp90=1,"; + if (jis04 ) markup += "jp04=1,"; + if (simplified ) markup += "smpl=1,"; + if (traditional ) markup += "trad=1,"; + + if (fwid ) markup += "fwid=1,"; + if (pwid ) markup += "pwid=1,"; + + if (ruby ) markup += "ruby=1,"; + + // Feature settings + Glib::ustring feature_string; + for (auto i: _features) { + feature_string += i.second->get_css(); + } + + feature_string += _feature_entry.get_text(); + if (!feature_string.empty()) { + markup += feature_string; + } + + // std::cout << "|" << markup << "|" << std::endl; + return markup; + } + +} // namespace Widget +} // 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 : diff --git a/src/ui/widget/font-variants.h b/src/ui/widget/font-variants.h new file mode 100644 index 0000000..b581aa5 --- /dev/null +++ b/src/ui/widget/font-variants.h @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2015, 2018 Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_FONT_VARIANT_H +#define INKSCAPE_UI_WIDGET_FONT_VARIANT_H + +#include <gtkmm/expander.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/entry.h> +#include <gtkmm/grid.h> +#include <gtkmm/hvbox.h> + +class SPDesktop; +class SPObject; +class SPStyle; +class SPCSSAttr; + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Feature; + +/** + * A container for selecting font variants (OpenType Features). + */ +class FontVariants : public Gtk::Box +{ + +public: + + /** + * Constructor + */ + FontVariants(); + +protected: + // Ligatures: To start, use four check buttons. + Gtk::Expander _ligatures_frame; + Gtk::Grid _ligatures_grid; + Gtk::CheckButton _ligatures_common; + Gtk::CheckButton _ligatures_discretionary; + Gtk::CheckButton _ligatures_historical; + Gtk::CheckButton _ligatures_contextual; + Gtk::Label _ligatures_label_common; + Gtk::Label _ligatures_label_discretionary; + Gtk::Label _ligatures_label_historical; + Gtk::Label _ligatures_label_contextual; + + // Position: Exclusive options + Gtk::Expander _position_frame; + Gtk::Grid _position_grid; + Gtk::RadioButton _position_normal; + Gtk::RadioButton _position_sub; + Gtk::RadioButton _position_super; + + // Caps: Exclusive options (maybe a dropdown menu to save space?) + Gtk::Expander _caps_frame; + Gtk::Grid _caps_grid; + Gtk::RadioButton _caps_normal; + Gtk::RadioButton _caps_small; + Gtk::RadioButton _caps_all_small; + Gtk::RadioButton _caps_petite; + Gtk::RadioButton _caps_all_petite; + Gtk::RadioButton _caps_unicase; + Gtk::RadioButton _caps_titling; + + // Numeric: Complicated! + Gtk::Expander _numeric_frame; + Gtk::Grid _numeric_grid; + + Gtk::RadioButton _numeric_default_style; + Gtk::RadioButton _numeric_lining; + Gtk::Label _numeric_lining_label; + Gtk::RadioButton _numeric_old_style; + Gtk::Label _numeric_old_style_label; + + Gtk::RadioButton _numeric_default_width; + Gtk::RadioButton _numeric_proportional; + Gtk::Label _numeric_proportional_label; + Gtk::RadioButton _numeric_tabular; + Gtk::Label _numeric_tabular_label; + + Gtk::RadioButton _numeric_default_fractions; + Gtk::RadioButton _numeric_diagonal; + Gtk::Label _numeric_diagonal_label; + Gtk::RadioButton _numeric_stacked; + Gtk::Label _numeric_stacked_label; + + Gtk::CheckButton _numeric_ordinal; + Gtk::Label _numeric_ordinal_label; + + Gtk::CheckButton _numeric_slashed_zero; + Gtk::Label _numeric_slashed_zero_label; + + // East Asian: Complicated! + Gtk::Expander _asian_frame; + Gtk::Grid _asian_grid; + + Gtk::RadioButton _asian_default_variant; + Gtk::RadioButton _asian_jis78; + Gtk::RadioButton _asian_jis83; + Gtk::RadioButton _asian_jis90; + Gtk::RadioButton _asian_jis04; + Gtk::RadioButton _asian_simplified; + Gtk::RadioButton _asian_traditional; + + Gtk::RadioButton _asian_default_width; + Gtk::RadioButton _asian_full_width; + Gtk::RadioButton _asian_proportional_width; + + Gtk::CheckButton _asian_ruby; + + // ----- + Gtk::Expander _feature_frame; + Gtk::Grid _feature_grid; + Gtk::Box _feature_vbox; + Gtk::Entry _feature_entry; + Gtk::Label _feature_label; + Gtk::Label _feature_list; + Gtk::Label _feature_substitutions; + +private: + void ligatures_init(); + void ligatures_callback(); + + void position_init(); + void position_callback(); + + void caps_init(); + void caps_callback(); + + void numeric_init(); + void numeric_callback(); + + void asian_init(); + void asian_callback(); + + void feature_init(); +public: + void feature_callback(); + +private: + // To determine if we need to write out property (may not be necessary) + unsigned _ligatures_all; + unsigned _position_all; + unsigned _caps_all; + unsigned _numeric_all; + unsigned _asian_all; + + unsigned _ligatures_mix; + unsigned _position_mix; + unsigned _caps_mix; + unsigned _numeric_mix; + unsigned _asian_mix; + + bool _ligatures_changed; + bool _position_changed; + bool _caps_changed; + bool _numeric_changed; + bool _feature_changed; + bool _asian_changed; + + std::map<std::string, Feature*> _features; + + sigc::signal<void> _changed_signal; + +public: + + /** + * Update GUI based on query results. + */ + void update( SPStyle const *query, bool different_features, Glib::ustring& font_spec ); + + /** + * Update GUI based on OpenType features of selected font. + */ + void update_opentype( Glib::ustring& font_spec ); + + /** + * Fill SPCSSAttr based on settings of buttons. + */ + void fill_css( SPCSSAttr* css ); + + /** + * Get CSS string for markup. + */ + Glib::ustring get_markup(); + + /** + * Let others know that user has changed GUI settings. + * (Used to enable 'Apply' and 'Default' buttons.) + */ + sigc::connection connectChanged(sigc::slot<void> slot) { + return _changed_signal.connect(slot); + } +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_FONT_VARIANT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/font-variations.cpp b/src/ui/widget/font-variations.cpp new file mode 100644 index 0000000..66af8e7 --- /dev/null +++ b/src/ui/widget/font-variations.cpp @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2018 Felipe Corrêa da Silva Sanches, Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iostream> +#include <iomanip> + +#include <gtkmm.h> +#include <glibmm/i18n.h> + +#include <libnrtype/font-instance.h> + +#include "font-variations.h" + +// For updating from selection +#include "desktop.h" +#include "object/sp-text.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +FontVariationAxis::FontVariationAxis (Glib::ustring name, OTVarAxis& axis) + : name (name) +{ + + // std::cout << "FontVariationAxis::FontVariationAxis:: " + // << " name: " << name + // << " min: " << axis.minimum + // << " def: " << axis.def + // << " max: " << axis.maximum + // << " val: " << axis.set_val << std::endl; + + label = Gtk::manage( new Gtk::Label( name ) ); + add( *label ); + + precision = 2 - int( log10(axis.maximum - axis.minimum)); + if (precision < 0) precision = 0; + + scale = Gtk::manage( new Gtk::Scale() ); + scale->set_range (axis.minimum, axis.maximum); + scale->set_value (axis.set_val); + scale->set_digits (precision); + scale->set_hexpand(true); + add( *scale ); + + def = axis.def; // Default value +} + + +// ------------------------------------------------------------- // + +FontVariations::FontVariations () : + Gtk::Grid () +{ + // std::cout << "FontVariations::FontVariations" << std::endl; + set_orientation( Gtk::ORIENTATION_VERTICAL ); + set_name ("FontVariations"); + size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + show_all_children(); +} + + +// Update GUI based on query. +void +FontVariations::update (const Glib::ustring& font_spec) { + + font_instance* res = font_factory::Default()->FaceFromFontSpecification (font_spec.c_str()); + + auto children = get_children(); + for (auto child: children) { + remove ( *child ); + } + axes.clear(); + + for (auto a: res->openTypeVarAxes) { + // std::cout << "Creating axis: " << a.first << std::endl; + FontVariationAxis* axis = Gtk::manage( new FontVariationAxis( a.first, a.second )); + axes.push_back( axis ); + add( *axis ); + size_group->add_widget( *(axis->get_label()) ); // Keep labels the same width + axis->get_scale()->signal_value_changed().connect( + sigc::mem_fun(*this, &FontVariations::on_variations_change) + ); + } + + show_all_children(); +} + +void +FontVariations::fill_css( SPCSSAttr *css ) { + + // Eventually will want to favor using 'font-weight', etc. but at the moment these + // can't handle "fractional" values. See CSS Fonts Module Level 4. + sp_repr_css_set_property(css, "font-variation-settings", get_css_string().c_str()); +} + +Glib::ustring +FontVariations::get_css_string() { + + Glib::ustring css_string; + + for (auto axis: axes) { + Glib::ustring name = axis->get_name(); + + // Translate the "named" axes. (Additional names in 'stat' table, may need to handle them.) + if (name == "Width") name = "wdth"; // 'font-stretch' + if (name == "Weight") name = "wght"; // 'font-weight' + if (name == "OpticalSize") name = "opsz"; // 'font-optical-sizing' Can trigger glyph substitution. + if (name == "Slant") name = "slnt"; // 'font-style' + if (name == "Italic") name = "ital"; // 'font-style' Toggles from Roman to Italic. + + std::stringstream value; + value << std::fixed << std::setprecision(axis->get_precision()) << axis->get_value(); + css_string += "'" + name + "' " + value.str() + "', "; + } + + return css_string; +} + +Glib::ustring +FontVariations::get_pango_string() { + + Glib::ustring pango_string; + + if (!axes.empty()) { + + pango_string += "@"; + + for (auto axis: axes) { + if (axis->get_value() == axis->get_def()) continue; + Glib::ustring name = axis->get_name(); + + // Translate the "named" axes. (Additional names in 'stat' table, may need to handle them.) + if (name == "Width") name = "wdth"; // 'font-stretch' + if (name == "Weight") name = "wght"; // 'font-weight' + if (name == "OpticalSize") name = "opsz"; // 'font-optical-sizing' Can trigger glyph substitution. + if (name == "Slant") name = "slnt"; // 'font-style' + if (name == "Italic") name = "ital"; // 'font-style' Toggles from Roman to Italic. + + std::stringstream value; + value << std::fixed << std::setprecision(axis->get_precision()) << axis->get_value(); + pango_string += name + "=" + value.str() + ","; + } + + pango_string.erase (pango_string.size() - 1); // Erase last ',' or '@' + } + + return pango_string; +} + +void +FontVariations::on_variations_change() { + // std::cout << "FontVariations::on_variations_change: " << get_css_string() << std::endl;; + signal_changed.emit (); +} + +bool FontVariations::variations_present() const { + return !axes.empty(); +} + +} // namespace Widget +} // 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 : diff --git a/src/ui/widget/font-variations.h b/src/ui/widget/font-variations.h new file mode 100644 index 0000000..e3c09aa --- /dev/null +++ b/src/ui/widget/font-variations.h @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2018 Felipe Corrêa da Silva Sanches, Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_FONT_VARIATIONS_H +#define INKSCAPE_UI_WIDGET_FONT_VARIATIONS_H + +#include <gtkmm/grid.h> +#include <gtkmm/sizegroup.h> +#include <gtkmm/label.h> +#include <gtkmm/scale.h> + +#include "libnrtype/OpenTypeUtil.h" + +#include "style.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + + +/** + * A widget for a single axis: Label and Slider + */ +class FontVariationAxis : public Gtk::Grid +{ +public: + FontVariationAxis(Glib::ustring name, OTVarAxis& axis); + Glib::ustring get_name() { return name; } + Gtk::Label* get_label() { return label; } + double get_value() { return scale->get_value(); } + int get_precision() { return precision; } + Gtk::Scale* get_scale() { return scale; } + double get_def() { return def; } + +private: + + // Widgets + Glib::ustring name; + Gtk::Label* label; + Gtk::Scale* scale; + + int precision; + double def = 0.0; // Default value + + // Signals + sigc::signal<void> signal_changed; +}; + +/** + * A widget for selecting font variations (OpenType Variations). + */ +class FontVariations : public Gtk::Grid +{ + +public: + + /** + * Constructor + */ + FontVariations(); + +protected: + +public: + + /** + * Update GUI. + */ + void update(const Glib::ustring& font_spec); + + /** + * Fill SPCSSAttr based on settings of buttons. + */ + void fill_css( SPCSSAttr* css ); + + /** + * Get CSS String + */ + Glib::ustring get_css_string(); + + Glib::ustring get_pango_string(); + + void on_variations_change(); + + /** + * Let others know that user has changed GUI settings. + * (Used to enable 'Apply' and 'Default' buttons.) + */ + sigc::connection connectChanged(sigc::slot<void> slot) { + return signal_changed.connect(slot); + } + + // return true if there are some variations present + bool variations_present() const; + +private: + + std::vector<FontVariationAxis*> axes; + Glib::RefPtr<Gtk::SizeGroup> size_group; + + sigc::signal<void> signal_changed; +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_FONT_VARIATIONS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/frame.cpp b/src/ui/widget/frame.cpp new file mode 100644 index 0000000..eac4e22 --- /dev/null +++ b/src/ui/widget/frame.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Murray C + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "frame.h" + + +// Inkscape::UI::Widget::Frame + +namespace Inkscape { +namespace UI { +namespace Widget { + +Frame::Frame(Glib::ustring const &label_text /*= ""*/, gboolean label_bold /*= TRUE*/ ) + : _label(label_text, Gtk::ALIGN_END, Gtk::ALIGN_CENTER, true) +{ + set_shadow_type(Gtk::SHADOW_NONE); + + set_label_widget(_label); + set_label(label_text, label_bold); +} + +void +Frame::add(Widget& widget) +{ + Gtk::Frame::add(widget); + set_padding(4, 0, 8, 0); + show_all_children(); +} + +void +Frame::set_label(const Glib::ustring &label_text, gboolean label_bold /*= TRUE*/) +{ + if (label_bold) { + _label.set_markup(Glib::ustring("<b>") + label_text + "</b>"); + } else { + _label.set_text(label_text); + } +} + +void +Frame::set_padding (guint padding_top, guint padding_bottom, guint padding_left, guint padding_right) +{ + auto child = get_child(); + + if(child) + { + child->set_margin_top(padding_top); + child->set_margin_bottom(padding_bottom); + child->set_margin_start(padding_left); + child->set_margin_end(padding_right); + } +} + +Gtk::Label const * +Frame::get_label_widget() const +{ + return &_label; +} + +} // namespace Widget +} // 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/widget/frame.h b/src/ui/widget/frame.h new file mode 100644 index 0000000..b2934b6 --- /dev/null +++ b/src/ui/widget/frame.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Murray C + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_FRAME_H +#define INKSCAPE_UI_WIDGET_FRAME_H + +#include <gtkmm/frame.h> +#include <gtkmm/label.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Creates a Gnome HIG style indented frame with bold label + * See http://developer.gnome.org/hig-book/stable/controls-frames.html.en + */ +class Frame : public Gtk::Frame +{ +public: + + /** + * Construct a Frame Widget. + * + * @param label The frame text. + */ + Frame(Glib::ustring const &label = "", gboolean label_bold = TRUE); + + /** + * Return the label widget + */ + Gtk::Label const *get_label_widget() const; + + /** + * Add a widget to this frame + */ + void add(Widget& widget) override; + + /** + * Set the frame label text and if bold or not + */ + void set_label(const Glib::ustring &label, gboolean label_bold = TRUE); + + /** + * Set the frame padding + */ + void set_padding (guint padding_top, guint padding_bottom, guint padding_left, guint padding_right); + +protected: + Gtk::Label _label; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_FRAME_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/widget/framecheck.cpp b/src/ui/widget/framecheck.cpp new file mode 100644 index 0000000..27b3d5b --- /dev/null +++ b/src/ui/widget/framecheck.cpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include <fstream> +#include <iostream> +#include <boost/filesystem.hpp> // Using boost::filesystem instead of std::filesystem due to broken C++17 on MacOS. +#include "framecheck.h" +namespace fs = boost::filesystem; + +namespace Inkscape { +namespace FrameCheck { + +std::ostream &logfile() +{ + static std::ofstream f; + + if (!f.is_open()) { + try { + auto path = fs::temp_directory_path() / "framecheck.txt"; + auto mode = std::ios_base::out | std::ios_base::app | std::ios_base::binary; + f.open(path.string(), mode); + } catch (...) { + std::cerr << "failed to create framecheck logfile" << std::endl; + } + } + + return f; +} + +} // namespace FrameCheck +} // namespace Inkscape diff --git a/src/ui/widget/framecheck.h b/src/ui/widget/framecheck.h new file mode 100644 index 0000000..36eeea1 --- /dev/null +++ b/src/ui/widget/framecheck.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Functions for logging timing events. + * Copyright (C) 2022 PBS <pbs3141@gmail.com> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef FRAMECHECK_H +#define FRAMECHECK_H + +#include <ostream> +#include <glib.h> + +namespace Inkscape { +namespace FrameCheck { + +extern std::ostream &logfile(); + +// RAII object that logs a timing event for the duration of its lifetime. +struct Event +{ + gint64 start; + const char *name; + int subtype; + + Event() : start(-1) {} + + Event(const char *name, int subtype = 0) : start(g_get_monotonic_time()), name(name), subtype(subtype) {} + + Event(Event &&p) + { + movefrom(p); + } + + ~Event() + { + finish(); + } + + Event &operator=(Event &&p) + { + finish(); + movefrom(p); + return *this; + } + + void movefrom(Event &p) + { + start = p.start; + name = p.name; + subtype = p.subtype; + p.start = -1; + } + + void finish() + { + if (start != -1) { + logfile() << name << ' ' << start << ' ' << g_get_monotonic_time() << ' ' << subtype << '\n'; + } + } +}; + +} // namespace FrameCheck +} // namespace Inkscape + +#endif // FRAMECHECK_H diff --git a/src/ui/widget/gradient-editor.cpp b/src/ui/widget/gradient-editor.cpp new file mode 100644 index 0000000..df1b51b --- /dev/null +++ b/src/ui/widget/gradient-editor.cpp @@ -0,0 +1,653 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Gradient editor widget for "Fill and Stroke" dialog + * + * Author: + * Michael Kowalski + * + * Copyright (C) 2020-2021 Michael Kowalski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "gradient-editor.h" + +#include <gtkmm/builder.h> +#include <gtkmm/grid.h> +#include <gtkmm/togglebutton.h> +#include <gtkmm/button.h> +#include <gtkmm/menubutton.h> +#include <gtkmm/treeview.h> +#include <gtkmm/treemodelcolumn.h> +#include <glibmm/i18n.h> +#include <cairo.h> + +#include "document-undo.h" +#include "gradient-chemistry.h" +#include "gradient-selector.h" +#include "preferences.h" + +#include "display/cairo-utils.h" + +#include "io/resource.h" + +#include "object/sp-gradient-vector.h" +#include "object/sp-linear-gradient.h" + +#include "svg/css-ostringstream.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/color-preview.h" + + +namespace Inkscape { +namespace UI { +namespace Widget { + +using namespace Inkscape::IO; +using Inkscape::UI::Widget::ColorNotebook; + +class scope { +public: + scope(bool& flag): _flag(flag) { + flag = true; + } + + ~scope() { + _flag = false; + } + +private: + bool& _flag; +}; + +void set_icon(Gtk::Button& btn, gchar const* pixmap) { + if (Gtk::Image* img = sp_get_icon_image(pixmap, Gtk::ICON_SIZE_BUTTON)) { + btn.set_image(*img); + } +} + +// draw solid color circle with black outline; right side is to show checkerboard if color's alpha is > 0 +Glib::RefPtr<Gdk::Pixbuf> draw_circle(int size, guint32 rgba) { + int width = size; + int height = size; + gint w2 = width / 2; + + cairo_surface_t* s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t* cr = cairo_create(s); + + int x = 0, y = 0; + double radius = size / 2; + double degrees = M_PI / 180.0; + cairo_new_sub_path(cr); + cairo_arc(cr, x + radius, y + radius, radius, 0, 2 * M_PI); + cairo_close_path(cr); + // semi-transparent black outline + cairo_set_source_rgba(cr, 0, 0, 0, 0.2); + cairo_fill(cr); + + radius--; + + cairo_new_sub_path(cr); + cairo_line_to(cr, x + w2, 0); + cairo_line_to(cr, x + w2, height); + cairo_arc(cr, x + w2, y + w2, radius, 90 * degrees, 270 * degrees); + cairo_close_path(cr); + + // solid part + ink_cairo_set_source_rgba32(cr, rgba | 0xff); + cairo_fill(cr); + + x = w2; + + cairo_new_sub_path(cr); + cairo_arc(cr, x, y + w2, radius, -90 * degrees, 90 * degrees); + cairo_line_to(cr, x, y); + cairo_close_path(cr); + + // (semi)transparent part + if ((rgba & 0xff) != 0xff) { + cairo_pattern_t* checkers = ink_cairo_pattern_create_checkerboard(); + cairo_set_source(cr, checkers); + cairo_fill_preserve(cr); + cairo_pattern_destroy(checkers); + } + ink_cairo_set_source_rgba32(cr, rgba); + cairo_fill(cr); + + cairo_destroy(cr); + cairo_surface_flush(s); + + GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s); + return Glib::wrap(pixbuf); +} + + +Glib::RefPtr<Gdk::Pixbuf> get_stop_pixmap(SPStop* stop) { + const int size = 30; + return draw_circle(size, stop->getColor().toRGBA32(stop->getOpacity())); +} + +// get widget from builder or throw +template<class W> W& get_widget(Glib::RefPtr<Gtk::Builder>& builder, const char* id) { + W* widget; + builder->get_widget(id, widget); + if (!widget) { + throw std::runtime_error("Missing widget in a glade resource file"); + } + return *widget; +} + +Glib::RefPtr<Gtk::Builder> create_builder() { + auto glade = Resource::get_filename(Resource::UIS, "gradient-edit.glade"); + Glib::RefPtr<Gtk::Builder> builder; + try { + return Gtk::Builder::create_from_file(glade); + } + catch (Glib::Error& ex) { + g_error("Cannot load glade file for gradient editor: %s", + ex.what().c_str()); + throw; + } +} + +Glib::ustring get_repeat_icon(SPGradientSpread mode) { + const char* ico = ""; + switch (mode) { + case SP_GRADIENT_SPREAD_PAD: + ico = "gradient-spread-pad"; + break; + case SP_GRADIENT_SPREAD_REPEAT: + ico = "gradient-spread-repeat"; + break; + case SP_GRADIENT_SPREAD_REFLECT: + ico = "gradient-spread-reflect"; + break; + default: + g_warning("Missing case in %s\n", __func__); + break; + } + return ico; +} + +GradientEditor::GradientEditor(const char* prefs) : + _builder(create_builder()), + _selector(Gtk::manage(new GradientSelector())), + _repeat_icon(get_widget<Gtk::Image>(_builder, "repeatIco")), + _popover(get_widget<Gtk::Popover>(_builder, "libraryPopover")), + _stop_tree(get_widget<Gtk::TreeView>(_builder, "stopList")), + _offset_btn(get_widget<Gtk::SpinButton>(_builder, "offsetSpin")), + _show_stops_list(get_widget<Gtk::Expander>(_builder, "stopsBtn")), + _add_stop(get_widget<Gtk::Button>(_builder, "stopAdd")), + _delete_stop(get_widget<Gtk::Button>(_builder, "stopDelete")), + _stops_gallery(get_widget<Gtk::Box>(_builder, "stopsGallery")), + _colors_box(get_widget<Gtk::Box>(_builder, "colorsBox")), + _linear_btn(get_widget<Gtk::ToggleButton>(_builder, "linearBtn")), + _radial_btn(get_widget<Gtk::ToggleButton>(_builder, "radialBtn")), + _main_grid(get_widget<Gtk::Grid>(_builder, "mainGrid")), + _prefs(prefs) +{ + // gradient type buttons; not currently used, hidden, WIP + set_icon(_linear_btn, INKSCAPE_ICON("paint-gradient-linear")); + set_icon(_radial_btn, INKSCAPE_ICON("paint-gradient-radial")); + + auto& reverse = get_widget<Gtk::Button>(_builder, "reverseBtn"); + set_icon(reverse, INKSCAPE_ICON("object-flip-horizontal")); + reverse.signal_clicked().connect([=](){ reverse_gradient(); }); + + auto& gradBox = get_widget<Gtk::Box>(_builder, "gradientBox"); + const int dot_size = 8; + _gradient_image.show(); + _gradient_image.set_margin_start(dot_size / 2); + _gradient_image.set_margin_end(dot_size / 2); + // gradient stop selected in a gradient widget; sync list selection + _gradient_image.signal_stop_selected().connect([=](size_t index) { + select_stop(index); + fire_stop_selected(get_current_stop()); + }); + _gradient_image.signal_stop_offset_changed().connect([=](size_t index, double offset) { + set_stop_offset(index, offset); + }); + _gradient_image.signal_add_stop_at().connect([=](double offset) { + insert_stop_at(offset); + }); + _gradient_image.signal_delete_stop().connect([=](size_t index) { + delete_stop(index); + }); + + gradBox.pack_start(_gradient_image, true, true, 0); + + // add color selector + auto color_selector = Gtk::manage(new ColorNotebook(_selected_color)); + color_selector->set_label(_("Stop color")); + color_selector->show(); + _colors_box.pack_start(*color_selector, true, true, 0); + + // gradient library in a popup + _popover.add(*_selector); + const int h = 5; + const int v = 3; + _selector->set_margin_start(h); + _selector->set_margin_end(h); + _selector->set_margin_top(v); + _selector->set_margin_bottom(v); + _selector->show(); + _selector->show_edit_button(false); + _selector->set_gradient_size(160, 20); + _selector->set_name_col_size(120); + // gradient changed is currently the only signal that GradientSelector can emit: + _selector->signal_changed().connect([=](SPGradient* gradient) { + // new gradient selected from the library + _signal_changed.emit(gradient); + }); + + // construct store for a list of stops + _stop_columns.add(_stopObj); + _stop_columns.add(_stopIdx); + _stop_columns.add(_stopID); + _stop_columns.add(_stop_color); + _stop_list_store = Gtk::ListStore::create(_stop_columns); + _stop_tree.set_model(_stop_list_store); + // indices in the stop list view; currently hidden + // _stop_tree.append_column("n", _stopID); // 1-based stop index + _stop_tree.append_column("c", _stop_color); // and its color + + auto selection = _stop_tree.get_selection(); + selection->signal_changed().connect([=]() { + if (!_update.pending()) { + stop_selected(); + fire_stop_selected(get_current_stop()); + } + }); + + _show_stops_list.property_expanded().signal_changed().connect( + [&](){ show_stops(_show_stops_list.get_expanded()); } + ); + + set_icon(_add_stop, "list-add"); + _add_stop.signal_clicked().connect([=](){ + if (auto row = current_stop()) { + auto index = row->get_value(_stopIdx); + add_stop(static_cast<int>(index)); + } + }); + + set_icon(_delete_stop, "list-remove"); + _delete_stop.signal_clicked().connect([=]() { + if (auto row = current_stop()) { + auto index = row->get_value(_stopIdx); + delete_stop(static_cast<int>(index)); + } + }); + + // connect gradient repeat modes menu + std::tuple<const char*, SPGradientSpread> repeats[3] = { + {"repeatNone", SP_GRADIENT_SPREAD_PAD}, + {"repeatDirect", SP_GRADIENT_SPREAD_REPEAT}, + {"repeatReflected", SP_GRADIENT_SPREAD_REFLECT} + }; + for (auto& el : repeats) { + auto& item = get_widget<Gtk::MenuItem>(_builder, std::get<0>(el)); + auto mode = std::get<1>(el); + item.signal_activate().connect([=](){ set_repeat_mode(mode); }); + // pack icon and text into MenuItem, since MenuImageItem is deprecated + auto text = item.get_label(); + auto hbox = Gtk::manage(new Gtk::Box); + Gtk::Image* img = sp_get_icon_image(get_repeat_icon(mode), Gtk::ICON_SIZE_BUTTON); + hbox->pack_start(*img, false, true, 8); + auto label = Gtk::manage(new Gtk::Label); + label->set_label(text); + hbox->pack_start(*label, false, true, 8); + hbox->show_all(); + item.remove(); + item.add(*hbox); + } + + set_repeat_icon(SP_GRADIENT_SPREAD_PAD); + + _selected_color.signal_changed.connect([=]() { + set_stop_color(_selected_color.color(), _selected_color.alpha()); + }); + _selected_color.signal_dragged.connect([=]() { + set_stop_color(_selected_color.color(), _selected_color.alpha()); + }); + + _offset_btn.signal_changed().connect([=]() { + if (auto row = current_stop()) { + auto index = row->get_value(_stopIdx); + double offset = _offset_btn.get_value(); + set_stop_offset(index, offset); + } + }); + + pack_start(_main_grid); + + // restore visibility of the stop list view + _stops_list_visible = Inkscape::Preferences::get()->getBool(_prefs + "/stoplist", true); + _show_stops_list.set_expanded(_stops_list_visible); + update_stops_layout(); +} + +GradientEditor::~GradientEditor() noexcept { +} + +void GradientEditor::set_stop_color(SPColor color, float opacity) { + if (_update.pending()) return; + + SPGradient* vector = get_gradient_vector(); + if (!vector) return; + + if (auto row = current_stop()) { + auto index = row->get_value(_stopIdx); + SPStop* stop = sp_get_nth_stop(vector, index); + if (stop && _document) { + auto scoped(_update.block()); + + // update list view too + row->set_value(_stop_color, get_stop_pixmap(stop)); + + sp_set_gradient_stop_color(_document, stop, color, opacity); + } + } +} + +std::optional<Gtk::TreeRow> GradientEditor::current_stop() { + auto sel = _stop_tree.get_selection(); + auto it = sel->get_selected(); + if (!it) { + return std::nullopt; + } + else { + return *it; + } +} + +SPStop* GradientEditor::get_nth_stop(size_t index) { + if (SPGradient* vector = get_gradient_vector()) { + return sp_get_nth_stop(vector, index); + } + return nullptr; +} + +// stop has been selected in a list view +void GradientEditor::stop_selected() { + if (auto row = current_stop()) { + SPStop* stop = row->get_value(_stopObj); + if (stop) { + auto scoped(_update.block()); + + _selected_color.setColor(stop->getColor()); + _selected_color.setAlpha(stop->getOpacity()); + + auto stops = sp_get_before_after_stops(stop); + if (stops.first && stops.second) { + _offset_btn.set_range(stops.first->offset, stops.second->offset); + } + else { + _offset_btn.set_range(stops.first ? stops.first->offset : 0, stops.second ? stops.second->offset : 1); + } + _offset_btn.set_sensitive(); + _offset_btn.set_value(stop->offset); + + int index = row->get_value(_stopIdx); + _gradient_image.set_focused_stop(index); + } + } + else { + // no selection + auto scoped(_update.block()); + + _selected_color.setColor(SPColor()); + + _offset_btn.set_range(0, 0); + _offset_btn.set_value(0); + _offset_btn.set_sensitive(false); + } +} + +void GradientEditor::insert_stop_at(double offset) { + if (SPGradient* vector = get_gradient_vector()) { + // only insert new stop if there are some stops present + if (vector->hasStops()) { + SPStop* stop = sp_gradient_add_stop_at(vector, offset); + // just select next stop; newly added stop will be in a list view after selection refresh (on idle) + auto pos = sp_number_of_stops_before_stop(vector, stop); + auto selected = select_stop(pos); + fire_stop_selected(stop); + if (!selected) { + select_stop(pos); + } + } + } +} + +void GradientEditor::add_stop(int index) { + if (SPGradient* vector = get_gradient_vector()) { + if (SPStop* current = sp_get_nth_stop(vector, index)) { + SPStop* stop = sp_gradient_add_stop(vector, current); + // just select next stop; newly added stop will be in a list view after selection refresh (on idle) + select_stop(sp_number_of_stops_before_stop(vector, stop)); + fire_stop_selected(stop); + } + } +} + +void GradientEditor::delete_stop(int index) { + if (SPGradient* vector = get_gradient_vector()) { + if (SPStop* stop = sp_get_nth_stop(vector, index)) { + // try deleting a stop, if it can be + sp_gradient_delete_stop(vector, stop); + } + } +} + +// collapse/expand list of stops in the UI +void GradientEditor::show_stops(bool visible) { + _stops_list_visible = visible; + update_stops_layout(); + Inkscape::Preferences::get()->setBool(_prefs + "/stoplist", _stops_list_visible); +} + +void GradientEditor::update_stops_layout() { + if (_stops_list_visible) { + _stops_gallery.show(); + } + else { + _stops_gallery.hide(); + } +} + +void GradientEditor::reverse_gradient() { + if (_document && _gradient) { + // reverse works on a gradient definition, the one with stops: + SPGradient* vector = get_gradient_vector(); + + if (vector) { + sp_gradient_reverse_vector(vector); + DocumentUndo::done(_document, _("Reverse gradient"), INKSCAPE_ICON("color-gradient")); + } + } +} + +void GradientEditor::set_repeat_mode(SPGradientSpread mode) { + if (_update.pending()) return; + + if (_document && _gradient) { + auto scoped(_update.block()); + + // spread is set on a gradient reference, which is _gradient object + _gradient->setSpread(mode); + _gradient->updateRepr(); + + DocumentUndo::done(_document, _("Set gradient repeat"), INKSCAPE_ICON("color-gradient")); + + set_repeat_icon(mode); + } +} + +void GradientEditor::set_repeat_icon(SPGradientSpread mode) { + auto ico = get_repeat_icon(mode); + if (!ico.empty()) { + _repeat_icon.set_from_icon_name(ico, Gtk::ICON_SIZE_BUTTON); + } +} + +void GradientEditor::setGradient(SPGradient* gradient) { + auto scoped(_update.block()); + auto scoped2(_notification.block()); + _gradient = gradient; + _document = gradient ? gradient->document : nullptr; + set_gradient(gradient); +} + +SPGradient* GradientEditor::getVector() { + return _selector->getVector(); +} + +void GradientEditor::setVector(SPDocument* doc, SPGradient* vector) { + auto scoped(_update.block()); + _selector->setVector(doc, vector); +} + +void GradientEditor::setMode(SelectorMode mode) { + _selector->setMode(mode); +} + +void GradientEditor::setUnits(SPGradientUnits units) { + _selector->setUnits(units); +} + +SPGradientUnits GradientEditor::getUnits() { + return _selector->getUnits(); +} + +void GradientEditor::setSpread(SPGradientSpread spread) { + _selector->setSpread(spread); +} + +SPGradientSpread GradientEditor::getSpread() { + return _selector->getSpread(); +} + +void GradientEditor::selectStop(SPStop* selected) { + if (_notification.pending()) return; + + auto scoped(_notification.block()); + // request from the outside to sync stop selection + const auto& items = _stop_tree.get_model()->children(); + auto it = std::find_if(items.begin(), items.end(), [=](const auto& row) { + SPStop* stop = row->get_value(_stopObj); + return stop == selected; + }); + if (it != items.end()) { + select_stop(std::distance(items.begin(), it)); + } +} + +SPGradient* GradientEditor::get_gradient_vector() { + if (!_gradient) return nullptr; + return sp_gradient_get_forked_vector_if_necessary(_gradient, false); +} + +void GradientEditor::set_gradient(SPGradient* gradient) { + auto scoped(_update.block()); + + // remember which stop is selected, so we can restore it + size_t selected_stop_index = 0; + if (auto it = _stop_tree.get_selection()->get_selected()) { + selected_stop_index = it->get_value(_stopIdx); + } + + _stop_list_store->clear(); + + SPGradient* vector = gradient ? gradient->getVector() : nullptr; + + if (vector) { + vector->ensureVector(); + } + + _gradient_image.set_gradient(vector); + + if (!vector || !vector->hasStops()) return; + + size_t index = 0; + for (auto& child : vector->children) { + if (SP_IS_STOP(&child)) { + auto stop = SP_STOP(&child); + auto it = _stop_list_store->append(); + it->set_value(_stopObj, stop); + it->set_value(_stopIdx, index); + it->set_value(_stopID, Glib::ustring::compose("%1.", index + 1)); + it->set_value(_stop_color, get_stop_pixmap(stop)); + + ++index; + } + } + + auto mode = gradient->isSpreadSet() ? gradient->getSpread() : SP_GRADIENT_SPREAD_PAD; + set_repeat_icon(mode); + + // list not empty? + if (index > 0) { + select_stop(std::min(selected_stop_index, index - 1)); + // update related widgets + stop_selected(); + // + // emit_stop_selected(get_current_stop()); + } +} + +void GradientEditor::set_stop_offset(size_t index, double offset) { + if (_update.pending()) return; + + // adjust stop's offset after user edits it in offset spin button or drags stop handle + SPStop* stop = get_nth_stop(index); + if (stop) { + auto scoped(_update.block()); + + stop->offset = offset; + if (auto repr = stop->getRepr()) { + repr->setAttributeCssDouble("offset", stop->offset); + } + + DocumentUndo::maybeDone(stop->document, "gradient:stop:offset", _("Change gradient stop offset"), INKSCAPE_ICON("color-gradient")); + } +} + +// select requested stop in a list view +bool GradientEditor::select_stop(size_t index) { + if (!_gradient) return false; + + bool selected = false; + const auto& items = _stop_tree.get_model()->children(); + if (index < items.size()) { + auto it = items.begin(); + std::advance(it, index); + auto path = _stop_tree.get_model()->get_path(it); + _stop_tree.get_selection()->select(it); + _stop_tree.scroll_to_cell(path, *_stop_tree.get_column(0)); + selected = true; + } + + return selected; +} + +SPStop* GradientEditor::get_current_stop() { + if (auto row = current_stop()) { + SPStop* stop = row->get_value(_stopObj); + return stop; + } + return nullptr; +} + +void GradientEditor::fire_stop_selected(SPStop* stop) { + if (!_notification.pending()) { + auto scoped(_notification.block()); + emit_stop_selected(stop); + } +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape diff --git a/src/ui/widget/gradient-editor.h b/src/ui/widget/gradient-editor.h new file mode 100644 index 0000000..6b62164 --- /dev/null +++ b/src/ui/widget/gradient-editor.h @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_GRADIENT_EDITOR_H +#define SEEN_GRADIENT_EDITOR_H + +#include <gtkmm/box.h> +#include <gtkmm/grid.h> +#include <gtkmm/button.h> +#include <gtkmm/spinbutton.h> +#include <gtkmm/popover.h> +#include <gtkmm/image.h> +#include <gtkmm/expander.h> +#include <gtkmm/liststore.h> +#include <gtkmm/treemodelcolumn.h> +#include <gtkmm/builder.h> +#include <optional> + +#include "object/sp-gradient.h" +#include "object/sp-stop.h" +#include "ui/selected-color.h" +#include "spin-scale.h" +#include "gradient-with-stops.h" +#include "gradient-selector-interface.h" +#include "ui/operation-blocker.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +class GradientSelector; + +class GradientEditor : public Gtk::Box, public GradientSelectorInterface { +public: + GradientEditor(const char* prefs); + ~GradientEditor() noexcept override; + +private: + sigc::signal<void> _signal_grabbed; + sigc::signal<void> _signal_dragged; + sigc::signal<void> _signal_released; + sigc::signal<void, SPGradient*> _signal_changed; + +public: + decltype(_signal_changed) signal_changed() const { return _signal_changed; } + decltype(_signal_grabbed) signal_grabbed() const { return _signal_grabbed; } + decltype(_signal_dragged) signal_dragged() const { return _signal_dragged; } + decltype(_signal_released) signal_released() const { return _signal_released; } + + void setGradient(SPGradient* gradient) override; + SPGradient* getVector() override; + void setVector(SPDocument* doc, SPGradient* vector) override; + void setMode(SelectorMode mode) override; + void setUnits(SPGradientUnits units) override; + SPGradientUnits getUnits() override; + void setSpread(SPGradientSpread spread) override; + SPGradientSpread getSpread() override; + void selectStop(SPStop* selected) override; + +private: + void set_gradient(SPGradient* gradient); + void stop_selected(); + void insert_stop_at(double offset); + void add_stop(int index); + void duplicate_stop(); + void delete_stop(int index); + void show_stops(bool visible); + void update_stops_layout(); + void set_repeat_mode(SPGradientSpread mode); + void set_repeat_icon(SPGradientSpread mode); + void reverse_gradient(); + void set_stop_color(SPColor color, float opacity); + std::optional<Gtk::TreeRow> current_stop(); + SPStop* get_nth_stop(size_t index); + SPStop* get_current_stop(); + bool select_stop(size_t index); + void set_stop_offset(size_t index, double offset); + SPGradient* get_gradient_vector(); + void fire_stop_selected(SPStop* stop); + + Glib::RefPtr<Gtk::Builder> _builder; + GradientSelector* _selector; + Inkscape::UI::SelectedColor _selected_color; + Gtk::Popover& _popover; + Gtk::Image& _repeat_icon; + GradientWithStops _gradient_image; + Glib::RefPtr<Gtk::ListStore> _stop_list_store; + Gtk::TreeModelColumnRecord _stop_columns; + Gtk::TreeModelColumn<SPStop*> _stopObj; + Gtk::TreeModelColumn<size_t> _stopIdx; + Gtk::TreeModelColumn<Glib::ustring> _stopID; + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> _stop_color; + Gtk::TreeView& _stop_tree; + Gtk::SpinButton& _offset_btn; + Gtk::Button& _add_stop; + Gtk::Button& _delete_stop; + Gtk::Expander& _show_stops_list; + bool _stops_list_visible = true; + Gtk::Box& _stops_gallery; + Gtk::Box& _colors_box; + Gtk::ToggleButton& _linear_btn; + Gtk::ToggleButton& _radial_btn; + Gtk::Grid& _main_grid; + SPGradient* _gradient = nullptr; + SPDocument* _document = nullptr; + OperationBlocker _update; + OperationBlocker _notification; + Glib::ustring _prefs; +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif diff --git a/src/ui/widget/gradient-image.cpp b/src/ui/widget/gradient-image.cpp new file mode 100644 index 0000000..662082b --- /dev/null +++ b/src/ui/widget/gradient-image.cpp @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A simple gradient preview + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/sigc++.h> + +#include <glibmm/refptr.h> +#include <gdkmm/pixbuf.h> + +#include <cairomm/surface.h> + +#include "gradient-image.h" + +#include "display/cairo-utils.h" + +#include "object/sp-gradient.h" +#include "object/sp-stop.h" + +namespace Inkscape { +namespace UI { +namespace Widget { +GradientImage::GradientImage(SPGradient *gradient) + : _gradient(nullptr) +{ + set_has_window(false); + set_gradient(gradient); +} + +GradientImage::~GradientImage() +{ + if (_gradient) { + _release_connection.disconnect(); + _modified_connection.disconnect(); + _gradient = nullptr; + } +} + +void +GradientImage::size_request(GtkRequisition *requisition) const +{ + requisition->width = 54; + requisition->height = 12; +} + +void +GradientImage::get_preferred_width_vfunc(int &minimal_width, int &natural_width) const +{ + GtkRequisition requisition; + size_request(&requisition); + minimal_width = natural_width = requisition.width; +} + +void +GradientImage::get_preferred_height_vfunc(int &minimal_height, int &natural_height) const +{ + GtkRequisition requisition; + size_request(&requisition); + minimal_height = natural_height = requisition.height; +} + +bool +GradientImage::on_draw(const Cairo::RefPtr<Cairo::Context> &cr) +{ + auto allocation = get_allocation(); + + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + auto ct = cr->cobj(); + + cairo_set_source(ct, check); + cairo_paint(ct); + cairo_pattern_destroy(check); + + if (_gradient) { + auto p = _gradient->create_preview_pattern(allocation.get_width()); + cairo_set_source(ct, p); + cairo_paint(ct); + cairo_pattern_destroy(p); + } + + return true; +} + +void +GradientImage::set_gradient(SPGradient *gradient) +{ + if (_gradient) { + _release_connection.disconnect(); + _modified_connection.disconnect(); + } + + _gradient = gradient; + + if (gradient) { + _release_connection = gradient->connectRelease(sigc::mem_fun(this, &GradientImage::gradient_release)); + _modified_connection = gradient->connectModified(sigc::mem_fun(this, &GradientImage::gradient_modified)); + } + + update(); +} + +void +GradientImage::gradient_release(SPObject *) +{ + if (_gradient) { + _release_connection.disconnect(); + _modified_connection.disconnect(); + } + + _gradient = nullptr; + + update(); +} + +void +GradientImage::gradient_modified(SPObject *, guint /*flags*/) +{ + update(); +} + +void +GradientImage::update() +{ + if (get_is_drawable()) { + queue_draw(); + } +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +GdkPixbuf* +sp_gradient_to_pixbuf (SPGradient *gr, int width, int height) +{ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *ct = cairo_create(s); + + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_set_source(ct, check); + cairo_paint(ct); + cairo_pattern_destroy(check); + + if (gr) { + cairo_pattern_t *p = gr->create_preview_pattern(width); + cairo_set_source(ct, p); + cairo_paint(ct); + cairo_pattern_destroy(p); + } + + cairo_destroy(ct); + cairo_surface_flush(s); + + // no need to free s - the call below takes ownership + GdkPixbuf *pixbuf = ink_pixbuf_create_from_cairo_surface(s); + return pixbuf; +} + + +Glib::RefPtr<Gdk::Pixbuf> +sp_gradient_to_pixbuf_ref (SPGradient *gr, int width, int height) +{ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *ct = cairo_create(s); + + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_set_source(ct, check); + cairo_paint(ct); + cairo_pattern_destroy(check); + + if (gr) { + cairo_pattern_t *p = gr->create_preview_pattern(width); + cairo_set_source(ct, p); + cairo_paint(ct); + cairo_pattern_destroy(p); + } + + cairo_destroy(ct); + cairo_surface_flush(s); + + Cairo::RefPtr<Cairo::Surface> sref = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s)); + Glib::RefPtr<Gdk::Pixbuf> pixbuf = + Gdk::Pixbuf::create(sref, 0, 0, width, height); + + cairo_surface_destroy(s); + + return pixbuf; +} + + +Glib::RefPtr<Gdk::Pixbuf> +sp_gradstop_to_pixbuf_ref (SPStop *stop, int width, int height) +{ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *ct = cairo_create(s); + + /* Checkerboard background */ + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_rectangle(ct, 0, 0, width, height); + cairo_set_source(ct, check); + cairo_fill_preserve(ct); + cairo_pattern_destroy(check); + + if (stop) { + /* Alpha area */ + cairo_rectangle(ct, 0, 0, width/2, height); + ink_cairo_set_source_rgba32(ct, stop->get_rgba32()); + cairo_fill(ct); + + /* Solid area */ + cairo_rectangle(ct, width/2, 0, width, height); + ink_cairo_set_source_rgba32(ct, stop->get_rgba32() | 0xff); + cairo_fill(ct); + } + + cairo_destroy(ct); + cairo_surface_flush(s); + + Cairo::RefPtr<Cairo::Surface> sref = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s)); + Glib::RefPtr<Gdk::Pixbuf> pixbuf = + Gdk::Pixbuf::create(sref, 0, 0, width, height); + + cairo_surface_destroy(s); + + return pixbuf; +} + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/gradient-image.h b/src/ui/widget/gradient-image.h new file mode 100644 index 0000000..d583dbe --- /dev/null +++ b/src/ui/widget/gradient-image.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_GRADIENT_IMAGE_H +#define SEEN_SP_GRADIENT_IMAGE_H + +/** + * A simple gradient preview + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/refptr.h> +#include <gtkmm/widget.h> + +class SPGradient; +class SPObject; +class SPStop; + +namespace Gdk { + class Pixbuf; +} + +#include <sigc++/connection.h> + +namespace Inkscape { +namespace UI { +namespace Widget { +class GradientImage : public Gtk::Widget { + private: + SPGradient *_gradient; + + sigc::connection _release_connection; + sigc::connection _modified_connection; + + void gradient_release(SPObject *obj); + void gradient_modified(SPObject *obj, guint flags); + void update(); + void size_request(GtkRequisition *requisition) const; + + protected: + void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; + bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override; + + public: + GradientImage(SPGradient *gradient); + ~GradientImage() override; + + void set_gradient(SPGradient *gr); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +GdkPixbuf *sp_gradient_to_pixbuf (SPGradient *gr, int width, int height); +Glib::RefPtr<Gdk::Pixbuf> sp_gradient_to_pixbuf_ref (SPGradient *gr, int width, int height); +Glib::RefPtr<Gdk::Pixbuf> sp_gradstop_to_pixbuf_ref (SPStop *gr, int width, int height); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/gradient-selector-interface.h b/src/ui/widget/gradient-selector-interface.h new file mode 100644 index 0000000..b6833cf --- /dev/null +++ b/src/ui/widget/gradient-selector-interface.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_GRADIENT_SELECTOR_INTERFACE_H +#define SEEN_GRADIENT_SELECTOR_INTERFACE_H + +#include "object/sp-gradient.h" +#include "object/sp-gradient-spread.h" +#include "object/sp-gradient-units.h" + +class GradientSelectorInterface { +public: + enum SelectorMode { MODE_LINEAR, MODE_RADIAL, MODE_SWATCH }; + + // pass gradient object (SPLinearGradient or SPRadialGradient) + virtual void setGradient(SPGradient* gradient) = 0; + + virtual SPGradient* getVector() = 0; + virtual void setVector(SPDocument* doc, SPGradient* vector) = 0; + virtual void setMode(SelectorMode mode) = 0; + virtual void setUnits(SPGradientUnits units) = 0; + virtual SPGradientUnits getUnits() = 0; + virtual void setSpread(SPGradientSpread spread) = 0; + virtual SPGradientSpread getSpread() = 0; + virtual void selectStop(SPStop* selected) {}; + + sigc::signal<void (SPStop*)>& signal_stop_selected() { return _signal_stop_selected; } + void emit_stop_selected(SPStop* stop) { _signal_stop_selected.emit(stop); } + +private: + sigc::signal<void (SPStop*)> _signal_stop_selected; +}; + +#endif diff --git a/src/ui/widget/gradient-selector.cpp b/src/ui/widget/gradient-selector.cpp new file mode 100644 index 0000000..e6bfa7e --- /dev/null +++ b/src/ui/widget/gradient-selector.cpp @@ -0,0 +1,612 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gradient vector widget + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gtkmm/treeview.h> +#include <vector> + +#include "document-undo.h" +#include "document.h" +#include "gradient-chemistry.h" +#include "id-clash.h" +#include "inkscape.h" +#include "preferences.h" + +#include "object/sp-defs.h" +#include "style.h" + +#include "actions/actions-tools.h" // Invoke gradient tool +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/widget/gradient-vector-selector.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +void GradientSelector::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(*manage(Glib::wrap(child))); + btn->set_relief(Gtk::RELIEF_NONE); +} + +GradientSelector::GradientSelector() + : _blocked(false) + , _mode(MODE_LINEAR) + , _gradientUnits(SP_GRADIENT_UNITS_USERSPACEONUSE) + , _gradientSpread(SP_GRADIENT_SPREAD_PAD) +{ + set_orientation(Gtk::ORIENTATION_VERTICAL); + + /* Vectors */ + _vectors = Gtk::manage(new GradientVectorSelector(nullptr, nullptr)); + _store = _vectors->get_store(); + _columns = _vectors->get_columns(); + + _treeview = Gtk::manage(new Gtk::TreeView()); + _treeview->set_model(_store); + _treeview->set_headers_clickable(true); + _treeview->set_search_column(1); + _treeview->set_vexpand(); + _icon_renderer = Gtk::manage(new Gtk::CellRendererPixbuf()); + _text_renderer = Gtk::manage(new Gtk::CellRendererText()); + + _treeview->append_column(_("Gradient"), *_icon_renderer); + auto icon_column = _treeview->get_column(0); + icon_column->add_attribute(_icon_renderer->property_pixbuf(), _columns->pixbuf); + icon_column->set_sort_column(_columns->color); + icon_column->set_clickable(true); + + _treeview->append_column(_("Name"), *_text_renderer); + auto name_column = _treeview->get_column(1); + _text_renderer->property_editable() = true; + name_column->add_attribute(_text_renderer->property_text(), _columns->name); + name_column->set_min_width(180); + name_column->set_clickable(true); + name_column->set_resizable(true); + + _treeview->append_column("#", _columns->refcount); + auto count_column = _treeview->get_column(2); + count_column->set_clickable(true); + count_column->set_resizable(true); + + _treeview->signal_key_press_event().connect(sigc::mem_fun(this, &GradientSelector::onKeyPressEvent), false); + + _treeview->show(); + + icon_column->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::onTreeColorColClick)); + name_column->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::onTreeNameColClick)); + count_column->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::onTreeCountColClick)); + + auto tree_select_connection = _treeview->get_selection()->signal_changed().connect(sigc::mem_fun(this, &GradientSelector::onTreeSelection)); + _vectors->set_tree_select_connection(tree_select_connection); + _text_renderer->signal_edited().connect(sigc::mem_fun(this, &GradientSelector::onGradientRename)); + + _scrolled_window = Gtk::manage(new Gtk::ScrolledWindow()); + _scrolled_window->add(*_treeview); + _scrolled_window->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _scrolled_window->set_shadow_type(Gtk::SHADOW_IN); + _scrolled_window->set_size_request(0, 180); + _scrolled_window->set_hexpand(); + _scrolled_window->show(); + + pack_start(*_scrolled_window, true, true, 4); + + + /* Create box for buttons */ + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + hb->set_homogeneous(false); + pack_start(*hb, false, false, 0); + + _add = Gtk::manage(new Gtk::Button()); + style_button(_add, INKSCAPE_ICON("list-add")); + + _nonsolid.push_back(_add); + hb->pack_start(*_add, false, false, 0); + + _add->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::add_vector_clicked)); + _add->set_sensitive(false); + _add->set_relief(Gtk::RELIEF_NONE); + _add->set_tooltip_text(_("Create a duplicate gradient")); + + _del2 = Gtk::manage(new Gtk::Button()); + style_button(_del2, INKSCAPE_ICON("list-remove")); + + _nonsolid.push_back(_del2); + hb->pack_start(*_del2, false, false, 0); + _del2->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::delete_vector_clicked_2)); + _del2->set_sensitive(false); + _del2->set_relief(Gtk::RELIEF_NONE); + _del2->set_tooltip_text(_("Delete unused gradient")); + + // The only use of this button is hidden! + _edit = Gtk::manage(new Gtk::Button()); + style_button(_edit, INKSCAPE_ICON("edit")); + + _nonsolid.push_back(_edit); + hb->pack_start(*_edit, false, false, 0); + _edit->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::edit_vector_clicked)); + _edit->set_sensitive(false); + _edit->set_relief(Gtk::RELIEF_NONE); + _edit->set_tooltip_text(_("Edit gradient")); + _edit->set_no_show_all(); + + _del = Gtk::manage(new Gtk::Button()); + style_button(_del, INKSCAPE_ICON("list-remove")); + + _swatch_widgets.push_back(_del); + hb->pack_start(*_del, false, false, 0); + _del->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::delete_vector_clicked)); + _del->set_sensitive(false); + _del->set_relief(Gtk::RELIEF_NONE); + _del->set_tooltip_text(_("Delete swatch")); + + hb->show_all(); +} + +void GradientSelector::setSpread(SPGradientSpread spread) +{ + _gradientSpread = spread; + // gtk_combo_box_set_active (GTK_COMBO_BOX(this->spread), gradientSpread); +} + +void GradientSelector::setMode(SelectorMode mode) +{ + if (mode != _mode) { + _mode = mode; + if (mode == MODE_SWATCH) { + for (auto &it : _nonsolid) { + it->hide(); + } + for (auto &swatch_widget : _swatch_widgets) { + swatch_widget->show_all(); + } + + auto icon_column = _treeview->get_column(0); + icon_column->set_title(_("Swatch")); + + _vectors->setSwatched(); + } else { + for (auto &it : _nonsolid) { + it->show_all(); + } + for (auto &swatch_widget : _swatch_widgets) { + swatch_widget->hide(); + } + auto icon_column = _treeview->get_column(0); + icon_column->set_title(_("Gradient")); + } + } +} + +void GradientSelector::setUnits(SPGradientUnits units) { _gradientUnits = units; } + +SPGradientUnits GradientSelector::getUnits() { return _gradientUnits; } + +SPGradientSpread GradientSelector::getSpread() { return _gradientSpread; } + +void GradientSelector::onGradientRename(const Glib::ustring &path_string, const Glib::ustring &new_text) +{ + Gtk::TreePath path(path_string); + auto iter = _store->get_iter(path); + + if (iter) { + Gtk::TreeModel::Row row = *iter; + if (row) { + SPObject *obj = row[_columns->data]; + if (obj) { + row[_columns->name] = gr_prepare_label(obj); + if (!new_text.empty() && new_text != row[_columns->name]) { + rename_id(obj, new_text); + Inkscape::DocumentUndo::done(obj->document, _("Rename gradient"), INKSCAPE_ICON("color-gradient")); + } + } + } + } +} + +void GradientSelector::onTreeColorColClick() +{ + auto column = _treeview->get_column(0); + column->set_sort_column(_columns->color); +} + +void GradientSelector::onTreeNameColClick() +{ + auto column = _treeview->get_column(1); + column->set_sort_column(_columns->name); +} + + +void GradientSelector::onTreeCountColClick() +{ + auto column = _treeview->get_column(2); + column->set_sort_column(_columns->refcount); +} + +void GradientSelector::moveSelection(int amount, bool down, bool toEnd) +{ + auto select = _treeview->get_selection(); + auto iter = select->get_selected(); + + if (amount < 0) { + down = !down; + amount = -amount; + } + + auto canary = iter; + if (down) { + ++canary; + } else { + --canary; + } + while (canary && (toEnd || amount > 0)) { + --amount; + if (down) { + ++canary; + ++iter; + } else { + --canary; + --iter; + } + } + + select->select(iter); + _treeview->scroll_to_row(_store->get_path(iter), 0.5); +} + +bool GradientSelector::onKeyPressEvent(GdkEventKey *event) +{ + bool consume = false; + auto display = Gdk::Display::get_default(); + auto keymap = display->get_keymap(); + guint key = 0; + gdk_keymap_translate_keyboard_state(keymap, event->hardware_keycode, static_cast<GdkModifierType>(event->state), 0, + &key, 0, 0, 0); + + switch (key) { + case GDK_KEY_Up: + case GDK_KEY_KP_Up: { + moveSelection(-1); + consume = true; + break; + } + case GDK_KEY_Down: + case GDK_KEY_KP_Down: { + moveSelection(1); + consume = true; + break; + } + case GDK_KEY_Page_Up: + case GDK_KEY_KP_Page_Up: { + moveSelection(-5); + consume = true; + break; + } + + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Down: { + moveSelection(5); + consume = true; + break; + } + + case GDK_KEY_End: + case GDK_KEY_KP_End: { + moveSelection(0, true, true); + consume = true; + break; + } + + case GDK_KEY_Home: + case GDK_KEY_KP_Home: { + moveSelection(0, false, true); + consume = true; + break; + } + } + return consume; +} + +void GradientSelector::onTreeSelection() +{ + if (!_treeview) { + return; + } + + if (_blocked) { + return; + } + + if (!_treeview->has_focus()) { + /* Workaround for GTK bug on Windows/OS X + * When the treeview initially doesn't have focus and is clicked + * sometimes get_selection()->signal_changed() has the wrong selection + */ + _treeview->grab_focus(); + } + + const auto sel = _treeview->get_selection(); + if (!sel) { + return; + } + + SPGradient *obj = nullptr; + /* Single selection */ + auto iter = sel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + obj = row[_columns->data]; + } + + if (obj) { + vector_set(obj); + } + + check_del_button(); +} + +void GradientSelector::check_del_button() { + const auto sel = _treeview->get_selection(); + if (!sel) { + return; + } + + SPGradient *obj = nullptr; + /* Single selection */ + auto iter = sel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + obj = row[_columns->data]; + } + if (_del2) { + _del2->set_sensitive(obj && sp_get_gradient_refcount(obj->document, obj) < 2 && _store->children().size() > 1); + } +} + +bool GradientSelector::_checkForSelected(const Gtk::TreePath &path, const Gtk::TreeIter &iter, SPGradient *vector) +{ + bool found = false; + + Gtk::TreeModel::Row row = *iter; + if (vector == row[_columns->data]) { + _treeview->scroll_to_row(path, 0.5); + auto select = _treeview->get_selection(); + bool wasBlocked = _blocked; + _blocked = true; + select->select(iter); + _blocked = wasBlocked; + found = true; + } + + return found; +} + +void GradientSelector::selectGradientInTree(SPGradient *vector) +{ + _store->foreach (sigc::bind<SPGradient *>(sigc::mem_fun(*this, &GradientSelector::_checkForSelected), vector)); +} + +void GradientSelector::setVector(SPDocument *doc, SPGradient *vector) +{ + g_return_if_fail(!vector || SP_IS_GRADIENT(vector)); + g_return_if_fail(!vector || (vector->document == doc)); + + if (vector && !vector->hasStops()) { + return; + } + + _vectors->set_gradient(doc, vector); + + selectGradientInTree(vector); + + if (vector) { + if ((_mode == MODE_SWATCH) && vector->isSwatch()) { + if (vector->isSolid()) { + for (auto &it : _nonsolid) { + it->hide(); + } + } else { + for (auto &it : _nonsolid) { + it->show_all(); + } + } + } else if (_mode != MODE_SWATCH) { + + for (auto &swatch_widget : _swatch_widgets) { + swatch_widget->hide(); + } + for (auto &it : _nonsolid) { + it->show_all(); + } + } + + if (_edit) { + _edit->set_sensitive(true); + } + if (_add) { + _add->set_sensitive(true); + } + if (_del) { + _del->set_sensitive(true); + } + check_del_button(); + } else { + if (_edit) { + _edit->set_sensitive(false); + } + if (_add) { + _add->set_sensitive(doc != nullptr); + } + if (_del) { + _del->set_sensitive(false); + } + if (_del2) { + _del2->set_sensitive(false); + } + } +} + +SPGradient *GradientSelector::getVector() +{ + return _vectors->get_gradient(); +} + + +void GradientSelector::vector_set(SPGradient *gr) +{ + if (!_blocked) { + _blocked = true; + gr = sp_gradient_ensure_vector_normalized(gr); + setVector((gr) ? gr->document : nullptr, gr); + _signal_changed.emit(gr); + _blocked = false; + } +} + +void GradientSelector::delete_vector_clicked_2() { + const auto selection = _treeview->get_selection(); + if (!selection) { + return; + } + + SPGradient *obj = nullptr; + /* Single selection */ + Gtk::TreeModel::iterator iter = selection->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + obj = row[_columns->data]; + } + + if (obj) { + if (auto repr = obj->getRepr()) { + repr->setAttribute("inkscape:collect", "always"); + + auto move = iter; + --move; + if (!move) { + move = iter; + ++move; + } + if (move) { + selection->select(move); + _treeview->scroll_to_row(_store->get_path(move), 0.5); + } + } + } +} + +void GradientSelector::delete_vector_clicked() +{ + const auto selection = _treeview->get_selection(); + if (!selection) { + return; + } + + SPGradient *obj = nullptr; + /* Single selection */ + Gtk::TreeModel::iterator iter = selection->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + obj = row[_columns->data]; + } + + if (obj) { + std::string id = obj->getId(); + sp_gradient_unset_swatch(SP_ACTIVE_DESKTOP, id); + } +} + +void GradientSelector::edit_vector_clicked() +{ + // Invoke the gradient tool.... never actually called as button is hidden in only use! + set_active_tool(SP_ACTIVE_DESKTOP, "Gradient"); +} + +void GradientSelector::add_vector_clicked() +{ + auto doc = _vectors->get_document(); + + if (!doc) + return; + + auto gr = _vectors->get_gradient(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + Inkscape::XML::Node *repr = nullptr; + + if (gr) { + gr->getRepr()->removeAttribute("inkscape:collect"); + repr = gr->getRepr()->duplicate(xml_doc); + // Rename the new gradients id to be similar to the cloned gradients + auto new_id = generate_unique_id(doc, gr->getId()); + gr->setAttribute("id", new_id.c_str()); + doc->getDefs()->getRepr()->addChild(repr, nullptr); + } else { + repr = xml_doc->createElement("svg:linearGradient"); + Inkscape::XML::Node *stop = xml_doc->createElement("svg:stop"); + stop->setAttribute("offset", "0"); + stop->setAttribute("style", "stop-color:#000;stop-opacity:1;"); + repr->appendChild(stop); + Inkscape::GC::release(stop); + stop = xml_doc->createElement("svg:stop"); + stop->setAttribute("offset", "1"); + stop->setAttribute("style", "stop-color:#fff;stop-opacity:1;"); + repr->appendChild(stop); + Inkscape::GC::release(stop); + doc->getDefs()->getRepr()->addChild(repr, nullptr); + gr = SP_GRADIENT(doc->getObjectByRepr(repr)); + } + + _vectors->set_gradient(doc, gr); + + selectGradientInTree(gr); + + // assign gradient to selection + vector_set(gr); + + Inkscape::GC::release(repr); +} + +void GradientSelector::show_edit_button(bool show) { + if (show) _edit->show(); else _edit->hide(); +} + +void GradientSelector::set_name_col_size(int min_width) { + auto name_column = _treeview->get_column(1); + name_column->set_min_width(min_width); +} + +void GradientSelector::set_gradient_size(int width, int height) { + _vectors->set_pixmap_size(width, height); +} + +} // namespace Widget +} // 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 : diff --git a/src/ui/widget/gradient-selector.h b/src/ui/widget/gradient-selector.h new file mode 100644 index 0000000..f76949d --- /dev/null +++ b/src/ui/widget/gradient-selector.h @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_GRADIENT_SELECTOR_H +#define SEEN_GRADIENT_SELECTOR_H + +/* + * Gradient vector and position widget + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include <gtkmm/liststore.h> +#include <gtkmm/scrolledwindow.h> + +#include "object/sp-gradient-spread.h" +#include "object/sp-gradient-units.h" +#include <vector> +#include "gradient-selector-interface.h" + +class SPDocument; +class SPGradient; + +namespace Gtk { +class Button; +class CellRendererPixbuf; +class CellRendererText; +class ScrolledWindow; +class TreeView; +} // namespace Gtk + + +namespace Inkscape { +namespace UI { +namespace Widget { +class GradientVectorSelector; + +class GradientSelector : public Gtk::Box, public GradientSelectorInterface { + public: + // enum SelectorMode { MODE_LINEAR, MODE_RADIAL, MODE_SWATCH }; + + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns() + { + add(name); + add(refcount); + add(color); + add(data); + add(pixbuf); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<unsigned long> color; + Gtk::TreeModelColumn<gint> refcount; + Gtk::TreeModelColumn<SPGradient *> data; + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> pixbuf; + }; + + + private: + sigc::signal<void> _signal_grabbed; + sigc::signal<void> _signal_dragged; + sigc::signal<void> _signal_released; + sigc::signal<void, SPGradient *> _signal_changed; + SelectorMode _mode; + + SPGradientUnits _gradientUnits; + SPGradientSpread _gradientSpread; + + /* Vector selector */ + GradientVectorSelector *_vectors; + + /* Tree */ + bool _checkForSelected(const Gtk::TreePath &path, const Gtk::TreeIter &iter, SPGradient *vector); + bool onKeyPressEvent(GdkEventKey *event); + void onTreeSelection(); + void onGradientRename(const Glib::ustring &path_string, const Glib::ustring &new_text); + void onTreeNameColClick(); + void onTreeColorColClick(); + void onTreeCountColClick(); + + Gtk::TreeView *_treeview; + Gtk::ScrolledWindow *_scrolled_window; + ModelColumns *_columns; + Glib::RefPtr<Gtk::ListStore> _store; + Gtk::CellRendererPixbuf *_icon_renderer; + Gtk::CellRendererText *_text_renderer; + + /* Editing buttons */ + Gtk::Button *_edit; + Gtk::Button *_add; + Gtk::Button *_del; + Gtk::Button *_del2; + + bool _blocked; + + std::vector<Gtk::Widget *> _nonsolid; + std::vector<Gtk::Widget *> _swatch_widgets; + + void selectGradientInTree(SPGradient *vector); + void moveSelection(int amount, bool down = true, bool toEnd = false); + + void style_button(Gtk::Button *btn, char const *iconName); + void check_del_button(); + + // Signal handlers + void add_vector_clicked(); + void edit_vector_clicked(); + void delete_vector_clicked(); + void delete_vector_clicked_2(); + void vector_set(SPGradient *gr); + + public: + GradientSelector(); + + void show_edit_button(bool show); + void set_name_col_size(int min_width); + void set_gradient_size(int width, int height); + + inline decltype(_signal_changed) signal_changed() const { return _signal_changed; } + inline decltype(_signal_grabbed) signal_grabbed() const { return _signal_grabbed; } + inline decltype(_signal_dragged) signal_dragged() const { return _signal_dragged; } + inline decltype(_signal_released) signal_released() const { return _signal_released; } + + void setGradient(SPGradient* gradient) override { /* no op */ } + SPGradient *getVector() override; + void setVector(SPDocument *doc, SPGradient *vector) override; + void setMode(SelectorMode mode) override; + void setUnits(SPGradientUnits units) override; + SPGradientUnits getUnits() override; + void setSpread(SPGradientSpread spread) override; + SPGradientSpread getSpread() override; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_GRADIENT_SELECTOR_H + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/gradient-vector-selector.cpp b/src/ui/widget/gradient-vector-selector.cpp new file mode 100644 index 0000000..ac6624c --- /dev/null +++ b/src/ui/widget/gradient-vector-selector.cpp @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gradient vector selection widget + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * MenTaLguY <mental@rydia.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2004 Monash University + * Copyright (C) 2004 David Turner + * Copyright (C) 2006 MenTaLguY + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include "ui/widget/gradient-vector-selector.h" + +#include <set> + +#include <glibmm.h> +#include <glibmm/i18n.h> + +#include "gradient-chemistry.h" +#include "inkscape.h" +#include "preferences.h" +#include "desktop.h" +#include "document-undo.h" +#include "layer-manager.h" +#include "include/macros.h" +#include "selection-chemistry.h" + +#include "io/resource.h" + +#include "object/sp-defs.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-root.h" +#include "object/sp-stop.h" +#include "style.h" + +#include "svg/css-ostringstream.h" + +#include "ui/dialog-events.h" +#include "ui/selected-color.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/color-preview.h" +#include "ui/widget/gradient-image.h" + +#include "xml/repr.h" + +using Inkscape::UI::SelectedColor; + +void gr_get_usage_counts(SPDocument *doc, std::map<SPGradient *, gint> *mapUsageCount ); +unsigned long sp_gradient_to_hhssll(SPGradient *gr); + +// TODO FIXME kill these globals!!! +static Glib::ustring const prefs_path = "/dialogs/gradienteditor/"; + +namespace Inkscape { +namespace UI { +namespace Widget { + +GradientVectorSelector::GradientVectorSelector(SPDocument *doc, SPGradient *gr) +{ + _columns = new GradientSelector::ModelColumns(); + _store = Gtk::ListStore::create(*_columns); + set_orientation(Gtk::ORIENTATION_VERTICAL); + + if (doc) { + set_gradient(doc, gr); + } else { + rebuild_gui_full(); + } +} + +GradientVectorSelector::~GradientVectorSelector() +{ + if (_gr) { + _gradient_release_connection.disconnect(); + _tree_select_connection.disconnect(); + _gr = nullptr; + } + + if (_doc) { + _defs_release_connection.disconnect(); + _defs_modified_connection.disconnect(); + _doc = nullptr; + } +} + +void GradientVectorSelector::set_gradient(SPDocument *doc, SPGradient *gr) +{ +// g_message("sp_gradient_vector_selector_set_gradient(%p, %p, %p) [%s] %d %d", gvs, doc, gr, +// (gr ? gr->getId():"N/A"), +// (gr ? gr->isSwatch() : -1), +// (gr ? gr->isSolid() : -1)); + static gboolean suppress = FALSE; + + g_return_if_fail(!gr || (doc != nullptr)); + g_return_if_fail(!gr || SP_IS_GRADIENT(gr)); + g_return_if_fail(!gr || (gr->document == doc)); + g_return_if_fail(!gr || gr->hasStops()); + + if (doc != _doc) { + /* Disconnect signals */ + if (_gr) { + _gradient_release_connection.disconnect(); + _gr = nullptr; + } + if (_doc) { + _defs_release_connection.disconnect(); + _defs_modified_connection.disconnect(); + _doc = nullptr; + } + + // Connect signals + if (doc) { + _defs_release_connection = doc->getDefs()->connectRelease(sigc::mem_fun(this, &GradientVectorSelector::defs_release)); + _defs_modified_connection = doc->getDefs()->connectModified(sigc::mem_fun(this, &GradientVectorSelector::defs_modified)); + } + if (gr) { + _gradient_release_connection = gr->connectRelease(sigc::mem_fun(this, &GradientVectorSelector::gradient_release)); + } + _doc = doc; + _gr = gr; + rebuild_gui_full(); + if (!suppress) _signal_vector_set.emit(gr); + } else if (gr != _gr) { + // Harder case - keep document, rebuild list and stuff + // fixme: (Lauris) + suppress = TRUE; + set_gradient(nullptr, nullptr); + set_gradient(doc, gr); + suppress = FALSE; + _signal_vector_set.emit(gr); + } + /* The case of setting NULL -> NULL is not very interesting */ +} + +void +GradientVectorSelector::gradient_release(SPObject * /*obj*/) +{ + /* Disconnect gradient */ + if (_gr) { + _gradient_release_connection.disconnect(); + _gr = nullptr; + } + + /* Rebuild GUI */ + rebuild_gui_full(); +} + +void +GradientVectorSelector::defs_release(SPObject * /*defs*/) +{ + _doc = nullptr; + + _defs_release_connection.disconnect(); + _defs_modified_connection.disconnect(); + + /* Disconnect gradient as well */ + if (_gr) { + _gradient_release_connection.disconnect(); + _gr = nullptr; + } + + /* Rebuild GUI */ + rebuild_gui_full(); +} + +void +GradientVectorSelector::defs_modified(SPObject *defs, guint flags) +{ + /* fixme: We probably have to check some flags here (Lauris) */ + rebuild_gui_full(); +} + +void +GradientVectorSelector::rebuild_gui_full() +{ + _tree_select_connection.block(); + + /* Clear old list, if there is any */ + _store->clear(); + + /* Pick up all gradients with vectors */ + std::vector<SPGradient *> gl; + if (_gr) { + auto gradients = _gr->document->getResourceList("gradient"); + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + if ( grad->hasStops() && (grad->isSwatch() == _swatched) ) { + gl.push_back(SP_GRADIENT(gradient)); + } + } + } + + /* Get usage count of all the gradients */ + std::map<SPGradient *, gint> usageCount; + gr_get_usage_counts(_doc, &usageCount); + + if (!_doc) { + Gtk::TreeModel::Row row = *(_store->append()); + row[_columns->name] = _("No document selected"); + + } else if (gl.empty()) { + Gtk::TreeModel::Row row = *(_store->append()); + row[_columns->name] = _("No gradients in document"); + + } else if (!_gr) { + Gtk::TreeModel::Row row = *(_store->append()); + row[_columns->name] = _("No gradient selected"); + + } else { + for (auto gr:gl) { + unsigned long hhssll = sp_gradient_to_hhssll(gr); + GdkPixbuf *pixb = sp_gradient_to_pixbuf (gr, _pix_width, _pix_height); + Glib::ustring label = gr_prepare_label(gr); + + Gtk::TreeModel::Row row = *(_store->append()); + row[_columns->name] = label.c_str(); + row[_columns->color] = hhssll; + row[_columns->refcount] = usageCount[gr]; + row[_columns->data] = gr; + row[_columns->pixbuf] = Glib::wrap(pixb); + } + } + + _tree_select_connection.unblock(); +} + +void +GradientVectorSelector::setSwatched() +{ + _swatched = true; + rebuild_gui_full(); +} + +void GradientVectorSelector::set_pixmap_size(int width, int height) { + _pix_width = width; + _pix_height = height; +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +Glib::ustring gr_prepare_label(SPObject *obj) +{ + const gchar *id = obj->label() ? obj->label() : obj->getId(); + if (!id) { + id = obj->getRepr()->name(); + } + + if (strlen(id) > 14 && (!strncmp (id, "linearGradient", 14) || !strncmp (id, "radialGradient", 14))) + return gr_ellipsize_text(id+14, 35); + return gr_ellipsize_text (id, 35); +} + +/* + * Ellipse text if longer than maxlen, "50% start text + ... + ~50% end text" + * Text should be > length 8 or just return the original text + */ +Glib::ustring gr_ellipsize_text(Glib::ustring const &src, size_t maxlen) +{ + if (src.length() > maxlen && maxlen > 8) { + size_t p1 = (size_t) maxlen / 2; + size_t p2 = (size_t) src.length() - (maxlen - p1 - 1); + return src.substr(0, p1) + "…" + src.substr(p2); + } + return src; +} + + +/* + * Return a "HHSSLL" version of the first stop color so we can sort by it + */ +unsigned long sp_gradient_to_hhssll(SPGradient *gr) +{ + SPStop *stop = gr->getFirstStop(); + unsigned long rgba = stop->get_rgba32(); + float hsl[3]; + SPColor::rgb_to_hsl_floatv (hsl, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba)); + + return ((int)(hsl[0]*100 * 10000)) + ((int)(hsl[1]*100 * 100)) + ((int)(hsl[2]*100 * 1)); +} + +/* + * Map each gradient to its usage count for both fill and stroke styles + */ +void gr_get_usage_counts(SPDocument *doc, std::map<SPGradient *, gint> *mapUsageCount ) +{ + if (!doc) + return; + + for (auto item : sp_get_all_document_items(doc)) { + if (!item->getId()) + continue; + SPGradient *gr = nullptr; + gr = sp_item_get_gradient(item, true); // fill + if (gr) { + mapUsageCount->count(gr) > 0 ? (*mapUsageCount)[gr] += 1 : (*mapUsageCount)[gr] = 1; + } + gr = sp_item_get_gradient(item, false); // stroke + if (gr) { + mapUsageCount->count(gr) > 0 ? (*mapUsageCount)[gr] += 1 : (*mapUsageCount)[gr] = 1; + } + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/gradient-vector-selector.h b/src/ui/widget/gradient-vector-selector.h new file mode 100644 index 0000000..6ce8edb --- /dev/null +++ b/src/ui/widget/gradient-vector-selector.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_GRADIENT_VECTOR_H +#define SEEN_GRADIENT_VECTOR_H + +/* + * Gradient vector selection widget + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/gradient-selector.h" + +#include <gtkmm/liststore.h> +#include <sigc++/connection.h> + +class SPDocument; +class SPObject; +class SPGradient; +class SPStop; + +namespace Inkscape { +namespace UI { +namespace Widget { + +class GradientVectorSelector : public Gtk::Box { + private: + bool _swatched = false; + + SPDocument *_doc = nullptr; + SPGradient *_gr = nullptr; + + /* Gradient vectors store */ + Glib::RefPtr<Gtk::ListStore> _store; + Inkscape::UI::Widget::GradientSelector::ModelColumns *_columns; + + sigc::connection _gradient_release_connection; + sigc::connection _defs_release_connection; + sigc::connection _defs_modified_connection; + sigc::connection _tree_select_connection; + + sigc::signal<void, SPGradient *> _signal_vector_set; + + void gradient_release(SPObject *obj); + void defs_release(SPObject *defs); + void defs_modified(SPObject *defs, guint flags); + void rebuild_gui_full(); + + public: + GradientVectorSelector(SPDocument *doc, SPGradient *gradient); + ~GradientVectorSelector() override; + + void setSwatched(); + void set_gradient(SPDocument *doc, SPGradient *gr); + // width and height of gradient preview pixmap + void set_pixmap_size(int width, int height); + + inline decltype(_columns) get_columns() const { return _columns; } + inline decltype(_doc) get_document() const { return _doc; } + inline decltype(_gr) get_gradient() const { return _gr; } + inline decltype(_store) get_store() const { return _store; } + + inline decltype(_signal_vector_set) signal_vector_set() const { return _signal_vector_set; } + + inline void set_tree_select_connection(sigc::connection &connection) { _tree_select_connection = connection; } + +private: + int _pix_width = 64; + int _pix_height = 18; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +Glib::ustring gr_prepare_label (SPObject *obj); +Glib::ustring gr_ellipsize_text(Glib::ustring const &src, size_t maxlen); + +#endif // SEEN_GRADIENT_VECTOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/gradient-with-stops.cpp b/src/ui/widget/gradient-with-stops.cpp new file mode 100644 index 0000000..1d413dd --- /dev/null +++ b/src/ui/widget/gradient-with-stops.cpp @@ -0,0 +1,552 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Gradient image widget with stop handles + * + * Author: + * Michael Kowalski + * + * Copyright (C) 2020-2021 Michael Kowalski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <string> + +#include "gradient-with-stops.h" +#include "object/sp-gradient.h" +#include "object/sp-stop.h" +#include "display/cairo-utils.h" +#include "io/resource.h" +#include "ui/cursor-utils.h" +#include "ui/util.h" + +// widget's height; it should take stop template's height into account +// current value is fine-tuned to make stop handles overlap gradient image just the right amount +const int GRADIENT_WIDGET_HEIGHT = 33; +// gradient's image height (multiple of checkerboard tiles, they are 6x6) +const int GRADIENT_IMAGE_HEIGHT = 3 * 6; + +namespace Inkscape { +namespace UI { +namespace Widget { + +using namespace Inkscape::IO; + +std::string get_stop_template_path(const char* filename) { + // "stop handle" template files path + return Resource::get_filename(Resource::UIS, filename); +} + +GradientWithStops::GradientWithStops() : + _template(get_stop_template_path("gradient-stop.svg").c_str()), + _tip_template(get_stop_template_path("gradient-tip.svg").c_str()) + { + // default color, it will be updated + _background_color.set_grey(0.5); + // for theming, but not used + set_name("GradientEdit"); + // we need some events + add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::BUTTON_MOTION_MASK | + Gdk::POINTER_MOTION_MASK | Gdk::KEY_PRESS_MASK); + set_can_focus(); +} + +void GradientWithStops::set_gradient(SPGradient* gradient) { + _gradient = gradient; + + // listen to release & changes + _release = gradient ? gradient->connectRelease([=](SPObject*){ set_gradient(nullptr); }) : sigc::connection(); + _modified = gradient ? gradient->connectModified([=](SPObject*, guint){ modified(); }) : sigc::connection(); + + // TODO: check selected/focused stop index + + modified(); + + set_sensitive(gradient != nullptr); +} + +void GradientWithStops::modified() { + // gradient has been modified + + // read all stops + _stops.clear(); + + if (_gradient) { + SPStop* stop = _gradient->getFirstStop(); + while (stop) { + _stops.push_back(stop_t { + .offset = stop->offset, .color = stop->getColor(), .opacity = stop->getOpacity() + }); + stop = stop->getNextStop(); + } + } + + update(); +} + +void GradientWithStops::size_request(GtkRequisition* requisition) const { + requisition->width = 60; + requisition->height = GRADIENT_WIDGET_HEIGHT; +} + +void GradientWithStops::get_preferred_width_vfunc(int& minimal_width, int& natural_width) const { + GtkRequisition requisition; + size_request(&requisition); + minimal_width = natural_width = requisition.width; +} + +void GradientWithStops::get_preferred_height_vfunc(int& minimal_height, int& natural_height) const { + GtkRequisition requisition; + size_request(&requisition); + minimal_height = natural_height = requisition.height; +} + +void GradientWithStops::update() { + if (get_is_drawable()) { + queue_draw(); + } +} + +// capture background color when styles change +void GradientWithStops::on_style_updated() { + if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_toplevel())) { + auto sc = wnd->get_style_context(); + _background_color = get_background_color(sc); + } + + // load and cache cursors + auto wnd = get_window(); + if (wnd && !_cursor_mouseover) { + // use standard cursors: + _cursor_mouseover = Gdk::Cursor::create(get_display(), "grab"); + _cursor_dragging = Gdk::Cursor::create(get_display(), "grabbing"); + _cursor_insert = Gdk::Cursor::create(get_display(), "crosshair"); + // or custom cursors: + // _cursor_mouseover = load_svg_cursor(get_display(), wnd, "gradient-over-stop.svg"); + // _cursor_dragging = load_svg_cursor(get_display(), wnd, "gradient-drag-stop.svg"); + // _cursor_insert = load_svg_cursor(get_display(), wnd, "gradient-add-stop.svg"); + wnd->set_cursor(); + } +} + +void draw_gradient(const Cairo::RefPtr<Cairo::Context>& cr, SPGradient* gradient, int x, int width) { + cairo_pattern_t* check = ink_cairo_pattern_create_checkerboard(); + + cairo_set_source(cr->cobj(), check); + cr->fill_preserve(); + cairo_pattern_destroy(check); + + if (gradient) { + auto p = gradient->create_preview_pattern(width); + cairo_matrix_t m; + cairo_matrix_init_translate(&m, -x, 0); + cairo_pattern_set_matrix(p, &m); + cairo_set_source(cr->cobj(), p); + cr->fill(); + cairo_pattern_destroy(p); + } +} + +// return on-screen position of the UI stop corresponding to the gradient's color stop at 'index' +GradientWithStops::stop_pos_t GradientWithStops::get_stop_position(size_t index, const layout_t& layout) const { + if (!_gradient || index >= _stops.size()) { + return stop_pos_t {}; + } + + // half of the stop template width; round it to avoid half-pixel coordinates + const auto dx = round((_template.get_width_px() + 1) / 2); + + auto pos = [&](double offset) { return round(layout.x + layout.width * CLAMP(offset, 0, 1)); }; + const auto& v = _stops; + + auto offset = pos(v[index].offset); + auto left = offset - dx; + if (index > 0) { + // check previous stop; it may overlap + auto prev = pos(v[index - 1].offset) + dx; + if (prev > left) { + // overlap + left = round((left + prev) / 2); + } + } + + auto right = offset + dx; + if (index + 1 < v.size()) { + // check next stop for overlap + auto next = pos(v[index + 1].offset) - dx; + if (right > next) { + // overlap + right = round((right + next) / 2); + } + } + + return stop_pos_t { + .left = left, + .tip = offset, + .right = right, + .top = layout.height - _template.get_height_px(), + .bottom = layout.height + }; +} + +// widget's layout; mainly location of the gradient's image and stop handles +GradientWithStops::layout_t GradientWithStops::get_layout() const { + auto allocation = get_allocation(); + + const auto stop_width = _template.get_width_px(); + const auto half_stop = round((stop_width + 1) / 2); + const auto x = half_stop; + const double width = allocation.get_width() - stop_width; + const double height = allocation.get_height(); + + return layout_t { + .x = x, + .y = 0, + .width = width, + .height = height + }; +} + +// check if stop handle is under (x, y) location, return its index or -1 if not hit +int GradientWithStops::find_stop_at(double x, double y) const { + if (!_gradient) return -1; + + const auto& v = _stops; + const auto& layout = get_layout(); + + // find stop handle at (x, y) position; note: stops may not be ordered by offsets + for (size_t i = 0; i < v.size(); ++i) { + auto pos = get_stop_position(i, layout); + if (x >= pos.left && x <= pos.right && y >= pos.top && y <= pos.bottom) { + return static_cast<int>(i); + } + } + + return -1; +} + +// this is range of offset adjustment for a given stop +GradientWithStops::limits_t GradientWithStops::get_stop_limits(int maybe_index) const { + if (!_gradient) return limits_t {}; + + // let negative index turn into a large out-of-range number + auto index = static_cast<size_t>(maybe_index); + + const auto& v = _stops; + + if (index < v.size()) { + double min = 0; + double max = 1; + + if (v.size() > 1) { + std::vector<double> offsets; + offsets.reserve(v.size()); + for (auto& s : _stops) { + offsets.push_back(s.offset); + } + std::sort(offsets.begin(), offsets.end()); + + // special cases: + if (index == 0) { // first stop + max = offsets[index + 1]; + } + else if (index + 1 == v.size()) { // last stop + min = offsets[index - 1]; + } + else { + // stops "inside" gradient + min = offsets[index - 1]; + max = offsets[index + 1]; + } + } + return limits_t { .min_offset = min, .max_offset = max, .offset = v[index].offset }; + } + else { + return limits_t {}; + } +} + +bool GradientWithStops::on_focus_out_event(GdkEventFocus* event) { + update(); + return false; +} + +bool GradientWithStops::on_focus_in_event(GdkEventFocus* event) { + update(); + return false; +} + +bool GradientWithStops::on_focus(Gtk::DirectionType direction) { + if (has_focus()) { + return false; // let focus go + } + + grab_focus(); + // TODO - add focus indicator frame or some focus indicator + return true; +} + +bool GradientWithStops::on_key_press_event(GdkEventKey* key_event) { + bool consumed = false; + // currently all keyboard activity involves acting on focused stop handle; bail if nothing's selected + if (_focused_stop < 0) return consumed; + + unsigned int key = 0; + auto modifier = static_cast<GdkModifierType>(key_event->state); + gdk_keymap_translate_keyboard_state(Gdk::Display::get_default()->get_keymap(), + key_event->hardware_keycode, modifier, 0, &key, nullptr, nullptr, nullptr); + + auto delta = _stop_move_increment; + if (modifier & GDK_SHIFT_MASK) { + delta *= 10; + } + + consumed = true; + + switch (key) { + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + move_stop(_focused_stop, -delta); + break; + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + move_stop(_focused_stop, delta); + break; + case GDK_KEY_BackSpace: + case GDK_KEY_Delete: + _signal_delete_stop.emit(_focused_stop); + break; + default: + consumed = false; + break; + } + + return consumed; +} + +bool GradientWithStops::on_button_press_event(GdkEventButton* event) { + // single button press selects stop and can start dragging it + constexpr auto LEFT_BTN = 1; + if (event->button == LEFT_BTN && _gradient && event->type == GDK_BUTTON_PRESS) { + _focused_stop = -1; + + if (!has_focus()) { + // grab focus, so we can show selection indicator and move selected stop with left/right keys + grab_focus(); + } + update(); + + // find stop handle + auto index = find_stop_at(event->x, event->y); + + if (index >= 0) { + _focused_stop = index; + // fire stop selection, whether stop can be moved or not + _signal_stop_selected.emit(index); + + auto limits = get_stop_limits(index); + + // check if clicked stop can be moved + if (limits.min_offset < limits.max_offset) { + // TODO: to facilitate selecting stops without accidentally moving them, + // delay dragging mode until mouse cursor moves certain distance... + _dragging = true; + _pointer_x = event->x; + _stop_offset = _stops.at(index).offset; + + if (_cursor_dragging) { + gdk_window_set_cursor(event->window, _cursor_dragging->gobj()); + } + } + } + } + else if (event->button == LEFT_BTN && _gradient && event->type == GDK_2BUTTON_PRESS) { + // double-click may insert a new stop + auto index = find_stop_at(event->x, event->y); + if (index < 0) { + auto layout = get_layout(); + if (layout.width > 0 && event->x > layout.x && event->x < layout.x + layout.width) { + double position = (event->x - layout.x) / layout.width; + // request new stop + _signal_add_stop_at.emit(position); + } + } + } + + return false; +} + +bool GradientWithStops::on_button_release_event(GdkEventButton* event) { + GdkCursor* cursor = get_cursor(event->x, event->y); + gdk_window_set_cursor(event->window, cursor); + + _dragging = false; + return false; +} + +// move stop by a given amount (delta) +void GradientWithStops::move_stop(int stop_index, double offset_shift) { + auto layout = get_layout(); + if (layout.width > 0) { + auto limits = get_stop_limits(stop_index); + if (limits.min_offset < limits.max_offset) { + auto new_offset = CLAMP(limits.offset + offset_shift, limits.min_offset, limits.max_offset); + if (new_offset != limits.offset) { + _signal_stop_offset_changed.emit(stop_index, new_offset); + } + } + } +} + +bool GradientWithStops::on_motion_notify_event(GdkEventMotion* event) { + if (_dragging && _gradient) { + // move stop to a new position (adjust offset) + auto dx = event->x - _pointer_x; + auto layout = get_layout(); + if (layout.width > 0) { + auto delta = dx / layout.width; + auto limits = get_stop_limits(_focused_stop); + if (limits.min_offset < limits.max_offset) { + auto new_offset = CLAMP(_stop_offset + delta, limits.min_offset, limits.max_offset); + _signal_stop_offset_changed.emit(_focused_stop, new_offset); + } + } + } + else if (!_dragging && _gradient) { + GdkCursor* cursor = get_cursor(event->x, event->y); + gdk_window_set_cursor(event->window, cursor); + } + + return false; +} + +GdkCursor* GradientWithStops::get_cursor(double x, double y) const { + GdkCursor* cursor = nullptr; + if (_gradient) { + // check if mouse if over stop handle that we can adjust + auto index = find_stop_at(x, y); + if (index >= 0) { + auto limits = get_stop_limits(index); + if (limits.min_offset < limits.max_offset && _cursor_mouseover) { + cursor = _cursor_mouseover->gobj(); + } + } + else { + if (_cursor_insert) { + cursor = _cursor_insert->gobj(); + } + } + } + return cursor; +} + +bool GradientWithStops::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) { + auto allocation = get_allocation(); + auto context = get_style_context(); + const double scale = get_scale_factor(); + const auto layout = get_layout(); + + if (layout.width <= 0) return true; + + context->render_background(cr, 0, 0, allocation.get_width(), allocation.get_height()); + + // empty gradient checkboard or gradient itself + cr->rectangle(layout.x, layout.y, layout.width, GRADIENT_IMAGE_HEIGHT); + draw_gradient(cr, _gradient, layout.x, layout.width); + + if (!_gradient) return true; + + // draw stop handles + + cr->begin_new_path(); + + Gdk::RGBA fg = context->get_color(get_state_flags()); + Gdk::RGBA bg = _background_color; + + // stop handle outlines and selection indicator use theme colors: + _template.set_style(".outer", "fill", rgba_to_css_color(fg)); + _template.set_style(".inner", "stroke", rgba_to_css_color(bg)); + _template.set_style(".hole", "fill", rgba_to_css_color(bg)); + + auto tip = _tip_template.render(scale); + + for (size_t i = 0; i < _stops.size(); ++i) { + const auto& stop = _stops[i]; + + // stop handle shows stop color and opacity: + _template.set_style(".color", "fill", rgba_to_css_color(stop.color)); + _template.set_style(".opacity", "opacity", double_to_css_value(stop.opacity)); + + // show/hide selection indicator + const auto is_selected = _focused_stop == static_cast<int>(i); + _template.set_style(".selected", "opacity", double_to_css_value(is_selected ? 1 : 0)); + + // render stop handle + auto pix = _template.render(scale); + + if (!pix) { + g_warning("Rendering gradient stop failed."); + break; + } + + auto pos = get_stop_position(i, layout); + + // selected handle sports a 'tip' to make it easily noticeable + if (is_selected && tip) { + if (auto surface = Gdk::Cairo::create_surface_from_pixbuf(tip, 1)) { + cr->save(); + // scale back to physical pixels + cr->scale(1 / scale, 1 / scale); + // paint tip bitmap + cr->set_source(surface, round(pos.tip * scale - tip->get_width() / 2), layout.y * scale); + cr->paint(); + cr->restore(); + } + } + + // surface from pixbuf *without* scaling (scale = 1) + auto surface = Gdk::Cairo::create_surface_from_pixbuf(pix, 1); + if (!surface) continue; + + // calc space available for stop marker + cr->save(); + cr->rectangle(pos.left, layout.y, pos.right - pos.left, layout.height); + cr->clip(); + // scale back to physical pixels + cr->scale(1 / scale, 1 / scale); + // paint bitmap + cr->set_source(surface, round(pos.tip * scale - pix->get_width() / 2), pos.top * scale); + cr->paint(); + cr->restore(); + cr->reset_clip(); + } + + return true; +} + +// focused/selected stop indicator +void GradientWithStops::set_focused_stop(int index) { + if (_focused_stop != index) { + _focused_stop = index; + + if (has_focus()) { + update(); + } + } +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/gradient-with-stops.h b/src/ui/widget/gradient-with-stops.h new file mode 100644 index 0000000..0276fa2 --- /dev/null +++ b/src/ui/widget/gradient-with-stops.h @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_GRADIENT_WITH_STOPS_H +#define SEEN_GRADIENT_WITH_STOPS_H + +#include <gtkmm/widget.h> +#include <gdkmm/color.h> +#include "ui/svg-renderer.h" +#include "helper/auto-connection.h" + +class SPGradient; + +namespace Inkscape { +namespace UI { +namespace Widget { + +class GradientWithStops : public Gtk::DrawingArea { +public: + GradientWithStops(); + + // gradient to draw or nullptr + void set_gradient(SPGradient* gradient); + + // set selected stop handle (or pass -1 to deselect) + void set_focused_stop(int index); + + // stop has been selected + sigc::signal<void (size_t)>& signal_stop_selected() { + return _signal_stop_selected; + } + + // request to change stop's offset + sigc::signal<void (size_t, double)>& signal_stop_offset_changed() { + return _signal_stop_offset_changed; + } + + sigc::signal<void (double)>& signal_add_stop_at() { + return _signal_add_stop_at; + } + + sigc::signal<void (size_t)>& signal_delete_stop() { + return _signal_delete_stop; + } + +private: + void get_preferred_width_vfunc(int& minimum_width, int& natural_width) const override; + void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override; + bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override; + void on_style_updated() override; + bool on_button_press_event(GdkEventButton* event) override; + bool on_button_release_event(GdkEventButton* event) override; + bool on_motion_notify_event(GdkEventMotion* event) override; + bool on_key_press_event(GdkEventKey* key_event) override; + bool on_focus_in_event(GdkEventFocus* event) override; + bool on_focus_out_event(GdkEventFocus* event) override; + bool on_focus(Gtk::DirectionType direction) override; + void size_request(GtkRequisition* requisition) const; + void modified(); + // repaint widget + void update(); + // index of gradient stop handle under (x, y) or -1 + int find_stop_at(double x, double y) const; + // request stop move + void move_stop(int stop_index, double offset_shift); + + // layout of gradient image/editor + struct layout_t { + double x, y, width, height; + }; + layout_t get_layout() const; + + // position of single gradient stop handle + struct stop_pos_t { + double left, tip, right, top, bottom; + }; + stop_pos_t get_stop_position(size_t index, const layout_t& layout) const; + + struct limits_t { + double min_offset, max_offset, offset; + }; + limits_t get_stop_limits(int index) const; + GdkCursor* get_cursor(double x, double y) const; + + SPGradient* _gradient = nullptr; + struct stop_t { + double offset; + SPColor color; + double opacity; + }; + std::vector<stop_t> _stops; + // handle stop SVG template + svg_renderer _template; + // selected handle indicator + svg_renderer _tip_template; + auto_connection _release; + auto_connection _modified; + Gdk::RGBA _background_color; + sigc::signal<void (size_t)> _signal_stop_selected; + sigc::signal<void (size_t, double)> _signal_stop_offset_changed; + sigc::signal<void (double)> _signal_add_stop_at; + sigc::signal<void (size_t)> _signal_delete_stop; + bool _dragging = false; + // index of handle stop that user clicked; may be out of range + int _focused_stop = -1; + double _pointer_x = 0; + double _stop_offset = 0; + Glib::RefPtr<Gdk::Cursor> _cursor_mouseover; + Glib::RefPtr<Gdk::Cursor> _cursor_dragging; + Glib::RefPtr<Gdk::Cursor> _cursor_insert; + // TODO: customize this amount or read prefs + double _stop_move_increment = 0.01; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif diff --git a/src/ui/widget/icon-combobox.h b/src/ui/widget/icon-combobox.h new file mode 100644 index 0000000..6c1770c --- /dev/null +++ b/src/ui/widget/icon-combobox.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef ICON_COMBO_BOX_SEEN_ +#define ICON_COMBO_BOX_SEEN_ + +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class IconComboBox : public Gtk::ComboBox { +public: + IconComboBox() { + _model = Gtk::ListStore::create(_columns); + set_model(_model); + + pack_start(_renderer, false); + _renderer.set_property("stock_size", Gtk::ICON_SIZE_BUTTON); + _renderer.set_padding(2, 0); + add_attribute(_renderer, "icon_name", _columns.icon_name); + + pack_start(_columns.label); + } + + void add_row(const Glib::ustring& icon_name, const Glib::ustring& label, int id) { + Gtk::TreeModel::Row row = *_model->append(); + row[_columns.id] = id; + row[_columns.icon_name] = icon_name; + row[_columns.label] = ' ' + label; + } + + void set_active_by_id(int id) { + for (auto i = _model->children().begin(); i != _model->children().end(); ++i) { + const int data = (*i)[_columns.id]; + if (data == id) { + set_active(i); + break; + } + } + }; + + int get_active_row_id() const { + if (auto it = get_active()) { + return (*it)[_columns.id]; + } + return -1; + } + +private: + class Columns : public Gtk::TreeModel::ColumnRecord + { + public: + Columns() { + add(icon_name); + add(label); + add(id); + } + + Gtk::TreeModelColumn<Glib::ustring> icon_name; + Gtk::TreeModelColumn<Glib::ustring> label; + Gtk::TreeModelColumn<int> id; + }; + + Columns _columns; + Glib::RefPtr<Gtk::ListStore> _model; + Gtk::CellRendererPixbuf _renderer; +}; + +}}} + +#endif diff --git a/src/ui/widget/iconrenderer.cpp b/src/ui/widget/iconrenderer.cpp new file mode 100644 index 0000000..f761316 --- /dev/null +++ b/src/ui/widget/iconrenderer.cpp @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * Martin Owens 2018 <doctormo@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/iconrenderer.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +IconRenderer::IconRenderer() : + Glib::ObjectBase(typeid(IconRenderer)), + Gtk::CellRendererPixbuf(), + _property_icon(*this, "icon", 0) +{ + property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; + set_pixbuf(); +} + +/* + * Called when an icon is clicked. + */ +IconRenderer::type_signal_activated IconRenderer::signal_activated() +{ + return m_signal_activated; +} + +void IconRenderer::get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const +{ + Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h); + + if (min_h) { + min_h += (min_h) >> 1; + } + + if (nat_h) { + nat_h += (nat_h) >> 1; + } +} + +void IconRenderer::get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const +{ + Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w); + + if (min_w) { + min_w += (min_w) >> 1; + } + + if (nat_w) { + nat_w += (nat_w) >> 1; + } +} + +void IconRenderer::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) +{ + set_pixbuf(); + + Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags ); +} + +bool IconRenderer::activate_vfunc(GdkEvent* /*event*/, + Gtk::Widget& /*widget*/, + const Glib::ustring& path, + const Gdk::Rectangle& /*background_area*/, + const Gdk::Rectangle& /*cell_area*/, + Gtk::CellRendererState /*flags*/) +{ + m_signal_activated.emit(path); + return true; +} + +void IconRenderer::add_icon(Glib::ustring name) +{ + _icons.push_back(sp_get_icon_pixbuf(name.c_str(), GTK_ICON_SIZE_BUTTON)); +} + +void IconRenderer::set_pixbuf() +{ + int icon_index = property_icon().get_value(); + if(icon_index >= 0 && icon_index < _icons.size()) { + property_pixbuf() = _icons[icon_index]; + } else { + property_pixbuf() = sp_get_icon_pixbuf("image-missing", GTK_ICON_SIZE_BUTTON); + } +} + + +} // namespace Widget +} // 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/widget/iconrenderer.h b/src/ui/widget/iconrenderer.h new file mode 100644 index 0000000..27c389b --- /dev/null +++ b/src/ui/widget/iconrenderer.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __UI_WIDGET_ICONRENDERER_H__ +#define __UI_WIDGET_ICONRENDERER_H__ +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * Martin Owens 2018 <doctormo@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/cellrendererpixbuf.h> +#include <gtkmm/widget.h> +#include <glibmm/property.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class IconRenderer : public Gtk::CellRendererPixbuf { +public: + IconRenderer(); + ~IconRenderer() override = default;; + + Glib::PropertyProxy<int> property_icon() { return _property_icon.get_proxy(); } + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on(); + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off(); + + void add_icon(Glib::ustring name); + + typedef sigc::signal<void, Glib::ustring> type_signal_activated; + type_signal_activated signal_activated(); +protected: + type_signal_activated m_signal_activated; + + 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; + + void get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const override; + + void get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const override; + + bool activate_vfunc(GdkEvent *event, + Gtk::Widget &widget, + const Glib::ustring &path, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags) override; + +private: + + Glib::Property<int> _property_icon; + std::vector<Glib::RefPtr<Gdk::Pixbuf>> _icons; + void set_pixbuf(); +}; + + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + + +#endif /* __UI_WIDGET_ICONRENDERER_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/widget/imagetoggler.cpp b/src/ui/widget/imagetoggler.cpp new file mode 100644 index 0000000..7a0c295 --- /dev/null +++ b/src/ui/widget/imagetoggler.cpp @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Jon A. Cruz + * Johan B. C. Engelen + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/iconinfo.h> + +#include "ui/widget/imagetoggler.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +ImageToggler::ImageToggler( char const* on, char const* off) : + Glib::ObjectBase(typeid(ImageToggler)), + Gtk::CellRenderer(), + _pixOnName(on), + _pixOffName(off), + _property_active(*this, "active", false), + _property_activatable(*this, "activatable", true), + _property_gossamer(*this, "gossamer", false), + _property_pixbuf_on(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr)), + _property_pixbuf_off(*this, "pixbuf_off", Glib::RefPtr<Gdk::Pixbuf>(nullptr)) +{ + property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; + Gtk::IconSize::lookup(Gtk::ICON_SIZE_MENU, _size, _size); +} + +void ImageToggler::get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const +{ + min_h = _size + 6; + nat_h = _size + 8; +} + +void ImageToggler::get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const +{ + min_w = _size + 12; + nat_w = _size + 16; +} + +void ImageToggler::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) +{ + // Lazy/late pixbuf rendering to get access to scale factor from widget. + if(!_property_pixbuf_on.get_value()) { + int scale = widget.get_scale_factor(); + _property_pixbuf_on = sp_get_icon_pixbuf(_pixOnName, _size * scale); + _property_pixbuf_off = sp_get_icon_pixbuf(_pixOffName, _size * scale); + } + + // Hide when not being used. + double alpha = 1.0; + bool visible = _property_activatable.get_value() + || _property_active.get_value(); + if (!visible) { + // XXX There is conflict about this value, some users want 0.2, others want 0.0 + alpha = 0.0; + } + if (_property_gossamer.get_value()) { + alpha += 0.2; + } + if (alpha <= 0.0) { + return; + } + + Glib::RefPtr<Gdk::Pixbuf> pixbuf; + if(_property_active.get_value()) { + pixbuf = _property_pixbuf_on.get_value(); + } else { + pixbuf = _property_pixbuf_off.get_value(); + } + + cairo_surface_t *surface = gdk_cairo_surface_create_from_pixbuf( + pixbuf->gobj(), 0, widget.get_window()->gobj()); + g_return_if_fail(surface); + + // Center the icon in the cell area + int x = cell_area.get_x() + int((cell_area.get_width() - _size) * 0.5); + int y = cell_area.get_y() + int((cell_area.get_height() - _size) * 0.5); + + cairo_set_source_surface(cr->cobj(), surface, x, y); + cr->set_operator(Cairo::OPERATOR_ATOP); + cr->rectangle(x, y, _size, _size); + if (alpha < 1.0) { + cr->clip(); + cr->paint_with_alpha(alpha); + } else { + cr->fill(); + } + cairo_surface_destroy(surface); // free! +} + +bool +ImageToggler::activate_vfunc(GdkEvent* event, + Gtk::Widget& /*widget*/, + const Glib::ustring& path, + const Gdk::Rectangle& /*background_area*/, + const Gdk::Rectangle& /*cell_area*/, + Gtk::CellRendererState /*flags*/) +{ + _signal_pre_toggle.emit(event); + _signal_toggled.emit(path); + + return false; +} + + +} // namespace Widget +} // 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/widget/imagetoggler.h b/src/ui/widget/imagetoggler.h new file mode 100644 index 0000000..8ec406b --- /dev/null +++ b/src/ui/widget/imagetoggler.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __UI_DIALOG_IMAGETOGGLER_H__ +#define __UI_DIALOG_IMAGETOGGLER_H__ +/* + * Authors: + * Jon A. Cruz + * Johan B. C. Engelen + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/cellrendererpixbuf.h> +#include <gtkmm/widget.h> +#include <glibmm/property.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class ImageToggler : public Gtk::CellRenderer { +public: + ImageToggler( char const *on, char const *off); + ~ImageToggler() override = default;; + + sigc::signal<void, const Glib::ustring&> signal_toggled() { return _signal_toggled;} + sigc::signal<void, GdkEvent const *> signal_pre_toggle() { return _signal_pre_toggle; } + + Glib::PropertyProxy<bool> property_active() { return _property_active.get_proxy(); } + Glib::PropertyProxy<bool> property_activatable() { return _property_activatable.get_proxy(); } + Glib::PropertyProxy<bool> property_gossamer() { return _property_gossamer.get_proxy(); } + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on(); + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off(); + +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; + + void get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const override; + + void get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const override; + + bool activate_vfunc(GdkEvent *event, + Gtk::Widget &widget, + const Glib::ustring &path, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags) override; + + +private: + int _size; + Glib::ustring _pixOnName; + Glib::ustring _pixOffName; + + Glib::Property<bool> _property_active; + Glib::Property<bool> _property_activatable; + Glib::Property<bool> _property_gossamer; + Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_on; + Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_off; + + sigc::signal<void, const Glib::ustring&> _signal_toggled; + sigc::signal<void, GdkEvent const *> _signal_pre_toggle; +}; + + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + + +#endif /* __UI_DIALOG_IMAGETOGGLER_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/widget/ink-color-wheel.cpp b/src/ui/widget/ink-color-wheel.cpp new file mode 100644 index 0000000..4d6cc8c --- /dev/null +++ b/src/ui/widget/ink-color-wheel.cpp @@ -0,0 +1,1507 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * HSLuv color wheel widget, based on the web implementation at + * https://www.hsluv.org + *//* + * Authors: + * Tavmjong Bah + * Massinissa Derriche <massinissa.derriche@gmail.com> + * + * Copyright (C) 2018, 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ink-color-wheel.h" + +#include <cstring> +#include <algorithm> + +#include "hsluv.h" + +// Sizes in pixels +static int const SIZE = 400; +static int const OUTER_CIRCLE_RADIUS = 190; + +static double const MAX_HUE = 360.0; +static double const MAX_SATURATION = 100.0; +static double const MAX_LIGHTNESS = 100.0; +static double const MIN_HUE = 0.0; +static double const MIN_SATURATION = 0.0; +static double const MIN_LIGHTNESS = 0.0; +static double const OUTER_CIRCLE_DASH_SIZE = 10.0; +static double const VERTEX_EPSILON = 0.01; + +using Hsluv::Line; + +class ColorPoint { +public: + ColorPoint(); + ColorPoint(double x, double y, double r, double g, double b); + ColorPoint(double x, double y, guint color); + + guint32 get_color(); + void set_color(double red, double green, double blue); + + double x; + double y; + double r; + double g; + double b; +}; + +/* FIXME: replace with Geom::Point */ +class Point { +public: + Point(); + Point(double x, double y); + + double x; + double y; +}; + +class Intersection { +public: + Intersection(); + Intersection (int line1, int line2, Point intersectionPoint, + double intersectionPointAngle, double relativeAngle); + + int line1; + int line2; + Point intersectionPoint; + double intersectionPointAngle; + double relativeAngle; +}; + +static Point intersect_line_line(Line a, Line b); +static double distance_from_origin(Point point); +static double distance_line_from_origin(Line line); +static double angle_from_origin(Point point); +static double normalize_angle(double angle); +static double lerp(double v0, double v1, double t0, double t1, double t); +static ColorPoint lerp(ColorPoint const &v0, ColorPoint const &v1, double t0, double t1, double t); +static guint32 hsv_to_rgb(double h, double s, double v); +static double luminance(guint32 color); +static Point to_pixel_coordinate(Point const &point, double scale, double resize); +static Point from_pixel_coordinate(Point const &point, double scale, double resize); +static std::vector<Point> to_pixel_coordinate( std::vector<Point> const &points, double scale, + double resize); +static void draw_vertical_padding(ColorPoint p0, ColorPoint p1, int padding, + bool pad_upwards, guint32 *buffer, int height, int stride); + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Used to represent the in RGB gamut colors polygon of the color wheel. + * + * @struct + */ +struct PickerGeometry { + std::vector<Line> lines; + /** Ordered such that 1st vertex is intersection between first and second + * line, 2nd vertex between second and third line etc. */ + std::vector<Point> vertices; + /** Angles from origin to corresponding vertex, in radians */ + std::vector<double> angles; + /** Smallest circle with center at origin such that polygon fits inside */ + double outerCircleRadius; + /** Largest circle with center at origin such that it fits inside polygon */ + double innerCircleRadius; +}; + +/** + * Update the passed in PickerGeometry structure to the given lightness value. + * + * @param[out] pickerGeometry The PickerGeometry instance to update. + * @param lightness The lightness value. + */ +static void get_picker_geometry(PickerGeometry *pickerGeometry, double lightness); + +/* Base Color Wheel */ +ColorWheel::ColorWheel() + : _adjusting(false) +{ + set_name("ColorWheel"); + + add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::BUTTON_MOTION_MASK | Gdk::KEY_PRESS_MASK); + set_can_focus(); +} + +void ColorWheel::setRgb(double /*r*/, double /*g*/, double /*b*/, bool /*overrideHue*/) +{} + +void ColorWheel::getRgb(double */*r*/, double */*g*/, double */*b*/) const +{} + +void ColorWheel::getRgbV(double *rgb) const {} + +guint32 ColorWheel::getRgb() const { return 0; } + +void ColorWheel::setHue(double h) +{ + _values[0] = std::clamp(h, MIN_HUE, MAX_HUE); +} + +void ColorWheel::setSaturation(double s) +{ + _values[1] = std::clamp(s, MIN_SATURATION, MAX_SATURATION); +} + +void ColorWheel::setLightness(double l) +{ + _values[2] = std::clamp(l, MIN_LIGHTNESS, MAX_LIGHTNESS); +} + +void ColorWheel::getValues(double *a, double *b, double *c) const +{ + if (a) *a = _values[0]; + if (b) *b = _values[1]; + if (c) *c = _values[2]; +} + +void ColorWheel::_set_from_xy(double const x, double const y) +{} + +bool ColorWheel::on_key_release_event(GdkEventKey* key_event) +{ + unsigned int key = 0; + gdk_keymap_translate_keyboard_state(Gdk::Display::get_default()->get_keymap(), + key_event->hardware_keycode, + (GdkModifierType)key_event->state, + 0, &key, nullptr, nullptr, nullptr); + + switch (key) { + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + _adjusting = false; + return true; + } + + return false; +} + +sigc::signal<void> ColorWheel::signal_color_changed() +{ + return _signal_color_changed; +} + +/* HSL Color Wheel */ +void ColorWheelHSL::setRgb(double r, double g, double b, bool overrideHue) +{ + double min = std::min({r, g, b}); + double max = std::max({r, g, b}); + + _values[2] = max; + + if (min == max) { + if (overrideHue) { + _values[0] = 0.0; + } + } else { + if (max == r) { + _values[0] = ((g - b) / (max - min) ) / 6.0; + } else if (max == g) { + _values[0] = ((b - r) / (max - min) + 2) / 6.0; + } else { + _values[0] = ((r - g) / (max - min) + 4) / 6.0; + } + + if (_values[0] < 0.0) { + _values[0] += 1.0; + } + } + + if (max == 0) { + _values[1] = 0; + } else { + _values[1] = (max - min) / max; + } +} + +void ColorWheelHSL::getRgb(double *r, double *g, double *b) const +{ + guint32 color = getRgb(); + *r = ((color & 0x00ff0000) >> 16) / 255.0; + *g = ((color & 0x0000ff00) >> 8) / 255.0; + *b = ((color & 0x000000ff) ) / 255.0; +} + +void ColorWheelHSL::getRgbV(double *rgb) const +{ + guint32 color = getRgb(); + rgb[0] = ((color & 0x00ff0000) >> 16) / 255.0; + rgb[1] = ((color & 0x0000ff00) >> 8) / 255.0; + rgb[2] = ((color & 0x000000ff) ) / 255.0; +} + +guint32 ColorWheelHSL::getRgb() const +{ + return hsv_to_rgb(_values[0], _values[1], _values[2]); +} + +void ColorWheelHSL::getHsl(double *h, double *s, double *l) const +{ + getValues(h, s, l); +} + +bool ColorWheelHSL::on_draw(::Cairo::RefPtr<::Cairo::Context> const &cr) +{ + Gtk::Allocation allocation = get_allocation(); + int const width = allocation.get_width(); + int const height = allocation.get_height(); + + int const cx = width/2; + int const cy = height/2; + + int const stride = Cairo::ImageSurface::format_stride_for_width(Cairo::FORMAT_RGB24, width); + + int focus_line_width; + int focus_padding; + get_style_property("focus-line-width", focus_line_width); + get_style_property("focus-padding", focus_padding); + + // Paint ring + guint32* buffer_ring = g_new (guint32, height * stride / 4); + double r_max = std::min(width, height)/2.0 - 2 * (focus_line_width + focus_padding); + double r_min = r_max * (1.0 - _ring_width); + double r2_max = (r_max+2) * (r_max+2); // Must expand a bit to avoid edge effects. + double r2_min = (r_min-2) * (r_min-2); // Must shrink a bit to avoid edge effects. + + for (int i = 0; i < height; ++i) { + guint32* p = buffer_ring + i * width; + double dy = (cy - i); + for (int j = 0; j < width; ++j) { + double dx = (j - cx); + double r2 = dx * dx + dy * dy; + if (r2 < r2_min || r2 > r2_max) { + *p++ = 0; // Save calculation time. + } else { + double angle = atan2 (dy, dx); + if (angle < 0.0) { + angle += 2.0 * M_PI; + } + double hue = angle/(2.0 * M_PI); + + *p++ = hsv_to_rgb(hue, 1.0, 1.0); + } + } + } + + Cairo::RefPtr<::Cairo::ImageSurface> source_ring = + ::Cairo::ImageSurface::create((unsigned char *)buffer_ring, + Cairo::FORMAT_RGB24, + width, height, stride); + + cr->set_antialias(Cairo::ANTIALIAS_SUBPIXEL); + + // Paint line on ring in source (so it gets clipped by stroke). + double l = 0.0; + guint32 color_on_ring = hsv_to_rgb(_values[0], 1.0, 1.0); + if (luminance(color_on_ring) < 0.5) l = 1.0; + + Cairo::RefPtr<::Cairo::Context> cr_source_ring = ::Cairo::Context::create(source_ring); + cr_source_ring->set_source_rgb(l, l, l); + + cr_source_ring->move_to (cx, cy); + cr_source_ring->line_to (cx + cos(_values[0] * M_PI * 2.0) * r_max+1, + cy - sin(_values[0] * M_PI * 2.0) * r_max+1); + cr_source_ring->stroke(); + + // Paint with ring surface, clipping to ring. + cr->save(); + cr->set_source(source_ring, 0, 0); + cr->set_line_width (r_max - r_min); + cr->begin_new_path(); + cr->arc(cx, cy, (r_max + r_min)/2.0, 0, 2.0 * M_PI); + cr->stroke(); + cr->restore(); + + g_free(buffer_ring); + + // Draw focus + if (has_focus() && _focus_on_ring) { + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + style_context->render_focus(cr, 0, 0, width, height); + } + + // Paint triangle. + /* The triangle is painted by first finding color points on the + * edges of the triangle at the same y value via linearly + * interpolating between corner values, and then interpolating along + * x between the those edge points. The interpolation is in sRGB + * space which leads to a complicated mapping between x/y and + * saturation/value. This was probably done to remove the need to + * convert between HSV and RGB for each pixel. + * Black corner: v = 0, s = 1 + * White corner: v = 1, s = 0 + * Color corner; v = 1, s = 1 + */ + const int padding = 3; // Avoid edge artifacts. + double x0, y0, x1, y1, x2, y2; + _triangle_corners(x0, y0, x1, y1, x2, y2); + guint32 color0 = hsv_to_rgb(_values[0], 1.0, 1.0); + guint32 color1 = hsv_to_rgb(_values[0], 1.0, 0.0); + guint32 color2 = hsv_to_rgb(_values[0], 0.0, 1.0); + + ColorPoint p0 (x0, y0, color0); + ColorPoint p1 (x1, y1, color1); + ColorPoint p2 (x2, y2, color2); + + // Reorder so we paint from top down. + if (p1.y > p2.y) { + std::swap(p1, p2); + } + + if (p0.y > p2.y) { + std::swap(p0, p2); + } + + if (p0.y > p1.y) { + std::swap(p0, p1); + } + + guint32* buffer_triangle = g_new(guint32, height * stride / 4); + + for (int y = 0; y < height; ++y) { + guint32 *p = buffer_triangle + y * (stride / 4); + + if (p0.y <= y+padding && y-padding < p2.y) { + + // Get values on side at position y. + ColorPoint side0; + double y_inter = std::clamp(static_cast<double>(y), p0.y, p2.y); + if (y < p1.y) { + side0 = lerp(p0, p1, p0.y, p1.y, y_inter); + } else { + side0 = lerp(p1, p2, p1.y, p2.y, y_inter); + } + ColorPoint side1 = lerp(p0, p2, p0.y, p2.y, y_inter); + + // side0 should be on left + if (side0.x > side1.x) { + std::swap (side0, side1); + } + + int x_start = std::max(0, int(side0.x)); + int x_end = std::min(int(side1.x), width); + + for (int x = 0; x < width; ++x) { + if (x <= x_start) { + *p++ = side0.get_color(); + } else if (x < x_end) { + *p++ = lerp(side0, side1, side0.x, side1.x, x).get_color(); + } else { + *p++ = side1.get_color(); + } + } + } + } + + // add vertical padding to each side separately + ColorPoint temp_point = lerp(p0, p1, p0.x, p1.x, (p0.x + p1.x) / 2.0); + bool pad_upwards = _is_in_triangle(temp_point.x, temp_point.y + 1); + draw_vertical_padding(p0, p1, padding, pad_upwards, buffer_triangle, height, stride / 4); + + temp_point = lerp(p0, p2, p0.x, p2.x, (p0.x + p2.x) / 2.0); + pad_upwards = _is_in_triangle(temp_point.x, temp_point.y + 1); + draw_vertical_padding(p0, p2, padding, pad_upwards, buffer_triangle, height, stride / 4); + + temp_point = lerp(p1, p2, p1.x, p2.x, (p1.x + p2.x) / 2.0); + pad_upwards = _is_in_triangle(temp_point.x, temp_point.y + 1); + draw_vertical_padding(p1, p2, padding, pad_upwards, buffer_triangle, height, stride / 4); + + Cairo::RefPtr<::Cairo::ImageSurface> source_triangle = + ::Cairo::ImageSurface::create((unsigned char *)buffer_triangle, + Cairo::FORMAT_RGB24, + width, height, stride); + + // Paint with triangle surface, clipping to triangle. + cr->save(); + cr->set_source(source_triangle, 0, 0); + cr->move_to(p0.x, p0.y); + cr->line_to(p1.x, p1.y); + cr->line_to(p2.x, p2.y); + cr->close_path(); + cr->fill(); + cr->restore(); + + g_free(buffer_triangle); + + // Draw marker + double mx = x1 + (x2-x1) * _values[2] + (x0-x2) * _values[1] * _values[2]; + double my = y1 + (y2-y1) * _values[2] + (y0-y2) * _values[1] * _values[2]; + + double a = 0.0; + guint32 color_at_marker = getRgb(); + if (luminance(color_at_marker) < 0.5) a = 1.0; + + cr->set_source_rgb(a, a, a); + cr->begin_new_path(); + cr->arc(mx, my, 4, 0, 2 * M_PI); + cr->stroke(); + + // Draw focus + if (has_focus() && !_focus_on_ring) { + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + style_context->render_focus(cr, mx-4, my-4, 8, 8); // This doesn't seem to work. + cr->set_line_width(0.5); + cr->set_source_rgb(1-a, 1-a, 1-a); + cr->begin_new_path(); + cr->arc(mx, my, 7, 0, 2 * M_PI); + cr->stroke(); + } + + return true; +} + +bool ColorWheelHSL::on_focus(Gtk::DirectionType direction) +{ + // In forward direction, focus passes from no focus to ring focus to triangle + // focus to no focus. + if (!has_focus()) { + _focus_on_ring = (direction == Gtk::DIR_TAB_FORWARD); + grab_focus(); + return true; + } + + // Already have focus + bool keep_focus = false; + + switch (direction) { + case Gtk::DIR_UP: + case Gtk::DIR_LEFT: + case Gtk::DIR_TAB_BACKWARD: + if (!_focus_on_ring) { + _focus_on_ring = true; + keep_focus = true; + } + break; + + case Gtk::DIR_DOWN: + case Gtk::DIR_RIGHT: + case Gtk::DIR_TAB_FORWARD: + if (_focus_on_ring) { + _focus_on_ring = false; + keep_focus = true; + } + break; + } + + queue_draw(); // Update focus indicators. + + return keep_focus; +} + +void ColorWheelHSL::_set_from_xy(double const x, double const y) +{ + Gtk::Allocation allocation = get_allocation(); + int const width = allocation.get_width(); + int const height = allocation.get_height(); + + double const cx = width/2.0; + double const cy = height/2.0; + double const r = std::min(cx, cy) * (1 - _ring_width); + + // We calculate RGB value under the cursor by rotating the cursor + // and triangle by the hue value and looking at position in the + // now right pointing triangle. + double angle = _values[0] * 2 * M_PI; + double sin = std::sin(angle); + double cos = std::cos(angle); + double xp = ((x - cx) * cos - (y - cy) * sin) / r; + double yp = ((x - cx) * sin + (y - cy) * cos) / r; + + double xt = lerp(0.0, 1.0, -0.5, 1.0, xp); + xt = std::clamp(xt, 0.0, 1.0); + + double dy = (1-xt) * std::cos(M_PI / 6.0); + double yt = lerp(0.0, 1.0, -dy, dy, yp); + yt = std::clamp(yt, 0.0, 1.0); + + ColorPoint c0(0, 0, yt, yt, yt); // Grey point along base. + ColorPoint c1(0, 0, hsv_to_rgb(_values[0], 1, 1)); // Hue point at apex + ColorPoint c = lerp(c0, c1, 0, 1, xt); + + setRgb(c.r, c.g, c.b, false); // Don't override previous hue. +} + +bool ColorWheelHSL::_is_in_ring(double x, double y) +{ + Gtk::Allocation allocation = get_allocation(); + int const width = allocation.get_width(); + int const height = allocation.get_height(); + + int const cx = width/2; + int const cy = height/2; + + int focus_line_width; + int focus_padding; + get_style_property("focus-line-width", focus_line_width); + get_style_property("focus-padding", focus_padding); + + double r_max = std::min( width, height)/2.0 - 2 * (focus_line_width + focus_padding); + double r_min = r_max * (1.0 - _ring_width); + double r2_max = r_max * r_max; + double r2_min = r_min * r_min; + + double dx = x - cx; + double dy = y - cy; + double r2 = dx * dx + dy * dy; + + return (r2_min < r2 && r2 < r2_max); +} + +bool ColorWheelHSL::_is_in_triangle(double x, double y) +{ + double x0, y0, x1, y1, x2, y2; + _triangle_corners(x0, y0, x1, y1, x2, y2); + + double det = (x2 - x1) * (y0 - y1) - (y2 - y1) * (x0 - x1); + double s = ((x - x1) * (y0 - y1) - (y - y1) * (x0 - x1)) / det; + double t = ((x2 - x1) * (y - y1) - (y2 - y1) * (x - x1)) / det; + + return (s >= 0.0 && t >= 0.0 && s + t <= 1.0); +} + +void ColorWheelHSL::_update_triangle_color(double x, double y) +{ + _set_from_xy(x, y); + _signal_color_changed.emit(); + queue_draw(); +} + +void ColorWheelHSL::_triangle_corners(double &x0, double &y0, double &x1, double &y1, + double &x2, double &y2) +{ + Gtk::Allocation allocation = get_allocation(); + int const width = allocation.get_width(); + int const height = allocation.get_height(); + + int const cx = width / 2; + int const cy = height / 2; + + int focus_line_width; + int focus_padding; + get_style_property("focus-line-width", focus_line_width); + get_style_property("focus-padding", focus_padding); + + double r_max = std::min(width, height) / 2.0 - 2 * (focus_line_width + focus_padding); + double r_min = r_max * (1.0 - _ring_width); + + double angle = _values[0] * 2.0 * M_PI; + + x0 = cx + std::cos(angle) * r_min; + y0 = cy - std::sin(angle) * r_min; + x1 = cx + std::cos(angle + 2.0 * M_PI / 3.0) * r_min; + y1 = cy - std::sin(angle + 2.0 * M_PI / 3.0) * r_min; + x2 = cx + std::cos(angle + 4.0 * M_PI / 3.0) * r_min; + y2 = cy - std::sin(angle + 4.0 * M_PI / 3.0) * r_min; +} + +void ColorWheelHSL::_update_ring_color(double x, double y) +{ + Gtk::Allocation allocation = get_allocation(); + int const width = allocation.get_width(); + int const height = allocation.get_height(); + + double cx = width / 2.0; + double cy = height / 2.0; + double angle = -atan2(y - cy, x - cx); + + if (angle < 0) { + angle += 2.0 * M_PI; + } + _values[0] = angle / (2.0 * M_PI); + + queue_draw(); + _signal_color_changed.emit(); +} + +bool ColorWheelHSL::on_button_press_event(GdkEventButton* event) +{ + // Seat is automatically grabbed. + double x = event->x; + double y = event->y; + + if (_is_in_ring(x, y) ) { + _adjusting = true; + _mode = DragMode::HUE; + grab_focus(); + _focus_on_ring = true; + _update_ring_color(x, y); + return true; + } else if (_is_in_triangle(x, y)) { + _adjusting = true; + _mode = DragMode::SATURATION_VALUE; + grab_focus(); + _focus_on_ring = false; + _update_triangle_color(x, y); + return true; + } + + return false; +} + +bool ColorWheelHSL::on_button_release_event(GdkEventButton */*event*/) +{ + _mode = DragMode::NONE; + + _adjusting = false; + return true; +} + +bool ColorWheelHSL::on_motion_notify_event(GdkEventMotion* event) +{ + if (!_adjusting) { return false; } + + double x = event->x; + double y = event->y; + + if (_mode == DragMode::HUE) { + _update_ring_color(x, y); + return true; + } else if (_mode == DragMode::SATURATION_VALUE) { + _update_triangle_color(x, y); + return true; + } else { + return false; + } +} + +bool ColorWheelHSL::on_key_press_event(GdkEventKey* key_event) +{ + bool consumed = false; + + unsigned int key = 0; + gdk_keymap_translate_keyboard_state(Gdk::Display::get_default()->get_keymap(), + key_event->hardware_keycode, + (GdkModifierType)key_event->state, + 0, &key, nullptr, nullptr, nullptr); + + double x0, y0, x1, y1, x2, y2; + _triangle_corners(x0, y0, x1, y1, x2, y2); + + // Marker position + double mx = x1 + (x2-x1) * _values[2] + (x0-x2) * _values[1] * _values[2]; + double my = y1 + (y2-y1) * _values[2] + (y0-y2) * _values[1] * _values[2]; + + double const delta_hue = 2.0 / 360.0; + + switch (key) { + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + if (_focus_on_ring) { + _values[0] += delta_hue; + } else { + my -= 1.0; + _set_from_xy(mx, my); + } + consumed = true; + break; + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + if (_focus_on_ring) { + _values[0] -= delta_hue; + } else { + my += 1.0; + _set_from_xy(mx, my); + } + consumed = true; + break; + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + if (_focus_on_ring) { + _values[0] += delta_hue; + } else { + mx -= 1.0; + _set_from_xy(mx, my); + } + consumed = true; + break; + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + if (_focus_on_ring) { + _values[0] -= delta_hue; + } else { + mx += 1.0; + _set_from_xy(mx, my); + } + consumed = true; + break; + } + + if (consumed) { + if (_values[0] >= 1.0) { + _values[0] -= 1.0; + } else if (_values[0] < 0.0) { + _values[0] += 1.0; + } + + _signal_color_changed.emit(); + queue_draw(); + } + + return consumed; +} + +/* HSLuv Color Wheel */ +ColorWheelHSLuv::ColorWheelHSLuv() + : _scale(1.0) + , _cache_width(0) + , _cache_height(0) + , _square_size(1) +{ + _picker_geometry = new PickerGeometry; + setHsluv(0.0, 100.0, 50.0); +} + +ColorWheelHSLuv::~ColorWheelHSLuv() +{ + delete _picker_geometry; +} + +void ColorWheelHSLuv::setRgb(double r, double g, double b, bool /*overrideHue*/) +{ + double h, s ,l; + Hsluv::rgb_to_hsluv(r, g, b, &h, &s, &l); + + setHue(h); + setSaturation(s); + setLightness(l); +} + +void ColorWheelHSLuv::getRgb(double *r, double *g, double *b) const +{ + Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2], r, g, b); +} + +void ColorWheelHSLuv::getRgbV(double *rgb) const +{ + Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2], &rgb[0], &rgb[1], &rgb[2]); +} + +guint32 ColorWheelHSLuv::getRgb() const +{ + double r, g, b; + Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2], &r, &g, &b); + + return ( + (static_cast<guint32>(r * 255.0) << 16) | + (static_cast<guint32>(g * 255.0) << 8) | + (static_cast<guint32>(b * 255.0) ) + ); +} + +void ColorWheelHSLuv::setHsluv(double h, double s, double l) +{ + setHue(h); + setSaturation(s); + setLightness(l); +} + +void ColorWheelHSLuv::setLightness(double l) +{ + _values[2] = std::clamp(l, MIN_LIGHTNESS, MAX_LIGHTNESS); + + // Update polygon + get_picker_geometry(_picker_geometry, _values[2]); + _scale = OUTER_CIRCLE_RADIUS / _picker_geometry->outerCircleRadius; + _update_polygon(); + + queue_draw(); +} + +void ColorWheelHSLuv::getHsluv(double *h, double *s, double *l) const +{ + getValues(h, s, l); +} + +bool ColorWheelHSLuv::on_draw(::Cairo::RefPtr<::Cairo::Context> const &cr) +{ + Gtk::Allocation allocation = get_allocation(); + int const width = allocation.get_width(); + int const height = allocation.get_height(); + + int const cx = width/2; + int const cy = height/2; + + double const resize = std::min(width, height) / static_cast<double>(SIZE); + + int const marginX = std::max(0.0, (width - height) / 2.0); + int const marginY = std::max(0.0, (height - width) / 2.0); + + std::vector<Point> shapePointsPixel = + to_pixel_coordinate(_picker_geometry->vertices, _scale, resize); + for (Point &point : shapePointsPixel) { + point.x += marginX; + point.y += marginY; + } + + // Detect if we're at the top or bottom vertex of the color space + bool is_vertex = (_values[2] < VERTEX_EPSILON || _values[2] > 100.0 - VERTEX_EPSILON); + + cr->set_antialias(Cairo::ANTIALIAS_SUBPIXEL); + + if (width > _square_size && height > _square_size) { + if (_cache_width != width || _cache_height != height) { + _update_polygon(); + } + if (!is_vertex) { + // Paint with surface, clipping to polygon + cr->save(); + cr->set_source(_surface_polygon, 0, 0); + cr->move_to(shapePointsPixel[0].x, shapePointsPixel[0].y); + for (size_t i = 1; i < shapePointsPixel.size(); i++) { + Point const &point = shapePointsPixel[i]; + cr->line_to(point.x, point.y); + } + cr->close_path(); + cr->fill(); + cr->restore(); + } + } + + // Draw foreground + + // Outer circle + std::vector<double> dashes{OUTER_CIRCLE_DASH_SIZE}; + cr->set_line_width(1); + // White dashes + cr->set_source_rgb(1.0, 1.0, 1.0); + cr->set_dash(dashes, 0.0); + cr->begin_new_path(); + cr->arc(cx, cy, _scale * resize * _picker_geometry->outerCircleRadius, 0, 2 * M_PI); + cr->stroke(); + // Black dashes + cr->set_source_rgb(0.0, 0.0, 0.0); + cr->set_dash(dashes, OUTER_CIRCLE_DASH_SIZE); + cr->begin_new_path(); + cr->arc(cx, cy, _scale * resize * _picker_geometry->outerCircleRadius, 0, 2 * M_PI); + cr->stroke(); + cr->unset_dash(); + + // Contrast + double a = (_values[2] > 70.0) ? 0.0 : 1.0; + cr->set_source_rgb(a, a, a); + + // Pastel circle + double const innerRadius = is_vertex ? 0.01 : _picker_geometry->innerCircleRadius; + cr->set_line_width(2); + cr->begin_new_path(); + cr->arc(cx, cy, _scale * resize * innerRadius, 0, 2 * M_PI); + cr->stroke(); + + // Center + cr->begin_new_path(); + cr->arc(cx, cy, 2, 0, 2 * M_PI); + cr->fill(); + + // Draw marker + double l, u, v; + Hsluv::hsluv_to_luv(_values[0], _values[1], _values[2], &l, &u, &v); + Point mp = to_pixel_coordinate(Point(u, v), _scale, resize); + mp.x += marginX; + mp.y += marginY; + + cr->set_line_width(2); + cr->begin_new_path(); + cr->arc(mp.x, mp.y, 4, 0, 2 * M_PI); + cr->stroke(); + + // Focus + if (has_focus()) { + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + style_context->render_focus(cr, mp.x-4, mp.y-4, 8, 8); + + cr->set_line_width(0.5); + cr->set_source_rgb(1-a, 1-a, 1-a); + cr->begin_new_path(); + cr->arc(mp.x, mp.y, 7, 0, 2 * M_PI); + cr->stroke(); + } + + return true; +} + +void ColorWheelHSLuv::_set_from_xy(double const x, double const y) +{ + Gtk::Allocation allocation = get_allocation(); + int const width = allocation.get_width(); + int const height = allocation.get_height(); + + double const resize = std::min(width, height) / static_cast<double>(SIZE); + + int const margin_x = std::max(0.0, (width - height) / 2.0); + int const margin_y = std::max(0.0, (height - width) / 2.0); + + Point const p = from_pixel_coordinate(Point( + x - margin_x, + y - margin_y + ), _scale, resize); + + double h, s, l; + Hsluv::luv_to_hsluv(_values[2], p.x, p.y, &h, &s, &l); + + setHue(h); + setSaturation(s); + + _signal_color_changed.emit(); + queue_draw(); +} + +void ColorWheelHSLuv::_update_polygon() +{ + Gtk::Allocation allocation = get_allocation(); + int const width = allocation.get_width(); + int const height = allocation.get_height(); + int const size = std::min(width, height); + + // Update square size + _square_size = std::max(1, static_cast<int>(size/50)); + + if (width < _square_size || height < _square_size) { + return; + } + + _cache_width = width; + _cache_height = height; + + double const resize = size / static_cast<double>(SIZE); + + int const marginX = std::max(0.0, (width - height) / 2.0); + int const marginY = std::max(0.0, (height - width) / 2.0); + + std::vector<Point> shapePointsPixel = + to_pixel_coordinate(_picker_geometry->vertices, _scale, resize); + + for (Point &point : shapePointsPixel) { + point.x += marginX; + point.y += marginY; + } + + std::vector<double> xs; + std::vector<double> ys; + + for (Point const &point : shapePointsPixel) { + xs.emplace_back(point.x); + ys.emplace_back(point.y); + } + + int const xmin = std::floor(*std::min_element(xs.begin(), xs.end()) / _square_size); + int const ymin = std::floor(*std::min_element(ys.begin(), ys.end()) / _square_size); + int const xmax = std::ceil(*std::max_element(xs.begin(), xs.end()) / _square_size); + int const ymax = std::ceil(*std::max_element(ys.begin(), ys.end()) / _square_size); + + int const stride = + Cairo::ImageSurface::format_stride_for_width(Cairo::FORMAT_RGB24, width); + + _buffer_polygon.resize(height * stride / 4); + std::vector<guint32> buffer_line; + buffer_line.resize(stride / 4); + + ColorPoint clr; + + // Set the color of each pixel/square + for (int y = ymin; y < ymax; y++) { + for (int x = xmin; x < xmax; x++) { + double px = x * _square_size; + double py = y * _square_size; + Point point = from_pixel_coordinate(Point( + px + (_square_size / 2) - marginX, + py + (_square_size / 2) - marginY + ), _scale, resize); + + double r, g ,b; + Hsluv::luv_to_rgb(_values[2], point.x, point.y, &r, &g, &b); // safe with _values[2] == 0 + + r = std::clamp(r, 0.0, 1.0); + g = std::clamp(g, 0.0, 1.0); + b = std::clamp(b, 0.0, 1.0); + + clr.set_color(r, g, b); + + guint32 *p = buffer_line.data() + (x * _square_size); + for (int i = 0; i < _square_size; i++) { + p[i] = clr.get_color(); + } + } + + // Copy the line buffer to the surface buffer + int Y = y * _square_size; + for (int i = 0; i < _square_size; i++) { + guint32 *t = _buffer_polygon.data() + (Y + i) * (stride / 4); + std::memcpy(t, buffer_line.data(), stride); + } + } + + _surface_polygon = ::Cairo::ImageSurface::create( + reinterpret_cast<unsigned char *>(_buffer_polygon.data()), + Cairo::FORMAT_RGB24, width, height, stride + ); +} + +bool ColorWheelHSLuv::on_button_press_event(GdkEventButton* event) +{ + double const x = event->x; + double const y = event->y; + + Gtk::Allocation allocation = get_allocation(); + int const width = allocation.get_width(); + int const height = allocation.get_height(); + + int const margin_x = std::max(0.0, (width - height) / 2.0); + int const margin_y = std::max(0.0, (height - width) / 2.0); + int const size = std::min(width, height); + + if (x > margin_x && x < (margin_x+size) && y > margin_y && y < (margin_y+size)) { + _adjusting = true; + grab_focus(); + _set_from_xy(x, y); + return true; + } + + return false; +} + +bool ColorWheelHSLuv::on_button_release_event(GdkEventButton */*event*/) +{ + _adjusting = false; + return true; +} + +bool ColorWheelHSLuv::on_motion_notify_event(GdkEventMotion* event) +{ + if (!_adjusting) { return false; } + + double x = event->x; + double y = event->y; + + _set_from_xy(x, y); + + return true; +} + +bool ColorWheelHSLuv::on_key_press_event(GdkEventKey* key_event) +{ + bool consumed = false; + + unsigned int key = 0; + gdk_keymap_translate_keyboard_state(Gdk::Display::get_default()->get_keymap(), + key_event->hardware_keycode, + (GdkModifierType)key_event->state, + 0, &key, nullptr, nullptr, nullptr); + + // Get current point + double l, u, v; + Hsluv::hsluv_to_luv(_values[0], _values[1], _values[2], &l, &u, &v); + + double const marker_move = 1.0 / _scale; + + switch (key) { + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + v += marker_move; + consumed = true; + break; + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + v -= marker_move; + consumed = true; + break; + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + u -= marker_move; + consumed = true; + break; + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + u += marker_move; + consumed = true; + break; + } + + if (consumed) { + double h, s, l; + Hsluv::luv_to_hsluv(_values[2], u, v, &h, &s, &l); + + setHue(h); + setSaturation(s); + + _adjusting = true; + _signal_color_changed.emit(); + queue_draw(); + } + + return consumed; +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* ColorPoint */ +ColorPoint::ColorPoint() + : x(0), y(0), r(0), g(0), b(0) +{} + +ColorPoint::ColorPoint(double x, double y, double r, double g, double b) + : x(x), y(y), r(r), g(g), b(b) +{} + +ColorPoint::ColorPoint(double x, double y, guint color) + : x(x) + , y(y) + , r(((color & 0xff0000) >> 16) / 255.0) + , g(((color & 0x00ff00) >> 8) / 255.0) + , b(((color & 0x0000ff) ) / 255.0) +{} + +guint32 ColorPoint::get_color() +{ + return (static_cast<int>(r * 255) << 16 | + static_cast<int>(g * 255) << 8 | + static_cast<int>(b * 255) + ); +}; + +void ColorPoint::set_color(double red, double green, double blue) +{ + r = red; + g = green; + b = blue; +}; + +/* Point */ +Point::Point() : x(0), y(0) +{} + +Point::Point(double x, double y) : x(x), y(y) +{} + +/* Intersection */ +Intersection::Intersection() : line1(0), line2(0) +{} + +Intersection::Intersection(int line1, int line2, Point intersectionPoint, + double intersectionPointAngle, double relativeAngle) + : line1(line1) + , line2(line2) + , intersectionPoint(intersectionPoint) + , intersectionPointAngle(intersectionPointAngle) + , relativeAngle(relativeAngle) +{} + +/* FIXME: replace these utility functions with calls into lib2geom */ +/* Utility functions */ +static Point intersect_line_line(Line a, Line b) +{ + double x = (a.intercept - b.intercept) / (b.slope - a.slope); + double y = a.slope * x + a.intercept; + return {x, y}; +} + +static double distance_from_origin(Point point) +{ + return std::sqrt(std::pow(point.x, 2) + std::pow(point.y, 2)); +} + +static double distance_line_from_origin(Line line) +{ + // https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line + return std::abs(line.intercept) / std::sqrt(std::pow(line.slope, 2) + 1); +} + +static double angle_from_origin(Point point) +{ + return std::atan2(point.y, point.x); +} + +static double normalize_angle(double angle) +{ + double m = 2 * M_PI; + return std::fmod(std::fmod(angle, m) + m, m); +} + +static double lerp(double v0, double v1, double t0, double t1, double t) +{ + double s = 0; + + if (t0 != t1) { + s = (t - t0) / (t1 - t0); + } + + return (1.0 - s) * v0 + s * v1; +} + +static ColorPoint lerp(ColorPoint const &v0, ColorPoint const &v1, double t0, double t1, + double t) +{ + double x = lerp(v0.x, v1.x, t0, t1, t); + double y = lerp(v0.y, v1.y, t0, t1, t); + double r = lerp(v0.r, v1.r, t0, t1, t); + double g = lerp(v0.g, v1.g, t0, t1, t); + double b = lerp(v0.b, v1.b, t0, t1, t); + + return ColorPoint(x, y, r, g, b); +} + +/** + * @param h Hue. Between 0 and 1. + * @param s Saturation. Between 0 and 1. + * @param v Value. Between 0 and 1. + */ +static guint32 hsv_to_rgb(double h, double s, double v) +{ + h = std::clamp(h, 0.0, 1.0); + s = std::clamp(s, 0.0, 1.0); + v = std::clamp(v, 0.0, 1.0); + + double r = v; + double g = v; + double b = v; + + if (s != 0.0) { + if (h == 1.0) h = 0.0; + h *= 6.0; + + double f = h - (int)h; + double p = v * (1.0 - s); + double q = v * (1.0 - s * f); + double t = v * (1.0 - s * (1.0 - f)); + + switch (static_cast<int>(h)) { + case 0: r = v; g = t; b = p; break; + case 1: r = q; g = v; b = p; break; + case 2: r = p; g = v; b = t; break; + case 3: r = p; g = q; b = v; break; + case 4: r = t; g = p; b = v; break; + case 5: r = v; g = p; b = q; break; + default: g_assert_not_reached(); + } + } + + guint32 rgb = (static_cast<int>(floor(r * 255 + 0.5)) << 16) | + (static_cast<int>(floor(g * 255 + 0.5)) << 8) | + (static_cast<int>(floor(b * 255 + 0.5)) ); + return rgb; +} + +double luminance(guint32 color) +{ + double r = ((color & 0xff0000) >> 16) / 255.0; + double g = ((color & 0xff00) >> 8) / 255.0; + double b = ((color & 0xff) ) / 255.0; + return (r * 0.2125 + g * 0.7154 + b * 0.0721); +} + +/** + * Convert the vertice of the in gamut color polygon (Luv) to pixel coordinates. + * + * @param point The point in Luv coordinates. + * @param scale Zoom amount to fit polygon to outer circle. + * @param resize Zoom amount to fit wheel in widget. + */ +static Point to_pixel_coordinate(Point const &point, double scale, double resize) +{ + return Point( + point.x * scale * resize + (SIZE * resize / 2.0), + (SIZE * resize / 2.0) - point.y * scale * resize + ); +} + +/** + * Convert a point in pixels on the widget to Luv coordinates. + * + * @param point The point in pixel coordinates. + * @param scale Zoom amount to fit polygon to outer circle. + * @param resize Zoom amount to fit wheel in widget. + */ +static Point from_pixel_coordinate(Point const &point, double scale, double resize) +{ + return Point( + (point.x - (SIZE * resize / 2.0)) / (scale * resize), + ((SIZE * resize / 2.0) - point.y) / (scale * resize) + ); +} + +/** + * @overload + * @param point A vector of points in Luv coordinates. + * @param scale Zoom amount to fit polygon to outer circle. + * @param resize Zoom amount to fit wheel in widget. + */ +static std::vector<Point> to_pixel_coordinate(std::vector<Point> const &points, + double scale, double resize) +{ + std::vector<Point> result; + + for (Point const &p : points) { + result.emplace_back(to_pixel_coordinate(p, scale, resize)); + } + + return result; +} + +/** + * Paints padding for an edge of the triangle, + * using the (vertically) closest point. + * + * @param p0 A corner of the triangle. Not the same corner as p1 + * @param p1 A corner of the triangle. Not the same corner as p0 + * @param padding The height of the padding + * @param pad_upwards True if padding is above the line + * @param buffer Array that the triangle is painted to + * @param height Height of buffer + * @param stride Stride of buffer +*/ +void draw_vertical_padding(ColorPoint p0, ColorPoint p1, int padding, bool pad_upwards, + guint32 *buffer, int height, int stride) +{ + // skip if horizontal padding is more accurate, e.g. if the edge is vertical + double gradient = (p1.y - p0.y) / (p1.x - p0.x); + if (std::abs(gradient) > 1.0) { + return; + } + + double min_y = std::min(p0.y, p1.y); + double max_y = std::max(p0.y, p1.y); + + double min_x = std::min(p0.x, p1.x); + double max_x = std::max(p0.x, p1.x); + + // go through every point on the line + for (int y = min_y; y <= max_y; ++y) { + double start_x = lerp(p0, p1, p0.y, p1.y, std::clamp(static_cast<double>(y), min_y, + max_y)).x; + double end_x = lerp(p0, p1, p0.y, p1.y, std::clamp(static_cast<double>(y) + 1, min_y, + max_y)).x; + if (start_x > end_x) { + std::swap(start_x, end_x); + } + + guint32 *p = buffer + y * stride; + p += static_cast<int>(start_x); + for (int x = start_x; x <= end_x; ++x) { + // get the color at this point on the line + ColorPoint point = lerp(p0, p1, p0.x, p1.x, std::clamp(static_cast<double>(x), + min_x, max_x)); + // paint the padding vertically above or below this point + for (int offset = 0; offset <= padding; ++offset) { + if (pad_upwards && (point.y - offset) >= 0) { + *(p - (offset * stride)) = point.get_color(); + } else if (!pad_upwards && (point.y + offset) < height) { + *(p + (offset * stride)) = point.get_color(); + } + } + ++p; + } + } +} + +static void Inkscape::UI::Widget::get_picker_geometry(PickerGeometry *pickerGeometry, double lightness) +{ + // Add a lambda to avoid overlapping intersections + lightness = std::clamp(lightness + 0.01, 0.1, 99.9); + + // Array of lines + std::array<Line, 6> const lines = Hsluv::getBounds(lightness); + int numLines = lines.size(); + double outerCircleRadius = 0.0; + + // Find the line closest to origin + int closestIndex2 = -1; + double closestLineDistance = -1; + + for (int i = 0; i < numLines; i++) { + double d = distance_line_from_origin(lines[i]); + if (closestLineDistance < 0 || d < closestLineDistance) { + closestLineDistance = d; + closestIndex2 = i; + } + } + + Line closestLine = lines[closestIndex2]; + Line perpendicularLine (0 - (1 / closestLine.slope), 0.0); + + Point intersectionPoint = intersect_line_line(closestLine, + perpendicularLine); + double startingAngle = angle_from_origin(intersectionPoint); + + std::vector<Intersection> intersections; + double intersectionPointAngle; + double relativeAngle; + + for (int i = 0; i < numLines - 1; i++) { + for (int j = i + 1; j < numLines; j++) { + intersectionPoint = intersect_line_line(lines[i], lines[j]); + intersectionPointAngle = angle_from_origin(intersectionPoint); + relativeAngle = normalize_angle( + intersectionPointAngle - startingAngle); + intersections.emplace_back(i, j, intersectionPoint, + intersectionPointAngle, relativeAngle); + } + } + + std::sort(intersections.begin(), intersections.end(), + [] (Intersection const &lhs, Intersection const &rhs) + { + return lhs.relativeAngle >= rhs.relativeAngle; + }); + + std::vector<Line> orderedLines; + std::vector<Point> orderedVertices; + std::vector<double> orderedAngles; + + int nextIndex; + double intersectionPointDistance; + int currentIndex = closestIndex2; + + for (Intersection intersection : intersections) { + nextIndex = -1; + + if (intersection.line1 == currentIndex) { + nextIndex = intersection.line2; + } + else if (intersection.line2 == currentIndex) { + nextIndex = intersection.line1; + } + + if (nextIndex > -1) { + currentIndex = nextIndex; + + orderedLines.emplace_back(lines[nextIndex]); + orderedVertices.emplace_back(intersection.intersectionPoint); + orderedAngles.emplace_back(intersection.intersectionPointAngle); + + intersectionPointDistance = distance_from_origin(intersection.intersectionPoint); + if (intersectionPointDistance > outerCircleRadius) { + outerCircleRadius = intersectionPointDistance; + } + } + } + + pickerGeometry->lines = orderedLines; + pickerGeometry->vertices = orderedVertices; + pickerGeometry->angles = orderedAngles; + pickerGeometry->outerCircleRadius = outerCircleRadius; + pickerGeometry->innerCircleRadius = closestLineDistance; +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace .0)(inline-open . 0)(case-label . +)) + indent-tabs-mode: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/widget/ink-color-wheel.h b/src/ui/widget/ink-color-wheel.h new file mode 100644 index 0000000..57af896 --- /dev/null +++ b/src/ui/widget/ink-color-wheel.h @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * @file + * HSLuv color wheel widget, based on the web implementation at + * https://www.hsluv.org + * + * Authors: + * Tavmjong Bah + * Massinissa Derriche <massinissa.derriche@gmail.com> + * + * Copyright (C) 2018, 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INK_COLORWHEEL_H +#define INK_COLORWHEEL_H + +#include <gtkmm.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +struct PickerGeometry; + +/** + * @class ColorWheel + */ +class ColorWheel : public Gtk::DrawingArea +{ +public: + ColorWheel(); + + virtual void setRgb(double r, double g, double b, bool overrideHue = true); + virtual void getRgb(double *r, double *g, double *b) const; + virtual void getRgbV(double *rgb) const; + virtual guint32 getRgb() const; + + void setHue(double h); + void setSaturation(double s); + virtual void setLightness(double l); + void getValues(double *a, double *b, double *c) const; + + bool isAdjusting() const { return _adjusting; } + +protected: + virtual void _set_from_xy(double const x, double const y); + + double _values[3]; + bool _adjusting; + +private: + // Callbacks + bool on_key_release_event(GdkEventKey* key_event) override; + + // Signals +public: + sigc::signal<void> signal_color_changed(); + +protected: + sigc::signal<void> _signal_color_changed; +}; + +/** + * @class ColorWheelHSL + */ +class ColorWheelHSL : public ColorWheel +{ +public: + void setRgb(double r, double g, double b, bool overrideHue = true) override; + void getRgb(double *r, double *g, double *b) const override; + void getRgbV(double *rgb) const override; + guint32 getRgb() const override; + + void getHsl(double *h, double *s, double *l) const; + +protected: + bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override; + bool on_focus(Gtk::DirectionType direction) override; + +private: + void _set_from_xy(double const x, double const y) override; + bool _is_in_ring(double x, double y); + bool _is_in_triangle(double x, double y); + void _update_triangle_color(double x, double y); + void _update_ring_color(double x, double y); + void _triangle_corners(double& x0, double& y0, double& x1, double& y1, double& x2, + double& y2); + + enum class DragMode { + NONE, + HUE, + SATURATION_VALUE + }; + + double _ring_width = 0.2; + DragMode _mode = DragMode::NONE; + bool _focus_on_ring = true; + + // Callbacks + bool on_button_press_event(GdkEventButton* event) override; + bool on_button_release_event(GdkEventButton* event) override; + bool on_motion_notify_event(GdkEventMotion* event) override; + bool on_key_press_event(GdkEventKey* key_event) override; +}; + +/** + * @class ColorWheelHSLuv + */ +class ColorWheelHSLuv : public ColorWheel +{ +public: + ColorWheelHSLuv(); + ~ColorWheelHSLuv() override; + + void setRgb(double r, double g, double b, bool overrideHue = true) override; + void getRgb(double *r, double *g, double *b) const override; + void getRgbV(double *rgb) const override; + guint32 getRgb() const override; + + void setHsluv(double h, double s, double l); + void setLightness(double l) override; + + void getHsluv(double *h, double *s, double *l) const; + +protected: + bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override; + +private: + void _set_from_xy(double const x, double const y) override; + void _update_polygon(); + + // Callbacks + bool on_button_press_event(GdkEventButton* event) override; + bool on_button_release_event(GdkEventButton* event) override; + bool on_motion_notify_event(GdkEventMotion* event) override; + bool on_key_press_event(GdkEventKey* key_event) override; + + double _scale; + PickerGeometry *_picker_geometry; + std::vector<guint32> _buffer_polygon; + Cairo::RefPtr<::Cairo::ImageSurface> _surface_polygon; + int _cache_width, _cache_height; + int _square_size; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INK_COLORWHEEL_HSLUV_H diff --git a/src/ui/widget/ink-flow-box.cpp b/src/ui/widget/ink-flow-box.cpp new file mode 100644 index 0000000..86eb8a0 --- /dev/null +++ b/src/ui/widget/ink-flow-box.cpp @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkflow-box widget. + * This widget allow pack widgets in a flowbox with a controller to show-hide + * + * Author: + * Jabier Arraiza <jabier.arraiza@marker.es> + * + * Copyright (C) 2018 Jabier Arraiza + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "preferences.h" +#include "ui/icon-loader.h" +#include "ui/widget/ink-flow-box.h" +#include <gtkmm/adjustment.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +InkFlowBox::InkFlowBox(const gchar *name) +: Gtk::Box(Gtk::ORIENTATION_VERTICAL) +{ + set_name(name); + this->pack_start(_controller, false, false, 0); + this->pack_start(_flowbox, true, true, 0); + _flowbox.set_activate_on_single_click(true); + Gtk::ToggleButton *tbutton = new Gtk::ToggleButton("", false); + tbutton->set_always_show_image(true); + _flowbox.set_selection_mode(Gtk::SelectionMode::SELECTION_NONE); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), false); + tbutton->set_active(prefs->getBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), true)); + Glib::ustring iconname = "object-unlocked"; + if (tbutton->get_active()) { + iconname = "object-locked"; + } + tbutton->set_image(*sp_get_icon_image(iconname, Gtk::ICON_SIZE_MENU)); + tbutton->signal_toggled().connect( + sigc::bind<Gtk::ToggleButton *>(sigc::mem_fun(*this, &InkFlowBox::on_global_toggle), tbutton)); + _controller.pack_start(*tbutton); + tbutton->hide(); + tbutton->set_no_show_all(true); + showing = 0; + sensitive = true; +} + +InkFlowBox::~InkFlowBox() = default; + +Glib::ustring InkFlowBox::getPrefsPath(gint pos) +{ + return Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/index_") + std::to_string(pos); +} + +bool InkFlowBox::on_filter(Gtk::FlowBoxChild *child) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool(getPrefsPath(child->get_index()), true)) { + showing++; + return true; + } + return false; +} + +void InkFlowBox::on_toggle(gint pos, Gtk::ToggleButton *tbutton) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool global = prefs->getBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), true); + if (global && sensitive) { + sensitive = false; + bool active = true; + for (auto child : tbutton->get_parent()->get_children()) { + if (tbutton != child) { + static_cast<Gtk::ToggleButton *>(child)->set_active(active); + active = false; + } + } + prefs->setBool(getPrefsPath(pos), true); + tbutton->set_active(true); + sensitive = true; + } else { + prefs->setBool(getPrefsPath(pos), tbutton->get_active()); + } + showing = 0; + _flowbox.set_filter_func(sigc::mem_fun(*this, &InkFlowBox::on_filter)); + _flowbox.set_max_children_per_line(showing); +} + +void InkFlowBox::on_global_toggle(Gtk::ToggleButton *tbutton) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), tbutton->get_active()); + sensitive = true; + if (tbutton->get_active()) { + sensitive = false; + bool active = true; + for (auto child : tbutton->get_parent()->get_children()) { + if (tbutton != child) { + static_cast<Gtk::ToggleButton *>(child)->set_active(active); + active = false; + } + } + } + Glib::ustring iconname = "object-unlocked"; + if (tbutton->get_active()) { + iconname = "object-locked"; + } + tbutton->set_image(*sp_get_icon_image(iconname, Gtk::ICON_SIZE_MENU)); + sensitive = true; +} + +void InkFlowBox::insert(Gtk::Widget *widget, Glib::ustring label, gint pos, bool active, int minwidth) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Gtk::ToggleButton *tbutton = new Gtk::ToggleButton(label, true); + tbutton->set_active(prefs->getBool(getPrefsPath(pos), active)); + tbutton->signal_toggled().connect( + sigc::bind<gint, Gtk::ToggleButton *>(sigc::mem_fun(*this, &InkFlowBox::on_toggle), pos, tbutton)); + _controller.pack_start(*tbutton); + tbutton->show(); + prefs->setBool(getPrefsPath(pos), prefs->getBool(getPrefsPath(pos), active)); + widget->set_size_request(minwidth, -1); + _flowbox.insert(*widget, pos); + showing = 0; + _flowbox.set_filter_func(sigc::mem_fun(*this, &InkFlowBox::on_filter)); + _flowbox.set_max_children_per_line(showing); +} + +} // namespace Widget +} // 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/widget/ink-flow-box.h b/src/ui/widget/ink-flow-box.h new file mode 100644 index 0000000..9db1d21 --- /dev/null +++ b/src/ui/widget/ink-flow-box.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkflow-box widget. + * This widget allow pack widgets in a flowbox with a controller to show-hide + * + * Author: + * Jabier Arraiza <jabier.arraiza@marker.es> + * + * Copyright (C) 2018 Jabier Arraiza + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_INK_FLOW_BOX_H +#define INKSCAPE_INK_FLOW_BOX_H + +#include <gtkmm/actionbar.h> +#include <gtkmm/box.h> +#include <gtkmm/flowbox.h> +#include <gtkmm/flowboxchild.h> +#include <gtkmm/togglebutton.h> +#include <sigc++/signal.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A flowbox widget with filter controller for dialogs. + */ + +class InkFlowBox : public Gtk::Box { + public: + InkFlowBox(const gchar *name); + ~InkFlowBox() override; + void insert(Gtk::Widget *widget, Glib::ustring label, gint pos, bool active, int minwidth); + void on_toggle(gint pos, Gtk::ToggleButton *tbutton); + void on_global_toggle(Gtk::ToggleButton *tbutton); + void set_visible(gint pos, bool visible); + bool on_filter(Gtk::FlowBoxChild *child); + Glib::ustring getPrefsPath(gint pos); + /** + * Construct a InkFlowBox. + */ + + private: + Gtk::FlowBox _flowbox; + Gtk::ActionBar _controller; + gint showing; + bool sensitive; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_INK_FLOW_BOX_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/widget/ink-ruler.cpp b/src/ui/widget/ink-ruler.cpp new file mode 100644 index 0000000..535eddf --- /dev/null +++ b/src/ui/widget/ink-ruler.cpp @@ -0,0 +1,476 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Ruler widget. Indicates horizontal or vertical position of a cursor in a specified widget. + * + * Copyright (C) 2019 Tavmjong Bah + * + * Rewrite of the 'C' ruler code which came originally from Gimp. + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * + */ + +#include "ink-ruler.h" + +#include <iostream> +#include <cmath> + +#include "util/units.h" + +struct SPRulerMetric +{ + gdouble ruler_scale[16]; + gint subdivide[5]; +}; + +// Ruler metric for general use. +static SPRulerMetric const ruler_metric_general = { + { 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000 }, + { 1, 5, 10, 50, 100 } +}; + +// Ruler metric for inch scales. +static SPRulerMetric const ruler_metric_inches = { + { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768 }, + { 1, 2, 4, 8, 16 } +}; + +// Half width of pointer triangle. +static double half_width = 5.0; + +namespace Inkscape { +namespace UI { +namespace Widget { + +Ruler::Ruler(Gtk::Orientation orientation) + : _orientation(orientation) + , _backing_store(nullptr) + , _lower(0) + , _upper(1000) + , _max_size(1000) + , _unit(nullptr) + , _backing_store_valid(false) + , _rect() + , _position(0) +{ + set_name("InkRuler"); + + set_events(Gdk::POINTER_MOTION_MASK | + Gdk::BUTTON_PRESS_MASK | // For guide creation + Gdk::BUTTON_RELEASE_MASK ); + + signal_motion_notify_event().connect(sigc::mem_fun(*this, &Ruler::draw_marker_callback)); + set_no_show_all(); +} + +// Set display unit for ruler. +void +Ruler::set_unit(Inkscape::Util::Unit const *unit) +{ + if (_unit != unit) { + _unit = unit; + + _backing_store_valid = false; + queue_draw(); + } +} + +// Set range for ruler, update ticks. +void +Ruler::set_range(const double& lower, const double& upper) +{ + if (_lower != lower || _upper != upper) { + + _lower = lower; + _upper = upper; + _max_size = _upper - _lower; + if (_max_size == 0) { + _max_size = 1; + } + + _backing_store_valid = false; + queue_draw(); + } +} + +// Add a widget (i.e. canvas) to monitor. Note, we don't worry about removing this signal as +// our ruler is tied tightly to the canvas, if one is destroyed, so is the other. +void +Ruler::add_track_widget(Gtk::Widget& widget) +{ + widget.signal_motion_notify_event().connect(sigc::mem_fun(*this, &Ruler::draw_marker_callback), false); // false => connect first +} + + +// Draws marker in response to motion events from canvas. Position is defined in ruler pixel +// coordinates. The routine assumes that the ruler is the same width (height) as the canvas. If +// not, one could use Gtk::Widget::translate_coordinates() to convert the coordinates. +bool +Ruler::draw_marker_callback(GdkEventMotion* motion_event) +{ + double position = 0; + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + position = motion_event->x; + } else { + position = motion_event->y; + } + + if (position != _position) { + + _position = position; + + // Find region to repaint (old and new marker positions). + Cairo::RectangleInt new_rect = marker_rect(); + Cairo::RefPtr<Cairo::Region> region = Cairo::Region::create(new_rect); + region->do_union(_rect); + + // Queue repaint + queue_draw_region(region); + + _rect = new_rect; + } + + return false; +} + + +// Find smallest dimension of ruler based on font size. +void +Ruler::size_request (Gtk::Requisition& requisition) const +{ + // Get border size + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + Gtk::Border border = style_context->get_border(get_state_flags()); + + // Get font size + Pango::FontDescription font = style_context->get_font(get_state_flags()); + int font_size = font.get_size(); + if (!font.get_size_is_absolute()) { + font_size /= Pango::SCALE; + } + + int size = 2 + font_size * 2.0; // Room for labels and ticks + + int width = border.get_left() + border.get_right(); + int height = border.get_top() + border.get_bottom(); + + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + width += 1; + height += size; + } else { + width += size; + height += 1; + } + + // Only valid for orientation in question (smallest dimension)! + requisition.width = width; + requisition.height = height; +} + +void +Ruler::get_preferred_width_vfunc (int& minimum_width, int& natural_width) const +{ + Gtk::Requisition requisition; + size_request(requisition); + minimum_width = natural_width = requisition.width; +} + +void +Ruler::get_preferred_height_vfunc (int& minimum_height, int& natural_height) const +{ + Gtk::Requisition requisition; + size_request(requisition); + minimum_height = natural_height = requisition.height; +} + +// Update backing store when scale changes. +// Note: in principle, there should not be a border (ruler ends should match canvas ends). If there +// is a border, we calculate tick position ignoring border width at ends of ruler but move the +// ticks and position marker inside the border. +bool +Ruler::draw_scale(const::Cairo::RefPtr<::Cairo::Context>& cr_in) +{ + + // Get style information + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + Gtk::Border border = style_context->get_border(get_state_flags()); + Gdk::RGBA foreground = style_context->get_color(get_state_flags()); + + Pango::FontDescription font = style_context->get_font(get_state_flags()); + int font_size = font.get_size(); + if (!font.get_size_is_absolute()) { + font_size /= Pango::SCALE; + } + + Gtk::Allocation allocation = get_allocation(); + int awidth = allocation.get_width(); + int aheight = allocation.get_height(); + + // if (allocation.get_x() != 0 || allocation.get_y() != 0) { + // std::cerr << "Ruler::draw_scale: maybe we do have to handle allocation x and y! " + // << " x: " << allocation.get_x() << " y: " << allocation.get_y() << std::endl; + // } + + // Create backing store (need surface_in to get scale factor correct). + Cairo::RefPtr<Cairo::Surface> surface_in = cr_in->get_target(); + _backing_store = Cairo::Surface::create(surface_in, Cairo::CONTENT_COLOR_ALPHA, awidth, aheight); + + // Get context + Cairo::RefPtr<::Cairo::Context> cr = ::Cairo::Context::create(_backing_store); + style_context->render_background(cr, 0, 0, awidth, aheight); + + cr->set_line_width(1.0); + Gdk::Cairo::set_source_rgba(cr, foreground); + + // Ruler size (only smallest dimension used later). + int rwidth = awidth - (border.get_left() + border.get_right()); + int rheight = aheight - (border.get_top() + border.get_bottom()); + + // Draw bottom/right line of ruler + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + cr->rectangle( 0, aheight - border.get_bottom() - 1, awidth, 1); + } else { + cr->rectangle( awidth - border.get_left() - 1, 0, 1, aheight); + std::swap(awidth, aheight); + std::swap(rwidth, rheight); + } + cr->fill(); + + // From here on, awidth is the longest dimension of the ruler, rheight is the shortest. + + // Figure out scale. Largest ticks must be far enough apart to fit largest text in vertical ruler. + // We actually require twice the distance. + unsigned int scale = std::ceil (_max_size); // Largest number + Glib::ustring scale_text = std::to_string(scale); + unsigned int digits = scale_text.length() + 1; // Add one for negative sign. + unsigned int minimum = digits * font_size * 2; + + double pixels_per_unit = awidth/_max_size; // pixel per distance + + SPRulerMetric ruler_metric = ruler_metric_general; + if (_unit == Inkscape::Util::unit_table.getUnit("in")) { + ruler_metric = ruler_metric_inches; + } + + unsigned scale_index; + for (scale_index = 0; scale_index < G_N_ELEMENTS (ruler_metric.ruler_scale)-1; ++scale_index) { + if (ruler_metric.ruler_scale[scale_index] * std::abs (pixels_per_unit) > minimum) break; + } + + // Now we find out what is the subdivide index for the closest ticks we can draw + unsigned divide_index; + for (divide_index = 0; divide_index < G_N_ELEMENTS (ruler_metric.subdivide)-1; ++divide_index) { + if (ruler_metric.ruler_scale[scale_index] * std::abs (pixels_per_unit) < 5 * ruler_metric.subdivide[divide_index+1]) break; + } + + // We'll loop over all ticks. + double pixels_per_tick = pixels_per_unit * + ruler_metric.ruler_scale[scale_index] / ruler_metric.subdivide[divide_index]; + + double units_per_tick = pixels_per_tick/pixels_per_unit; + double ticks_per_unit = 1.0/units_per_tick; + + // Find first and last ticks + int start = 0; + int end = 0; + if (_lower < _upper) { + start = std::floor (_lower * ticks_per_unit); + end = std::ceil (_upper * ticks_per_unit); + } else { + start = std::floor (_upper * ticks_per_unit); + end = std::ceil (_lower * ticks_per_unit); + } + + // std::cout << " start: " << start + // << " end: " << end + // << " pixels_per_unit: " << pixels_per_unit + // << " pixels_per_tick: " << pixels_per_tick + // << std::endl; + + // Loop over all ticks + for (int i = start; i < end+1; ++i) { + + // Position of tick (add 0.5 to center tick on pixel). + double position = std::floor(i*pixels_per_tick - _lower*pixels_per_unit) + 0.5; + + // Height of tick + int height = rheight; + for (int j = divide_index; j > 0; --j) { + if (i%ruler_metric.subdivide[j] == 0) break; + height = height/2 + 1; + } + + // Draw text for major ticks. + if (i%ruler_metric.subdivide[divide_index] == 0) { + + int label_value = std::round(i*units_per_tick); + Glib::ustring label = std::to_string(label_value); + + Glib::RefPtr<Pango::Layout> layout = create_pango_layout(""); + layout->set_font_description(font); + + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + layout->set_text(label); + cr->move_to (position+4, border.get_top()); // Magic number offset lables + layout->show_in_cairo_context(cr); + } else { + cr->move_to (border.get_left(), position); + int n = 0; + for (char const &c : label) { + std::string s(1, c); + layout->set_text(s); + int text_width; + int text_height; + layout->get_pixel_size(text_width, text_height); + cr->move_to(border.get_left() + (aheight-text_width)/2.0 - 1, + position + n*0.8*text_height + 1); + layout->show_in_cairo_context(cr); + ++n; + } + // Glyphs are not centered in vertical text... should specify fixed width numbers. + // Glib::RefPtr<Pango::Context> context = layout->get_context(); + // if (_orientation == Gtk::ORIENTATION_VERTICAL) { + // context->set_base_gravity(Pango::GRAVITY_EAST); + // context->set_gravity_hint(Pango::GRAVITY_HINT_STRONG); + // cr->move_to(...) + // cr->save(); + // cr->rotate(M_PI_2); + // layout->show_in_cairo_context(cr); + // cr->restore(); + // } + } + } + + // Draw ticks + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + cr->move_to(position, rheight + border.get_top() - height); + cr->line_to(position, rheight + border.get_top()); + } else { + cr->move_to(rheight + border.get_left() - height, position); + cr->line_to(rheight + border.get_left(), position); + } + cr->stroke(); + } + + _backing_store_valid = true; + + return true; +} + +// Draw position marker, we use doubles here. +void +Ruler::draw_marker(const Cairo::RefPtr<::Cairo::Context>& cr) +{ + + // Get style information + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + Gtk::Border border = style_context->get_border(get_state_flags()); + Gdk::RGBA foreground = style_context->get_color(get_state_flags()); + + Gtk::Allocation allocation = get_allocation(); + const int awidth = allocation.get_width(); + const int aheight = allocation.get_height(); + + // Temp (to verify our redraw rectangle encloses position marker). + // Cairo::RectangleInt rect = marker_rect(); + // cr->set_source_rgb(0, 1.0, 0); + // cr->rectangle (rect.x, rect.y, rect.width, rect.height); + // cr->fill(); + + Gdk::Cairo::set_source_rgba(cr, foreground); + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + double offset = aheight - border.get_bottom(); + cr->move_to(_position, offset); + cr->line_to(_position - half_width, offset - half_width); + cr->line_to(_position + half_width, offset - half_width); + cr->close_path(); + } else { + double offset = awidth - border.get_right(); + cr->move_to(offset, _position); + cr->line_to(offset - half_width, _position - half_width); + cr->line_to(offset - half_width, _position + half_width); + cr->close_path(); + } + cr->fill(); +} + +// This is a pixel aligned integer rectangle that encloses the position marker. Used to define the +// redraw area. +Cairo::RectangleInt +Ruler::marker_rect() +{ + // Get border size + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + Gtk::Border border = style_context->get_border(get_state_flags()); + + Gtk::Allocation allocation = get_allocation(); + const int awidth = allocation.get_width(); + const int aheight = allocation.get_height(); + + int rwidth = awidth - border.get_left() - border.get_right(); + int rheight = aheight - border.get_top() - border.get_bottom(); + + Cairo::RectangleInt rect; + rect.x = 0; + rect.y = 0; + rect.width = 0; + rect.height = 0; + + // Find size of rectangle to enclose triangle. + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + rect.x = std::floor(_position - half_width); + rect.y = std::floor(border.get_top() + rheight - half_width); + rect.width = std::ceil(half_width * 2.0 + 1); + rect.height = std::ceil(half_width); + } else { + rect.x = std::floor(border.get_left() + rwidth - half_width); + rect.y = std::floor(_position - half_width); + rect.width = std::ceil(half_width); + rect.height = std::ceil(half_width * 2.0 + 1); + } + + return rect; +} + +// Draw the ruler using the tick backing store. +bool +Ruler::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) { + + if (!_backing_store_valid) { + draw_scale (cr); + } + + cr->set_source (_backing_store, 0, 0); + cr->paint(); + + draw_marker (cr); + + return true; +} + +// Update ruler on style change (font-size, etc.) +void +Ruler::on_style_updated() { + + Gtk::DrawingArea::on_style_updated(); + + _backing_store_valid = false; // If font-size changed we need to regenerate store. + + queue_resize(); + queue_draw(); +} + +} // 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/widget/ink-ruler.h b/src/ui/widget/ink-ruler.h new file mode 100644 index 0000000..f34960b --- /dev/null +++ b/src/ui/widget/ink-ruler.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Ruler widget. Indicates horizontal or vertical position of a cursor in a specified widget. + * + * Copyright (C) 2019 Tavmjong Bah + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * + */ + +#ifndef INK_RULER_H +#define INK_RULER_H + +/* Rewrite of the C Ruler. */ + +#include <gtkmm.h> + +namespace Inkscape { +namespace Util { +class Unit; +} +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Ruler : public Gtk::DrawingArea +{ +public: + Ruler(Gtk::Orientation orientation); + + void set_unit(Inkscape::Util::Unit const *unit); + void set_range(const double& lower, const double& upper); + + void add_track_widget(Gtk::Widget& widget); + bool draw_marker_callback(GdkEventMotion* motion_event); + + void size_request(Gtk::Requisition& requisition) const; + void get_preferred_width_vfunc( int& minimum_width, int& natural_width ) const override; + void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override; + +protected: + bool draw_scale(const Cairo::RefPtr<::Cairo::Context>& cr); + void draw_marker(const Cairo::RefPtr<::Cairo::Context>& cr); + Cairo::RectangleInt marker_rect(); + bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override; + void on_style_updated() override; + +private: + Gtk::Orientation _orientation; + + Inkscape::Util::Unit const* _unit; + double _lower; + double _upper; + double _position; + double _max_size; + + bool _backing_store_valid; + + Cairo::RefPtr<::Cairo::Surface> _backing_store; + Cairo::RectangleInt _rect; +}; + +} // Namespace Inkscape +} +} +#endif // INK_RULER_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/widget/ink-spinscale.cpp b/src/ui/widget/ink-spinscale.cpp new file mode 100644 index 0000000..b977d21 --- /dev/null +++ b/src/ui/widget/ink-spinscale.cpp @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/** \file + A widget that allows entering a numerical value either by + clicking/dragging on a custom Gtk::Scale or by using a + Gtk::SpinButton. The custom Gtk::Scale differs from the stock + Gtk::Scale in that it includes a label to save space and has a + "slow dragging" mode triggered by the Alt key. +*/ + +#include "ink-spinscale.h" +#include <gdkmm/general.h> +#include <gdkmm/cursor.h> +#include <gdkmm/event.h> + +#include <gtkmm/spinbutton.h> + +#include <gdk/gdk.h> + +#include <iostream> +#include <utility> + +InkScale::InkScale(Glib::RefPtr<Gtk::Adjustment> adjustment, Gtk::SpinButton* spinbutton) + : Glib::ObjectBase("InkScale") + , parent_type(adjustment) + , _spinbutton(spinbutton) + , _dragging(false) + , _drag_start(0) + , _drag_offset(0) +{ + set_name("InkScale"); + // std::cout << "GType name: " << G_OBJECT_TYPE_NAME(gobj()) << std::endl; +} + +void +InkScale::set_label(Glib::ustring label) { + _label = label; +} + +bool +InkScale::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) { + + Gtk::Range::on_draw(cr); + + // Get SpinButton style info... + auto style_spin = _spinbutton->get_style_context(); + auto state_spin = style_spin->get_state(); + Gdk::RGBA text_color = style_spin->get_color( state_spin ); + + // Create Pango layout. + auto layout_label = create_pango_layout(_label); + layout_label->set_ellipsize( Pango::ELLIPSIZE_END ); + layout_label->set_width(PANGO_SCALE * get_width()); + + // Get y location of SpinButton text (to match vertical position of SpinButton text). + int x, y; + _spinbutton->get_layout_offsets(x, y); + auto btn_alloc = _spinbutton->get_allocation(); + auto alloc = get_allocation(); + y += btn_alloc.get_y() - alloc.get_y(); + + // Fill widget proportional to value. + double fraction = get_fraction(); + + // Get through rectangle and clipping point for text. + Gdk::Rectangle slider_area = get_range_rect(); + double clip_text_x = slider_area.get_x() + slider_area.get_width() * fraction; + + // Render text in normal text color. + cr->save(); + cr->rectangle(clip_text_x, 0, get_width(), get_height()); + cr->clip(); + Gdk::Cairo::set_source_rgba(cr, text_color); + //cr->set_source_rgba(0, 0, 0, 1); + cr->move_to(5, y ); + layout_label->show_in_cairo_context(cr); + cr->restore(); + + // Render text, clipped, in white over bar (TODO: use same color as SpinButton progress bar). + cr->save(); + cr->rectangle(0, 0, clip_text_x, get_height()); + cr->clip(); + cr->set_source_rgba(1, 1, 1, 1); + cr->move_to(5, y); + layout_label->show_in_cairo_context(cr); + cr->restore(); + + return true; +} + +bool +InkScale::on_button_press_event(GdkEventButton* button_event) { + + if (! (button_event->state & GDK_MOD1_MASK) ) { + bool constrained = button_event->state & GDK_CONTROL_MASK; + set_adjustment_value(button_event->x, constrained); + } + + // Dragging must be initialized after any adjustment due to button press. + _dragging = true; + _drag_start = button_event->x; + _drag_offset = get_width() * get_fraction(); + + return true; +} + +bool +InkScale::on_button_release_event(GdkEventButton* button_event) { + + _dragging = false; + return true; +} + +bool +InkScale::on_motion_notify_event(GdkEventMotion* motion_event) { + + double x = motion_event->x; + + if (_dragging) { + + if (! (motion_event->state & GDK_MOD1_MASK) ) { + // Absolute change + bool constrained = motion_event->state & GDK_CONTROL_MASK; + set_adjustment_value(x, constrained); + } else { + // Relative change + double xx = (_drag_offset + (x - _drag_start) * 0.1); + set_adjustment_value(xx); + } + return true; + } + + if (! (motion_event->state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK))) { + + auto display = get_display(); + auto cursor = Gdk::Cursor::create(display, Gdk::SB_UP_ARROW); + // Get Gdk::window (not Gtk::window).. set cursor for entire window. + // Would need to unset with leave event. + // get_window()->set_cursor( cursor ); + + // Can't see how to do this the C++ way since GdkEventMotion + // is a structure with a C window member. There is a gdkmm + // wrapping function for Gdk::EventMotion but only in unstable. + + // If the cursor theme doesn't have the `sb_up_arrow` cursor then the pointer will be NULL + if (cursor) + gdk_window_set_cursor( motion_event->window, cursor->gobj() ); + } + + return false; +} + +double +InkScale::get_fraction() { + + Glib::RefPtr<Gtk::Adjustment> adjustment = get_adjustment(); + double upper = adjustment->get_upper(); + double lower = adjustment->get_lower(); + double value = adjustment->get_value(); + double fraction = (value - lower)/(upper - lower); + + return fraction; +} + +void +InkScale::set_adjustment_value(double x, bool constrained) { + + Glib::RefPtr<Gtk::Adjustment> adjustment = get_adjustment(); + double upper = adjustment->get_upper(); + double lower = adjustment->get_lower(); + double range = upper-lower; + + Gdk::Rectangle slider_area = get_range_rect(); + double fraction = (x - slider_area.get_x()) / (double)slider_area.get_width(); + double value = fraction * range + lower; + + if (constrained) { + // TODO: do we want preferences for (any of) these? + if (fmod(range+1,16) == 0) { + value = round(value/16) * 16; + } else if (range >= 1000 && fmod(upper,100) == 0) { + value = round(value/100) * 100; + } else if (range >= 100 && fmod(upper,10) == 0) { + value = round(value/10) * 10; + } else if (range > 20 && fmod(upper,5) == 0) { + value = round(value/5) * 5; + } else if (range > 2) { + value = round(value); + } else if (range <= 2) { + value = round(value*10) / 10; + } + } + + adjustment->set_value( value ); +} + +/*******************************************************************/ + +InkSpinScale::InkSpinScale(double value, double lower, + double upper, double step_increment, + double page_increment, double page_size) +{ + set_name("InkSpinScale"); + + g_assert (upper - lower > 0); + + _adjustment = Gtk::Adjustment::create(value, + lower, + upper, + step_increment, + page_increment, + page_size); + + _spinbutton = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton>(_adjustment)); + _spinbutton->set_valign(Gtk::ALIGN_CENTER); + _spinbutton->set_numeric(); + _spinbutton->signal_key_release_event().connect(sigc::mem_fun(*this,&InkSpinScale::on_key_release_event),false); + + _scale = Gtk::manage(new InkScale(_adjustment, _spinbutton)); + _scale->set_draw_value(false); + + pack_end( *_spinbutton, Gtk::PACK_SHRINK ); + pack_end( *_scale, Gtk::PACK_EXPAND_WIDGET ); +} + +InkSpinScale::InkSpinScale(Glib::RefPtr<Gtk::Adjustment> adjustment) + : _adjustment(std::move(adjustment)) +{ + set_name("InkSpinScale"); + + g_assert (_adjustment->get_upper() - _adjustment->get_lower() > 0); + + _spinbutton = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton>(_adjustment)); + _spinbutton->set_numeric(); + + _scale = Gtk::manage(new InkScale(_adjustment, _spinbutton)); + _scale->set_draw_value(false); + + pack_end( *_spinbutton, Gtk::PACK_SHRINK ); + pack_end( *_scale, Gtk::PACK_EXPAND_WIDGET ); +} + +void +InkSpinScale::set_label(Glib::ustring label) { + _scale->set_label(label); +} + +void +InkSpinScale::set_digits(int digits) { + _spinbutton->set_digits(digits); +} + +int +InkSpinScale::get_digits() const { + return _spinbutton->get_digits(); +} + +void +InkSpinScale::set_focus_widget(GtkWidget * focus_widget) { + _focus_widget = focus_widget; +} + +// Return focus to canvas. +bool +InkSpinScale::on_key_release_event(GdkEventKey* key_event) { + + switch (key_event->keyval) { + case GDK_KEY_Escape: + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + { + if (_focus_widget) { + gtk_widget_grab_focus( _focus_widget ); + } + } + break; + } + + return false; +} diff --git a/src/ui/widget/ink-spinscale.h b/src/ui/widget/ink-spinscale.h new file mode 100644 index 0000000..4f07b27 --- /dev/null +++ b/src/ui/widget/ink-spinscale.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INK_SPINSCALE_H +#define INK_SPINSCALE_H + +/* + * Authors: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/** + A widget that allows entering a numerical value either by + clicking/dragging on a custom Gtk::Scale or by using a + Gtk::SpinButton. The custom Gtk::Scale differs from the stock + Gtk::Scale in that it includes a label to save space and has a + "slow-dragging" mode triggered by the Alt key. +*/ + +#include <glibmm/ustring.h> + +#include <gtkmm/box.h> +#include <gtkmm/scale.h> + +#include "scrollprotected.h" + +namespace Gtk { + class SpinButton; +} + +class InkScale : public Inkscape::UI::Widget::ScrollProtected<Gtk::Scale> +{ + using parent_type = ScrollProtected<Gtk::Scale>; + + public: + InkScale(Glib::RefPtr<Gtk::Adjustment>, Gtk::SpinButton* spinbutton); + ~InkScale() override = default;; + + void set_label(Glib::ustring label); + + bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override; + + protected: + + bool on_button_press_event(GdkEventButton* button_event) override; + bool on_button_release_event(GdkEventButton* button_event) override; + bool on_motion_notify_event(GdkEventMotion* motion_event) override; + + private: + + double get_fraction(); + void set_adjustment_value(double x, bool constrained = false); + + Gtk::SpinButton * _spinbutton; // Needed to get placement/text color. + Glib::ustring _label; + + bool _dragging; + double _drag_start; + double _drag_offset; +}; + +class InkSpinScale : public Gtk::Box +{ + public: + + // Create an InkSpinScale with a new adjustment. + InkSpinScale(double value, + double lower, + double upper, + double step_increment = 1, + double page_increment = 10, + double page_size = 0); + + // Create an InkSpinScale with a preexisting adjustment. + InkSpinScale(Glib::RefPtr<Gtk::Adjustment>); + + ~InkSpinScale() override = default;; + + void set_label(Glib::ustring label); + void set_digits(int digits); + int get_digits() const; + void set_focus_widget(GtkWidget *focus_widget); + Glib::RefPtr<Gtk::Adjustment> get_adjustment() { return _adjustment; }; + + protected: + + InkScale* _scale; + Gtk::SpinButton* _spinbutton; + Glib::RefPtr<Gtk::Adjustment> _adjustment; + GtkWidget* _focus_widget = nullptr; + + bool on_key_release_event(GdkEventKey* key_event) override; + + private: + +}; + +#endif // INK_SPINSCALE_H diff --git a/src/ui/widget/label-tool-item.cpp b/src/ui/widget/label-tool-item.cpp new file mode 100644 index 0000000..979cfa2 --- /dev/null +++ b/src/ui/widget/label-tool-item.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A label that can be added to a toolbar + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "label-tool-item.h" + +#include <gtkmm/label.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * \brief Create a tool-item containing a label + * + * \param[in] label The text to display in the label + * \param[in] mnemonic True if text should use a mnemonic + */ +LabelToolItem::LabelToolItem(const Glib::ustring& label, bool mnemonic) + : _label(Gtk::manage(new Gtk::Label(label, mnemonic))) +{ + add(*_label); + show_all(); +} + +/** + * \brief Set the markup text in the label + * + * \param[in] str The markup text + */ +void +LabelToolItem::set_markup(const Glib::ustring& str) +{ + _label->set_markup(str); +} + +/** + * \brief Sets whether label uses Pango markup + * + * \param[in] setting true if the label text should be parsed for markup + */ +void +LabelToolItem::set_use_markup(bool setting) +{ + _label->set_use_markup(setting); +} + +} +} +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode: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/widget/label-tool-item.h b/src/ui/widget/label-tool-item.h new file mode 100644 index 0000000..1fe6892 --- /dev/null +++ b/src/ui/widget/label-tool-item.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A label that can be added to a toolbar + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_LABEL_TOOL_ITEM_H +#define SEEN_LABEL_TOOL_ITEM_H + +#include <gtkmm/toolitem.h> + +namespace Gtk { +class Label; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * \brief A label that can be added to a toolbar + */ +class LabelToolItem : public Gtk::ToolItem { +private: + Gtk::Label *_label; + +public: + LabelToolItem(const Glib::ustring& label, bool mnemonic = false); + + void set_markup(const Glib::ustring& str); + void set_use_markup(bool setting = true); +}; +} +} +} + +#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/widget/labelled.cpp b/src/ui/widget/labelled.cpp new file mode 100644 index 0000000..3b47e84 --- /dev/null +++ b/src/ui/widget/labelled.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * + * Copyright (C) 2004 Carl Hetherington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "labelled.h" +#include "ui/icon-loader.h" +#include <gtkmm/image.h> +#include <gtkmm/label.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +Labelled::Labelled(Glib::ustring const &label, Glib::ustring const &tooltip, + Gtk::Widget *widget, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL), + _widget(widget), + _label(new Gtk::Label(label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, mnemonic)), + _suffix(nullptr) +{ + g_assert(g_utf8_validate(icon.c_str(), -1, nullptr)); + if (icon != "") { + _icon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_LARGE_TOOLBAR)); + pack_start(*_icon, Gtk::PACK_SHRINK); + } + + set_spacing(6); + // Setting margins separately allows for more control over them + set_margin_start(6); + set_margin_end(6); + pack_start(*Gtk::manage(_label), Gtk::PACK_SHRINK); + pack_start(*Gtk::manage(_widget), Gtk::PACK_SHRINK); + if (mnemonic) { + _label->set_mnemonic_widget(*_widget); + } + widget->set_tooltip_text(tooltip); +} + + +void Labelled::setWidgetSizeRequest(int width, int height) +{ + if (_widget) + _widget->set_size_request(width, height); + + +} + +Gtk::Label const * +Labelled::getLabel() const +{ + return _label; +} + +void +Labelled::setLabelText(const Glib::ustring &str) +{ + _label->set_text(str); +} + +void +Labelled::setTooltipText(const Glib::ustring &tooltip) +{ + _label->set_tooltip_text(tooltip); + _widget->set_tooltip_text(tooltip); +} + +bool Labelled::on_mnemonic_activate ( bool group_cycling ) +{ + return _widget->mnemonic_activate ( group_cycling ); +} + +void +Labelled::set_hexpand(bool expand) +{ + // should only have 2 children, but second child may not be _widget + child_property_pack_type(*get_children().back()) = expand ? Gtk::PACK_END + : Gtk::PACK_START; + + Gtk::Box::set_hexpand(expand); +} + +} // namespace Widget +} // 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/widget/labelled.h b/src/ui/widget/labelled.h new file mode 100644 index 0000000..bd82090 --- /dev/null +++ b/src/ui/widget/labelled.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * + * Copyright (C) 2004 Carl Hetherington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_LABELLED_H +#define INKSCAPE_UI_WIDGET_LABELLED_H + +#include <gtkmm/box.h> + +namespace Gtk { +class Image; +class Label; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Adds a label with optional icon or suffix to another widget. + */ +class Labelled : public Gtk::Box +{ +protected: + Gtk::Widget *_widget; + Gtk::Label *_label; + Gtk::Label *_suffix; + Gtk::Image *_icon; + +public: + /** + * Construct a Labelled Widget. + * + * @param label Label. + * @param widget Widget to label; should be allocated with new, as it will + * be passed to Gtk::manage(). + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the text + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to true). + */ + Labelled(Glib::ustring const &label, Glib::ustring const &tooltip, + Gtk::Widget *widget, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Allow the setting of the width of the labelled widget + */ + void setWidgetSizeRequest(int width, int height); + + inline decltype(_widget) getWidget() const { return _widget; } + Gtk::Label const *getLabel() const; + + void setLabelText(const Glib::ustring &str); + void setTooltipText(const Glib::ustring &tooltip); + + void set_hexpand(bool expand = true); + +private: + bool on_mnemonic_activate( bool group_cycling ) override; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_LABELLED_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/widget/layer-selector.cpp b/src/ui/widget/layer-selector.cpp new file mode 100644 index 0000000..a96c120 --- /dev/null +++ b/src/ui/widget/layer-selector.cpp @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Widgets::LayerSelector - layer selector widget + * + * Authors: + * MenTaLguY <mental@rydia.net> + * Abhishek Sharma + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <boost/range/adaptor/filtered.hpp> +#include <boost/range/adaptor/reversed.hpp> + +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "layer-manager.h" + +#include "ui/widget/layer-selector.h" +#include "ui/dialog/dialog-container.h" +#include "ui/dialog/objects.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/util.h" + +#include "object/sp-root.h" +#include "object/sp-item-group.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +class AlternateIcons : public Gtk::Box { +public: + AlternateIcons(Gtk::BuiltinIconSize size, Glib::ustring const &a, Glib::ustring const &b) + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) + , _a(nullptr) + , _b(nullptr) + { + set_name("AlternateIcons"); + if (!a.empty()) { + _a = Gtk::manage(sp_get_icon_image(a, size)); + _a->set_no_show_all(true); + add(*_a); + } + if (!b.empty()) { + _b = Gtk::manage(sp_get_icon_image(b, size)); + _b->set_no_show_all(true); + add(*_b); + } + setState(false); + } + + bool state() const { return _state; } + void setState(bool state) { + _state = state; + if (_state) { + if (_a) _a->hide(); + if (_b) _b->show(); + } else { + if (_a) _a->show(); + if (_b) _b->hide(); + } + } +private: + Gtk::Image *_a; + Gtk::Image *_b; + bool _state; +}; + +LayerSelector::LayerSelector(SPDesktop *desktop) + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) + , _desktop(nullptr) + , _observer(new Inkscape::XML::SignalObserver) +{ + set_name("LayerSelector"); + + _layer_name.signal_clicked().connect(sigc::mem_fun(*this, &LayerSelector::_layerChoose)); + _layer_name.set_relief(Gtk::RELIEF_NONE); + _layer_name.set_tooltip_text(_("Current layer")); + pack_start(_layer_name, Gtk::PACK_EXPAND_WIDGET); + + _eye_label = Gtk::manage(new AlternateIcons(Gtk::ICON_SIZE_MENU, + INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden"))); + _eye_toggle.add(*_eye_label); + _hide_layer_connection = _eye_toggle.signal_toggled().connect(sigc::mem_fun(*this, &LayerSelector::_hideLayer)); + + _eye_toggle.set_relief(Gtk::RELIEF_NONE); + _eye_toggle.set_tooltip_text(_("Toggle current layer visibility")); + pack_start(_eye_toggle, Gtk::PACK_EXPAND_PADDING); + + _lock_label = Gtk::manage(new AlternateIcons(Gtk::ICON_SIZE_MENU, + INKSCAPE_ICON("object-unlocked"), INKSCAPE_ICON("object-locked"))); + _lock_toggle.add(*_lock_label); + _lock_layer_connection = _lock_toggle.signal_toggled().connect(sigc::mem_fun(*this, &LayerSelector::_lockLayer)); + + _lock_toggle.set_relief(Gtk::RELIEF_NONE); + _lock_toggle.set_tooltip_text(_("Lock or unlock current layer")); + pack_start(_lock_toggle, Gtk::PACK_EXPAND_PADDING); + + _layer_name.add(_layer_label); + _layer_label.set_max_width_chars(16); + _layer_label.set_ellipsize(Pango::ELLIPSIZE_END); + _layer_label.set_markup("<i>Unset</i>"); + _layer_label.set_valign(Gtk::ALIGN_CENTER); + + _observer->signal_changed().connect(sigc::mem_fun(*this, &LayerSelector::_layerModified)); + setDesktop(desktop); +} + +LayerSelector::~LayerSelector() { + setDesktop(nullptr); +} + +void LayerSelector::setDesktop(SPDesktop *desktop) { + if ( desktop == _desktop ) + return; + + _layer_changed.disconnect(); + _desktop = desktop; + + if (_desktop) { + _layer_changed = _desktop->layerManager().connectCurrentLayerChanged(sigc::mem_fun(*this, &LayerSelector::_layerChanged)); + _layerChanged(_desktop->layerManager().currentLayer()); + } +} + +/** + * Selects the given layer in the widget. + */ +void LayerSelector::_layerChanged(SPGroup *layer) +{ + _layer = layer; + _observer->set(layer); + _layerModified(); +} + +/** + * If anything happens to the layer, refresh it. + */ +void LayerSelector::_layerModified() +{ + auto root = _desktop->layerManager().currentRoot(); + bool active = _layer && _layer != root; + + if (_label_style) { + _layer_label.get_style_context()->remove_provider(_label_style); + } + auto color_str = std::string("white"); + + if (active) { + _layer_label.set_text(_layer->defaultLabel()); + color_str = SPColor(_layer->highlight_color()).toString(); + } else { + _layer_label.set_markup(_layer ? "<i>[root]</i>" : "<i>nothing</i>"); + } + + Glib::RefPtr<Gtk::StyleContext> style_context = _layer_label.get_style_context(); + _label_style = Gtk::CssProvider::create(); + _label_style->load_from_data("#LayerSelector label {border-color:" + color_str + ";}"); + _layer_label.get_style_context()->add_provider(_label_style, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + _hide_layer_connection.block(); + _lock_layer_connection.block(); + _eye_toggle.set_sensitive(active); + _lock_toggle.set_sensitive(active); + _eye_label->setState(active && _layer->isHidden()); + _eye_toggle.set_active(active && _layer->isHidden()); + _lock_label->setState(active && _layer->isLocked()); + _lock_toggle.set_active(active && _layer->isLocked()); + _hide_layer_connection.unblock(); + _lock_layer_connection.unblock(); +} + +void LayerSelector::_lockLayer() +{ + bool lock = _lock_toggle.get_active(); + if (auto layer = _desktop->layerManager().currentLayer()) { + layer->setLocked(lock); + DocumentUndo::done(_desktop->getDocument(), lock ? _("Lock layer") : _("Unlock layer"), ""); + } +} + +void LayerSelector::_hideLayer() +{ + bool hide = _eye_toggle.get_active(); + if (auto layer = _desktop->layerManager().currentLayer()) { + layer->setHidden(hide); + DocumentUndo::done(_desktop->getDocument(), hide ? _("Hide layer") : _("Unhide layer"), ""); + } +} + +void LayerSelector::_layerChoose() +{ + _desktop->getContainer()->new_dialog("Objects"); +} + +} // namespace Widget +} // 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/widget/layer-selector.h b/src/ui/widget/layer-selector.h new file mode 100644 index 0000000..6300cba --- /dev/null +++ b/src/ui/widget/layer-selector.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::UI::Widget::LayerSelector - layer selector widget + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_WIDGETS_LAYER_SELECTOR +#define SEEN_INKSCAPE_WIDGETS_LAYER_SELECTOR + +#include <gtkmm/box.h> +#include <gtkmm/combobox.h> +#include <gtkmm/togglebutton.h> +#include <gtkmm/cellrenderertext.h> +#include <gtkmm/treemodel.h> +#include <gtkmm/liststore.h> +#include <gtkmm/cssprovider.h> +#include <sigc++/slot.h> + +#include "xml/helper-observer.h" + +class SPDesktop; +class SPDocument; +class SPGroup; + +namespace Inkscape { +namespace UI { +namespace Widget { + +class AlternateIcons; + +class LayerSelector : public Gtk::Box { +public: + LayerSelector(SPDesktop *desktop = nullptr); + ~LayerSelector() override; + + void setDesktop(SPDesktop *desktop); +private: + SPDesktop *_desktop; + SPGroup *_layer; + + Gtk::ToggleButton _eye_toggle; + Gtk::ToggleButton _lock_toggle; + Gtk::Button _layer_name; + Gtk::Label _layer_label; + Glib::RefPtr<Gtk::CssProvider> _label_style; + AlternateIcons * _eye_label; + AlternateIcons * _lock_label; + + sigc::connection _layer_changed; + sigc::connection _hide_layer_connection; + sigc::connection _lock_layer_connection; + std::unique_ptr<Inkscape::XML::SignalObserver> _observer; + + void _layerChanged(SPGroup *layer); + void _layerModified(); + void _selectLayer(); + void _hideLayer(); + void _lockLayer(); + void _layerChoose(); +}; + +} // namespace Widget +} // 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/widget/licensor.cpp b/src/ui/widget/licensor.cpp new file mode 100644 index 0000000..d6ed8ff --- /dev/null +++ b/src/ui/widget/licensor.cpp @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * 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) + * Abhishek Sharma + * + * Copyright (C) 2000 - 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "licensor.h" + +#include <gtkmm/entry.h> +#include <gtkmm/radiobutton.h> + +#include "rdf.h" +#include "inkscape.h" +#include "document-undo.h" + +#include "ui/widget/entity-entry.h" +#include "ui/widget/registry.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +//=================================================== + +const struct rdf_license_t _proprietary_license = + {_("Proprietary"), "", nullptr}; + +const struct rdf_license_t _other_license = + {Q_("MetadataLicence|Other"), "", nullptr}; + +class LicenseItem : public Gtk::RadioButton { +public: + LicenseItem (struct rdf_license_t const* license, EntityEntry* entity, Registry &wr, Gtk::RadioButtonGroup *group); +protected: + void on_toggled() override; + struct rdf_license_t const *_lic; + EntityEntry *_eep; + Registry &_wr; +}; + +LicenseItem::LicenseItem (struct rdf_license_t const* license, EntityEntry* entity, Registry &wr, Gtk::RadioButtonGroup *group) +: Gtk::RadioButton(_(license->name)), _lic(license), _eep(entity), _wr(wr) +{ + if (group) { + set_group (*group); + } +} + +/// \pre it is assumed that the license URI entry is a Gtk::Entry +void LicenseItem::on_toggled() +{ + if (_wr.isUpdating() || !_wr.desktop()) + return; + + _wr.setUpdating (true); + SPDocument *doc = _wr.desktop()->getDocument(); + rdf_set_license (doc, _lic->details ? _lic : nullptr); + if (doc->isSensitive()) { + DocumentUndo::done(doc, _("Document license updated"), ""); + } + _wr.setUpdating (false); + static_cast<Gtk::Entry*>(_eep->_packable)->set_text (_lic->uri); + _eep->on_changed(); +} + +//--------------------------------------------------- + +Licensor::Licensor() +: Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4), + _eentry (nullptr) +{ +} + +Licensor::~Licensor() +{ + if (_eentry) delete _eentry; +} + +void Licensor::init (Registry& wr) +{ + /* add license-specific metadata entry areas */ + rdf_work_entity_t* entity = rdf_find_entity ( "license_uri" ); + _eentry = EntityEntry::create (entity, wr); + + LicenseItem *i; + wr.setUpdating (true); + i = Gtk::manage (new LicenseItem (&_proprietary_license, _eentry, wr, nullptr)); + Gtk::RadioButtonGroup group = i->get_group(); + add (*i); + LicenseItem *pd = i; + + for (struct rdf_license_t * license = rdf_licenses; + license && license->name; + license++) { + i = Gtk::manage (new LicenseItem (license, _eentry, wr, &group)); + add(*i); + } + // add Other at the end before the URI field for the confused ppl. + LicenseItem *io = Gtk::manage (new LicenseItem (&_other_license, _eentry, wr, &group)); + add (*io); + + pd->set_active(); + wr.setUpdating (false); + + Gtk::Box *box = Gtk::manage (new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + pack_start (*box, true, true, 0); + + box->pack_start (_eentry->_label, false, false, 5); + box->pack_start (*_eentry->_packable, true, true, 0); + + show_all_children(); +} + +void Licensor::update (SPDocument *doc) +{ + /* identify the license info */ + struct rdf_license_t * license = rdf_get_license (doc); + + if (license) { + int i; + for (i=0; rdf_licenses[i].name; i++) + if (license == &rdf_licenses[i]) + break; + static_cast<LicenseItem*>(get_children()[i+1])->set_active(); + } + else { + static_cast<LicenseItem*>(get_children()[0])->set_active(); + } + + /* update the URI */ + _eentry->update (doc); +} + +} // 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/widget/licensor.h b/src/ui/widget/licensor.h new file mode 100644 index 0000000..214ffca --- /dev/null +++ b/src/ui/widget/licensor.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_LICENSOR_H +#define INKSCAPE_UI_WIDGET_LICENSOR_H + +#include <gtkmm/box.h> + +class SPDocument; + +namespace Inkscape { + namespace UI { + namespace Widget { + +class EntityEntry; +class Registry; + + +/** + * Widget for specifying a document's license; part of document + * preferences dialog. + */ +class Licensor : public Gtk::Box { +public: + Licensor(); + ~Licensor() override; + void init (Registry&); + void update (SPDocument *doc); + +protected: + EntityEntry *_eentry; +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_LICENSOR_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/widget/marker-combo-box.cpp b/src/ui/widget/marker-combo-box.cpp new file mode 100644 index 0000000..a8ea110 --- /dev/null +++ b/src/ui/widget/marker-combo-box.cpp @@ -0,0 +1,1033 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Combobox for selecting dash patterns - implementation. + */ +/* Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Maximilian Albert <maximilian.albert@gmail.com> + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "marker-combo-box.h" + +#include <glibmm/fileutils.h> +#include <glibmm/i18n.h> +#include <gtkmm/icontheme.h> +#include <gtkmm/menubutton.h> + +#include "desktop-style.h" +#include "helper/stock-items.h" +#include "io/resource.h" +#include "io/sys.h" +#include "object/sp-defs.h" +#include "object/sp-marker.h" +#include "object/sp-root.h" +#include "path-prefix.h" +#include "style.h" +#include "ui/builder-utils.h" +#include "ui/cache/svg_preview_cache.h" +#include "ui/dialog-events.h" +#include "ui/icon-loader.h" +#include "ui/svg-renderer.h" +#include "ui/util.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/stroke-style.h" + +#define noTIMING_INFO 1; + +using Inkscape::UI::get_widget; +using Inkscape::UI::create_builder; + +// size of marker image in a list +static const int ITEM_WIDTH = 40; +static const int ITEM_HEIGHT = 32; + +namespace Inkscape { +namespace UI { +namespace Widget { + +// separator for FlowBox widget +static cairo_surface_t* create_separator(double alpha, int width, int height, int device_scale) { + width *= device_scale; + height *= device_scale; + cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t* ctx = cairo_create(surface); + cairo_set_source_rgba(ctx, 0.5, 0.5, 0.5, alpha); + cairo_move_to(ctx, 0.5, height / 2 + 0.5); + cairo_line_to(ctx, width + 0.5, height / 2 + 0.5); + cairo_set_line_width(ctx, 1.0 * device_scale); + cairo_stroke(ctx); + cairo_surface_flush(surface); + cairo_surface_set_device_scale(surface, device_scale, device_scale); + return surface; +} + +// empty image; "no marker" +static Cairo::RefPtr<Cairo::Surface> g_image_none; +// error extracting/rendering marker; "bad marker" +static Cairo::RefPtr<Cairo::Surface> g_bad_marker; + +Glib::ustring get_attrib(SPMarker* marker, const char* attrib) { + auto value = marker->getAttribute(attrib); + return value ? value : ""; +} + +double get_attrib_num(SPMarker* marker, const char* attrib) { + auto val = get_attrib(marker, attrib); + return strtod(val.c_str(), nullptr); +} + +MarkerComboBox::MarkerComboBox(Glib::ustring id, int l) : + _combo_id(std::move(id)), + _loc(l), + _builder(create_builder("marker-popup.glade")), + _marker_list(get_widget<Gtk::FlowBox>(_builder, "flowbox")), + _preview(get_widget<Gtk::Image>(_builder, "preview")), + _marker_name(get_widget<Gtk::Label>(_builder, "marker-id")), + _link_scale(get_widget<Gtk::Button>(_builder, "link-scale")), + _scale_x(get_widget<Gtk::SpinButton>(_builder, "scale-x")), + _scale_y(get_widget<Gtk::SpinButton>(_builder, "scale-y")), + _scale_with_stroke(get_widget<Gtk::CheckButton>(_builder, "scale-with-stroke")), + _menu_btn(get_widget<Gtk::MenuButton>(_builder, "menu-btn")), + _angle_btn(get_widget<Gtk::SpinButton>(_builder, "angle")), + _offset_x(get_widget<Gtk::SpinButton>(_builder, "offset-x")), + _offset_y(get_widget<Gtk::SpinButton>(_builder, "offset-y")), + _input_grid(get_widget<Gtk::Grid>(_builder, "input-grid")), + _orient_auto_rev(get_widget<Gtk::RadioButton>(_builder, "orient-auto-rev")), + _orient_auto(get_widget<Gtk::RadioButton>(_builder, "orient-auto")), + _orient_angle(get_widget<Gtk::RadioButton>(_builder, "orient-angle")), + _orient_flip_horz(get_widget<Gtk::Button>(_builder, "btn-horz-flip")), + _current_img(get_widget<Gtk::Image>(_builder, "current-img")), + _edit_marker(get_widget<Gtk::Button>(_builder, "edit-marker")) +{ + _background_color = 0x808080ff; + _foreground_color = 0x808080ff; + + if (!g_image_none) { + auto device_scale = get_scale_factor(); + g_image_none = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(create_separator(1, ITEM_WIDTH, ITEM_HEIGHT, device_scale))); + } + + if (!g_bad_marker) { + auto path = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::UIS, "bad-marker.svg"); + Inkscape::svg_renderer renderer(path.c_str()); + g_bad_marker = renderer.render_surface(1.0); + } + + add(_menu_btn); + + _preview.signal_size_allocate().connect([=](Gtk::Allocation& a){ + // refresh after preview widget has been finally resized/expanded + if (_preview_no_alloc) update_preview(find_marker_item(get_current())); + }); + + _marker_store = Gio::ListStore<MarkerItem>::create(); + _marker_list.bind_list_store(_marker_store, [=](const Glib::RefPtr<MarkerItem>& item){ + auto image = Gtk::make_managed<Gtk::Image>(item->pix); + image->show(); + auto box = Gtk::make_managed<Gtk::FlowBoxChild>(); + box->add(*image); + if (item->separator) { + image->set_sensitive(false); + image->set_can_focus(false); + image->set_size_request(-1, 10); + box->set_sensitive(false); + box->set_can_focus(false); + box->get_style_context()->add_class("marker-separator"); + } + else { + box->get_style_context()->add_class("marker-item-box"); + } + _widgets_to_markers[image] = item; + box->set_size_request(item->width, item->height); + return box; + }); + + _sandbox = ink_markers_preview_doc(_combo_id); + + set_sensitive(true); + + _marker_list.signal_selected_children_changed().connect([=](){ + auto item = get_active(); + if (!item && !_marker_list.get_selected_children().empty()) { + _marker_list.unselect_all(); + } + }); + + _marker_list.signal_child_activated().connect([=](Gtk::FlowBoxChild* box){ + if (box->get_sensitive()) _signal_changed.emit(); + }); + + auto set_orient = [=](bool enable_angle, const char* value) { + if (_update.pending()) return; + _angle_btn.set_sensitive(enable_angle); + sp_marker_set_orient(get_current(), value); + }; + _orient_auto_rev.signal_toggled().connect([=](){ set_orient(false, "auto-start-reverse"); }); + _orient_auto.signal_toggled().connect([=]() { set_orient(false, "auto"); }); + _orient_angle.signal_toggled().connect([=]() { set_orient(true, _angle_btn.get_text().c_str()); }); + _orient_flip_horz.signal_clicked().connect([=]() { sp_marker_flip_horizontally(get_current()); }); + + _angle_btn.signal_value_changed().connect([=]() { + if (_update.pending() || !_angle_btn.is_sensitive()) return; + sp_marker_set_orient(get_current(), _angle_btn.get_text().c_str()); + }); + + auto set_scale = [=](bool changeWidth) { + if (_update.pending()) return; + if (auto marker = get_current()) { + auto sx = _scale_x.get_value(); + auto sy = _scale_y.get_value(); + auto width = get_attrib_num(marker, "markerWidth"); + auto height = get_attrib_num(marker, "markerHeight"); + if (_scale_linked && width > 0.0 && height > 0.0) { + auto scoped(_update.block()); + if (changeWidth) { + // scale height proportionally + sy = height * (sx / width); + _scale_y.set_value(sy); + } + else { + // scale width proportionally + sx = width * (sy / height); + _scale_x.set_value(sx); + } + } + sp_marker_set_size(marker, sx, sy); + } + }; + + _link_scale.signal_clicked().connect([=](){ + if (_update.pending()) return; + _scale_linked = !_scale_linked; + sp_marker_set_uniform_scale(get_current(), _scale_linked); + update_scale_link(); + }); + + _scale_x.signal_value_changed().connect([=]() { set_scale(true); }); + _scale_y.signal_value_changed().connect([=]() { set_scale(false); }); + + _scale_with_stroke.signal_toggled().connect([=](){ + if (_update.pending()) return; + sp_marker_scale_with_stroke(get_current(), _scale_with_stroke.get_active()); + }); + + auto set_offset = [=](){ + if (_update.pending()) return; + sp_marker_set_offset(get_current(), _offset_x.get_value(), _offset_y.get_value()); + }; + _offset_x.signal_value_changed().connect([=]() { set_offset(); }); + _offset_y.signal_value_changed().connect([=]() { set_offset(); }); + + // request to edit marker on canvas; close popup to get it out of the way and call marker edit tool + _edit_marker.signal_clicked().connect([=]() { _menu_btn.get_popover()->popdown(); edit_signal(); }); + + // before showing popover refresh marker attributes + _menu_btn.get_popover()->signal_show().connect([=](){ update_ui(get_current(), false); }, false); + + update_scale_link(); + _current_img.set(g_image_none); + show(); +} + +MarkerComboBox::~MarkerComboBox() { + if (_document) { + modified_connection.disconnect(); + } +} + +void MarkerComboBox::update_widgets_from_marker(SPMarker* marker) { + _input_grid.set_sensitive(marker != nullptr); + + if (marker) { + marker->updateRepr(); + + _scale_x.set_value(get_attrib_num(marker, "markerWidth")); + _scale_y.set_value(get_attrib_num(marker, "markerHeight")); + auto units = get_attrib(marker, "markerUnits"); + _scale_with_stroke.set_active(units == "strokeWidth" || units == ""); + auto aspect = get_attrib(marker, "preserveAspectRatio"); + _scale_linked = aspect != "none"; + update_scale_link(); + // marker->setAttribute("markerUnits", scale_with_stroke ? "strokeWidth" : "userSpaceOnUse"); + _offset_x.set_value(get_attrib_num(marker, "refX")); + _offset_y.set_value(get_attrib_num(marker, "refY")); + auto orient = get_attrib(marker, "orient"); + + // try parsing as number + _angle_btn.set_value(strtod(orient.c_str(), nullptr)); + if (orient == "auto-start-reverse") { + _orient_auto_rev.set_active(); + _angle_btn.set_sensitive(false); + } + else if (orient == "auto") { + _orient_auto.set_active(); + _angle_btn.set_sensitive(false); + } + else { + _orient_angle.set_active(); + _angle_btn.set_sensitive(true); + } + } +} + +void MarkerComboBox::update_scale_link() { + _link_scale.remove(); + _link_scale.add(get_widget<Gtk::Image>(_builder, _scale_linked ? "image-linked" : "image-unlinked")); +} + +// update marker image inside the menu button +void MarkerComboBox::update_menu_btn(Glib::RefPtr<MarkerItem> marker) { + _current_img.set(marker ? marker->pix : g_image_none); +} + +// update marker preview image in the popover panel +void MarkerComboBox::update_preview(Glib::RefPtr<MarkerItem> item) { + Cairo::RefPtr<Cairo::Surface> surface; + Glib::ustring label; + + if (!item) { + // TRANSLATORS: None - no marker selected for a path + label = _("None"); + } + + if (item && item->source && !item->id.empty()) { + Inkscape::Drawing drawing; + unsigned const visionkey = SPItem::display_key_new(1); + drawing.setRoot(_sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY)); + // generate preview + auto alloc = _preview.get_allocation(); + auto size = Geom::IntPoint(alloc.get_width() - 10, alloc.get_height() - 10); + if (size.x() > 0 && size.y() > 0) { + surface = create_marker_image(size, item->id.c_str(), item->source, drawing, visionkey, true, true, 2.60); + } + else { + // too early, preview hasn't been expanded/resized yet + _preview_no_alloc = true; + } + _sandbox->getRoot()->invoke_hide(visionkey); + label = item->label; + } + + _preview.set(surface); + std::ostringstream ost; + ost << "<small>" << label.raw() << "</small>"; + _marker_name.set_markup(ost.str().c_str()); +} + +bool MarkerComboBox::MarkerItem::operator == (const MarkerItem& item) const { + return + id == item.id && + label == item.label && + separator == item.separator && + stock == item.stock && + history == item.history && + source == item.source && + width == item.width && + height == item.height; +} + +// find marker object by ID in a document +SPMarker* find_marker(SPDocument* document, const Glib::ustring& marker_id) { + if (!document) return nullptr; + + SPDefs* defs = document->getDefs(); + if (!defs) return nullptr; + + for (auto& child : defs->children) { + if (SP_IS_MARKER(&child)) { + auto marker = SP_MARKER(&child); + auto id = marker->getId(); + if (id && marker_id == id) { + // found it + return marker; + } + } + } + + // not found + return nullptr; +} + +SPMarker* MarkerComboBox::get_current() const { + // find current marker + return find_marker(_document, _current_marker_id); +} + +void MarkerComboBox::set_active(Glib::RefPtr<MarkerItem> item) { + bool selected = false; + if (item) { + _marker_list.foreach([=,&selected](Gtk::Widget& widget){ + if (auto box = dynamic_cast<Gtk::FlowBoxChild*>(&widget)) { + if (auto marker = _widgets_to_markers[box->get_child()]) { + if (*marker.get() == *item.get()) { + _marker_list.select_child(*box); + selected = true; + } + } + } + }); + } + + if (!selected) { + _marker_list.unselect_all(); + } +} + +Glib::RefPtr<MarkerComboBox::MarkerItem> MarkerComboBox::find_marker_item(SPMarker* marker) { + std::string id; + if (marker != nullptr) { + if (auto markname = marker->getRepr()->attribute("id")) { + id = markname; + } + } + + Glib::RefPtr<MarkerItem> marker_item; + if (!id.empty()) { + for (auto&& item : _history_items) { + if (item->id == id) { + marker_item = item; + break; + } + } + } + + return marker_item; +} + +Glib::RefPtr<MarkerComboBox::MarkerItem> MarkerComboBox::get_active() { + auto empty = Glib::RefPtr<MarkerItem>(); + auto sel = _marker_list.get_selected_children(); + if (sel.size() == 1) { + auto item = _widgets_to_markers[sel.front()->get_child()]; + if (item && item->separator) { + return empty; + } + return item; + } + else { + return empty; + } +} + +void MarkerComboBox::setDocument(SPDocument *document) +{ + if (_document != document) { + + if (_document) { + modified_connection.disconnect(); + } + + _document = document; + + if (_document) { + modified_connection = _document->getDefs()->connectModified([=](SPObject*, unsigned int){ + refresh_after_markers_modified(); + }); + } + + _current_marker_id = ""; + + refresh_after_markers_modified(); + } +} + +/** + * This function is invoked after document "defs" section changes. + * It will change when current marker's attributes are modified in this popup + * and this function will refresh the recent list and a preview to reflect the changes. + * It would be more efficient if there was a way to determine what has changed + * and perform only more targeted update. + */ +void MarkerComboBox::refresh_after_markers_modified() { + if (_update.pending()) return; + + auto scoped(_update.block()); + + // collect orphaned markers, so they are not listed; if they are listed then + // they disappear upon selection leaving dangling URLs + if (_document) _document->collectOrphans(); + + /* + * Seems to be no way to get notified of changes just to markers, + * so listen to changes in all defs and check if the number of markers has changed here + * to avoid unnecessary refreshes when things like gradients change + */ + // TODO: detect changes to markers; ignore changes to everything else; + // simple count check doesn't cut it, so just do it unconditionally for now + marker_list_from_doc(_document, true); + + auto marker = find_marker_item(get_current()); + update_menu_btn(marker); + update_preview(marker); +} + +Glib::RefPtr<MarkerComboBox::MarkerItem> MarkerComboBox::add_separator(bool filler) { + auto item = Glib::RefPtr<MarkerItem>(new MarkerItem); + item->history = false; + item->separator = true; + item->id = "None"; + item->label = filler ? "filler" : "Separator"; + item->stock = false; + if (!filler) { + auto device_scale = get_scale_factor(); + static Cairo::RefPtr<Cairo::Surface> separator(new Cairo::Surface(create_separator(0.7, ITEM_WIDTH, 10, device_scale))); + item->pix = separator; + } + item->height = 10; + item->width = -1; + return item; +} + +/** + * Init the combobox widget to display markers from markers.svg + */ +void +MarkerComboBox::init_combo() +{ + if (_update.pending()) return; + + static SPDocument *markers_doc = nullptr; + + // find and load markers.svg + if (markers_doc == nullptr) { + using namespace Inkscape::IO::Resource; + auto markers_source = get_path_string(SYSTEM, MARKERS, "markers.svg"); + if (Glib::file_test(markers_source, Glib::FILE_TEST_IS_REGULAR)) { + markers_doc = SPDocument::createNewDoc(markers_source.c_str(), false); + } + } + + // load markers from markers.svg + if (markers_doc) { + marker_list_from_doc(markers_doc, false); + } + + refresh_after_markers_modified(); +} + +/** + * Sets the current marker in the marker combobox. + */ +void MarkerComboBox::set_current(SPObject *marker) +{ + auto sp_marker = dynamic_cast<SPMarker*>(marker); + + bool reselect = sp_marker != get_current(); + + update_ui(sp_marker, reselect); +} + +void MarkerComboBox::update_ui(SPMarker* marker, bool select) { + auto scoped(_update.block()); + + auto id = marker ? marker->getId() : nullptr; + _current_marker_id = id ? id : ""; + + auto marker_item = find_marker_item(marker); + + if (select) { + set_active(marker_item); + } + + update_widgets_from_marker(marker); + update_menu_btn(marker_item); + update_preview(marker_item); +} + +/** + * Return a uri string representing the current selected marker used for setting the marker style in the document + */ +std::string MarkerComboBox::get_active_marker_uri() +{ + /* Get Marker */ + auto item = get_active(); + if (!item) { + return std::string(); + } + + std::string marker; + + if (item->id != "none") { + bool stockid = item->stock; + + std::string markurn = stockid ? "urn:inkscape:marker:" + item->id : item->id; + auto mark = dynamic_cast<SPMarker*>(get_stock_item(markurn.c_str(), stockid)); + + if (mark) { + Inkscape::XML::Node* repr = mark->getRepr(); + auto id = repr->attribute("id"); + if (id) { + std::ostringstream ost; + ost << "url(#" << id << ")"; + marker = ost.str(); + } + if (stockid) { + mark->getRepr()->setAttribute("inkscape:collect", "always"); + } + // adjust marker's attributes (or add missing ones) to stay in sync with marker tool + sp_validate_marker(mark, _document); + } + } else { + marker = item->id; + } + + return marker; +} + +/** + * Pick up all markers from source and add items to the list/store. + * If 'history' is true, then update recently used in-document portion of the list; + * otherwise update list of stock markers, which is displayed after recent ones + */ +void MarkerComboBox::marker_list_from_doc(SPDocument* source, bool history) { + std::vector<SPMarker*> markers = get_marker_list(source); + remove_markers(history); + add_markers(markers, source, history); + update_store(); +} + +void MarkerComboBox::update_store() { + _marker_store->freeze_notify(); + + auto selected = get_active(); + + _marker_store->remove_all(); + _widgets_to_markers.clear(); + + // recent and user-defined markers come first + for (auto&& item : _history_items) { + _marker_store->append(item); + } + + // separator + if (!_history_items.empty()) { + // add empty boxes to fill up the row to 'max' elements and then + // extra ones to create entire new empty row (a separator of sorts) + auto max = _marker_list.get_max_children_per_line(); + auto fillup = max - _history_items.size() % max; + + for (int i = 0; i < fillup; ++i) { + _marker_store->append(add_separator(true)); + } + for (int i = 0; i < max; ++i) { + _marker_store->append(add_separator(false)); + } + } + + // stock markers + for (auto&& item : _stock_items) { + _marker_store->append(item); + } + + _marker_store->thaw_notify(); + + // reselect current + set_active(selected); +} +/** + * Returns a vector of markers in the defs of the given source document as a vector. + * Returns empty vector if there are no markers in the document. + * If validate is true then it runs each marker through the validation routine that alters some attributes. + */ +std::vector<SPMarker*> MarkerComboBox::get_marker_list(SPDocument* source) +{ + std::vector<SPMarker *> ml; + if (source == nullptr) return ml; + + SPDefs *defs = source->getDefs(); + if (!defs) { + return ml; + } + + for (auto& child: defs->children) { + if (SP_IS_MARKER(&child)) { + auto marker = SP_MARKER(&child); + ml.push_back(marker); + } + } + return ml; +} + +/** + * Remove history or non-history markers from the combo + */ +void MarkerComboBox::remove_markers (gboolean history) +{ + if (history) { + _history_items.clear(); + } + else { + _stock_items.clear(); + } +} + +/** + * Adds markers in marker_list to the combo + */ +void MarkerComboBox::add_markers (std::vector<SPMarker *> const& marker_list, SPDocument *source, gboolean history) +{ + // Do this here, outside of loop, to speed up preview generation: + Inkscape::Drawing drawing; + unsigned const visionkey = SPItem::display_key_new(1); + drawing.setRoot(_sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY)); + + if (history) { + // add "None" + auto item = Glib::RefPtr<MarkerItem>(new MarkerItem); + item->pix = g_image_none; + item->history = true; + item->separator = false; + item->id = "None"; + item->label = "None"; + item->stock = false; + item->width = ITEM_WIDTH; + item->height = ITEM_HEIGHT; + _history_items.push_back(item); + } + +#if TIMING_INFO +auto old_time = std::chrono::high_resolution_clock::now(); +#endif + + for (auto i:marker_list) { + + Inkscape::XML::Node *repr = i->getRepr(); + gchar const *markid = repr->attribute("inkscape:stockid") ? repr->attribute("inkscape:stockid") : repr->attribute("id"); + + // generate preview + auto pixbuf = create_marker_image(Geom::IntPoint(ITEM_WIDTH, ITEM_HEIGHT), repr->attribute("id"), source, drawing, visionkey, false, true, 1.50); + + auto item = Glib::RefPtr<MarkerItem>(new MarkerItem); + item->source = source; + item->pix = pixbuf; + if (auto id = repr->attribute("id")) { + item->id = id; + } + item->label = markid ? markid : ""; + item->stock = !history; + item->history = history; + item->width = ITEM_WIDTH; + item->height = ITEM_HEIGHT; + + if (history) { + _history_items.push_back(item); + } + else { + _stock_items.push_back(item); + } + } + + _sandbox->getRoot()->invoke_hide(visionkey); + +#if TIMING_INFO +auto current_time = std::chrono::high_resolution_clock::now(); +auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - old_time); +g_warning("%s render time for %d markers: %d ms", combo_id, (int)marker_list.size(), static_cast<int>(elapsed.count())); +#endif +} + +/** + * Creates a copy of the marker named mname, determines its visible and renderable + * area in the bounding box, and then renders it. This allows us to fill in + * preview images of each marker in the marker combobox. + */ +Cairo::RefPtr<Cairo::Surface> +MarkerComboBox::create_marker_image(Geom::IntPoint pixel_size, gchar const *mname, + SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/, bool checkerboard, bool no_clip, double scale) +{ + // Retrieve the marker named 'mname' from the source SVG document + SPObject const *marker = source->getObjectById(mname); + if (marker == nullptr) { + g_warning("bad mname: %s", mname); + return g_bad_marker; + } + + SPObject *oldmarker = _sandbox->getObjectById("sample"); + if (oldmarker) { + oldmarker->deleteObject(false); + } + + // Create a copy repr of the marker with id="sample" + Inkscape::XML::Document *xml_doc = _sandbox->getReprDoc(); + Inkscape::XML::Node *mrepr = marker->getRepr()->duplicate(xml_doc); + mrepr->setAttribute("id", "sample"); + + // Replace the old sample in the sandbox by the new one + Inkscape::XML::Node *defsrepr = _sandbox->getObjectById("defs")->getRepr(); + + // TODO - This causes a SIGTRAP on windows + defsrepr->appendChild(mrepr); + + Inkscape::GC::release(mrepr); + + // If the marker color is a url link to a pattern or gradient copy that too + SPObject *mk = source->getObjectById(mname); + SPCSSAttr *css_marker = sp_css_attr_from_object(mk->firstChild(), SP_STYLE_FLAG_ALWAYS); + //const char *mfill = sp_repr_css_property(css_marker, "fill", "none"); + const char *mstroke = sp_repr_css_property(css_marker, "fill", "none"); + + if (!strncmp (mstroke, "url(", 4)) { + SPObject *linkObj = getMarkerObj(mstroke, source); + if (linkObj) { + Inkscape::XML::Node *grepr = linkObj->getRepr()->duplicate(xml_doc); + SPObject *oldmarker = _sandbox->getObjectById(linkObj->getId()); + if (oldmarker) { + oldmarker->deleteObject(false); + } + defsrepr->appendChild(grepr); + Inkscape::GC::release(grepr); + + if (SP_IS_GRADIENT(linkObj)) { + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (SP_GRADIENT(linkObj), false); + if (vector) { + Inkscape::XML::Node *grepr = vector->getRepr()->duplicate(xml_doc); + SPObject *oldmarker = _sandbox->getObjectById(vector->getId()); + if (oldmarker) { + oldmarker->deleteObject(false); + } + defsrepr->appendChild(grepr); + Inkscape::GC::release(grepr); + } + } + } + } + +// Uncomment this to get the sandbox documents saved (useful for debugging) + // FILE *fp = fopen (g_strconcat(combo_id, mname, ".svg", nullptr), "w"); + // sp_repr_save_stream(_sandbox->getReprDoc(), fp); + // fclose (fp); + + // object to render; note that the id is the same as that of the combo we're building + SPObject *object = _sandbox->getObjectById(_combo_id); + + if (object == nullptr || !SP_IS_ITEM(object)) { + g_warning("no obj: %s", _combo_id.c_str()); + return g_bad_marker; + } + + auto context = get_style_context(); + Gdk::RGBA fg = context->get_color(get_state_flags()); + auto fgcolor = rgba_to_css_color(fg); + fg.set_red(1 - fg.get_red()); + fg.set_green(1 - fg.get_green()); + fg.set_blue(1 - fg.get_blue()); + auto bgcolor = rgba_to_css_color(fg); + auto objects = _sandbox->getObjectsBySelector(".colors"); + for (auto el : objects) { + if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) { + sp_repr_css_set_property(css, "fill", bgcolor.c_str()); + sp_repr_css_set_property(css, "stroke", fgcolor.c_str()); + el->changeCSS(css, "style"); + sp_repr_css_attr_unref(css); + } + } + + auto cross = _sandbox->getObjectsBySelector(".cross"); + double stroke = 0.5; + for (auto el : cross) { + if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) { + sp_repr_css_set_property(css, "display", checkerboard ? "block" : "none"); + sp_repr_css_set_property_double(css, "stroke-width", stroke); + el->changeCSS(css, "style"); + sp_repr_css_attr_unref(css); + } + } + + SPDocument::install_reference_document scoped(_sandbox.get(), marker->document); + + _sandbox->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + _sandbox->ensureUpToDate(); + + SPItem *item = SP_ITEM(object); + // Find object's bbox in document + Geom::OptRect dbox = item->documentVisualBounds(); + + if (!dbox) { + g_warning("no dbox"); + return g_bad_marker; + } + + if (auto measure = dynamic_cast<SPItem*>(_sandbox->getObjectById("measure-marker"))) { + if (auto box = measure->documentVisualBounds()) { + // check size of the marker applied to a path with stroke of 1px + auto size = std::max(box->width(), box->height()); + const double small = 5.0; + // if too small, then scale up; clip needs to be enabled for scale to work + if (size > 0 && size < small) { + auto factor = 1 + small - size; + scale *= factor; + no_clip = false; + + // adjust cross stroke + stroke /= factor; + for (auto el : cross) { + if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) { + sp_repr_css_set_property_double(css, "stroke-width", stroke); + el->changeCSS(css, "style"); + sp_repr_css_attr_unref(css); + } + } + + _sandbox->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + _sandbox->ensureUpToDate(); + } + } + } + + /* Update to renderable state */ + const double device_scale = get_scale_factor(); + auto surface = render_surface(drawing, scale, *dbox, pixel_size, device_scale, checkerboard ? &_background_color : nullptr, no_clip); + cairo_surface_set_device_scale(surface, device_scale, device_scale); + return Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(surface, false)); +} + +// capture background color when styles change +void MarkerComboBox::on_style_updated() { + auto background = _background_color; + if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_toplevel())) { + auto sc = wnd->get_style_context(); + auto color = get_background_color(sc); + background = + gint32(0xff * color.get_red()) << 24 | + gint32(0xff * color.get_green()) << 16 | + gint32(0xff * color.get_blue()) << 8 | + 0xff; + } + + auto context = get_style_context(); + Gdk::RGBA color = context->get_color(get_state_flags()); + auto foreground = + gint32(0xff * color.get_red()) << 24 | + gint32(0xff * color.get_green()) << 16 | + gint32(0xff * color.get_blue()) << 8 | + 0xff; + if (foreground != _foreground_color || background != _background_color) { + _foreground_color = foreground; + _background_color = background; + // theme changed? + init_combo(); + } +} + +/*TODO: background for custom markers + <filter id="softGlow" height="1.2" width="1.2" x="0.0" y="0.0"> + <!-- <feMorphology operator="dilate" radius="1" in="SourceAlpha" result="thicken" id="feMorphology2" /> --> + <!-- Use a gaussian blur to create the soft blurriness of the glow --> + <feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blurred" id="feGaussianBlur4" /> + <!-- Change the color --> + <feFlood flood-color="rgb(255,255,255)" result="glowColor" id="feFlood6" flood-opacity="0.70" /> + <!-- Color in the glows --> + <feComposite in="glowColor" in2="blurred" operator="in" result="softGlow_colored" id="feComposite8" /> + <!-- Layer the effects together --> + <feMerge id="feMerge14"> + <feMergeNode in="softGlow_colored" id="feMergeNode10" /> + <feMergeNode in="SourceGraphic" id="feMergeNode12" /> + </feMerge> + </filter> +*/ + +/** + * Returns a new document containing default start, mid, and end markers. + * Note 1: group IDs are matched against "_combo_id" to render correct preview object. + * Note 2: paths/lines are kept outside of groups, so they don't inflate visible bounds + * Note 3: invisible rects inside groups keep visual bounds from getting too small, so we can see relative marker sizes + */ +std::unique_ptr<SPDocument> MarkerComboBox::ink_markers_preview_doc(const Glib::ustring& group_id) +{ +gchar const *buffer = R"A( + <svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + id="MarkerSample"> + + <defs id="defs"> + <filter id="softGlow" height="1.2" width="1.2" x="0.0" y="0.0"> + <!-- <feMorphology operator="dilate" radius="1" in="SourceAlpha" result="thicken" id="feMorphology2" /> --> + <!-- Use a gaussian blur to create the soft blurriness of the glow --> + <feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blurred" id="feGaussianBlur4" /> + <!-- Change the color --> + <feFlood flood-color="rgb(255,255,255)" result="glowColor" id="feFlood6" flood-opacity="0.70" /> + <!-- Color in the glows --> + <feComposite in="glowColor" in2="blurred" operator="in" result="softGlow_colored" id="feComposite8" /> + <!-- Layer the effects together --> + <feMerge id="feMerge14"> + <feMergeNode in="softGlow_colored" id="feMergeNode10" /> + <feMergeNode in="SourceGraphic" id="feMergeNode12" /> + </feMerge> + </filter> + </defs> + + <!-- cross at the end of the line to help position marker --> + <symbol id="cross" width="25" height="25" viewBox="0 0 25 25"> + <path class="cross" style="mix-blend-mode:difference;stroke:#7ff;stroke-opacity:1;fill:none;display:block" d="M 0,0 M 25,25 M 10,10 15,15 M 10,15 15,10" /> + <!-- <path class="cross" style="mix-blend-mode:difference;stroke:#7ff;stroke-width:1;stroke-opacity:1;fill:none;display:block;-inkscape-stroke:hairline" d="M 0,0 M 25,25 M 10,10 15,15 M 10,15 15,10" /> --> + </symbol> + + <!-- very short path with 1px stroke used to measure size of marker --> + <path id="measure-marker" style="stroke-width:1.0;stroke-opacity:0.01;marker-start:url(#sample)" d="M 0,9999 m 0,0.1" /> + + <path id="line-marker-start" class="line colors" style="stroke-width:2;stroke-opacity:0.2" d="M 12.5,12.5 l 1000,0" /> + <!-- <g id="marker-start" class="group" style="filter:url(#softGlow)"> --> + <g id="marker-start" class="group"> + <path class="colors" style="stroke-width:2;stroke-opacity:0;marker-start:url(#sample)" + d="M 12.5,12.5 L 25,12.5"/> + <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/> + <use xlink:href="#cross" width="25" height="25" /> + </g> + + <path id="line-marker-mid" class="line colors" style="stroke-width:2;stroke-opacity:0.2" d="M -1000,12.5 L 1000,12.5" /> + <g id="marker-mid" class="group"> + <path class="colors" style="stroke-width:2;stroke-opacity:0;marker-mid:url(#sample)" + d="M 0,12.5 L 12.5,12.5 L 25,12.5"/> + <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/> + <use xlink:href="#cross" width="25" height="25" /> + </g> + + <path id="line-marker-end" class="line colors" style="stroke-width:2;stroke-opacity:0.2" d="M -1000,12.5 L 12.5,12.5" /> + <g id="marker-end" class="group"> + <path class="colors" style="stroke-width:2;stroke-opacity:0;marker-end:url(#sample)" + d="M 0,12.5 L 12.5,12.5"/> + <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/> + <use xlink:href="#cross" width="25" height="25" /> + </g> + + </svg> +)A"; + + auto document = std::unique_ptr<SPDocument>(SPDocument::createNewDocFromMem(buffer, strlen(buffer), false)); + // only leave requested group, so nothing else gets rendered + for (auto&& group : document->getObjectsByClass("group")) { + assert(group->getId()); + if (group->getId() != group_id) { + group->deleteObject(); + } + } + auto id = "line-" + group_id; + for (auto&& line : document->getObjectsByClass("line")) { + assert(line->getId()); + if (line->getId() != id) { + line->deleteObject(); + } + } + return document; +} + +} // namespace Widget +} // 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/widget/marker-combo-box.h b/src/ui/widget/marker-combo-box.h new file mode 100644 index 0000000..46391a4 --- /dev/null +++ b/src/ui/widget/marker-combo-box.h @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MARKER_SELECTOR_NEW_H +#define SEEN_SP_MARKER_SELECTOR_NEW_H + +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Maximilian Albert <maximilian.albert> (gtkmm-ification) + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <vector> + +#include <gtkmm/bin.h> +#include <gtkmm/box.h> +#include <gtkmm/builder.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/flowbox.h> +#include <gtkmm/image.h> +#include <gtkmm/liststore.h> +#include <gtkmm/menubutton.h> +#include <gtkmm/spinbutton.h> +#include <gio/gliststore.h> + +#include <sigc++/signal.h> + +#include "document.h" +#include "inkscape.h" +#include "scrollprotected.h" +#include "display/drawing.h" +#include "ui/operation-blocker.h" + +class SPMarker; + +namespace Gtk { + +class Container; +class Adjustment; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * ComboBox-like class for selecting stroke markers. + */ +class MarkerComboBox : public Gtk::Bin { + using parent_type = Gtk::Bin; + +public: + MarkerComboBox(Glib::ustring id, int loc); + ~MarkerComboBox() override; + + void setDocument(SPDocument *); + + sigc::signal<void> changed_signal; + sigc::signal<void> edit_signal; + + void set_current(SPObject *marker); + std::string get_active_marker_uri(); + bool in_update() { return _update.pending(); }; + const char* get_id() { return _combo_id.c_str(); }; + int get_loc() { return _loc; }; + + sigc::signal<void()> signal_changed() { return _signal_changed; } + +private: + struct MarkerItem : Glib::Object { + Cairo::RefPtr<Cairo::Surface> pix; + SPDocument* source = nullptr; + std::string id; + std::string label; + bool stock = false; + bool history = false; + bool separator = false; + int width = 0; + int height = 0; + + bool operator == (const MarkerItem& item) const; + }; + + SPMarker* get_current() const; + Glib::ustring _current_marker_id; + // SPMarker* _current_marker = nullptr; + sigc::signal<void()> _signal_changed; + Glib::RefPtr<Gtk::Builder> _builder; + Gtk::FlowBox& _marker_list; + Gtk::Label& _marker_name; + Glib::RefPtr<Gio::ListStore<MarkerItem>> _marker_store; + std::vector<Glib::RefPtr<MarkerItem>> _stock_items; + std::vector<Glib::RefPtr<MarkerItem>> _history_items; + std::map<Gtk::Widget*, Glib::RefPtr<MarkerItem>> _widgets_to_markers; + Gtk::Image& _preview; + bool _preview_no_alloc = true; + Gtk::Button& _link_scale; + Gtk::SpinButton& _angle_btn; + Gtk::MenuButton& _menu_btn; + Gtk::SpinButton& _scale_x; + Gtk::SpinButton& _scale_y; + Gtk::CheckButton& _scale_with_stroke; + Gtk::SpinButton& _offset_x; + Gtk::SpinButton& _offset_y; + Gtk::Widget& _input_grid; + Gtk::RadioButton& _orient_auto_rev; + Gtk::RadioButton& _orient_auto; + Gtk::RadioButton& _orient_angle; + Gtk::Button& _orient_flip_horz; + Gtk::Image& _current_img; + Gtk::Button& _edit_marker; + bool _scale_linked = true; + guint32 _background_color; + guint32 _foreground_color; + Glib::ustring _combo_id; + int _loc; + OperationBlocker _update; + SPDocument *_document = nullptr; + std::unique_ptr<SPDocument> _sandbox; + Gtk::CellRendererPixbuf _image_renderer; + + class MarkerColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn<Glib::ustring> label; + Gtk::TreeModelColumn<const gchar *> marker; // ustring doesn't work here on windows due to unicode + Gtk::TreeModelColumn<gboolean> stock; + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> pixbuf; + Gtk::TreeModelColumn<gboolean> history; + Gtk::TreeModelColumn<gboolean> separator; + + MarkerColumns() { + add(label); add(stock); add(marker); add(history); add(separator); add(pixbuf); + } + }; + MarkerColumns marker_columns; + + void update_ui(SPMarker* marker, bool select); + void update_widgets_from_marker(SPMarker* marker); + void update_store(); + Glib::RefPtr<MarkerItem> add_separator(bool filler); + void update_scale_link(); + Glib::RefPtr<MarkerItem> get_active(); + Glib::RefPtr<MarkerItem> find_marker_item(SPMarker* marker); + void on_style_updated() override; + void update_preview(Glib::RefPtr<MarkerItem> marker_item); + void update_menu_btn(Glib::RefPtr<MarkerItem> marker_item); + void set_active(Glib::RefPtr<MarkerItem> item); + void init_combo(); + void set_history(Gtk::TreeModel::Row match_row); + void marker_list_from_doc(SPDocument* source, bool history); + std::vector<SPMarker*> get_marker_list(SPDocument* source); + void add_markers (std::vector<SPMarker *> const& marker_list, SPDocument *source, gboolean history); + void remove_markers (gboolean history); + std::unique_ptr<SPDocument> ink_markers_preview_doc(const Glib::ustring& group_id); + Cairo::RefPtr<Cairo::Surface> create_marker_image(Geom::IntPoint pixel_size, gchar const *mname, + SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/, bool checkerboard, bool no_clip, double scale); + void refresh_after_markers_modified(); + sigc::connection modified_connection; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape +#endif // SEEN_SP_MARKER_SELECTOR_NEW_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/notebook-page.cpp b/src/ui/widget/notebook-page.cpp new file mode 100644 index 0000000..876edb6 --- /dev/null +++ b/src/ui/widget/notebook-page.cpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Notebook page widget. + * + * Author: + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "notebook-page.h" + +# include <gtkmm/grid.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +NotebookPage::NotebookPage(int n_rows, int n_columns, bool expand, bool fill, guint padding) + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) + , _table(Gtk::manage(new Gtk::Grid())) +{ + set_name("NotebookPage"); + set_border_width(4); + set_spacing(4); + + _table->set_row_spacing(4); + _table->set_column_spacing(4); + + pack_start(*_table, expand, fill, padding); +} + +} // namespace Widget +} // 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/widget/notebook-page.h b/src/ui/widget/notebook-page.h new file mode 100644 index 0000000..9c9bd06 --- /dev/null +++ b/src/ui/widget/notebook-page.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_NOTEBOOK_PAGE_H +#define INKSCAPE_UI_WIDGET_NOTEBOOK_PAGE_H + +#include <gtkmm/box.h> + +namespace Gtk { +class Grid; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A tabbed notebook page for dialogs. + */ +class NotebookPage : public Gtk::Box +{ +public: + + /** + * Construct a NotebookPage. + */ + NotebookPage(int n_rows, int n_columns, bool expand=false, bool fill=false, guint padding=0); + + Gtk::Grid& table() { return *_table; } + +protected: + Gtk::Grid *_table; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_NOTEBOOK_PAGE_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/widget/object-composite-settings.cpp b/src/ui/widget/object-composite-settings.cpp new file mode 100644 index 0000000..9331e71 --- /dev/null +++ b/src/ui/widget/object-composite-settings.cpp @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A widget for controlling object compositing (filter, opacity, etc.) + * + * Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Gustav Broberg <broberg@kth.se> + * Niko Kiirala <niko@kiirala.com> + * Abhishek Sharma + * + * Copyright (C) 2004--2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object-composite-settings.h" + +#include <utility> + +#include "desktop.h" +#include "desktop-style.h" +#include "document.h" +#include "document-undo.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "style.h" + +#include "object/filters/blend.h" +#include "svg/css-ostringstream.h" +#include "ui/widget/style-subject.h" + +constexpr double BLUR_MULTIPLIER = 4.0; + +namespace Inkscape { +namespace UI { +namespace Widget { + +ObjectCompositeSettings::ObjectCompositeSettings(Glib::ustring icon_name, char const *history_prefix, int flags) +: Gtk::Box(Gtk::ORIENTATION_VERTICAL), + _icon_name(std::move(icon_name)), + _blend_tag(Glib::ustring(history_prefix) + ":blend"), + _blur_tag(Glib::ustring(history_prefix) + ":blur"), + _opacity_tag(Glib::ustring(history_prefix) + ":opacity"), + _isolation_tag(Glib::ustring(history_prefix) + ":isolation"), + _filter_modifier(flags), + _blocked(false) +{ + set_name( "ObjectCompositeSettings"); + + // Filter Effects + pack_start(_filter_modifier, false, false, 2); + + _filter_modifier.signal_blend_changed().connect(sigc::mem_fun(*this, &ObjectCompositeSettings::_blendBlurValueChanged)); + _filter_modifier.signal_blur_changed().connect(sigc::mem_fun(*this, &ObjectCompositeSettings::_blendBlurValueChanged)); + _filter_modifier.signal_opacity_changed().connect(sigc::mem_fun(*this, &ObjectCompositeSettings::_opacityValueChanged)); + _filter_modifier.signal_isolation_changed().connect( + sigc::mem_fun(*this, &ObjectCompositeSettings::_isolationValueChanged)); + + show_all_children(); +} + +ObjectCompositeSettings::~ObjectCompositeSettings() { + setSubject(nullptr); +} + +void ObjectCompositeSettings::setSubject(StyleSubject *subject) { + _subject_changed.disconnect(); + if (subject) { + _subject = subject; + _subject_changed = _subject->connectChanged(sigc::mem_fun(*this, &ObjectCompositeSettings::_subjectChanged)); + } +} + +// We get away with sharing one callback for blend and blur as this is used by +// * the Layers dialog where only one layer can be selected at a time, +// * the Fill and Stroke dialog where only blur is used. +// If both blend and blur are used in a dialog where more than one object can +// be selected then this should be split into separate functions for blend and +// blur (like in the Objects dialog). +void +ObjectCompositeSettings::_blendBlurValueChanged() +{ + if (!_subject) { + return; + } + + SPDesktop *desktop = _subject->getDesktop(); + if (!desktop) { + return; + } + SPDocument *document = desktop->getDocument(); + + if (_blocked) + return; + _blocked = true; + + Geom::OptRect bbox = _subject->getBounds(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? + double blur_value = _filter_modifier.get_blur_value() / 100.0; + radius = blur_value * blur_value * perimeter / BLUR_MULTIPLIER; + } else { + radius = 0; + } + + //apply created filter to every selected item + std::vector<SPObject*> sel = _subject->list(); + for (auto i : sel) { + if (!SP_IS_ITEM(i)) { + continue; + } + SPItem * item = SP_ITEM(i); + SPStyle *style = item->style; + g_assert(style != nullptr); + bool change_blend = (item->style->mix_blend_mode.set ? item->style->mix_blend_mode.value : SP_CSS_BLEND_NORMAL) != _filter_modifier.get_blend_mode(); + // < 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 (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) { + item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL; + } else { + item->style->mix_blend_mode.value = _filter_modifier.get_blend_mode(); + } + + if (radius == 0 && item->style->filter.set && item->style->getFilter() + && filter_is_single_gaussian_blur(item->style->getFilter())) { + remove_filter(item, false); + } else if (radius != 0) { + SPFilter *filter = modify_filter_gaussian_blur_from_item(document, item, radius); + filter->update_filter_region(item); + sp_style_set_property_url(item, "filter", filter, false); + } + if (change_blend) { //we do blend so we need update display style + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } else { + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } + + DocumentUndo::maybeDone(document, _blur_tag.c_str(), _("Change blur/blend filter"), _icon_name); + + _blocked = false; +} + +void +ObjectCompositeSettings::_opacityValueChanged() +{ + if (!_subject) { + return; + } + + SPDesktop *desktop = _subject->getDesktop(); + if (!desktop) { + return; + } + + if (_blocked) + return; + _blocked = true; + + SPCSSAttr *css = sp_repr_css_attr_new (); + + Inkscape::CSSOStringStream os; + os << CLAMP (_filter_modifier.get_opacity_value() / 100, 0.0, 1.0); + sp_repr_css_set_property (css, "opacity", os.str().c_str()); + + _subject->setCSS(css); + + sp_repr_css_attr_unref (css); + + DocumentUndo::maybeDone(desktop->getDocument(), _opacity_tag.c_str(), _("Change opacity"), _icon_name); + + _blocked = false; +} + +void ObjectCompositeSettings::_isolationValueChanged() +{ + if (!_subject) { + return; + } + + SPDesktop *desktop = _subject->getDesktop(); + if (!desktop) { + return; + } + + if (_blocked) + return; + _blocked = true; + + for (auto item : _subject->list()) { + 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; + } + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + + DocumentUndo::maybeDone(desktop->getDocument(), _isolation_tag.c_str(), _("Change isolation"), _icon_name); + + _blocked = false; +} + +void +ObjectCompositeSettings::_subjectChanged() { + if (!_subject) { + return; + } + + SPDesktop *desktop = _subject->getDesktop(); + if (!desktop) { + return; + } + + if (_blocked) + return; + _blocked = true; + SPStyle query(desktop->getDocument()); + int result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_MASTEROPACITY); + + switch (result) { + case QUERY_STYLE_NOTHING: + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: // TODO: treat this slightly differently + case QUERY_STYLE_MULTIPLE_SAME: + _filter_modifier.set_opacity_value(100 * SP_SCALE24_TO_FLOAT(query.opacity.value)); + break; + } + + //query now for current filter mode and average blurring of selection + const int isolation_result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_ISOLATION); + switch (isolation_result) { + case QUERY_STYLE_NOTHING: + _filter_modifier.set_isolation_mode(SP_CSS_ISOLATION_AUTO, false); + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_SAME: + _filter_modifier.set_isolation_mode(query.isolation.value, true); // here dont work mix_blend_mode.set + break; + case QUERY_STYLE_MULTIPLE_DIFFERENT: + _filter_modifier.set_isolation_mode(SP_CSS_ISOLATION_AUTO, false); + // TODO: set text + break; + } + + // query now for current filter mode and average blurring of selection + const int blend_result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_BLEND); + switch(blend_result) { + case QUERY_STYLE_NOTHING: + _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false); + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_SAME: + _filter_modifier.set_blend_mode(query.mix_blend_mode.value, true); // here dont work mix_blend_mode.set + break; + case QUERY_STYLE_MULTIPLE_DIFFERENT: + _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false); + break; + } + + int blur_result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_BLUR); + switch (blur_result) { + case QUERY_STYLE_NOTHING: // no blurring + _filter_modifier.set_blur_value(0); + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: + case QUERY_STYLE_MULTIPLE_SAME: + Geom::OptRect bbox = _subject->getBounds(SPItem::GEOMETRIC_BBOX); + if (bbox) { + double perimeter = + bbox->dimensions()[Geom::X] + + bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct? + // update blur widget value + float radius = query.filter_gaussianBlur_deviation.value; + float percent = std::sqrt(radius * BLUR_MULTIPLIER / perimeter) * 100; + _filter_modifier.set_blur_value(percent); + } + break; + } + + // If we have nothing selected, disable dialog. + if (result == QUERY_STYLE_NOTHING && + blend_result == QUERY_STYLE_NOTHING ) { + _filter_modifier.set_sensitive( false ); + } else { + _filter_modifier.set_sensitive( true ); + } + + _blocked = 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/widget/object-composite-settings.h b/src/ui/widget/object-composite-settings.h new file mode 100644 index 0000000..bb2ef85 --- /dev/null +++ b/src/ui/widget/object-composite-settings.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_UI_WIDGET_OBJECT_COMPOSITE_SETTINGS_H +#define SEEN_UI_WIDGET_OBJECT_COMPOSITE_SETTINGS_H + +/* + * Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2004--2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> + +#include "ui/widget/filter-effect-chooser.h" + +class SPDesktop; +struct InkscapeApplication; + +namespace Inkscape { + +namespace UI { +namespace Widget { + +class StyleSubject; + +/* + * A widget for controlling object compositing (filter, opacity, etc.) + */ +class ObjectCompositeSettings : public Gtk::Box { +public: + ObjectCompositeSettings(Glib::ustring icon_name, char const *history_prefix, int flags); + ~ObjectCompositeSettings() override; + + void setSubject(StyleSubject *subject); + +private: + Glib::ustring _icon_name; // Used by History dialog. + + Glib::ustring _blend_tag; + Glib::ustring _blur_tag; + Glib::ustring _opacity_tag; + Glib::ustring _isolation_tag; + + StyleSubject *_subject = nullptr; + + SimpleFilterModifier _filter_modifier; + + bool _blocked; + gulong _desktop_activated; + sigc::connection _subject_changed; + + static void _on_desktop_activate(SPDesktop *desktop, ObjectCompositeSettings *w); + static void _on_desktop_deactivate(SPDesktop *desktop, ObjectCompositeSettings *w); + void _subjectChanged(); + void _blendBlurValueChanged(); + void _opacityValueChanged(); + void _isolationValueChanged(); +}; + +} +} +} + +#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/widget/page-properties.cpp b/src/ui/widget/page-properties.cpp new file mode 100644 index 0000000..effc6b2 --- /dev/null +++ b/src/ui/widget/page-properties.cpp @@ -0,0 +1,515 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * + * Document properties widget: viewbox, document size, colors + */ +/* + * Authors: + * Mike Kowalski + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gtkmm/box.h> +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/expander.h> +#include <gtkmm/grid.h> +#include <gtkmm/label.h> +#include <gtkmm/menu.h> +#include <gtkmm/menubutton.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/spinbutton.h> +#include <gtkmm/togglebutton.h> + +#include <type_traits> + +#include "page-properties.h" +#include "page-size-preview.h" +#include "util/paper.h" +#include "ui/widget/registry.h" +#include "ui/widget/color-picker.h" +#include "ui/widget/unit-menu.h" +#include "ui/builder-utils.h" +#include "ui/operation-blocker.h" + +using Inkscape::UI::create_builder; +using Inkscape::UI::get_widget; + +namespace Inkscape { +namespace UI { +namespace Widget { + +void show_widget(Gtk::Widget& widget, bool show) { + if (show) { + widget.show(); + } + else { + widget.hide(); + } +}; + +const char* g_linked = "entries-linked-symbolic"; +const char* g_unlinked = "entries-unlinked-symbolic"; + +#define GET(prop, id) prop(get_widget<std::remove_reference_t<decltype(prop)>>(_builder, id)) + +class PagePropertiesBox : public PageProperties { +public: + PagePropertiesBox() : + _builder(create_builder("page-properties.glade")), + GET(_main_grid, "main-grid"), + GET(_left_grid, "left-grid"), + GET(_page_width, "page-width"), + GET(_page_height, "page-height"), + GET(_portrait, "page-portrait"), + GET(_landscape, "page-landscape"), + GET(_scale_x, "scale-x"), + GET(_doc_units, "user-units"), + GET(_unsupported_size, "unsupported"), + GET(_nonuniform_scale, "nonuniform-scale"), + GET(_viewbox_x, "viewbox-x"), + GET(_viewbox_y, "viewbox-y"), + GET(_viewbox_width, "viewbox-width"), + GET(_viewbox_height, "viewbox-height"), + GET(_page_templates_menu, "page-templates-menu"), + GET(_template_name, "page-template-name"), + GET(_preview_box, "preview-box"), + GET(_checkerboard, "checkerboard"), + GET(_antialias, "use-antialias"), + GET(_border, "border"), + GET(_border_on_top, "border-top"), + GET(_shadow, "shadow"), + GET(_link_width_height, "link-width-height"), + GET(_viewbox_expander, "viewbox-expander"), + GET(_linked_viewbox_scale, "linked-scale-img") + { +#undef GET + + _backgnd_color_picker = std::make_unique<ColorPicker>( + _("Background color"), "", 0xffffff00, true, + &get_widget<Gtk::Button>(_builder, "background-color")); + + _border_color_picker = std::make_unique<ColorPicker>( + _("Border and shadow color"), "", 0x0000001f, true, + &get_widget<Gtk::Button>(_builder, "border-color")); + + _desk_color_picker = std::make_unique<ColorPicker>( + _("Desk color"), "", 0xd0d0d0ff, true, + &get_widget<Gtk::Button>(_builder, "desk-color")); + _desk_color_picker->use_transparency(false); + + for (auto element : {Color::Background, Color::Border, Color::Desk}) { + get_color_picker(element).connectChanged([=](guint rgba) { + update_preview_color(element, rgba); + if (_update.pending()) return; + _signal_color_changed.emit(rgba, element); + }); + } + + _builder->get_widget_derived("display-units", _display_units); + _display_units->setUnitType(UNIT_TYPE_LINEAR); + _display_units->signal_changed().connect([=](){ set_display_unit(); }); + + _builder->get_widget_derived("page-units", _page_units); + _page_units->setUnitType(UNIT_TYPE_LINEAR); + _current_page_unit = _page_units->getUnit(); + _page_units->signal_changed().connect([=](){ set_page_unit(); }); + + for (auto&& page : PaperSize::getPageSizes()) { + auto item = Gtk::manage(new Gtk::MenuItem(page.getDescription(false))); + item->show(); + _page_templates_menu.append(*item); + item->signal_activate().connect([=](){ set_page_template(page); }); + } + + _preview->set_hexpand(); + _preview->set_vexpand(); + _preview_box.add(*_preview); + + for (auto check : {Check::Border, Check::Shadow, Check::Checkerboard, Check::BorderOnTop, Check::AntiAlias}) { + auto checkbutton = &get_checkbutton(check); + checkbutton->signal_toggled().connect([=](){ fire_checkbox_toggled(*checkbutton, check); }); + } + _border.signal_toggled().connect([=](){ + _preview->draw_border(_border.get_active()); + }); + _shadow.signal_toggled().connect([=](){ + // + _preview->enable_drop_shadow(_shadow.get_active()); + }); + _checkerboard.signal_toggled().connect([=](){ + _preview->enable_checkerboard(_checkerboard.get_active()); + }); + + _viewbox_expander.property_expanded().signal_changed().connect([=](){ + // hide/show viewbox controls + show_viewbox(_viewbox_expander.get_expanded()); + }); + show_viewbox(_viewbox_expander.get_expanded()); + + _link_width_height.signal_clicked().connect([=](){ + // toggle size link + _locked_size_ratio = !_locked_size_ratio; + // set image + _link_width_height.set_image_from_icon_name(_locked_size_ratio && _size_ratio > 0 ? g_linked : g_unlinked, Gtk::ICON_SIZE_LARGE_TOOLBAR); + }); + _link_width_height.set_image_from_icon_name(g_unlinked, Gtk::ICON_SIZE_LARGE_TOOLBAR); + // set image for linked scale + _linked_viewbox_scale.set_from_icon_name(g_linked, Gtk::ICON_SIZE_LARGE_TOOLBAR); + + // report page size changes + _page_width .signal_value_changed().connect([=](){ set_page_size_linked(true); }); + _page_height.signal_value_changed().connect([=](){ set_page_size_linked(false); }); + // enforce uniform scale thru viewbox + _viewbox_width. signal_value_changed().connect([=](){ set_viewbox_size_linked(true); }); + _viewbox_height.signal_value_changed().connect([=](){ set_viewbox_size_linked(false); }); + + _landscape.signal_toggled().connect([=](){ if (_landscape.get_active()) swap_width_height(); }); + _portrait .signal_toggled().connect([=](){ if (_portrait .get_active()) swap_width_height(); }); + + for (auto dim : {Dimension::Scale, Dimension::ViewboxPosition}) { + auto pair = get_dimension(dim); + auto b1 = &pair.first; + auto b2 = &pair.second; + if (dim == Dimension::Scale) { + // uniform scale: report the same x and y + b1->signal_value_changed().connect([=](){ fire_value_changed(*b1, *b1, nullptr, dim); }); + } + else { + b1->signal_value_changed().connect([=](){ fire_value_changed(*b1, *b2, nullptr, dim); }); + b2->signal_value_changed().connect([=](){ fire_value_changed(*b1, *b2, nullptr, dim); }); + } + } + + auto& page_resize = get_widget<Gtk::Button>(_builder, "page-resize"); + page_resize.signal_clicked().connect([=](){ _signal_resize_to_fit.emit(); }); + + add(_main_grid); + show(); + } + +private: + + void show_viewbox(bool show_widgets) { + auto show = [=](Gtk::Widget* w) { show_widget(*w, show_widgets); }; + + for (auto&& widget : _left_grid.get_children()) { + if (widget->get_style_context()->has_class("viewbox")) { + show(widget); + } + } + } + + void update_preview_color(Color element, guint rgba) { + switch (element) { + case Color::Desk: _preview->set_desk_color(rgba); break; + case Color::Border: _preview->set_border_color(rgba); break; + case Color::Background: _preview->set_page_color(rgba); break; + } + } + + void set_page_template(const PaperSize& page) { + if (_update.pending()) return; + + { + auto scoped(_update.block()); + auto width = page.width; + auto height = page.height; + if (_landscape.get_active() != (width > height)) { + std::swap(width, height); + } + _page_width.set_value(width); + _page_height.set_value(height); + _page_units->setUnit(page.unit->abbr); + _doc_units.set_text(page.unit->abbr); + _current_page_unit = _page_units->getUnit(); + if (width > 0 && height > 0) { + _size_ratio = width / height; + } + } + set_page_size(true); + } + + void changed_linked_value(bool width_changing, Gtk::SpinButton& wedit, Gtk::SpinButton& hedit) { + if (_size_ratio > 0) { + auto scoped(_update.block()); + if (width_changing) { + auto width = wedit.get_value(); + hedit.set_value(width / _size_ratio); + } + else { + auto height = hedit.get_value(); + wedit.set_value(height * _size_ratio); + } + } + } + + void set_viewbox_size_linked(bool width_changing) { + if (_update.pending()) return; + + if (_scale_is_uniform) { + // viewbox size - width and height always linked to make scaling uniform + changed_linked_value(width_changing, _viewbox_width, _viewbox_height); + } + + auto width = _viewbox_width.get_value(); + auto height = _viewbox_height.get_value(); + _signal_dimmension_changed.emit(width, height, nullptr, Dimension::ViewboxSize); + } + + void set_page_size_linked(bool width_changing) { + if (_update.pending()) return; + + // if size ratio is locked change the other dimension too + if (_locked_size_ratio) { + changed_linked_value(width_changing, _page_width, _page_height); + } + set_page_size(); + } + + void set_page_size(bool template_selected = false) { + auto pending = _update.pending(); + + auto scoped(_update.block()); + + auto unit = _page_units->getUnit(); + auto width = _page_width.get_value(); + auto height = _page_height.get_value(); + _preview->set_page_size(width, height); + if (width != height) { + (width > height ? _landscape : _portrait).set_active(); + _portrait.set_sensitive(); + _landscape.set_sensitive(); + } + else { + _portrait.set_sensitive(false); + _landscape.set_sensitive(false); + } + if (width > 0 && height > 0) { + _size_ratio = width / height; + } + + auto templ = find_page_template(width, height, *unit); + _template_name.set_label(templ ? templ->name : _("Custom")); + + if (!pending) { + _signal_dimmension_changed.emit(width, height, unit, template_selected ? Dimension::PageTemplate : Dimension::PageSize); + } + } + + void swap_width_height() { + if (_update.pending()) return; + + { + auto scoped(_update.block()); + auto width = _page_width.get_value(); + auto height = _page_height.get_value(); + _page_width.set_value(height); + _page_height.set_value(width); + } + set_page_size(); + }; + + void set_display_unit() { + if (_update.pending()) return; + + const auto unit = _display_units->getUnit(); + _signal_unit_changed.emit(unit, Units::Display); + } + + void set_page_unit() { + if (_update.pending()) return; + + const auto old_unit = _current_page_unit; + _current_page_unit = _page_units->getUnit(); + const auto new_unit = _current_page_unit; + + { + auto width = _page_width.get_value(); + auto height = _page_height.get_value(); + Quantity w(width, old_unit->abbr); + Quantity h(height, old_unit->abbr); + auto scoped(_update.block()); + _page_width.set_value(w.value(new_unit)); + _page_height.set_value(h.value(new_unit)); + } + _doc_units.set_text(new_unit->abbr); + set_page_size(); + _signal_unit_changed.emit(new_unit, Units::Document); + } + + void set_color(Color element, unsigned int color) override { + auto scoped(_update.block()); + + get_color_picker(element).setRgba32(color); + update_preview_color(element, color); + } + + void set_check(Check element, bool checked) override { + auto scoped(_update.block()); + + if (element == Check::NonuniformScale) { + show_widget(_nonuniform_scale, checked); + _scale_is_uniform = !checked; + _scale_x.set_sensitive(_scale_is_uniform); + _linked_viewbox_scale.set_from_icon_name(_scale_is_uniform ? g_linked : g_unlinked, Gtk::ICON_SIZE_LARGE_TOOLBAR); + } + else if (element == Check::DisabledScale) { + _scale_x.set_sensitive(!checked); + } + else if (element == Check::UnsupportedSize) { + show_widget(_unsupported_size, checked); + } + else { + get_checkbutton(element).set_active(checked); + + // special cases + if (element == Check::Checkerboard) _preview->enable_checkerboard(checked); + if (element == Check::Shadow) _preview->enable_drop_shadow(checked); + if (element == Check::Border) _preview->draw_border(checked); + } + } + + void set_dimension(Dimension dimension, double x, double y) override { + auto scoped(_update.block()); + + auto dim = get_dimension(dimension); + dim.first.set_value(x); + dim.second.set_value(y); + + set_page_size(); + } + + void set_unit(Units unit, const Glib::ustring& abbr) override { + auto scoped(_update.block()); + + if (unit == Units::Display) { + _display_units->setUnit(abbr); + } + else if (unit == Units::Document) { + _doc_units.set_text(abbr); + _page_units->setUnit(abbr); + _current_page_unit = _page_units->getUnit(); + set_page_size(); + } + } + + ColorPicker& get_color_picker(Color element) { + switch (element) { + case Color::Background: return *_backgnd_color_picker; + case Color::Desk: return *_desk_color_picker; + case Color::Border: return *_border_color_picker; + + default: + throw std::runtime_error("missing case in get_color_picker"); + } + } + + void fire_value_changed(Gtk::SpinButton& b1, Gtk::SpinButton& b2, const Util::Unit* unit, Dimension dim) { + if (!_update.pending()) { + _signal_dimmension_changed.emit(b1.get_value(), b2.get_value(), unit, dim); + } + } + + void fire_checkbox_toggled(Gtk::CheckButton& checkbox, Check check) { + if (!_update.pending()) { + _signal_check_toggled.emit(checkbox.get_active(), check); + } + } + + const PaperSize* find_page_template(double width, double height, const Unit& unit) { + Quantity w(std::min(width, height), &unit); + Quantity h(std::max(width, height), &unit); + + const double eps = 1e-6; + for (auto&& page : PaperSize::getPageSizes()) { + Quantity pw(std::min(page.width, page.height), page.unit); + Quantity ph(std::max(page.width, page.height), page.unit); + + if (are_near(w, pw, eps) && are_near(h, ph, eps)) { + return &page; + } + } + + return nullptr; + } + + Gtk::CheckButton& get_checkbutton(Check check) { + switch (check) { + case Check::AntiAlias: return _antialias; + case Check::Border: return _border; + case Check::Shadow: return _shadow; + case Check::BorderOnTop: return _border_on_top; + case Check::Checkerboard: return _checkerboard; + + default: + throw std::runtime_error("missing case in get_checkbutton"); + } + } + + typedef std::pair<Gtk::SpinButton&, Gtk::SpinButton&> spin_pair; + spin_pair get_dimension(Dimension dimension) { + switch (dimension) { + case Dimension::PageSize: return spin_pair(_page_width, _page_height); + case Dimension::PageTemplate: return spin_pair(_page_width, _page_height); + case Dimension::Scale: return spin_pair(_scale_x, _scale_x); + case Dimension::ViewboxPosition: return spin_pair(_viewbox_x, _viewbox_y); + case Dimension::ViewboxSize: return spin_pair(_viewbox_width, _viewbox_height); + + default: + throw std::runtime_error("missing case in get_dimension"); + } + } + + Glib::RefPtr<Gtk::Builder> _builder; + Gtk::Grid& _main_grid; + Gtk::Grid& _left_grid; + Gtk::SpinButton& _page_width; + Gtk::SpinButton& _page_height; + Gtk::RadioButton& _portrait; + Gtk::RadioButton& _landscape; + Gtk::SpinButton& _scale_x; + Gtk::Label& _unsupported_size; + Gtk::Label& _nonuniform_scale; + Gtk::Label& _doc_units; + Gtk::SpinButton& _viewbox_x; + Gtk::SpinButton& _viewbox_y; + Gtk::SpinButton& _viewbox_width; + Gtk::SpinButton& _viewbox_height; + std::unique_ptr<ColorPicker> _backgnd_color_picker; + std::unique_ptr<ColorPicker> _border_color_picker; + std::unique_ptr<ColorPicker> _desk_color_picker; + Gtk::Menu& _page_templates_menu; + Gtk::Label& _template_name; + Gtk::Box& _preview_box; + std::unique_ptr<PageSizePreview> _preview = std::make_unique<PageSizePreview>(); + Gtk::CheckButton& _border; + Gtk::CheckButton& _border_on_top; + Gtk::CheckButton& _shadow; + Gtk::CheckButton& _checkerboard; + Gtk::CheckButton& _antialias; + Gtk::Button& _link_width_height; + UnitMenu *_display_units; + UnitMenu *_page_units; + const Unit* _current_page_unit = nullptr; + OperationBlocker _update; + double _size_ratio = 1; // width to height ratio + bool _locked_size_ratio = false; + bool _scale_is_uniform = true; + Gtk::Expander& _viewbox_expander; + Gtk::Image& _linked_viewbox_scale; +}; + +PageProperties* PageProperties::create() { + return new PagePropertiesBox(); +} + + +} } } // namespace Inkscape/Widget/UI diff --git a/src/ui/widget/page-properties.h b/src/ui/widget/page-properties.h new file mode 100644 index 0000000..37779e5 --- /dev/null +++ b/src/ui/widget/page-properties.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Michael Kowalski + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_PAGE_PROPERTIES_H +#define INKSCAPE_UI_WIDGET_PAGE_PROPERTIES_H + +#include <gtkmm/box.h> + +namespace Inkscape { + namespace Util { class Unit; } +namespace UI { +namespace Widget { + +class PageProperties : public Gtk::Box { +public: + static PageProperties* create(); + + ~PageProperties() override = default; + + enum class Color { Background, Desk, Border }; + virtual void set_color(Color element, unsigned int rgba) = 0; + + sigc::signal<void (unsigned int, Color)>& signal_color_changed() { return _signal_color_changed; } + + enum class Check { Checkerboard, Border, Shadow, BorderOnTop, AntiAlias, NonuniformScale, DisabledScale, UnsupportedSize }; + virtual void set_check(Check element, bool checked) = 0; + + sigc::signal<void (bool, Check)>& signal_check_toggled() { return _signal_check_toggled; } + + enum class Dimension { PageSize, ViewboxSize, ViewboxPosition, Scale, PageTemplate }; + virtual void set_dimension(Dimension dim, double x, double y) = 0; + + sigc::signal<void (double, double, const Util::Unit*, Dimension)>& signal_dimmension_changed() { return _signal_dimmension_changed; } + + enum class Units { Display, Document }; + virtual void set_unit(Units unit, const Glib::ustring& abbr) = 0; + + sigc::signal<void (const Util::Unit*, Units)> signal_unit_changed() { return _signal_unit_changed; } + + sigc::signal<void ()> signal_resize_to_fit() { return _signal_resize_to_fit; } + +protected: + sigc::signal<void (unsigned int, Color)> _signal_color_changed; + sigc::signal<void (bool, Check)> _signal_check_toggled; + sigc::signal<void (double, double, const Util::Unit*, Dimension)> _signal_dimmension_changed; + sigc::signal<void (const Util::Unit*, Units)> _signal_unit_changed; + sigc::signal<void ()> _signal_resize_to_fit; +}; + +} } } // namespace Inkscape/Widget/UI + +#endif // INKSCAPE_UI_WIDGET_PAGE_PROPERTIES_H diff --git a/src/ui/widget/page-selector.cpp b/src/ui/widget/page-selector.cpp new file mode 100644 index 0000000..baa258b --- /dev/null +++ b/src/ui/widget/page-selector.cpp @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Widgets::PageSelector - select and move to pages + * + * Authors: + * Martin Owens + * + * Copyright (C) 2021 Martin Owens + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "page-selector.h" + +#include <cstring> +#include <glibmm/i18n.h> +#include <string> + +#include "desktop.h" +#include "document.h" +#include "object/sp-namedview.h" +#include "object/sp-page.h" +#include "page-manager.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +PageSelector::PageSelector(SPDesktop *desktop) + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) + , _desktop(desktop) +{ + set_name("PageSelector"); + + _prev_button.add(*Gtk::manage(sp_get_icon_image(INKSCAPE_ICON("pan-start"), Gtk::ICON_SIZE_MENU))); + _prev_button.set_relief(Gtk::RELIEF_NONE); + _prev_button.set_tooltip_text(_("Move to previous page")); + _prev_button.signal_clicked().connect(sigc::mem_fun(*this, &PageSelector::prevPage)); + + _next_button.add(*Gtk::manage(sp_get_icon_image(INKSCAPE_ICON("pan-end"), Gtk::ICON_SIZE_MENU))); + _next_button.set_relief(Gtk::RELIEF_NONE); + _next_button.set_tooltip_text(_("Move to next page")); + _next_button.signal_clicked().connect(sigc::mem_fun(*this, &PageSelector::nextPage)); + + _selector.set_tooltip_text(_("Current page")); + + _page_model = Gtk::ListStore::create(_model_columns); + _selector.set_model(_page_model); + _selector.pack_start(_label_renderer); + _selector.set_cell_data_func(_label_renderer, sigc::mem_fun(*this, &PageSelector::renderPageLabel)); + + _selector_changed_connection = + _selector.signal_changed().connect(sigc::mem_fun(*this, &PageSelector::setSelectedPage)); + + pack_start(_prev_button, Gtk::PACK_EXPAND_PADDING); + pack_start(_selector, Gtk::PACK_EXPAND_WIDGET); + pack_start(_next_button, Gtk::PACK_EXPAND_PADDING); + + _doc_replaced_connection = + _desktop->connectDocumentReplaced(sigc::hide<0>(sigc::mem_fun(*this, &PageSelector::setDocument))); + + this->show_all(); + this->set_no_show_all(); + setDocument(desktop->getDocument()); +} + +PageSelector::~PageSelector() +{ + _doc_replaced_connection.disconnect(); + _selector_changed_connection.disconnect(); + setDocument(nullptr); +} + +void PageSelector::setDocument(SPDocument *document) +{ + _document = document; + _pages_changed_connection.disconnect(); + _page_selected_connection.disconnect(); + if (document) { + auto &page_manager = document->getPageManager(); + _pages_changed_connection = + page_manager.connectPagesChanged(sigc::mem_fun(*this, &PageSelector::pagesChanged)); + _page_selected_connection = + page_manager.connectPageSelected(sigc::mem_fun(*this, &PageSelector::selectonChanged)); + pagesChanged(); + } +} + +void PageSelector::pagesChanged() +{ + _selector_changed_connection.block(); + auto &page_manager = _document->getPageManager(); + + // Destroy all existing pages in the model. + while (!_page_model->children().empty()) { + Gtk::ListStore::iterator row(_page_model->children().begin()); + // Put cleanup here if any + _page_model->erase(row); + } + + // Hide myself when there's no pages (single page document) + this->set_visible(page_manager.hasPages()); + + // Add in pages, do not use getResourcelist("page") because the items + // are not guaranteed to be in node order, they are in first-seen order. + for (auto &page : page_manager.getPages()) { + Gtk::ListStore::iterator row(_page_model->append()); + row->set_value(_model_columns.object, page); + } + + selectonChanged(page_manager.getSelected()); + + _selector_changed_connection.unblock(); +} + +void PageSelector::selectonChanged(SPPage *page) +{ + _next_button.set_sensitive(_document->getPageManager().hasNextPage()); + _prev_button.set_sensitive(_document->getPageManager().hasPrevPage()); + + auto active = _selector.get_active(); + + if (!active || active->get_value(_model_columns.object) != page) { + for (auto row : _page_model->children()) { + if (page == row->get_value(_model_columns.object)) { + _selector.set_active(row); + return; + } + } + } +} + +/** + * Render the page icon into a suitable label. + */ +void PageSelector::renderPageLabel(Gtk::TreeModel::const_iterator const &row) +{ + SPPage *page = (*row)[_model_columns.object]; + + if (page && page->getRepr()) { + int page_num = page->getPagePosition(); + + gchar *format; + if (auto label = page->label()) { + format = g_strdup_printf("<span size=\"smaller\"><tt>%d.</tt>%s</span>", page_num, label); + } else { + format = g_strdup_printf("<span size=\"smaller\"><i>%s</i></span>", page->getDefaultLabel().c_str()); + } + + _label_renderer.property_markup() = format; + g_free(format); + } else { + _label_renderer.property_markup() = "⚠️"; + } + + _label_renderer.property_ypad() = 1; +} + +void PageSelector::setSelectedPage() +{ + SPPage *page = _selector.get_active()->get_value(_model_columns.object); + if (page && _document->getPageManager().selectPage(page)) { + _document->getPageManager().zoomToSelectedPage(_desktop); + } +} + +void PageSelector::nextPage() +{ + if (_document->getPageManager().selectNextPage()) { + _document->getPageManager().zoomToSelectedPage(_desktop); + } +} + +void PageSelector::prevPage() +{ + if (_document->getPageManager().selectPrevPage()) { + _document->getPageManager().zoomToSelectedPage(_desktop); + } +} + +} // namespace Widget +} // 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/widget/page-selector.h b/src/ui/widget/page-selector.h new file mode 100644 index 0000000..a439386 --- /dev/null +++ b/src/ui/widget/page-selector.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::UI::Widget::PageSelector - page selector widget + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_WIDGETS_PAGE_SELECTOR +#define SEEN_INKSCAPE_WIDGETS_PAGE_SELECTOR + +#include <gtkmm/box.h> +#include <gtkmm/cellrenderertext.h> +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> +#include <gtkmm/togglebutton.h> +#include <gtkmm/treemodel.h> +#include <sigc++/slot.h> + +#include "object/sp-page.h" + +class SPDesktop; +class SPDocument; +class SPPage; + +namespace Inkscape { +namespace UI { +namespace Widget { + +// class DocumentTreeModel; + +class PageSelector : public Gtk::Box +{ +public: + PageSelector(SPDesktop *desktop = nullptr); + ~PageSelector() override; + +private: + class PageModelColumns : public Gtk::TreeModel::ColumnRecord + { + public: + Gtk::TreeModelColumn<SPPage *> object; + + PageModelColumns() { add(object); } + }; + + SPDesktop *_desktop; + SPDocument *_document; + + Gtk::ComboBox _selector; + Gtk::Button _prev_button; + Gtk::Button _next_button; + + PageModelColumns _model_columns; + Gtk::CellRendererText _label_renderer; + Glib::RefPtr<Gtk::ListStore> _page_model; + + sigc::connection _selector_changed_connection; + sigc::connection _pages_changed_connection; + sigc::connection _page_selected_connection; + sigc::connection _doc_replaced_connection; + + void setDocument(SPDocument *document); + void pagesChanged(); + void selectonChanged(SPPage *page); + + void renderPageLabel(Gtk::TreeModel::const_iterator const &row); + void setSelectedPage(); + void nextPage(); + void prevPage(); +}; + +} // namespace Widget +} // 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/widget/page-size-preview.cpp b/src/ui/widget/page-size-preview.cpp new file mode 100644 index 0000000..40e2ea5 --- /dev/null +++ b/src/ui/widget/page-size-preview.cpp @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * + * Page size preview widget + */ +/* + * Authors: + * Mike Kowalski + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "page-size-preview.h" +#include "display/cairo-utils.h" +#include "2geom/rect.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +PageSizePreview::PageSizePreview() { + show(); +} + +void rounded_rectangle(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double w, double h, double r) { + cr->begin_new_sub_path(); + cr->arc(x + r, y + r, r, M_PI, 3 * M_PI / 2); + cr->arc(x + w - r, y + r, r, 3 * M_PI / 2, 2 * M_PI); + cr->arc(x + w - r, y + h - r, r, 0, M_PI / 2); + cr->arc(x + r, y + h - r, r, M_PI / 2, M_PI); + cr->close_path(); +} + +void set_source_rgba(const Cairo::RefPtr<Cairo::Context>& ctx, unsigned int rgba) { + ctx->set_source_rgba(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba)); +} + +bool PageSizePreview::on_draw(const Cairo::RefPtr<Cairo::Context>& ctx) { + auto alloc = get_allocation(); + double width = alloc.get_width(); + double height = alloc.get_height(); + // too small to fit anything? + if (width <= 2 || height <= 2) return false; + + double x = 0;//alloc.get_x(); + double y = 0;//alloc.get_y(); + + if (_draw_checkerboard) { + // auto device_scale = get_scale_factor(); + Cairo::RefPtr<Cairo::Pattern> pattern(new Cairo::Pattern(ink_cairo_pattern_create_checkerboard(_desk_color))); + ctx->save(); + ctx->set_operator(Cairo::OPERATOR_SOURCE); + ctx->set_source(pattern); + rounded_rectangle(ctx, x, y, width, height, 2.0); + ctx->fill(); + ctx->restore(); + } + else { + rounded_rectangle(ctx, x, y, width, height, 2.0); + set_source_rgba(ctx, _desk_color); + ctx->fill(); + } + + // use lesser dimension to prevent page from changing size when + // switching from portrait to landscape or vice versa + auto size = std::round(std::min(width, height) * 0.90); // 90% to leave margins + double w, h; + if (_width > _height) { + w = size; + h = std::round(size * _height / _width); + } + else { + h = size; + w = std::round(size * _width / _height); + } + if (w < 2) w = 2; + if (h < 2) h = 2; + + // center page + double ox = std::round(x + (width - w) / 2); + double oy = std::round(y + (height - h) / 2); + Geom::Rect rect(ox, oy, ox + w, oy + h); + + ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height()); + + if (_draw_checkerboard) { + Cairo::RefPtr<Cairo::Pattern> pattern(new Cairo::Pattern(ink_cairo_pattern_create_checkerboard(_page_color))); + ctx->save(); + ctx->set_operator(Cairo::OPERATOR_SOURCE); + ctx->set_source(pattern); + ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height()); + ctx->fill(); + ctx->restore(); + } + else { + ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height()); + set_source_rgba(ctx, _page_color | 0xff); + ctx->fill(); + } + + // draw cross + { + double gradient_size = 4; + double cx = std::round(x + (width - gradient_size) / 2); + double cy = std::round(y + (height - gradient_size) / 2); + auto horz = Cairo::LinearGradient::create(x, cy, x, cy + gradient_size); + auto vert = Cairo::LinearGradient::create(cx, y, cx + gradient_size, y); + + horz->add_color_stop_rgba(0.0, 0, 0, 0, 0.0); + horz->add_color_stop_rgba(0.5, 0, 0, 0, 0.2); + horz->add_color_stop_rgba(0.5, 1, 1, 1, 0.8); + horz->add_color_stop_rgba(1.0, 1, 1, 1, 0.0); + + vert->add_color_stop_rgba(0.0, 0, 0, 0, 0.0); + vert->add_color_stop_rgba(0.5, 0, 0, 0, 0.2); + vert->add_color_stop_rgba(0.5, 1, 1, 1, 0.8); + vert->add_color_stop_rgba(1.0, 1, 1, 1, 0.0); + + ctx->rectangle(x, cy, width, gradient_size); + ctx->set_source(horz); + ctx->fill(); + + ctx->rectangle(cx, y, gradient_size, height); + ctx->set_source(vert); + ctx->fill(); + } + + ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height()); + set_source_rgba(ctx, _page_color); + ctx->fill(); + + if (_draw_border) { + // stoke; not pixel aligned, just like page on canvas + ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height()); + set_source_rgba(ctx, _border_color); + ctx->set_line_width(1); + ctx->stroke(); + + if (_draw_shadow) { + const auto a = (exp(-3 * SP_RGBA32_A_F(_border_color)) - 1) / (exp(-3) - 1); + ink_cairo_draw_drop_shadow(ctx, rect, 12, _border_color, a); + } + } + + return true; +} + +void PageSizePreview::draw_border(bool border) { + _draw_border = border; + queue_draw(); +} + +void PageSizePreview::set_desk_color(unsigned int rgba) { + _desk_color = rgba | 0xff; // desk always opaque + queue_draw(); +} +void PageSizePreview::set_page_color(unsigned int rgba) { + _page_color = rgba; + queue_draw(); +} +void PageSizePreview::set_border_color(unsigned int rgba) { + _border_color = rgba; + queue_draw(); +} + +void PageSizePreview::enable_drop_shadow(bool shadow) { + _draw_shadow = shadow; + queue_draw(); +} + +void PageSizePreview::enable_checkerboard(bool checkerboard) { + _draw_checkerboard = checkerboard; + queue_draw(); +} + +void PageSizePreview::set_page_size(double width, double height) { + _width = width; + _height = height; + queue_draw(); +} + +} } } // namespace Inkscape/Widget/UI diff --git a/src/ui/widget/page-size-preview.h b/src/ui/widget/page-size-preview.h new file mode 100644 index 0000000..093e79b --- /dev/null +++ b/src/ui/widget/page-size-preview.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Michael Kowalski + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_PAGE_SIZE_PREVIEW_H +#define INKSCAPE_UI_WIDGET_PAGE_SIZE_PREVIEW_H + +#include <gtkmm/drawingarea.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class PageSizePreview : public Gtk::DrawingArea { +public: + PageSizePreview(); + // static PageSizePreview* create(); + + void set_desk_color(unsigned int rgba); + void set_page_color(unsigned int rgba); + void set_border_color(unsigned int rgba); + void draw_border(bool border); + void enable_drop_shadow(bool shadow); + void set_page_size(double width, double height); + void enable_checkerboard(bool checkerboard); + + ~PageSizePreview() override = default; + +private: + bool on_draw(const Cairo::RefPtr<Cairo::Context>& ctx) override; + unsigned int _border_color = 0x0000001f; + unsigned int _page_color = 0xffffff00; + unsigned int _desk_color = 0xc8c8c8ff; + bool _draw_border = true; + bool _draw_shadow = true; + bool _draw_checkerboard = false; + double _width = 10; + double _height = 7; +}; + +} } } // namespace Inkscape/Widget/UI + +#endif // INKSCAPE_UI_WIDGET_PAGE_SIZE_PREVIEW_H + diff --git a/src/ui/widget/paint-selector.cpp b/src/ui/widget/paint-selector.cpp new file mode 100644 index 0000000..ed67d46 --- /dev/null +++ b/src/ui/widget/paint-selector.cpp @@ -0,0 +1,1476 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * PaintSelector: Generic paint selector widget. + *//* + * Authors: + * see git history + * Lauris Kaplinski + * bulia byak <buliabyak@users.sf.net> + * John Cliff <simarilius@yahoo.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noSP_PS_VERBOSE + +#include <cstring> +#include <string> +#include <vector> + +#include <glibmm/i18n.h> +#include <glibmm/fileutils.h> + +#include "desktop-style.h" +#include "inkscape.h" +#include "paint-selector.h" +#include "path-prefix.h" + +#include "helper/stock-items.h" +#include "ui/icon-loader.h" + +#include "style.h" + +#include "io/sys.h" +#include "io/resource.h" + +#include "object/sp-hatch.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-stop.h" + +#include "svg/css-ostringstream.h" + +#include "ui/icon-names.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/gradient-selector.h" +#include "ui/widget/gradient-editor.h" +#include "ui/widget/swatch-selector.h" +#include "ui/widget/scrollprotected.h" + +#include "widgets/widget-sizes.h" + +#include "xml/repr.h" + +#ifdef SP_PS_VERBOSE +#include "svg/svg-icc-color.h" +#endif // SP_PS_VERBOSE + +#include <gtkmm/label.h> +#include <gtkmm/combobox.h> + +using Inkscape::UI::SelectedColor; + +#ifdef SP_PS_VERBOSE +static gchar const *modeStrings[] = { + "MODE_EMPTY", + "MODE_MULTIPLE", + "MODE_NONE", + "MODE_SOLID_COLOR", + "MODE_GRADIENT_LINEAR", + "MODE_GRADIENT_RADIAL", +#ifdef WITH_MESH + "MODE_GRADIENT_MESH", +#endif + "MODE_PATTERN", + "MODE_SWATCH", + "MODE_UNSET", + ".", + ".", +}; +#endif + +namespace { +GtkWidget *_scrollprotected_combo_box_new_with_model(GtkTreeModel *model) +{ + auto combobox = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBox>()); + gtk_combo_box_set_model(combobox->gobj(), model); + return GTK_WIDGET(combobox->gobj()); +} +} // namespace + +namespace Inkscape { +namespace UI { +namespace Widget { + +class FillRuleRadioButton : public Gtk::RadioButton { + private: + PaintSelector::FillRule _fillrule; + + public: + FillRuleRadioButton() + : Gtk::RadioButton() + {} + + FillRuleRadioButton(Gtk::RadioButton::Group &group) + : Gtk::RadioButton(group) + {} + + inline void set_fillrule(PaintSelector::FillRule fillrule) { _fillrule = fillrule; } + inline PaintSelector::FillRule get_fillrule() const { return _fillrule; } +}; + +class StyleToggleButton : public Gtk::ToggleButton { + private: + PaintSelector::Mode _style; + + public: + inline void set_style(PaintSelector::Mode style) { _style = style; } + inline PaintSelector::Mode get_style() const { return _style; } +}; + +static bool isPaintModeGradient(PaintSelector::Mode mode) +{ + bool isGrad = (mode == PaintSelector::MODE_GRADIENT_LINEAR) || (mode == PaintSelector::MODE_GRADIENT_RADIAL) || + (mode == PaintSelector::MODE_SWATCH); + + return isGrad; +} + +GradientSelectorInterface *PaintSelector::getGradientFromData() const +{ + if (_mode == PaintSelector::MODE_SWATCH && _selector_swatch) { + return _selector_swatch->getGradientSelector(); + } + return _selector_gradient; +} + +#define XPAD 4 +#define YPAD 1 + +PaintSelector::PaintSelector(FillOrStroke kind) +{ + set_orientation(Gtk::ORIENTATION_VERTICAL); + + _mode = static_cast<PaintSelector::Mode>(-1); // huh? do you mean 0xff? -- I think this means "not in the enum" + + /* Paint style button box */ + _style = Gtk::manage(new Gtk::Box()); + _style->set_homogeneous(false); + _style->set_name("PaintSelector"); + _style->show(); + _style->set_border_width(0); + pack_start(*_style, false, false); + + /* Buttons */ + _none = style_button_add(INKSCAPE_ICON("paint-none"), PaintSelector::MODE_NONE, _("No paint")); + _solid = style_button_add(INKSCAPE_ICON("paint-solid"), PaintSelector::MODE_SOLID_COLOR, _("Flat color")); + _gradient = style_button_add(INKSCAPE_ICON("paint-gradient-linear"), PaintSelector::MODE_GRADIENT_LINEAR, + _("Linear gradient")); + _radial = style_button_add(INKSCAPE_ICON("paint-gradient-radial"), PaintSelector::MODE_GRADIENT_RADIAL, + _("Radial gradient")); +#ifdef WITH_MESH + _mesh = + style_button_add(INKSCAPE_ICON("paint-gradient-mesh"), PaintSelector::MODE_GRADIENT_MESH, _("Mesh gradient")); +#endif + _pattern = style_button_add(INKSCAPE_ICON("paint-pattern"), PaintSelector::MODE_PATTERN, _("Pattern")); + _swatch = style_button_add(INKSCAPE_ICON("paint-swatch"), PaintSelector::MODE_SWATCH, _("Swatch")); + _unset = style_button_add(INKSCAPE_ICON("paint-unknown"), PaintSelector::MODE_UNSET, + _("Unset paint (make it undefined so it can be inherited)")); + + /* Fillrule */ + { + _fillrulebox = Gtk::manage(new Gtk::Box()); + _fillrulebox->set_homogeneous(false); + _style->pack_end(*_fillrulebox, false, false, 0); + + _evenodd = Gtk::manage(new FillRuleRadioButton()); + _evenodd->set_relief(Gtk::RELIEF_NONE); + _evenodd->set_mode(false); + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/painting.html#FillRuleProperty + _evenodd->set_tooltip_text( + _("Any path self-intersections or subpaths create holes in the fill (fill-rule: evenodd)")); + _evenodd->set_fillrule(PaintSelector::FILLRULE_EVENODD); + auto w = sp_get_icon_image("fill-rule-even-odd", GTK_ICON_SIZE_MENU); + gtk_container_add(GTK_CONTAINER(_evenodd->gobj()), w); + _fillrulebox->pack_start(*_evenodd, false, false, 0); + _evenodd->signal_toggled().connect( + sigc::bind(sigc::mem_fun(*this, &PaintSelector::fillrule_toggled), _evenodd)); + + auto grp = _evenodd->get_group(); + _nonzero = Gtk::manage(new FillRuleRadioButton(grp)); + _nonzero->set_relief(Gtk::RELIEF_NONE); + _nonzero->set_mode(false); + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/painting.html#FillRuleProperty + _nonzero->set_tooltip_text(_("Fill is solid unless a subpath is counterdirectional (fill-rule: nonzero)")); + _nonzero->set_fillrule(PaintSelector::FILLRULE_NONZERO); + w = sp_get_icon_image("fill-rule-nonzero", GTK_ICON_SIZE_MENU); + gtk_container_add(GTK_CONTAINER(_nonzero->gobj()), w); + _fillrulebox->pack_start(*_nonzero, false, false, 0); + _nonzero->signal_toggled().connect( + sigc::bind(sigc::mem_fun(*this, &PaintSelector::fillrule_toggled), _nonzero)); + } + + /* Frame */ + _label = Gtk::manage(new Gtk::Label("")); + auto lbbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + lbbox->set_homogeneous(false); + _label->show(); + lbbox->pack_start(*_label, false, false, 4); + pack_start(*lbbox, false, false, 4); + + _frame = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + _frame->set_homogeneous(false); + _frame->show(); + // gtk_container_set_border_width(GTK_CONTAINER(psel->frame), 0); + pack_start(*_frame, true, true, 0); + + + /* Last used color */ + _selected_color = new SelectedColor; + _updating_color = false; + + _selected_color->signal_grabbed.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorGrabbed)); + _selected_color->signal_dragged.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorDragged)); + _selected_color->signal_released.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorReleased)); + _selected_color->signal_changed.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorChanged)); + + // from _new function + setMode(PaintSelector::MODE_MULTIPLE); + + if (kind == FILL) + _fillrulebox->show_all(); + else + _fillrulebox->hide(); + + show_all(); + + // don't let docking manager uncover hidden widgets + set_no_show_all(); +} + +PaintSelector::~PaintSelector() +{ + if (_selected_color) { + delete _selected_color; + _selected_color = nullptr; + } +} + +StyleToggleButton *PaintSelector::style_button_add(gchar const *pixmap, PaintSelector::Mode mode, gchar const *tip) +{ + GtkWidget *w; + + auto b = Gtk::manage(new StyleToggleButton()); + b->set_tooltip_text(tip); + b->show(); + b->set_border_width(0); + b->set_relief(Gtk::RELIEF_NONE); + b->set_mode(false); + b->set_style(mode); + + w = sp_get_icon_image(pixmap, GTK_ICON_SIZE_BUTTON); + gtk_container_add(GTK_CONTAINER(b->gobj()), w); + + _style->pack_start(*b, false, false); + b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &PaintSelector::style_button_toggled), b)); + + return b; +} + +void PaintSelector::style_button_toggled(StyleToggleButton *tb) +{ + if (!_update && tb->get_active()) { + // button toggled: explicit user action where fill/stroke style change is initiated/requested + set_mode_ex(tb->get_style(), true); + } +} + +void PaintSelector::fillrule_toggled(FillRuleRadioButton *tb) +{ + if (!_update && tb->get_active()) { + auto fr = tb->get_fillrule(); + _signal_fillrule_changed.emit(fr); + } +} + +void PaintSelector::setMode(Mode mode) { + set_mode_ex(mode, false); +} + +void PaintSelector::set_mode_ex(Mode mode, bool switch_style) { + if (_mode != mode) { + _update = true; + _label->show(); +#ifdef SP_PS_VERBOSE + g_print("Mode change %d -> %d %s -> %s\n", _mode, mode, modeStrings[_mode], modeStrings[mode]); +#endif + switch (mode) { + case MODE_EMPTY: + set_mode_empty(); + break; + case MODE_MULTIPLE: + set_mode_multiple(); + break; + case MODE_NONE: + set_mode_none(); + break; + case MODE_SOLID_COLOR: + set_mode_color(mode); + break; + case MODE_GRADIENT_LINEAR: + case MODE_GRADIENT_RADIAL: + set_mode_gradient(mode); + break; +#ifdef WITH_MESH + case MODE_GRADIENT_MESH: + set_mode_mesh(mode); + break; +#endif + case MODE_PATTERN: + set_mode_pattern(mode); + break; + case MODE_HATCH: + set_mode_hatch(mode); + break; + case MODE_SWATCH: + set_mode_swatch(mode); + break; + case MODE_UNSET: + set_mode_unset(); + break; + default: + g_warning("file %s: line %d: Unknown paint mode %d", __FILE__, __LINE__, mode); + break; + } + _mode = mode; + _signal_mode_changed.emit(_mode, switch_style); + _update = false; + } +} + +void PaintSelector::setFillrule(FillRule fillrule) +{ + if (_fillrulebox) { + // TODO this flips widgets but does not use a member to store state. Revisit + _evenodd->set_active(fillrule == FILLRULE_EVENODD); + _nonzero->set_active(fillrule == FILLRULE_NONZERO); + } +} + +void PaintSelector::setColorAlpha(SPColor const &color, float alpha) +{ + g_return_if_fail((0.0 <= alpha) && (alpha <= 1.0)); + /* + guint32 rgba = 0; + + if ( sp_color_get_colorspace_type(color) == SP_COLORSPACE_TYPE_CMYK ) + { + #ifdef SP_PS_VERBOSE + g_print("PaintSelector set CMYKA\n"); + #endif + sp_paint_selector_set_mode(psel, MODE_COLOR_CMYK); + } + else + */ + { +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set RGBA\n"); +#endif + setMode(MODE_SOLID_COLOR); + } + + _updating_color = true; + _selected_color->setColorAlpha(color, alpha); + _updating_color = false; + // rgba = color.toRGBA32( alpha ); +} + +void PaintSelector::setSwatch(SPGradient *vector) +{ +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set SWATCH\n"); +#endif + setMode(MODE_SWATCH); + + if (_selector_swatch) { + _selector_swatch->setVector((vector) ? vector->document : nullptr, vector); + } +} + +void PaintSelector::setGradientLinear(SPGradient *vector, SPLinearGradient* gradient, SPStop* selected) +{ +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set GRADIENT LINEAR\n"); +#endif + setMode(MODE_GRADIENT_LINEAR); + + auto gsel = getGradientFromData(); + + gsel->setMode(GradientSelector::MODE_LINEAR); + gsel->setGradient(gradient); + gsel->setVector((vector) ? vector->document : nullptr, vector); + gsel->selectStop(selected); +} + +void PaintSelector::setGradientRadial(SPGradient *vector, SPRadialGradient* gradient, SPStop* selected) +{ +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set GRADIENT RADIAL\n"); +#endif + setMode(MODE_GRADIENT_RADIAL); + + auto gsel = getGradientFromData(); + + gsel->setMode(GradientSelector::MODE_RADIAL); + gsel->setGradient(gradient); + gsel->setVector((vector) ? vector->document : nullptr, vector); + gsel->selectStop(selected); +} + +#ifdef WITH_MESH +void PaintSelector::setGradientMesh(SPMeshGradient *array) +{ +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set GRADIENT MESH\n"); +#endif + setMode(MODE_GRADIENT_MESH); + + // GradientSelector *gsel = getGradientFromData(this); + + // gsel->setMode(GradientSelector::MODE_GRADIENT_MESH); + // gsel->setVector((mesh) ? mesh->document : 0, mesh); +} +#endif + +void PaintSelector::setGradientProperties(SPGradientUnits units, SPGradientSpread spread) +{ + g_return_if_fail(isPaintModeGradient(_mode)); + + auto gsel = getGradientFromData(); + gsel->setUnits(units); + gsel->setSpread(spread); +} + +void PaintSelector::getGradientProperties(SPGradientUnits &units, SPGradientSpread &spread) const +{ + g_return_if_fail(isPaintModeGradient(_mode)); + + auto gsel = getGradientFromData(); + units = gsel->getUnits(); + spread = gsel->getSpread(); +} + + +/** + * \post (alpha == NULL) || (*alpha in [0.0, 1.0]). + */ +void PaintSelector::getColorAlpha(SPColor &color, gfloat &alpha) const +{ + _selected_color->colorAlpha(color, alpha); + + g_assert((0.0 <= alpha) && (alpha <= 1.0)); +} + +SPGradient *PaintSelector::getGradientVector() +{ + SPGradient *vect = nullptr; + + if (isPaintModeGradient(_mode)) { + auto gsel = getGradientFromData(); + vect = gsel->getVector(); + } + + return vect; +} + + +void PaintSelector::pushAttrsToGradient(SPGradient *gr) const +{ + SPGradientUnits units = SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX; + SPGradientSpread spread = SP_GRADIENT_SPREAD_PAD; + getGradientProperties(units, spread); + gr->setUnits(units); + gr->setSpread(spread); + gr->updateRepr(); +} + +void PaintSelector::clear_frame() +{ + if (_selector_solid_color) { + _selector_solid_color->hide(); + } + if (_selector_gradient) { + _selector_gradient->hide(); + } + if (_selector_mesh) { + _selector_mesh->hide(); + } + if (_selector_pattern) { + _selector_pattern->hide(); + } + if (_selector_swatch) { + _selector_swatch->hide(); + } +} + +void PaintSelector::set_mode_empty() +{ + set_style_buttons(nullptr); + _style->set_sensitive(false); + clear_frame(); + _label->set_markup(_("<b>No objects</b>")); +} + +void PaintSelector::set_mode_multiple() +{ + set_style_buttons(nullptr); + _style->set_sensitive(true); + clear_frame(); + _label->set_markup(_("<b>Multiple styles</b>")); +} + +void PaintSelector::set_mode_unset() +{ + set_style_buttons(_unset); + _style->set_sensitive(true); + clear_frame(); + _label->set_markup(_("<b>Paint is undefined</b>")); +} + +void PaintSelector::set_mode_none() +{ + set_style_buttons(_none); + _style->set_sensitive(true); + clear_frame(); + _label->set_markup(_("<b>No paint</b>")); +} + +/* Color paint */ + +void PaintSelector::onSelectedColorGrabbed() { _signal_grabbed.emit(); } + +void PaintSelector::onSelectedColorDragged() +{ + if (_updating_color) { + return; + } + + _signal_dragged.emit(); +} + +void PaintSelector::onSelectedColorReleased() { _signal_released.emit(); } + +void PaintSelector::onSelectedColorChanged() +{ + if (_updating_color) { + return; + } + + if (_mode == MODE_SOLID_COLOR) { + _signal_changed.emit(); + } else { + g_warning("PaintSelector::onSelectedColorChanged(): selected color changed while not in color selection mode"); + } +} + +void PaintSelector::set_mode_color(PaintSelector::Mode /*mode*/) +{ + using Inkscape::UI::Widget::ColorNotebook; + + if (_mode == PaintSelector::MODE_SWATCH) { + auto gsel = getGradientFromData(); + if (gsel) { + SPGradient *gradient = gsel->getVector(); + + // Gradient can be null if object paint is changed externally (ie. with a color picker tool) + if (gradient) { + SPColor color = gradient->getFirstStop()->getColor(); + float alpha = gradient->getFirstStop()->getOpacity(); + _selected_color->setColorAlpha(color, alpha, false); + } + } + } + + set_style_buttons(_solid); + _style->set_sensitive(true); + + if (_mode == PaintSelector::MODE_SOLID_COLOR) { + /* Already have color selector */ + // Do nothing + } else { + clear_frame(); + + /* Create new color selector */ + /* Create vbox */ + if (!_selector_solid_color) { + _selector_solid_color = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); + _selector_solid_color->set_homogeneous(false); + + /* Color selector */ + auto color_selector = Gtk::manage(new ColorNotebook(*(_selected_color))); + color_selector->show(); + _selector_solid_color->pack_start(*color_selector, true, true, 0); + /* Pack everything to frame */ + _frame->add(*_selector_solid_color); + color_selector->set_label(_("<b>Flat color</b>")); + } + + _selector_solid_color->show(); + } + + _label->set_markup(""); //_("<b>Flat color</b>")); + _label->hide(); + +#ifdef SP_PS_VERBOSE + g_print("Color req\n"); +#endif +} + +/* Gradient */ + +void PaintSelector::gradient_grabbed() { _signal_grabbed.emit(); } + +void PaintSelector::gradient_dragged() { _signal_dragged.emit(); } + +void PaintSelector::gradient_released() { _signal_released.emit(); } + +void PaintSelector::gradient_changed(SPGradient * /* gr */) { _signal_changed.emit(); } + +void PaintSelector::set_mode_gradient(PaintSelector::Mode mode) +{ + if (mode == PaintSelector::MODE_GRADIENT_LINEAR) { + set_style_buttons(_gradient); + } else if (mode == PaintSelector::MODE_GRADIENT_RADIAL) { + set_style_buttons(_radial); + } + _style->set_sensitive(true); + + if ((_mode == PaintSelector::MODE_GRADIENT_LINEAR) || (_mode == PaintSelector::MODE_GRADIENT_RADIAL)) { + // do nothing - the selector should already be a GradientSelector + } else { + clear_frame(); + if (!_selector_gradient) { + /* Create new gradient selector */ + try { + _selector_gradient = Gtk::manage(new GradientEditor("/gradient-edit")); + _selector_gradient->show(); + _selector_gradient->signal_grabbed().connect(sigc::mem_fun(this, &PaintSelector::gradient_grabbed)); + _selector_gradient->signal_dragged().connect(sigc::mem_fun(this, &PaintSelector::gradient_dragged)); + _selector_gradient->signal_released().connect(sigc::mem_fun(this, &PaintSelector::gradient_released)); + _selector_gradient->signal_changed().connect(sigc::mem_fun(this, &PaintSelector::gradient_changed)); + _selector_gradient->signal_stop_selected().connect([=](SPStop* stop) { _signal_stop_selected.emit(stop); }); + /* Pack everything to frame */ + _frame->add(*_selector_gradient); + } + catch (std::exception& ex) { + g_error("Creation of GradientEditor widget failed: %s.", ex.what()); + throw; + } + } else { + // Necessary when creating new gradients via the Fill and Stroke dialog + _selector_gradient->setVector(nullptr, nullptr); + } + _selector_gradient->show(); + } + + /* Actually we have to set option menu history here */ + if (mode == PaintSelector::MODE_GRADIENT_LINEAR) { + _selector_gradient->setMode(GradientSelector::MODE_LINEAR); + // sp_gradient_selector_set_mode(SP_GRADIENT_SELECTOR(gsel), SP_GRADIENT_SELECTOR_MODE_LINEAR); + // _label->set_markup(_("<b>Linear gradient</b>")); + _label->hide(); + } else if (mode == PaintSelector::MODE_GRADIENT_RADIAL) { + _selector_gradient->setMode(GradientSelector::MODE_RADIAL); + // _label->set_markup(_("<b>Radial gradient</b>")); + _label->hide(); + } + +#ifdef SP_PS_VERBOSE + g_print("Gradient req\n"); +#endif +} + +// ************************* MESH ************************ +#ifdef WITH_MESH +void PaintSelector::mesh_destroy(GtkWidget *widget, PaintSelector * /*psel*/) +{ + // drop our reference to the mesh menu widget + g_object_unref(G_OBJECT(widget)); +} + +void PaintSelector::mesh_change(GtkWidget * /*widget*/, PaintSelector *psel) { psel->_signal_changed.emit(); } + + +/** + * Returns a list of meshes in the defs of the given source document as a vector + */ +static std::vector<SPMeshGradient *> ink_mesh_list_get(SPDocument *source) +{ + std::vector<SPMeshGradient *> pl; + if (source == nullptr) + return pl; + + + std::vector<SPObject *> meshes = source->getResourceList("gradient"); + for (auto meshe : meshes) { + if (SP_IS_MESHGRADIENT(meshe) && SP_GRADIENT(meshe) == SP_GRADIENT(meshe)->getArray()) { // only if this is a + // root mesh + pl.push_back(SP_MESHGRADIENT(meshe)); + } + } + return pl; +} + +/** + * Adds menu items for mesh list. + */ +static void sp_mesh_menu_build(GtkWidget *combo, std::vector<SPMeshGradient *> &mesh_list, SPDocument * /*source*/) +{ + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + + for (auto i : mesh_list) { + + Inkscape::XML::Node *repr = i->getRepr(); + + gchar const *meshid = repr->attribute("id"); + gchar const *label = meshid; + + // Only relevant if we supply a set of canned meshes. + gboolean stockid = false; + if (repr->attribute("inkscape:stockid")) { + label = _(repr->attribute("inkscape:stockid")); + stockid = true; + } + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, COMBO_COL_LABEL, label, COMBO_COL_STOCK, stockid, COMBO_COL_MESH, meshid, + COMBO_COL_SEP, FALSE, -1); + } +} + +/** + * Pick up all meshes from source, except those that are in + * current_doc (if non-NULL), and add items to the mesh menu. + */ +static void sp_mesh_list_from_doc(GtkWidget *combo, SPDocument * /*current_doc*/, SPDocument *source, + SPDocument * /*mesh_doc*/) +{ + std::vector<SPMeshGradient *> pl = ink_mesh_list_get(source); + sp_mesh_menu_build(combo, pl, source); +} + + +static void ink_mesh_menu_populate_menu(GtkWidget *combo, SPDocument *doc) +{ + static SPDocument *meshes_doc = nullptr; + + // If we ever add a list of canned mesh gradients, uncomment following: + + // find and load meshes.svg + // if (meshes_doc == NULL) { + // char *meshes_source = g_build_filename(INKSCAPE_MESHESDIR, "meshes.svg", NULL); + // if (Inkscape::IO::file_test(meshes_source, G_FILE_TEST_IS_REGULAR)) { + // meshes_doc = SPDocument::createNewDoc(meshes_source, FALSE); + // } + // g_free(meshes_source); + // } + + // suck in from current doc + sp_mesh_list_from_doc(combo, nullptr, doc, meshes_doc); + + // add separator + // { + // GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + // GtkTreeIter iter; + // gtk_list_store_append (store, &iter); + // gtk_list_store_set(store, &iter, + // COMBO_COL_LABEL, "", COMBO_COL_STOCK, false, COMBO_COL_MESH, "", COMBO_COL_SEP, true, -1); + // } + + // suck in from meshes.svg + // if (meshes_doc) { + // doc->ensureUpToDate(); + // sp_mesh_list_from_doc ( combo, doc, meshes_doc, NULL ); + // } +} + + +static GtkWidget *ink_mesh_menu(GtkWidget *combo) +{ + SPDocument *doc = SP_ACTIVE_DOCUMENT; + + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + + if (!doc) { + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, COMBO_COL_LABEL, _("No document selected"), COMBO_COL_STOCK, false, + COMBO_COL_MESH, "", COMBO_COL_SEP, false, -1); + gtk_widget_set_sensitive(combo, FALSE); + + } else { + + ink_mesh_menu_populate_menu(combo, doc); + gtk_widget_set_sensitive(combo, TRUE); + } + + // Select the first item that is not a separator + if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter)) { + gboolean sep = false; + gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, COMBO_COL_SEP, &sep, -1); + if (sep) { + gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter); + } + gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter); + } + + return combo; +} + + +/*update mesh list*/ +void PaintSelector::updateMeshList(SPMeshGradient *mesh) +{ + if (_update) { + return; + } + + g_assert(_meshmenu != nullptr); + + /* Clear existing menu if any */ + GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(_meshmenu)); + gtk_list_store_clear(GTK_LIST_STORE(store)); + + ink_mesh_menu(_meshmenu); + + /* Set history */ + + if (mesh && !_meshmenu_update) { + _meshmenu_update = true; + gchar const *meshname = mesh->getRepr()->attribute("id"); + + // Find this mesh and set it active in the combo_box + GtkTreeIter iter; + gchar *meshid = nullptr; + bool valid = gtk_tree_model_get_iter_first(store, &iter); + if (!valid) { + return; + } + gtk_tree_model_get(store, &iter, COMBO_COL_MESH, &meshid, -1); + while (valid && strcmp(meshid, meshname) != 0) { + valid = gtk_tree_model_iter_next(store, &iter); + g_free(meshid); + meshid = nullptr; + gtk_tree_model_get(store, &iter, COMBO_COL_MESH, &meshid, -1); + } + + if (valid) { + gtk_combo_box_set_active_iter(GTK_COMBO_BOX(_meshmenu), &iter); + } + + _meshmenu_update = false; + g_free(meshid); + } +} + +#ifdef WITH_MESH +void PaintSelector::set_mode_mesh(PaintSelector::Mode mode) +{ + if (mode == PaintSelector::MODE_GRADIENT_MESH) { + set_style_buttons(_mesh); + } + _style->set_sensitive(true); + + if (_mode == PaintSelector::MODE_GRADIENT_MESH) { + /* Already have mesh menu */ + // Do nothing - the Selector is already a Gtk::Box with the required contents + } else { + clear_frame(); + + if (!_selector_mesh) { + /* Create vbox */ + _selector_mesh = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); + _selector_mesh->set_homogeneous(false); + + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 1)); + hb->set_homogeneous(false); + + /** + * Create a combo_box and store with 4 columns, + * The label, a pointer to the mesh, is stockid or not, is a separator or not. + */ + GtkListStore *store = + gtk_list_store_new(COMBO_N_COLS, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_BOOLEAN); + GtkWidget *combo = _scrollprotected_combo_box_new_with_model(GTK_TREE_MODEL(store)); + gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(combo), PaintSelector::isSeparator, nullptr, nullptr); + + GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); + gtk_cell_renderer_set_padding(renderer, 2, 0); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), renderer, TRUE); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), renderer, "text", COMBO_COL_LABEL, nullptr); + + ink_mesh_menu(combo); + g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(PaintSelector::mesh_change), this); + g_signal_connect(G_OBJECT(combo), "destroy", G_CALLBACK(PaintSelector::mesh_destroy), this); + _meshmenu = combo; + g_object_ref(G_OBJECT(combo)); + + gtk_container_add(GTK_CONTAINER(hb->gobj()), combo); + _selector_mesh->pack_start(*hb, false, false, AUX_BETWEEN_BUTTON_GROUPS); + + g_object_unref(G_OBJECT(store)); + + auto hb2 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + hb2->set_homogeneous(false); + + auto l = Gtk::manage(new Gtk::Label()); + l->set_markup(_("Use the <b>Mesh tool</b> to modify the mesh.")); + l->set_line_wrap(true); + l->set_size_request(180, -1); + hb2->pack_start(*l, true, true, AUX_BETWEEN_BUTTON_GROUPS); + _selector_mesh->pack_start(*hb2, false, false, AUX_BETWEEN_BUTTON_GROUPS); + _selector_mesh->show_all(); + + _frame->add(*_selector_mesh); + } + + _selector_mesh->show(); + _label->set_markup(_("<b>Mesh fill</b>")); + } +#ifdef SP_PS_VERBOSE + g_print("Mesh req\n"); +#endif +} +#endif // WITH_MESH + +SPMeshGradient *PaintSelector::getMeshGradient() +{ + g_return_val_if_fail((_mode == MODE_GRADIENT_MESH), NULL); + + /* no mesh menu if we were just selected */ + if (_meshmenu == nullptr) { + return nullptr; + } + GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(_meshmenu)); + + /* Get the selected mesh */ + GtkTreeIter iter; + if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(_meshmenu), &iter) || + !gtk_list_store_iter_is_valid(GTK_LIST_STORE(store), &iter)) { + return nullptr; + } + + gchar *meshid = nullptr; + gboolean stockid = FALSE; + // gchar *label = nullptr; + gtk_tree_model_get(store, &iter, COMBO_COL_STOCK, &stockid, COMBO_COL_MESH, &meshid, -1); + // gtk_tree_model_get (store, &iter, COMBO_COL_LABEL, &label, COMBO_COL_STOCK, &stockid, COMBO_COL_MESH, &meshid, + // -1); std::cout << " .. meshid: " << (meshid?meshid:"null") << " label: " << (label?label:"null") << std::endl; + // g_free(label); + if (meshid == nullptr) { + return nullptr; + } + + SPMeshGradient *mesh = nullptr; + if (strcmp(meshid, "none")) { + + gchar *mesh_name; + if (stockid) { + mesh_name = g_strconcat("urn:inkscape:mesh:", meshid, nullptr); + } else { + mesh_name = g_strdup(meshid); + } + + SPObject *mesh_obj = get_stock_item(mesh_name); + if (mesh_obj && SP_IS_MESHGRADIENT(mesh_obj)) { + mesh = SP_MESHGRADIENT(mesh_obj); + } + g_free(mesh_name); + } else { + std::cerr << "PaintSelector::getMeshGradient: Unexpected meshid value." << std::endl; + } + + g_free(meshid); + + return mesh; +} + +#endif +// ************************ End Mesh ************************ + +void PaintSelector::set_style_buttons(Gtk::ToggleButton *active) +{ + _none->set_active(active == _none); + _solid->set_active(active == _solid); + _gradient->set_active(active == _gradient); + _radial->set_active(active == _radial); +#ifdef WITH_MESH + _mesh->set_active(active == _mesh); +#endif + _pattern->set_active(active == _pattern); + _swatch->set_active(active == _swatch); + _unset->set_active(active == _unset); +} + +void PaintSelector::pattern_destroy(GtkWidget *widget, PaintSelector * /*psel*/) +{ + // drop our reference to the pattern menu widget + g_object_unref(G_OBJECT(widget)); +} + +void PaintSelector::pattern_change(GtkWidget * /*widget*/, PaintSelector *psel) { psel->_signal_changed.emit(); } + + +/** + * Returns a list of patterns in the defs of the given source document as a vector + */ +static std::vector<SPPattern *> ink_pattern_list_get(SPDocument *source) +{ + std::vector<SPPattern *> pl; + if (source == nullptr) + return pl; + + std::vector<SPObject *> patterns = source->getResourceList("pattern"); + for (auto pattern : patterns) { + if (SP_PATTERN(pattern) == SP_PATTERN(pattern)->rootPattern()) { // only if this is a root pattern + pl.push_back(SP_PATTERN(pattern)); + } + } + + return pl; +} + +/** + * Adds menu items for pattern list - derived from marker code, left hb etc in to make addition of previews easier at + * some point. + */ +static void sp_pattern_menu_build(GtkWidget *combo, std::vector<SPPattern *> &pl, SPDocument * /*source*/) +{ + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + + for (auto i = pl.rbegin(); i != pl.rend(); ++i) { + + Inkscape::XML::Node *repr = (*i)->getRepr(); + + // label for combobox + gchar const *label; + if (repr->attribute("inkscape:stockid")) { + label = _(repr->attribute("inkscape:stockid")); + } else { + label = _(repr->attribute("id")); + } + + gchar const *patid = repr->attribute("id"); + + gboolean stockid = false; + if (repr->attribute("inkscape:stockid")) { + stockid = true; + } + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, COMBO_COL_LABEL, label, COMBO_COL_STOCK, stockid, COMBO_COL_PATTERN, patid, + COMBO_COL_SEP, FALSE, -1); + } +} + +/** + * Pick up all patterns from source, except those that are in + * current_doc (if non-NULL), and add items to the pattern menu. + */ +static void sp_pattern_list_from_doc(GtkWidget *combo, SPDocument * /*current_doc*/, SPDocument *source, + SPDocument * /*pattern_doc*/) +{ + std::vector<SPPattern *> pl = ink_pattern_list_get(source); + sp_pattern_menu_build(combo, pl, source); +} + + +static void ink_pattern_menu_populate_menu(GtkWidget *combo, SPDocument *doc) +{ + static SPDocument *patterns_doc = nullptr; + + // find and load patterns.svg + if (patterns_doc == nullptr) { + using namespace Inkscape::IO::Resource; + auto patterns_source = get_path_string(SYSTEM, PAINT, "patterns.svg"); + if (Glib::file_test(patterns_source, Glib::FILE_TEST_IS_REGULAR)) { + patterns_doc = SPDocument::createNewDoc(patterns_source.c_str(), false); + } + } + + // suck in from current doc + sp_pattern_list_from_doc(combo, nullptr, doc, patterns_doc); + + // add separator + { + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, COMBO_COL_LABEL, "", COMBO_COL_STOCK, false, COMBO_COL_PATTERN, "", + COMBO_COL_SEP, true, -1); + } + + // suck in from patterns.svg + if (patterns_doc) { + doc->ensureUpToDate(); + sp_pattern_list_from_doc(combo, doc, patterns_doc, nullptr); + } +} + + +static GtkWidget *ink_pattern_menu(GtkWidget *combo) +{ + SPDocument *doc = SP_ACTIVE_DOCUMENT; + + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + + if (!doc) { + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, COMBO_COL_LABEL, _("No document selected"), COMBO_COL_STOCK, false, + COMBO_COL_PATTERN, "", COMBO_COL_SEP, false, -1); + gtk_widget_set_sensitive(combo, FALSE); + + } else { + + ink_pattern_menu_populate_menu(combo, doc); + gtk_widget_set_sensitive(combo, TRUE); + } + + // Select the first item that is not a separator + if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter)) { + gboolean sep = false; + gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, COMBO_COL_SEP, &sep, -1); + if (sep) { + gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter); + } + gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter); + } + + return combo; +} + + +/*update pattern list*/ +void PaintSelector::updatePatternList(SPPattern *pattern) +{ + if (_update) { + return; + } + g_assert(_patternmenu != nullptr); + + /* Clear existing menu if any */ + auto store = gtk_combo_box_get_model(GTK_COMBO_BOX(_patternmenu)); + gtk_list_store_clear(GTK_LIST_STORE(store)); + + ink_pattern_menu(_patternmenu); + + /* Set history */ + + if (pattern && !_patternmenu_update) { + _patternmenu_update = true; + gchar const *patname = pattern->getRepr()->attribute("id"); + + // Find this pattern and set it active in the combo_box + GtkTreeIter iter; + gchar *patid = nullptr; + bool valid = gtk_tree_model_get_iter_first(store, &iter); + if (!valid) { + return; + } + gtk_tree_model_get(store, &iter, COMBO_COL_PATTERN, &patid, -1); + while (valid && strcmp(patid, patname) != 0) { + valid = gtk_tree_model_iter_next(store, &iter); + g_free(patid); + patid = nullptr; + gtk_tree_model_get(store, &iter, COMBO_COL_PATTERN, &patid, -1); + } + g_free(patid); + + if (valid) { + gtk_combo_box_set_active_iter(GTK_COMBO_BOX(_patternmenu), &iter); + } + + _patternmenu_update = false; + } +} + +void PaintSelector::set_mode_pattern(PaintSelector::Mode mode) +{ + if (mode == PaintSelector::MODE_PATTERN) { + set_style_buttons(_pattern); + } + + _style->set_sensitive(true); + + if (_mode == PaintSelector::MODE_PATTERN) { + /* Already have pattern menu */ + } else { + clear_frame(); + + if (!_selector_pattern) { + /* Create vbox */ + _selector_pattern = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); + _selector_pattern->set_homogeneous(false); + + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 1)); + hb->set_homogeneous(false); + + /** + * Create a combo_box and store with 4 columns, + * The label, a pointer to the pattern, is stockid or not, is a separator or not. + */ + GtkListStore *store = + gtk_list_store_new(COMBO_N_COLS, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_BOOLEAN); + _patternmenu = _scrollprotected_combo_box_new_with_model(GTK_TREE_MODEL(store)); + gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(_patternmenu), PaintSelector::isSeparator, nullptr, + nullptr); + + GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); + gtk_cell_renderer_set_padding(renderer, 2, 0); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(_patternmenu), renderer, TRUE); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(_patternmenu), renderer, "text", COMBO_COL_LABEL, nullptr); + + ink_pattern_menu(_patternmenu); + g_signal_connect(G_OBJECT(_patternmenu), "changed", G_CALLBACK(PaintSelector::pattern_change), this); + g_signal_connect(G_OBJECT(_patternmenu), "destroy", G_CALLBACK(PaintSelector::pattern_destroy), this); + g_object_ref(G_OBJECT(_patternmenu)); + + gtk_container_add(GTK_CONTAINER(hb->gobj()), _patternmenu); + _selector_pattern->pack_start(*hb, false, false, AUX_BETWEEN_BUTTON_GROUPS); + + g_object_unref(G_OBJECT(store)); + + auto hb2 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + hb2->set_homogeneous(false); + auto l = Gtk::manage(new Gtk::Label()); + l->set_markup( + _("Use the <b>Node tool</b> to adjust position, scale, and rotation of the pattern on canvas. Use " + "<b>Object > Pattern > Objects to Pattern</b> to create a new pattern from selection.")); + l->set_line_wrap(true); + l->set_size_request(180, -1); + hb2->pack_start(*l, true, true, AUX_BETWEEN_BUTTON_GROUPS); + _selector_pattern->pack_start(*hb2, false, false, AUX_BETWEEN_BUTTON_GROUPS); + _selector_pattern->show_all(); + _frame->add(*_selector_pattern); + } + + _selector_pattern->show(); + _label->set_markup(_("<b>Pattern fill</b>")); + } +#ifdef SP_PS_VERBOSE + g_print("Pattern req\n"); +#endif +} + +void PaintSelector::set_mode_hatch(PaintSelector::Mode mode) +{ + if (mode == PaintSelector::MODE_HATCH) { + set_style_buttons(_unset); + } + + _style->set_sensitive(true); + + if (_mode == PaintSelector::MODE_HATCH) { + /* Already have hatch menu, for the moment unset */ + } else { + clear_frame(); + + _label->set_markup(_("<b>Hatch fill</b>")); + } +#ifdef SP_PS_VERBOSE + g_print("Hatch req\n"); +#endif +} + +gboolean PaintSelector::isSeparator(GtkTreeModel *model, GtkTreeIter *iter, gpointer /*data*/) +{ + + gboolean sep = FALSE; + gtk_tree_model_get(model, iter, COMBO_COL_SEP, &sep, -1); + return sep; +} + +SPPattern *PaintSelector::getPattern() +{ + SPPattern *pat = nullptr; + g_return_val_if_fail(_mode == MODE_PATTERN, nullptr); + + /* no pattern menu if we were just selected */ + if (!_patternmenu) { + return nullptr; + } + + auto store = gtk_combo_box_get_model(GTK_COMBO_BOX(_patternmenu)); + + /* Get the selected pattern */ + GtkTreeIter iter; + if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(_patternmenu), &iter) || + !gtk_list_store_iter_is_valid(GTK_LIST_STORE(store), &iter)) { + return nullptr; + } + + gchar *patid = nullptr; + gboolean stockid = FALSE; + // gchar *label = nullptr; + gtk_tree_model_get(store, &iter, + // COMBO_COL_LABEL, &label, + COMBO_COL_STOCK, &stockid, COMBO_COL_PATTERN, &patid, -1); + // g_free(label); + if (patid == nullptr) { + return nullptr; + } + + if (strcmp(patid, "none") != 0) { + gchar *paturn; + + if (stockid) { + paturn = g_strconcat("urn:inkscape:pattern:", patid, nullptr); + } else { + paturn = g_strdup(patid); + } + SPObject *pat_obj = get_stock_item(paturn); + if (pat_obj) { + pat = SP_PATTERN(pat_obj); + } + g_free(paturn); + } else { + SPDocument *doc = SP_ACTIVE_DOCUMENT; + SPObject *pat_obj = doc->getObjectById(patid); + + if (pat_obj && SP_IS_PATTERN(pat_obj)) { + pat = SP_PATTERN(pat_obj)->rootPattern(); + } + } + + g_free(patid); + + return pat; +} + +void PaintSelector::set_mode_swatch(PaintSelector::Mode mode) +{ + if (mode == PaintSelector::MODE_SWATCH) { + set_style_buttons(_swatch); + } + + _style->set_sensitive(true); + + if (_mode == PaintSelector::MODE_SWATCH) { + // Do nothing. The selector is already a SwatchSelector + } else { + clear_frame(); + + if (!_selector_swatch) { + // Create new gradient selector + _selector_swatch = Gtk::manage(new SwatchSelector()); + + auto gsel = _selector_swatch->getGradientSelector(); + gsel->signal_grabbed().connect(sigc::mem_fun(this, &PaintSelector::gradient_grabbed)); + gsel->signal_dragged().connect(sigc::mem_fun(this, &PaintSelector::gradient_dragged)); + gsel->signal_released().connect(sigc::mem_fun(this, &PaintSelector::gradient_released)); + gsel->signal_changed().connect(sigc::mem_fun(this, &PaintSelector::gradient_changed)); + + // Pack everything to frame + _frame->add(*_selector_swatch); + } else { + // Necessary when creating new swatches via the Fill and Stroke dialog + _selector_swatch->setVector(nullptr, nullptr); + } + _selector_swatch->show(); + _label->set_markup(_("<b>Swatch fill</b>")); + } + +#ifdef SP_PS_VERBOSE + g_print("Swatch req\n"); +#endif +} + +// TODO this seems very bad to be taking in a desktop pointer to muck with. Logic probably belongs elsewhere +void PaintSelector::setFlatColor(SPDesktop *desktop, gchar const *color_property, gchar const *opacity_property) +{ + SPCSSAttr *css = sp_repr_css_attr_new(); + + SPColor color; + gfloat alpha = 0; + getColorAlpha(color, alpha); + + std::string colorStr = color.toString(); + +#ifdef SP_PS_VERBOSE + guint32 rgba = color.toRGBA32(alpha); + g_message("sp_paint_selector_set_flat_color() to '%s' from 0x%08x::%s", colorStr.c_str(), rgba, + (color.icc ? color.icc->colorProfile.c_str() : "<null>")); +#endif // SP_PS_VERBOSE + + sp_repr_css_set_property(css, color_property, colorStr.c_str()); + Inkscape::CSSOStringStream osalpha; + osalpha << alpha; + sp_repr_css_set_property(css, opacity_property, osalpha.str().c_str()); + + sp_desktop_set_style(desktop, css); + + sp_repr_css_attr_unref(css); +} + +PaintSelector::Mode PaintSelector::getModeForStyle(SPStyle const &style, FillOrStroke kind) +{ + Mode mode = MODE_UNSET; + SPIPaint const &target = *style.getFillOrStroke(kind == FILL); + + if (!target.set) { + mode = MODE_UNSET; + } else if (target.isPaintserver()) { + SPPaintServer const *server = kind == FILL ? style.getFillPaintServer() : style.getStrokePaintServer(); + +#ifdef SP_PS_VERBOSE + g_message("PaintSelector::getModeForStyle(%p, %d)", &style, kind); + g_message("==== server:%p %s grad:%s swatch:%s", server, server->getId(), + (SP_IS_GRADIENT(server) ? "Y" : "n"), + (SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch() ? "Y" : "n")); +#endif // SP_PS_VERBOSE + + + if (server && SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()) { + mode = MODE_SWATCH; + } else if (SP_IS_LINEARGRADIENT(server)) { + mode = MODE_GRADIENT_LINEAR; + } else if (SP_IS_RADIALGRADIENT(server)) { + mode = MODE_GRADIENT_RADIAL; +#ifdef WITH_MESH + } else if (SP_IS_MESHGRADIENT(server)) { + mode = MODE_GRADIENT_MESH; +#endif + } else if (SP_IS_PATTERN(server)) { + mode = MODE_PATTERN; + } else if (SP_IS_HATCH(server)) { + mode = MODE_HATCH; + } else { + g_warning("file %s: line %d: Unknown paintserver", __FILE__, __LINE__); + mode = MODE_NONE; + } + } else if (target.isColor()) { + // TODO this is no longer a valid assertion: + mode = MODE_SOLID_COLOR; // so far only rgb can be read from svg + } else if (target.isNone()) { + mode = MODE_NONE; + } else { + g_warning("file %s: line %d: Unknown paint type", __FILE__, __LINE__); + mode = MODE_NONE; + } + + return mode; +} + +} // namespace Widget +} // 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/widget/paint-selector.h b/src/ui/widget/paint-selector.h new file mode 100644 index 0000000..ad069d8 --- /dev/null +++ b/src/ui/widget/paint-selector.h @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Generic paint selector widget + *//* + * Authors: + * Lauris + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_PAINT_SELECTOR_H +#define SEEN_SP_PAINT_SELECTOR_H + +#include "color.h" +#include "fill-or-stroke.h" +#include <glib.h> +#include <gtkmm/box.h> + +#include "object/sp-gradient-spread.h" +#include "object/sp-gradient-units.h" +#include "gradient-selector-interface.h" +#include "ui/selected-color.h" +#include "ui/widget/gradient-selector.h" +#include "ui/widget/swatch-selector.h" + +class SPGradient; +class SPLinearGradient; +class SPRadialGradient; +#ifdef WITH_MESH +class SPMeshGradient; +#endif +class SPDesktop; +class SPPattern; +class SPStyle; + +namespace Gtk { +class Label; +class RadioButton; +class ToggleButton; +} // namespace Gtk + +namespace Inkscape { +namespace UI { +namespace Widget { + +class FillRuleRadioButton; +class StyleToggleButton; +class GradientEditor; + +/** + * Generic paint selector widget. + */ +class PaintSelector : public Gtk::Box { + public: + enum Mode { + MODE_EMPTY, + MODE_MULTIPLE, + MODE_NONE, + MODE_SOLID_COLOR, + MODE_GRADIENT_LINEAR, + MODE_GRADIENT_RADIAL, +#ifdef WITH_MESH + MODE_GRADIENT_MESH, +#endif + MODE_PATTERN, + MODE_HATCH, + MODE_SWATCH, + MODE_UNSET + }; + + enum FillRule { FILLRULE_NONZERO, FILLRULE_EVENODD }; + + private: + bool _update = false; + + Mode _mode; + + Gtk::Box *_style; + StyleToggleButton *_none; + StyleToggleButton *_solid; + StyleToggleButton *_gradient; + StyleToggleButton *_radial; +#ifdef WITH_MESH + StyleToggleButton *_mesh; +#endif + StyleToggleButton *_pattern; + StyleToggleButton *_swatch; + StyleToggleButton *_unset; + + Gtk::Box *_fillrulebox; + FillRuleRadioButton *_evenodd; + FillRuleRadioButton *_nonzero; + + Gtk::Box *_frame; + + Gtk::Box *_selector_solid_color = nullptr; + GradientEditor *_selector_gradient = nullptr; + Gtk::Box *_selector_mesh = nullptr; + Gtk::Box *_selector_pattern = nullptr; + SwatchSelector *_selector_swatch = nullptr; + + Gtk::Label *_label; + GtkWidget *_patternmenu = nullptr; + bool _patternmenu_update = false; +#ifdef WITH_MESH + GtkWidget *_meshmenu = nullptr; + bool _meshmenu_update = false; +#endif + + Inkscape::UI::SelectedColor *_selected_color; + bool _updating_color; + + void getColorAlpha(SPColor &color, gfloat &alpha) const; + + static gboolean isSeparator(GtkTreeModel *model, GtkTreeIter *iter, gpointer data); + + private: + sigc::signal<void, FillRule> _signal_fillrule_changed; + sigc::signal<void> _signal_dragged; + sigc::signal<void, Mode, bool> _signal_mode_changed; + sigc::signal<void> _signal_grabbed; + sigc::signal<void> _signal_released; + sigc::signal<void> _signal_changed; + sigc::signal<void (SPStop*)> _signal_stop_selected; + + StyleToggleButton *style_button_add(gchar const *px, PaintSelector::Mode mode, gchar const *tip); + void style_button_toggled(StyleToggleButton *tb); + void fillrule_toggled(FillRuleRadioButton *tb); + void onSelectedColorGrabbed(); + void onSelectedColorDragged(); + void onSelectedColorReleased(); + void onSelectedColorChanged(); + void set_mode_empty(); + void set_style_buttons(Gtk::ToggleButton *active); + void set_mode_multiple(); + void set_mode_none(); + GradientSelectorInterface *getGradientFromData() const; + void clear_frame(); + void set_mode_unset(); + void set_mode_color(PaintSelector::Mode mode); + void set_mode_gradient(PaintSelector::Mode mode); +#ifdef WITH_MESH + void set_mode_mesh(PaintSelector::Mode mode); +#endif + void set_mode_pattern(PaintSelector::Mode mode); + void set_mode_hatch(PaintSelector::Mode mode); + void set_mode_swatch(PaintSelector::Mode mode); + void set_mode_ex(Mode mode, bool switch_style); + + void gradient_grabbed(); + void gradient_dragged(); + void gradient_released(); + void gradient_changed(SPGradient *gr); + + static void mesh_change(GtkWidget *widget, PaintSelector *psel); + static void mesh_destroy(GtkWidget *widget, PaintSelector *psel); + + static void pattern_change(GtkWidget *widget, PaintSelector *psel); + static void pattern_destroy(GtkWidget *widget, PaintSelector *psel); + + public: + PaintSelector(FillOrStroke kind); + ~PaintSelector() override; + + inline decltype(_signal_fillrule_changed) signal_fillrule_changed() const { return _signal_fillrule_changed; } + inline decltype(_signal_dragged) signal_dragged() const { return _signal_dragged; } + inline decltype(_signal_mode_changed) signal_mode_changed() const { return _signal_mode_changed; } + inline decltype(_signal_grabbed) signal_grabbed() const { return _signal_grabbed; } + inline decltype(_signal_released) signal_released() const { return _signal_released; } + inline decltype(_signal_changed) signal_changed() const { return _signal_changed; } + inline decltype(_signal_stop_selected) signal_stop_selected() const { return _signal_stop_selected; } + + void setMode(Mode mode); + static Mode getModeForStyle(SPStyle const &style, FillOrStroke kind); + void setFillrule(FillRule fillrule); + void setColorAlpha(SPColor const &color, float alpha); + void setSwatch(SPGradient *vector); + void setGradientLinear(SPGradient *vector, SPLinearGradient* gradient, SPStop* selected); + void setGradientRadial(SPGradient *vector, SPRadialGradient* gradient, SPStop* selected); +#ifdef WITH_MESH + void setGradientMesh(SPMeshGradient *array); +#endif + void setGradientProperties(SPGradientUnits units, SPGradientSpread spread); + void getGradientProperties(SPGradientUnits &units, SPGradientSpread &spread) const; + +#ifdef WITH_MESH + SPMeshGradient *getMeshGradient(); + void updateMeshList(SPMeshGradient *pat); +#endif + + void updatePatternList(SPPattern *pat); + inline decltype(_mode) get_mode() const { return _mode; } + + // TODO move this elsewhere: + void setFlatColor(SPDesktop *desktop, const gchar *color_property, const gchar *opacity_property); + + SPGradient *getGradientVector(); + void pushAttrsToGradient(SPGradient *gr) const; + SPPattern *getPattern(); +}; + +enum { + COMBO_COL_LABEL = 0, + COMBO_COL_STOCK = 1, + COMBO_COL_PATTERN = 2, + COMBO_COL_MESH = COMBO_COL_PATTERN, + COMBO_COL_SEP = 3, + COMBO_N_COLS = 4 +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape +#endif // SEEN_SP_PAINT_SELECTOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/point.cpp b/src/ui/widget/point.cpp new file mode 100644 index 0000000..5bc50ab --- /dev/null +++ b/src/ui/widget/point.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2007 Authors + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/point.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +Point::Point(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new Gtk::Box(Gtk::ORIENTATION_VERTICAL), suffix, icon, mnemonic), + xwidget("X:",""), + ywidget("Y:","") +{ + static_cast<Gtk::Box*>(_widget)->pack_start(xwidget, true, true); + static_cast<Gtk::Box*>(_widget)->pack_start(ywidget, true, true); + static_cast<Gtk::Box*>(_widget)->show_all_children(); +} + +Point::Point(Glib::ustring const &label, Glib::ustring const &tooltip, + unsigned digits, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new Gtk::Box(Gtk::ORIENTATION_VERTICAL), suffix, icon, mnemonic), + xwidget("X:","", digits), + ywidget("Y:","", digits) +{ + static_cast<Gtk::Box*>(_widget)->pack_start(xwidget, true, true); + static_cast<Gtk::Box*>(_widget)->pack_start(ywidget, true, true); + static_cast<Gtk::Box*>(_widget)->show_all_children(); +} + +Point::Point(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::RefPtr<Gtk::Adjustment> &adjust, + unsigned digits, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new Gtk::Box(Gtk::ORIENTATION_VERTICAL), suffix, icon, mnemonic), + xwidget("X:","", adjust, digits), + ywidget("Y:","", adjust, digits) +{ + static_cast<Gtk::Box*>(_widget)->pack_start(xwidget, true, true); + static_cast<Gtk::Box*>(_widget)->pack_start(ywidget, true, true); + static_cast<Gtk::Box*>(_widget)->show_all_children(); +} + +unsigned Point::getDigits() const +{ + return xwidget.getDigits(); +} + +double Point::getStep() const +{ + return xwidget.getStep(); +} + +double Point::getPage() const +{ + return xwidget.getPage(); +} + +double Point::getRangeMin() const +{ + return xwidget.getRangeMin(); +} + +double Point::getRangeMax() const +{ + return xwidget.getRangeMax(); +} + +double Point::getXValue() const +{ + return xwidget.getValue(); +} + +double Point::getYValue() const +{ + return ywidget.getValue(); +} + +Geom::Point Point::getValue() const +{ + return Geom::Point( getXValue() , getYValue() ); +} + +int Point::getXValueAsInt() const +{ + return xwidget.getValueAsInt(); +} + +int Point::getYValueAsInt() const +{ + return ywidget.getValueAsInt(); +} + + +void Point::setDigits(unsigned digits) +{ + xwidget.setDigits(digits); + ywidget.setDigits(digits); +} + +void Point::setIncrements(double step, double page) +{ + xwidget.setIncrements(step, page); + ywidget.setIncrements(step, page); +} + +void Point::setRange(double min, double max) +{ + xwidget.setRange(min, max); + ywidget.setRange(min, max); +} + +void Point::setValue(Geom::Point const & p) +{ + xwidget.setValue(p[0]); + ywidget.setValue(p[1]); +} + +void Point::update() +{ + xwidget.update(); + ywidget.update(); +} + +bool Point::setProgrammatically() +{ + return (xwidget.setProgrammatically || ywidget.setProgrammatically); +} + +void Point::clearProgrammatically() +{ + xwidget.setProgrammatically = false; + ywidget.setProgrammatically = false; +} + + +Glib::SignalProxy0<void> Point::signal_x_value_changed() +{ + return xwidget.signal_value_changed(); +} + +Glib::SignalProxy0<void> Point::signal_y_value_changed() +{ + return ywidget.signal_value_changed(); +} + + +} // namespace Widget +} // 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/widget/point.h b/src/ui/widget/point.h new file mode 100644 index 0000000..018be5b --- /dev/null +++ b/src/ui/widget/point.h @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2007 Authors + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_UI_WIDGET_POINT_H +#define INKSCAPE_UI_WIDGET_POINT_H + +#include "ui/widget/labelled.h" +#include <2geom/point.h> +#include "ui/widget/scalar.h" + +namespace Gtk { +class Adjustment; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A labelled text box, with spin buttons and optional icon or suffix, for + * entering arbitrary coordinate values. + */ +class Point : public Labelled +{ +public: + + + /** + * Construct a Point Widget. + * + * @param label Label. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Point( Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Construct a Point Widget. + * + * @param label Label. + * @param digits Number of decimal digits to display. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Point( Glib::ustring const &label, + Glib::ustring const &tooltip, + unsigned digits, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Construct a Point Widget. + * + * @param label Label. + * @param adjust Adjustment to use for the SpinButton. + * @param digits Number of decimal digits to display (defaults to 0). + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to true). + */ + Point( Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::RefPtr<Gtk::Adjustment> &adjust, + unsigned digits = 0, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Fetches the precision of the spin button. + */ + unsigned getDigits() const; + + /** + * Gets the current step increment used by the spin button. + */ + double getStep() const; + + /** + * Gets the current page increment used by the spin button. + */ + double getPage() const; + + /** + * Gets the minimum range value allowed for the spin button. + */ + double getRangeMin() const; + + /** + * Gets the maximum range value allowed for the spin button. + */ + double getRangeMax() const; + + bool getSnapToTicks() const; + + /** + * Get the value in the spin_button. + */ + double getXValue() const; + + double getYValue() const; + + Geom::Point getValue() const; + + /** + * Get the value spin_button represented as an integer. + */ + int getXValueAsInt() const; + + int getYValueAsInt() const; + + /** + * Sets the precision to be displayed by the spin button. + */ + void setDigits(unsigned digits); + + /** + * Sets the step and page increments for the spin button. + */ + void setIncrements(double step, double page); + + /** + * Sets the minimum and maximum range allowed for the spin button. + */ + void setRange(double min, double max); + + /** + * Sets the value of the spin button. + */ + void setValue(Geom::Point const & p); + + /** + * Manually forces an update of the spin button. + */ + void update(); + + /** + * Signal raised when the spin button's value changes. + */ + Glib::SignalProxy0<void> signal_x_value_changed(); + + Glib::SignalProxy0<void> signal_y_value_changed(); + + /** + * Check 'setProgrammatically' of both scalar widgets. False if value is changed by user by clicking the widget. + * true if the value was set by setValue, not changed by the user; + * if a callback checks it, it must reset it back to false. + */ + bool setProgrammatically(); + + void clearProgrammatically(); + +protected: + Scalar xwidget; + Scalar ywidget; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_POINT_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/widget/preferences-widget.cpp b/src/ui/widget/preferences-widget.cpp new file mode 100644 index 0000000..e19ebe3 --- /dev/null +++ b/src/ui/widget/preferences-widget.cpp @@ -0,0 +1,1118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape Preferences dialog. + * + * Authors: + * Marco Scholten + * Bruno Dilly <bruno.dilly@gmail.com> + * + * Copyright (C) 2004, 2006, 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <glibmm/convert.h> +#include <glibmm/regex.h> + +#include <gtkmm/box.h> +#include <gtkmm/frame.h> +#include <gtkmm/scale.h> +#include <gtkmm/table.h> + + +#include "desktop.h" +#include "inkscape.h" +#include "message-stack.h" +#include "preferences.h" +#include "selcue.h" +#include "selection-chemistry.h" + +#include "include/gtkmm_version.h" + +#include "io/sys.h" + +#include "ui/dialog/filedialog.h" +#include "ui/icon-loader.h" +#include "ui/util.h" +#include "ui/widget/preferences-widget.h" + + +#ifdef _WIN32 +#include <windows.h> +#endif + +using namespace Inkscape::UI::Widget; + +namespace Inkscape { +namespace UI { +namespace Widget { + +DialogPage::DialogPage() +{ + set_border_width(12); + + set_orientation(Gtk::ORIENTATION_VERTICAL); + set_column_spacing(12); + set_row_spacing(6); +} + +/** + * Add a widget to the bottom row of the dialog page + * + * \param[in] indent Whether the widget should be indented by one column + * \param[in] label The label text for the widget + * \param[in] widget The widget to add to the page + * \param[in] suffix Text for an optional label at the right of the widget + * \param[in] tip Tooltip text for the widget + * \param[in] expand_widget Whether to expand the widget horizontally + * \param[in] other_widget An optional additional widget to display at the right of the first one + */ +void DialogPage::add_line(bool indent, + Glib::ustring const &label, + Gtk::Widget &widget, + Glib::ustring const &suffix, + const Glib::ustring &tip, + bool expand_widget, + Gtk::Widget *other_widget) +{ + if (tip != "") + widget.set_tooltip_text (tip); + + auto hb = Gtk::manage(new Gtk::Box()); + hb->set_spacing(12); + hb->set_hexpand(true); + hb->pack_start(widget, expand_widget, expand_widget); + hb->set_valign(Gtk::ALIGN_CENTER); + + // Add a label in the first column if provided + if (label != "") + { + Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(label, Gtk::ALIGN_START, + Gtk::ALIGN_CENTER, true)); + label_widget->set_mnemonic_widget(widget); + label_widget->set_markup(label_widget->get_text()); + + if (indent) { + label_widget->set_margin_start(12); + } + + label_widget->set_valign(Gtk::ALIGN_CENTER); + add(*label_widget); + attach_next_to(*hb, *label_widget, Gtk::POS_RIGHT, 1, 1); + } + + // Now add the widget to the bottom of the dialog + if (label == "") + { + if (indent) { + hb->set_margin_start(12); + } + + add(*hb); + + GValue width = G_VALUE_INIT; + g_value_init(&width, G_TYPE_INT); + g_value_set_int(&width, 2); + gtk_container_child_set_property(GTK_CONTAINER(gobj()), GTK_WIDGET(hb->gobj()), "width", &width); + } + + // Add a label on the right of the widget if desired + if (suffix != "") + { + Gtk::Label* suffix_widget = Gtk::manage(new Gtk::Label(suffix , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true)); + suffix_widget->set_markup(suffix_widget->get_text()); + hb->pack_start(*suffix_widget,false,false); + } + + // Pack an additional widget into a box with the widget if desired + if (other_widget) + hb->pack_start(*other_widget, expand_widget, expand_widget); +} + +void DialogPage::add_group_header(Glib::ustring name, int columns) +{ + if (name != "") + { + Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(Glib::ustring(/*"<span size='large'>*/"<b>") + name + + Glib::ustring("</b>"/*</span>"*/) , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true)); + + label_widget->set_use_markup(true); + label_widget->set_valign(Gtk::ALIGN_CENTER); + add(*label_widget); + if (columns > 1) { + GValue width = G_VALUE_INIT; + g_value_init(&width, G_TYPE_INT); + g_value_set_int(&width, columns); + gtk_container_child_set_property(GTK_CONTAINER(gobj()), GTK_WIDGET(label_widget->gobj()), "width", &width); + } + } +} + +void DialogPage::add_group_note(Glib::ustring name) +{ + if (name != "") + { + Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(Glib::ustring("<i>") + name + + Glib::ustring("</i>") , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true)); + label_widget->set_use_markup(true); + label_widget->set_valign(Gtk::ALIGN_CENTER); + label_widget->set_line_wrap(true); + label_widget->set_line_wrap_mode(Pango::WRAP_WORD); + + add(*label_widget); + GValue width = G_VALUE_INIT; + g_value_init(&width, G_TYPE_INT); + g_value_set_int(&width, 2); + gtk_container_child_set_property(GTK_CONTAINER(gobj()), GTK_WIDGET(label_widget->gobj()), "width", &width); + } +} + +void DialogPage::set_tip(Gtk::Widget& widget, Glib::ustring const &tip) +{ + widget.set_tooltip_text (tip); +} + +void PrefCheckButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path, + bool default_value) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->set_label(label); + this->set_active( prefs->getBool(_prefs_path, default_value) ); +} + +void PrefCheckButton::on_toggled() +{ + if (this->get_visible()) //only take action if the user toggled it + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool(_prefs_path, this->get_active()); + } + this->changed_signal.emit(this->get_active()); +} + +void PrefRadioButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path, + Glib::ustring const &string_value, bool default_value, PrefRadioButton* group_member) +{ + _prefs_path = prefs_path; + _value_type = VAL_STRING; + _string_value = string_value; + (void)default_value; + this->set_label(label); + if (group_member) + { + Gtk::RadioButtonGroup rbg = group_member->get_group(); + this->set_group(rbg); + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring val = prefs->getString(_prefs_path); + if ( !val.empty() ) + this->set_active(val == _string_value); + else + this->set_active( false ); +} + +void PrefRadioButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path, + int int_value, bool default_value, PrefRadioButton* group_member) +{ + _prefs_path = prefs_path; + _value_type = VAL_INT; + _int_value = int_value; + this->set_label(label); + if (group_member) + { + Gtk::RadioButtonGroup rbg = group_member->get_group(); + this->set_group(rbg); + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (default_value) + this->set_active( prefs->getInt(_prefs_path, int_value) == _int_value ); + else + this->set_active( prefs->getInt(_prefs_path, int_value + 1) == _int_value ); +} + +void PrefRadioButton::on_toggled() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (this->get_visible() && this->get_active() ) //only take action if toggled by user (to active) + { + if ( _value_type == VAL_STRING ) + prefs->setString(_prefs_path, _string_value); + else if ( _value_type == VAL_INT ) + prefs->setInt(_prefs_path, _int_value); + } + this->changed_signal.emit(this->get_active()); +} + + +PrefRadioButtons::PrefRadioButtons(const std::vector<PrefItem>& buttons, const Glib::ustring& prefs_path) { + set_spacing(2); + + PrefRadioButton* group = nullptr; + for (auto&& item : buttons) { + auto* btn = Gtk::make_managed<PrefRadioButton>(); + btn->init(item.label, prefs_path, item.int_value, item.is_default, group); + btn->set_tooltip_text(item.tooltip); + add(*btn); + if (!group) group = btn; + } +} + + +void PrefSpinButton::init(Glib::ustring const &prefs_path, + double lower, double upper, double step_increment, double /*page_increment*/, + double default_value, bool is_int, bool is_percent) +{ + _prefs_path = prefs_path; + _is_int = is_int; + _is_percent = is_percent; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double value; + if (is_int) { + if (is_percent) { + value = 100 * prefs->getDoubleLimited(prefs_path, default_value, lower/100.0, upper/100.0); + } else { + value = (double) prefs->getIntLimited(prefs_path, (int) default_value, (int) lower, (int) upper); + } + } else { + value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper); + } + + this->set_range (lower, upper); + this->set_increments (step_increment, 0); + this->set_value (value); + this->set_width_chars(6); + if (is_int) + this->set_digits(0); + else if (step_increment < 0.1) + this->set_digits(4); + else + this->set_digits(2); + +} + +void PrefSpinButton::on_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (this->get_visible()) //only take action if user changed value + { + if (_is_int) { + if (_is_percent) { + prefs->setDouble(_prefs_path, this->get_value()/100.0); + } else { + prefs->setInt(_prefs_path, (int) this->get_value()); + } + } else { + prefs->setDouble(_prefs_path, this->get_value()); + } + } + this->changed_signal.emit(this->get_value()); +} + +void PrefSpinUnit::init(Glib::ustring const &prefs_path, + double lower, double upper, double step_increment, + double default_value, UnitType unit_type, Glib::ustring const &default_unit) +{ + _prefs_path = prefs_path; + _is_percent = (unit_type == UNIT_TYPE_DIMENSIONLESS); + + resetUnitType(unit_type); + setUnit(default_unit); + setRange (lower, upper); /// @fixme this disregards changes of units + setIncrements (step_increment, 0); + if (step_increment < 0.1) { + setDigits(4); + } else { + setDigits(2); + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper); + Glib::ustring unitstr = prefs->getUnit(prefs_path); + if (unitstr.length() == 0) { + unitstr = default_unit; + // write the assumed unit to preferences: + prefs->setDoubleUnit(_prefs_path, value, unitstr); + } + setValue(value, unitstr); + + signal_value_changed().connect_notify(sigc::mem_fun(*this, &PrefSpinUnit::on_my_value_changed)); +} + +void PrefSpinUnit::on_my_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (getWidget()->get_visible()) //only take action if user changed value + { + prefs->setDoubleUnit(_prefs_path, getValue(getUnit()->abbr), getUnit()->abbr); + } +} + +const double ZoomCorrRuler::textsize = 7; +const double ZoomCorrRuler::textpadding = 5; + +ZoomCorrRuler::ZoomCorrRuler(int width, int height) : + _unitconv(1.0), + _border(5) +{ + set_size(width, height); +} + +void ZoomCorrRuler::set_size(int x, int y) +{ + _min_width = x; + _height = y; + set_size_request(x + _border*2, y + _border*2); +} + +// The following two functions are borrowed from 2geom's toy-framework-2; if they are useful in +// other locations, we should perhaps make them (or adapted versions of them) publicly available +static void +draw_text(cairo_t *cr, Geom::Point loc, const char* txt, bool bottom = false, + double fontsize = ZoomCorrRuler::textsize, std::string fontdesc = "Sans") { + PangoLayout* layout = pango_cairo_create_layout (cr); + pango_layout_set_text(layout, txt, -1); + + // set font and size + std::ostringstream sizestr; + sizestr << fontsize; + fontdesc = fontdesc + " " + sizestr.str(); + PangoFontDescription *font_desc = pango_font_description_from_string(fontdesc.c_str()); + pango_layout_set_font_description(layout, font_desc); + pango_font_description_free (font_desc); + + PangoRectangle logical_extent; + pango_layout_get_pixel_extents(layout, nullptr, &logical_extent); + cairo_move_to(cr, loc[Geom::X], loc[Geom::Y] - (bottom ? logical_extent.height : 0)); + pango_cairo_show_layout(cr, layout); +} + +static void +draw_number(cairo_t *cr, Geom::Point pos, double num) { + std::ostringstream number; + number << num; + draw_text(cr, pos, number.str().c_str(), true); +} + +/* + * \arg dist The distance between consecutive minor marks + * \arg major_interval Number of marks after which to draw a major mark + */ +void +ZoomCorrRuler::draw_marks(Cairo::RefPtr<Cairo::Context> cr, double dist, int major_interval) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + const double zoomcorr = prefs->getDouble("/options/zoomcorrection/value", 1.0); + double mark = 0; + int i = 0; + double step = dist * zoomcorr / _unitconv; + bool draw_minor = true; + if (step <= 0) { + return; + } + else if (step < 2) { + // marks too dense + draw_minor = false; + } + int last_pos = -1; + while (mark <= _drawing_width) { + cr->move_to(mark, _height); + if ((i % major_interval) == 0) { + // don't overcrowd the marks + if (static_cast<int>(mark) > last_pos) { + // major mark + cr->line_to(mark, 0); + Geom::Point textpos(mark + 3, ZoomCorrRuler::textsize + ZoomCorrRuler::textpadding); + draw_number(cr->cobj(), textpos, dist * i); + + last_pos = static_cast<int>(mark) + 1; + } + } else if (draw_minor) { + // minor mark + cr->line_to(mark, ZoomCorrRuler::textsize + 2 * ZoomCorrRuler::textpadding); + } + mark += step; + ++i; + } +} + +bool +ZoomCorrRuler::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) { + Glib::RefPtr<Gdk::Window> window = get_window(); + + int w = window->get_width(); + _drawing_width = w - _border * 2; + + auto context = get_style_context(); + Gdk::RGBA fg = context->get_color(get_state_flags()); + Gdk::RGBA bg; + bg.set_grey(0.5); + if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_toplevel())) { + auto sc = wnd->get_style_context(); + bg = get_background_color(sc); + } + + cr->set_source_rgb(bg.get_red(), bg.get_green(), bg.get_blue()); + cr->set_fill_rule(Cairo::FILL_RULE_WINDING); + cr->rectangle(0, 0, w, _height + _border*2); + cr->fill(); + + cr->set_source_rgb(0.0, 0.0, 0.0); + cr->set_line_width(0.5); + + cr->translate(_border, _border); // so that we have a small white border around the ruler + cr->move_to (0, _height); + cr->line_to (_drawing_width, _height); + + cr->set_source_rgb(fg.get_red(), fg.get_green(), fg.get_blue()); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring abbr = prefs->getString("/options/zoomcorrection/unit"); + if (abbr == "cm") { + draw_marks(cr, 0.1, 10); + } else if (abbr == "in") { + draw_marks(cr, 0.25, 4); + } else if (abbr == "mm") { + draw_marks(cr, 10, 10); + } else if (abbr == "pc") { + draw_marks(cr, 1, 10); + } else if (abbr == "pt") { + draw_marks(cr, 10, 10); + } else if (abbr == "px") { + draw_marks(cr, 10, 10); + } else { + draw_marks(cr, 1, 1); + } + cr->stroke(); + + return true; +} + + +void +ZoomCorrRulerSlider::on_slider_value_changed() +{ + if (this->get_visible() || freeze) //only take action if user changed value + { + freeze = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/options/zoomcorrection/value", _slider->get_value() / 100.0); + _sb->set_value(_slider->get_value()); + _ruler.queue_draw(); + freeze = false; + } +} + +void +ZoomCorrRulerSlider::on_spinbutton_value_changed() +{ + if (this->get_visible() || freeze) //only take action if user changed value + { + freeze = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/options/zoomcorrection/value", _sb->get_value() / 100.0); + _slider->set_value(_sb->get_value()); + _ruler.queue_draw(); + freeze = false; + } +} + +void +ZoomCorrRulerSlider::on_unit_changed() { + if (!_unit.get_sensitive()) { + // when the unit menu is initialized, the unit is set to the default but + // it needs to be reset later so we don't perform the change in this case + return; + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/options/zoomcorrection/unit", _unit.getUnitAbbr()); + double conv = _unit.getConversion(_unit.getUnitAbbr(), "px"); + _ruler.set_unit_conversion(conv); + if (_ruler.get_visible()) { + _ruler.queue_draw(); + } +} + +bool ZoomCorrRulerSlider::on_mnemonic_activate ( bool group_cycling ) +{ + return _sb->mnemonic_activate ( group_cycling ); +} + + +void +ZoomCorrRulerSlider::init(int ruler_width, int ruler_height, double lower, double upper, + double step_increment, double page_increment, double default_value) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double value = prefs->getDoubleLimited("/options/zoomcorrection/value", default_value, lower, upper) * 100.0; + + freeze = false; + + _ruler.set_size(ruler_width, ruler_height); + + _slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL)); + + _slider->set_size_request(_ruler.width(), -1); + _slider->set_range (lower, upper); + _slider->set_increments (step_increment, page_increment); + _slider->set_value (value); + _slider->set_digits(2); + + _slider->signal_value_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_slider_value_changed)); + _sb = Gtk::manage(new Inkscape::UI::Widget::SpinButton()); + _sb->signal_value_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_spinbutton_value_changed)); + _unit.signal_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_unit_changed)); + + _sb->set_range (lower, upper); + _sb->set_increments (step_increment, 0); + _sb->set_value (value); + _sb->set_digits(2); + _sb->set_halign(Gtk::ALIGN_CENTER); + _sb->set_valign(Gtk::ALIGN_END); + + _unit.set_sensitive(false); + _unit.setUnitType(UNIT_TYPE_LINEAR); + _unit.set_sensitive(true); + _unit.setUnit(prefs->getString("/options/zoomcorrection/unit")); + _unit.set_halign(Gtk::ALIGN_CENTER); + _unit.set_valign(Gtk::ALIGN_END); + + _slider->set_hexpand(true); + _ruler.set_hexpand(true); + auto table = Gtk::manage(new Gtk::Grid()); + table->attach(*_slider, 0, 0, 1, 1); + table->attach(*_sb, 1, 0, 1, 1); + table->attach(_ruler, 0, 1, 1, 1); + table->attach(_unit, 1, 1, 1, 1); + + pack_start(*table, Gtk::PACK_SHRINK); +} + +void +PrefSlider::on_slider_value_changed() +{ + if (this->get_visible() || freeze) //only take action if user changed value + { + freeze = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(_prefs_path, _slider->get_value()); + if (_sb) _sb->set_value(_slider->get_value()); + freeze = false; + } +} + +void +PrefSlider::on_spinbutton_value_changed() +{ + if (this->get_visible() || freeze) //only take action if user changed value + { + freeze = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (_sb) { + prefs->setDouble(_prefs_path, _sb->get_value()); + _slider->set_value(_sb->get_value()); + } + freeze = false; + } +} + +bool PrefSlider::on_mnemonic_activate ( bool group_cycling ) +{ + return _sb ? _sb->mnemonic_activate ( group_cycling ) : false; +} + +void +PrefSlider::init(Glib::ustring const &prefs_path, + double lower, double upper, double step_increment, double page_increment, double default_value, int digits) +{ + _prefs_path = prefs_path; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper); + + freeze = false; + + _slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL)); + + _slider->set_range (lower, upper); + _slider->set_increments (step_increment, page_increment); + _slider->set_value (value); + _slider->set_digits(digits); + _slider->signal_value_changed().connect(sigc::mem_fun(*this, &PrefSlider::on_slider_value_changed)); + if (_spin) { + _sb = Gtk::manage(new Inkscape::UI::Widget::SpinButton()); + _sb->signal_value_changed().connect(sigc::mem_fun(*this, &PrefSlider::on_spinbutton_value_changed)); + _sb->set_range (lower, upper); + _sb->set_increments (step_increment, 0); + _sb->set_value (value); + _sb->set_digits(digits); + _sb->set_halign(Gtk::ALIGN_CENTER); + _sb->set_valign(Gtk::ALIGN_END); + } + + auto table = Gtk::manage(new Gtk::Grid()); + _slider->set_hexpand(); + table->attach(*_slider, 0, 0, 1, 1); + if (_sb) table->attach(*_sb, 1, 0, 1, 1); + + this->pack_start(*table, Gtk::PACK_EXPAND_WIDGET); +} + +void PrefCombo::init(Glib::ustring const &prefs_path, + Glib::ustring labels[], int values[], int num_items, int default_value) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int row = 0; + int value = prefs->getInt(_prefs_path, default_value); + + for (int i = 0 ; i < num_items; ++i) + { + this->append(labels[i]); + _values.push_back(values[i]); + if (value == values[i]) + row = i; + } + this->set_active(row); +} + +void PrefCombo::init(Glib::ustring const &prefs_path, + Glib::ustring labels[], Glib::ustring values[], int num_items, Glib::ustring default_value) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int row = 0; + Glib::ustring value = prefs->getString(_prefs_path); + if(value.empty()) + { + value = default_value; + } + + for (int i = 0 ; i < num_items; ++i) + { + this->append(labels[i]); + _ustr_values.push_back(values[i]); + if (value == values[i]) + row = i; + } + this->set_active(row); +} + +void PrefCombo::init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<int> values, + int default_value) +{ + size_t labels_size = labels.size(); + size_t values_size = values.size(); + if (values_size != labels_size) { + std::cout << "PrefCombo::" + << "Different number of values/labels in " << prefs_path << std::endl; + return; + } + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int row = 0; + int value = prefs->getInt(_prefs_path, default_value); + + for (int i = 0; i < labels_size; ++i) { + this->append(labels[i]); + _values.push_back(values[i]); + if (value == values[i]) + row = i; + } + this->set_active(row); +} + +void PrefCombo::init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, + std::vector<Glib::ustring> values, Glib::ustring default_value) +{ + size_t labels_size = labels.size(); + size_t values_size = values.size(); + if (values_size != labels_size) { + std::cout << "PrefCombo::" + << "Different number of values/labels in " << prefs_path << std::endl; + return; + } + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int row = 0; + Glib::ustring value = prefs->getString(_prefs_path); + if (value.empty()) { + value = default_value; + } + + for (int i = 0; i < labels_size; ++i) { + this->append(labels[i]); + _ustr_values.push_back(values[i]); + if (value == values[i]) + row = i; + } + this->set_active(row); +} + +void PrefCombo::init(Glib::ustring const &prefs_path, + std::vector<std::pair<Glib::ustring, Glib::ustring>> labels_and_values, + Glib::ustring default_value) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring value = prefs->getString(_prefs_path); + if (value.empty()) { + value = default_value; + } + + int row = 0; + int i = 0; + for (auto entry : labels_and_values) { + this->append(entry.first); + _ustr_values.push_back(entry.second); + if (value == entry.second) { + row = i; + } + ++i; + } + this->set_active(row); +} + +void PrefCombo::on_changed() +{ + if (this->get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if(!_values.empty()) + { + prefs->setInt(_prefs_path, _values[this->get_active_row_number()]); + } + else + { + prefs->setString(_prefs_path, _ustr_values[this->get_active_row_number()]); + } + } +} + +void PrefEntryButtonHBox::init(Glib::ustring const &prefs_path, + bool visibility, Glib::ustring const &default_string) +{ + _prefs_path = prefs_path; + _default_string = default_string; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + relatedEntry = new Gtk::Entry(); + relatedButton = new Gtk::Button(_("Reset")); + relatedEntry->set_invisible_char('*'); + relatedEntry->set_visibility(visibility); + relatedEntry->set_text(prefs->getString(_prefs_path)); + this->pack_start(*relatedEntry); + this->pack_start(*relatedButton); + relatedButton->signal_clicked().connect( + sigc::mem_fun(*this, &PrefEntryButtonHBox::onRelatedButtonClickedCallback)); + relatedEntry->signal_changed().connect( + sigc::mem_fun(*this, &PrefEntryButtonHBox::onRelatedEntryChangedCallback)); +} + +void PrefEntryButtonHBox::onRelatedEntryChangedCallback() +{ + if (this->get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(_prefs_path, relatedEntry->get_text()); + } +} + +void PrefEntryButtonHBox::onRelatedButtonClickedCallback() +{ + if (this->get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(_prefs_path, _default_string); + relatedEntry->set_text(_default_string); + } +} + +bool PrefEntryButtonHBox::on_mnemonic_activate ( bool group_cycling ) +{ + return relatedEntry->mnemonic_activate ( group_cycling ); +} + +void PrefEntryFileButtonHBox::init(Glib::ustring const &prefs_path, + bool visibility) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + relatedEntry = new Gtk::Entry(); + relatedEntry->set_invisible_char('*'); + relatedEntry->set_visibility(visibility); + relatedEntry->set_text(prefs->getString(_prefs_path)); + + relatedButton = new Gtk::Button(); + Gtk::Box* pixlabel = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 3); + Gtk::Image *im = sp_get_icon_image("applications-graphics", Gtk::ICON_SIZE_BUTTON); + pixlabel->pack_start(*im); + Gtk::Label *l = new Gtk::Label(); + l->set_markup_with_mnemonic(_("_Browse...")); + pixlabel->pack_start(*l); + relatedButton->add(*pixlabel); + + this->pack_end(*relatedButton, false, false, 4); + this->pack_start(*relatedEntry, true, true, 0); + + relatedButton->signal_clicked().connect( + sigc::mem_fun(*this, &PrefEntryFileButtonHBox::onRelatedButtonClickedCallback)); + relatedEntry->signal_changed().connect( + sigc::mem_fun(*this, &PrefEntryFileButtonHBox::onRelatedEntryChangedCallback)); +} + +void PrefEntryFileButtonHBox::onRelatedEntryChangedCallback() +{ + if (this->get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(_prefs_path, relatedEntry->get_text()); + } +} + +static Inkscape::UI::Dialog::FileOpenDialog * selectPrefsFileInstance = nullptr; + +void PrefEntryFileButtonHBox::onRelatedButtonClickedCallback() +{ + if (this->get_visible()) //only take action if user changed value + { + //# 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 = ""; + +#ifdef _WIN32 + //# If no open path, default to our win32 documents folder + if (open_path.empty()) + { + // The path to the My Documents folder is read from the + // value "HKEY_CURRENT_USER\Software\Windows\CurrentVersion\Explorer\Shell Folders\Personal" + HKEY key = NULL; + if(RegOpenKeyExA(HKEY_CURRENT_USER, + "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", + 0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS) + { + WCHAR utf16path[_MAX_PATH]; + DWORD value_type; + DWORD data_size = sizeof(utf16path); + if(RegQueryValueExW(key, L"Personal", NULL, &value_type, + (BYTE*)utf16path, &data_size) == ERROR_SUCCESS) + { + g_assert(value_type == REG_SZ); + gchar *utf8path = g_utf16_to_utf8( + (const gunichar2*)utf16path, -1, NULL, NULL, NULL); + if(utf8path) + { + open_path = Glib::ustring(utf8path); + g_free(utf8path); + } + } + } + } +#endif + + //# 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::EXE_TYPES, + _("Select a bitmap editor")); + } + + //# Show the dialog + bool const success = selectPrefsFileInstance->show(); + + if (!success) { + return; + } + + //# User selected something. Get name and type + Glib::ustring fileName = selectPrefsFileInstance->getFilename(); + + if (!fileName.empty()) + { + Glib::ustring newFileName = Glib::filename_to_utf8(fileName); + + if ( newFileName.size() > 0) + open_path = newFileName; + else + g_warning( "ERROR CONVERTING OPEN FILENAME TO UTF-8" ); + + prefs->setString(_prefs_path, open_path); + } + + relatedEntry->set_text(fileName); + } +} + +bool PrefEntryFileButtonHBox::on_mnemonic_activate ( bool group_cycling ) +{ + return relatedEntry->mnemonic_activate ( group_cycling ); +} + +void PrefOpenFolder::init(Glib::ustring const &entry_string, Glib::ustring const &tooltip) +{ + relatedEntry = new Gtk::Entry(); + relatedButton = new Gtk::Button(); + Gtk::Box *pixlabel = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 3); + Gtk::Image *im = sp_get_icon_image("document-open", Gtk::ICON_SIZE_BUTTON); + pixlabel->pack_start(*im); + Gtk::Label *l = new Gtk::Label(); + l->set_markup_with_mnemonic(_("Open")); + pixlabel->pack_start(*l); + relatedButton->add(*pixlabel); + relatedButton->set_tooltip_text(tooltip); + relatedEntry->set_text(entry_string); + relatedEntry->set_sensitive(false); + this->pack_end(*relatedButton, false, false, 4); + this->pack_start(*relatedEntry, true, true, 0); + relatedButton->signal_clicked().connect(sigc::mem_fun(*this, &PrefOpenFolder::onRelatedButtonClickedCallback)); +} + +void PrefOpenFolder::onRelatedButtonClickedCallback() +{ + g_mkdir_with_parents(relatedEntry->get_text().c_str(), 0700); + // https://stackoverflow.com/questions/42442189/how-to-open-spawn-a-file-with-glib-gtkmm-in-windows +#ifdef _WIN32 + ShellExecute(NULL, "open", relatedEntry->get_text().c_str(), NULL, NULL, SW_SHOWDEFAULT); +#elif defined(__APPLE__) + std::vector<std::string> argv = { "open", relatedEntry->get_text().raw() }; + Glib::spawn_async("", argv, Glib::SpawnFlags::SPAWN_SEARCH_PATH); +#else + gchar *path = g_filename_to_uri(relatedEntry->get_text().c_str(), NULL, NULL); + std::vector<std::string> argv = { "xdg-open", path }; + Glib::spawn_async("", argv, Glib::SpawnFlags::SPAWN_SEARCH_PATH); + g_free(path); +#endif +} + +void PrefFileButton::init(Glib::ustring const &prefs_path) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + select_filename(Glib::filename_from_utf8(prefs->getString(_prefs_path))); + + signal_selection_changed().connect(sigc::mem_fun(*this, &PrefFileButton::onFileChanged)); +} + +void PrefFileButton::onFileChanged() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(_prefs_path, Glib::filename_to_utf8(get_filename())); +} + +void PrefEntry::init(Glib::ustring const &prefs_path, bool visibility) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->set_invisible_char('*'); + this->set_visibility(visibility); + this->set_text(prefs->getString(_prefs_path)); +} + +void PrefEntry::on_changed() +{ + if (this->get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(_prefs_path, this->get_text()); + } +} + +void PrefMultiEntry::init(Glib::ustring const &prefs_path, int height) +{ + // TODO: Figure out if there's a way to specify height in lines instead of px + // and how to obtain a reasonable default width if 'expand_widget' is not used + set_size_request(100, height); + set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + set_shadow_type(Gtk::SHADOW_IN); + + add(_text); + + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring value = prefs->getString(_prefs_path); + value = Glib::Regex::create("\\|")->replace_literal(value, 0, "\n", (Glib::RegexMatchFlags)0); + _text.get_buffer()->set_text(value); + _text.get_buffer()->signal_changed().connect(sigc::mem_fun(*this, &PrefMultiEntry::on_changed)); +} + +void PrefMultiEntry::on_changed() +{ + if (get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring value = _text.get_buffer()->get_text(); + value = Glib::Regex::create("\\n")->replace_literal(value, 0, "|", (Glib::RegexMatchFlags)0); + prefs->setString(_prefs_path, value); + } +} + +void PrefColorPicker::init(Glib::ustring const &label, Glib::ustring const &prefs_path, + guint32 default_rgba) +{ + _prefs_path = prefs_path; + _title = label; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->setRgba32( prefs->getInt(_prefs_path, (int)default_rgba) ); +} + +void PrefColorPicker::on_changed (guint32 rgba) +{ + if (this->get_visible()) //only take action if the user toggled it + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt(_prefs_path, (int) rgba); + } +} + +void PrefUnit::init(Glib::ustring const &prefs_path) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + setUnitType(UNIT_TYPE_LINEAR); + setUnit(prefs->getString(_prefs_path)); +} + +void PrefUnit::on_changed() +{ + if (this->get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(_prefs_path, getUnitAbbr()); + } +} + +} // namespace Widget +} // 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/widget/preferences-widget.h b/src/ui/widget/preferences-widget.h new file mode 100644 index 0000000..07fee61 --- /dev/null +++ b/src/ui/widget/preferences-widget.h @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Widgets for Inkscape Preferences dialog. + */ +/* + * Authors: + * Marco Scholten + * Bruno Dilly <bruno.dilly@gmail.com> + * + * Copyright (C) 2004, 2006, 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_INKSCAPE_PREFERENCES_H +#define INKSCAPE_UI_WIDGET_INKSCAPE_PREFERENCES_H + +#include <iostream> +#include <vector> + +#include <gtkmm/filechooserbutton.h> +#include "ui/widget/spinbutton.h" +#include <cstddef> +#include <sigc++/sigc++.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/textview.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/drawingarea.h> +#include <gtkmm/grid.h> + +#include "ui/widget/color-picker.h" +#include "ui/widget/unit-menu.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/scalar-unit.h" + +namespace Gtk { +class Scale; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class PrefCheckButton : public Gtk::CheckButton +{ +public: + void init(Glib::ustring const &label, Glib::ustring const &prefs_path, + bool default_value); + sigc::signal<void, bool> changed_signal; +protected: + Glib::ustring _prefs_path; + void on_toggled() override; +}; + +class PrefRadioButton : public Gtk::RadioButton +{ +public: + void init(Glib::ustring const &label, Glib::ustring const &prefs_path, + int int_value, bool default_value, PrefRadioButton* group_member); + void init(Glib::ustring const &label, Glib::ustring const &prefs_path, + Glib::ustring const &string_value, bool default_value, PrefRadioButton* group_member); + sigc::signal<void, bool> changed_signal; +protected: + Glib::ustring _prefs_path; + Glib::ustring _string_value; + int _value_type; + enum + { + VAL_INT, + VAL_STRING + }; + int _int_value; + void on_toggled() override; +}; + +struct PrefItem { Glib::ustring label; int int_value; Glib::ustring tooltip; bool is_default = false; }; + +class PrefRadioButtons : public Gtk::Box { +public: + PrefRadioButtons(const std::vector<PrefItem>& buttons, const Glib::ustring& prefs_path); + +private: +}; + +class PrefSpinButton : public SpinButton +{ +public: + void init(Glib::ustring const &prefs_path, + double lower, double upper, double step_increment, double page_increment, + double default_value, bool is_int, bool is_percent); + sigc::signal<void, double> changed_signal; +protected: + Glib::ustring _prefs_path; + bool _is_int; + bool _is_percent; + void on_value_changed() override; +}; + +class PrefSpinUnit : public ScalarUnit +{ +public: + PrefSpinUnit() : ScalarUnit("", "") {}; + + void init(Glib::ustring const &prefs_path, + double lower, double upper, double step_increment, + double default_value, + UnitType unit_type, Glib::ustring const &default_unit); +protected: + Glib::ustring _prefs_path; + bool _is_percent; + void on_my_value_changed(); +}; + +class ZoomCorrRuler : public Gtk::DrawingArea { +public: + ZoomCorrRuler(int width = 100, int height = 20); + void set_size(int x, int y); + void set_unit_conversion(double conv) { _unitconv = conv; } + + int width() { return _min_width + _border*2; } + + static const double textsize; + static const double textpadding; + +private: + bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override; + + void draw_marks(Cairo::RefPtr<Cairo::Context> cr, double dist, int major_interval); + + double _unitconv; + int _min_width; + int _height; + int _border; + int _drawing_width; +}; + +class ZoomCorrRulerSlider : public Gtk::Box +{ +public: + ZoomCorrRulerSlider() : Gtk::Box(Gtk::ORIENTATION_VERTICAL) {} + + void init(int ruler_width, int ruler_height, double lower, double upper, + double step_increment, double page_increment, double default_value); + +private: + void on_slider_value_changed(); + void on_spinbutton_value_changed(); + void on_unit_changed(); + bool on_mnemonic_activate( bool group_cycling ) override; + + Inkscape::UI::Widget::SpinButton *_sb; + UnitMenu _unit; + Gtk::Scale* _slider; + ZoomCorrRuler _ruler; + bool freeze; // used to block recursive updates of slider and spinbutton +}; + +class PrefSlider : public Gtk::Box +{ +public: + PrefSlider(bool spin = true) : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) { _spin = spin; } + + void init(Glib::ustring const &prefs_path, + double lower, double upper, double step_increment, double page_increment, double default_value, int digits); + + Gtk::Scale* getSlider() {return _slider;}; + Inkscape::UI::Widget::SpinButton * getSpinButton() {return _sb;}; +private: + void on_slider_value_changed(); + void on_spinbutton_value_changed(); + bool on_mnemonic_activate( bool group_cycling ) override; + + Glib::ustring _prefs_path; + Inkscape::UI::Widget::SpinButton *_sb = nullptr; + bool _spin; + Gtk::Scale* _slider = nullptr; + + bool freeze; // used to block recursive updates of slider and spinbutton +}; + + +class PrefCombo : public Gtk::ComboBoxText +{ +public: + void init(Glib::ustring const &prefs_path, + Glib::ustring labels[], int values[], int num_items, int default_value); + + /** + * Initialize a combo box. + * second form uses strings as key values. + */ + void init(Glib::ustring const &prefs_path, + Glib::ustring labels[], Glib::ustring values[], int num_items, Glib::ustring default_value); + /** + * Initialize a combo box. + * with vectors. + */ + void init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<int> values, + int default_value); + + void init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<Glib::ustring> values, + Glib::ustring default_value); + + /** + * Initialize a combo box with a vector of Glib::ustring pairs. + */ + void init(Glib::ustring const &prefs_path, + std::vector<std::pair<Glib::ustring, Glib::ustring>> labels_and_values, + Glib::ustring default_value); + + protected: + Glib::ustring _prefs_path; + std::vector<int> _values; + std::vector<Glib::ustring> _ustr_values; ///< string key values used optionally instead of numeric _values + void on_changed() override; +}; + +class PrefEntry : public Gtk::Entry +{ +public: + void init(Glib::ustring const &prefs_path, bool mask); +protected: + Glib::ustring _prefs_path; + void on_changed() override; +}; + +class PrefMultiEntry : public Gtk::ScrolledWindow +{ +public: + void init(Glib::ustring const &prefs_path, int height); +protected: + Glib::ustring _prefs_path; + Gtk::TextView _text; + void on_changed(); +}; + +class PrefEntryButtonHBox : public Gtk::Box +{ +public: + PrefEntryButtonHBox() : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {} + + void init(Glib::ustring const &prefs_path, + bool mask, Glib::ustring const &default_string); + +protected: + Glib::ustring _prefs_path; + Glib::ustring _default_string; + Gtk::Button *relatedButton; + Gtk::Entry *relatedEntry; + void onRelatedEntryChangedCallback(); + void onRelatedButtonClickedCallback(); + bool on_mnemonic_activate( bool group_cycling ) override; +}; + +class PrefEntryFileButtonHBox : public Gtk::Box +{ +public: + PrefEntryFileButtonHBox() : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {} + + void init(Glib::ustring const &prefs_path, + bool mask); +protected: + Glib::ustring _prefs_path; + Gtk::Button *relatedButton; + Gtk::Entry *relatedEntry; + void onRelatedEntryChangedCallback(); + void onRelatedButtonClickedCallback(); + bool on_mnemonic_activate( bool group_cycling ) override; +}; + +class PrefOpenFolder : public Gtk::Box { + public: + PrefOpenFolder() : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {} + + void init(Glib::ustring const &entry_string, Glib::ustring const &tooltip); + + protected: + Gtk::Button *relatedButton; + Gtk::Entry *relatedEntry; + void onRelatedButtonClickedCallback(); +}; + +class PrefFileButton : public Gtk::FileChooserButton +{ +public: + void init(Glib::ustring const &prefs_path); + +protected: + Glib::ustring _prefs_path; + void onFileChanged(); +}; + +class PrefColorPicker : public ColorPicker +{ +public: + PrefColorPicker() : ColorPicker("", "", 0, false) {}; + ~PrefColorPicker() override = default;; + + void init(Glib::ustring const &abel, Glib::ustring const &prefs_path, + guint32 default_rgba); + +protected: + Glib::ustring _prefs_path; + void on_changed (guint32 rgba) override; +}; + +class PrefUnit : public UnitMenu +{ +public: + void init(Glib::ustring const &prefs_path); +protected: + Glib::ustring _prefs_path; + void on_changed() override; +}; + +class DialogPage : public Gtk::Grid +{ +public: + DialogPage(); + void add_line(bool indent, Glib::ustring const &label, Gtk::Widget& widget, Glib::ustring const &suffix, Glib::ustring const &tip, bool expand = true, Gtk::Widget *other_widget = nullptr); + void add_group_header(Glib::ustring name, int columns = 1); + void add_group_note(Glib::ustring name); + void set_tip(Gtk::Widget &widget, Glib::ustring const &tip); +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_UI_WIDGET_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/widget/preview.cpp b/src/ui/widget/preview.cpp new file mode 100644 index 0000000..663d4b8 --- /dev/null +++ b/src/ui/widget/preview.cpp @@ -0,0 +1,511 @@ +// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Eek Preview Stuffs. + * + * The Initial Developer of the Original Code is + * Jon A. Cruz. + * Portions created by the Initial Developer are Copyright (C) 2005 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include <algorithm> +#include <gdkmm/general.h> +#include "preview.h" +#include "preferences.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +#define PRIME_BUTTON_MAGIC_NUMBER 1 + +/* Keep in sync with last value in eek-preview.h */ +#define PREVIEW_SIZE_LAST PREVIEW_SIZE_HUGE +#define PREVIEW_SIZE_NEXTFREE (PREVIEW_SIZE_HUGE + 1) + +#define PREVIEW_MAX_RATIO 500 + +void +Preview::set_color(int r, int g, int b ) +{ + _r = r; + _g = g; + _b = b; + + queue_draw(); +} + + +void +Preview::set_pixbuf(const Glib::RefPtr<Gdk::Pixbuf> &pixbuf) +{ + _previewPixbuf = pixbuf; + + queue_draw(); + + if (_scaled) + { + _scaled.reset(); + } + + _scaledW = _previewPixbuf->get_width(); + _scaledH = _previewPixbuf->get_height(); +} + +static gboolean setupDone = FALSE; +static GtkRequisition sizeThings[PREVIEW_SIZE_NEXTFREE]; + +void +Preview::set_size_mappings( guint count, GtkIconSize const* sizes ) +{ + gint width = 0; + gint height = 0; + gint smallest = 512; + gint largest = 0; + guint i = 0; + guint delta = 0; + + for ( i = 0; i < count; ++i ) { + gboolean worked = gtk_icon_size_lookup( sizes[i], &width, &height ); + if ( worked ) { + if ( width < smallest ) { + smallest = width; + } + if ( width > largest ) { + largest = width; + } + } + } + + smallest = (smallest * 3) / 4; + + delta = largest - smallest; + + for ( i = 0; i < G_N_ELEMENTS(sizeThings); ++i ) { + guint val = smallest + ( (i * delta) / (G_N_ELEMENTS(sizeThings) - 1) ); + sizeThings[i].width = val; + sizeThings[i].height = val; + } + + setupDone = TRUE; +} + +void Preview::set_freesize(bool en) { + _freesize = en; +} + +void +Preview::size_request(GtkRequisition* req) const +{ + if (_freesize) { + req->width = req->height = 1; + return; + } + + int width = 0; + int height = 0; + + if ( !setupDone ) { + GtkIconSize sizes[] = { + GTK_ICON_SIZE_MENU, + GTK_ICON_SIZE_SMALL_TOOLBAR, + GTK_ICON_SIZE_LARGE_TOOLBAR, + GTK_ICON_SIZE_BUTTON, + GTK_ICON_SIZE_DIALOG + }; + set_size_mappings( G_N_ELEMENTS(sizes), sizes ); + } + + width = sizeThings[_size].width; + height = sizeThings[_size].height; + + if ( _view == VIEW_TYPE_LIST ) { + width *= 3; + } + + if ( _ratio != 100 ) { + width = (width * _ratio) / 100; + if ( width < 0 ) { + width = 1; + } + } + + req->width = width; + req->height = height; +} + +void +Preview::get_preferred_width_vfunc(int &minimal_width, int &natural_width) const +{ + GtkRequisition requisition; + size_request(&requisition); + minimal_width = natural_width = requisition.width; +} + +void +Preview::get_preferred_height_vfunc(int &minimal_height, int &natural_height) const +{ + GtkRequisition requisition; + size_request(&requisition); + minimal_height = natural_height = requisition.height; +} + +bool +Preview::on_draw(const Cairo::RefPtr<Cairo::Context> &cr) +{ + auto allocation = get_allocation(); + + gint insetTop = 0, insetBottom = 0; + gint insetLeft = 0, insetRight = 0; + + if (_border == BORDER_SOLID) { + insetTop = 1; + insetLeft = 1; + } + if (_border == BORDER_SOLID_LAST_ROW) { + insetTop = insetBottom = 1; + insetLeft = 1; + } + if (_border == BORDER_WIDE) { + insetTop = insetBottom = 1; + insetLeft = insetRight = 1; + } + + auto context = get_style_context(); + + context->render_frame(cr, + 0, 0, + allocation.get_width(), allocation.get_height()); + + context->render_background(cr, + 0, 0, + allocation.get_width(), allocation.get_height()); + + // Border + if (_border != BORDER_NONE) { + cr->set_source_rgb(0.0, 0.0, 0.0); + cr->rectangle(0, 0, allocation.get_width(), allocation.get_height()); + cr->fill(); + } + + cr->set_source_rgb(_r/65535.0, _g/65535.0, _b/65535.0 ); + cr->rectangle(insetLeft, insetTop, allocation.get_width() - (insetLeft + insetRight), allocation.get_height() - (insetTop + insetBottom)); + cr->fill(); + + if (_previewPixbuf ) + { + if ((allocation.get_width() != _scaledW) || (allocation.get_height() != _scaledH)) { + if (_scaled) + { + _scaled.reset(); + } + + _scaledW = allocation.get_width() - (insetLeft + insetRight); + _scaledH = allocation.get_height() - (insetTop + insetBottom); + + _scaled = _previewPixbuf->scale_simple(_scaledW, + _scaledH, + Gdk::INTERP_BILINEAR); + } + + Glib::RefPtr<Gdk::Pixbuf> pix = (_scaled) ? _scaled : _previewPixbuf; + + // Border + if (_border != BORDER_NONE) { + cr->set_source_rgb(0.0, 0.0, 0.0); + cr->rectangle(0, 0, allocation.get_width(), allocation.get_height()); + cr->fill(); + } + + Gdk::Cairo::set_source_pixbuf(cr, pix, insetLeft, insetTop); + cr->paint(); + } + + if (_linked) + { + /* Draw arrow */ + GdkRectangle possible = {insetLeft, + insetTop, + (allocation.get_width() - (insetLeft + insetRight)), + (allocation.get_height() - (insetTop + insetBottom)) + }; + + GdkRectangle area = {possible.x, + possible.y, + possible.width / 2, + possible.height / 2 }; + + /* Make it square */ + if ( area.width > area.height ) + area.width = area.height; + if ( area.height > area.width ) + area.height = area.width; + + /* Center it horizontally */ + if ( area.width < possible.width ) { + int diff = (possible.width - area.width) / 2; + area.x += diff; + } + + if (_linked & PREVIEW_LINK_IN) + { + context->render_arrow(cr, + G_PI, // Down-pointing arrow + area.x, area.y, + std::min(area.width, area.height) + ); + } + + if (_linked & PREVIEW_LINK_OUT) + { + GdkRectangle otherArea = {area.x, area.y, area.width, area.height}; + if ( otherArea.height < possible.height ) { + otherArea.y = possible.y + (possible.height - otherArea.height); + } + + context->render_arrow(cr, + G_PI, // Down-pointing arrow + otherArea.x, otherArea.y, + std::min(otherArea.width, otherArea.height) + ); + } + + if (_linked & PREVIEW_LINK_OTHER) + { + GdkRectangle otherArea = {insetLeft, area.y, area.width, area.height}; + if ( otherArea.height < possible.height ) { + otherArea.y = possible.y + (possible.height - otherArea.height) / 2; + } + + context->render_arrow(cr, + 1.5*G_PI, // Left-pointing arrow + otherArea.x, otherArea.y, + std::min(otherArea.width, otherArea.height) + ); + } + + + if (_linked & PREVIEW_FILL) + { + GdkRectangle otherArea = {possible.x + ((possible.width / 4) - (area.width / 2)), + area.y, + area.width, area.height}; + if ( otherArea.height < possible.height ) { + otherArea.y = possible.y + (possible.height - otherArea.height) / 2; + } + context->render_check(cr, + otherArea.x, otherArea.y, + otherArea.width, otherArea.height ); + } + + if (_linked & PREVIEW_STROKE) + { + GdkRectangle otherArea = {possible.x + (((possible.width * 3) / 4) - (area.width / 2)), + area.y, + area.width, area.height}; + if ( otherArea.height < possible.height ) { + otherArea.y = possible.y + (possible.height - otherArea.height) / 2; + } + // This should be a diamond too? + context->render_check(cr, + otherArea.x, otherArea.y, + otherArea.width, otherArea.height ); + } + } + + + if ( has_focus() ) { + allocation = get_allocation(); + + context->render_focus(cr, + 0 + 1, 0 + 1, + allocation.get_width() - 2, allocation.get_height() - 2 ); + } + + return false; +} + + +bool +Preview::on_enter_notify_event(GdkEventCrossing* event ) +{ + _within = true; + set_state_flags(_hot ? Gtk::STATE_FLAG_ACTIVE : Gtk::STATE_FLAG_PRELIGHT, false); + + return false; +} + +bool +Preview::on_leave_notify_event(GdkEventCrossing* event) +{ + _within = false; + set_state_flags(Gtk::STATE_FLAG_NORMAL, false); + + return false; +} + +bool +Preview::on_button_press_event(GdkEventButton *event) +{ + if (_takesFocus && !has_focus() ) + { + grab_focus(); + } + + if ( event->button == PRIME_BUTTON_MAGIC_NUMBER || + event->button == 2 ) + { + _hot = true; + + if ( _within ) + { + set_state_flags(Gtk::STATE_FLAG_ACTIVE, false); + } + } + + return false; +} + +bool +Preview::on_button_release_event(GdkEventButton* event) +{ + _hot = false; + set_state_flags(Gtk::STATE_FLAG_NORMAL, false); + + if (_within && + (event->button == PRIME_BUTTON_MAGIC_NUMBER || + event->button == 2)) + { + gboolean isAlt = ( ((event->state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK) || + (event->button == 2)); + + if ( isAlt ) + { + _signal_alt_clicked(2); + } + else + { + _signal_clicked.emit(); + } + } + + return false; +} + +void +Preview::set_linked(LinkType link) +{ + link = (LinkType)(link & PREVIEW_LINK_ALL); + + if (link != _linked) + { + _linked = link; + + queue_draw(); + } +} + +LinkType +Preview::get_linked() const +{ + return (LinkType)_linked; +} + +void +Preview::set_details(ViewType view, + PreviewSize size, + guint ratio, + guint border) +{ + _view = view; + + if ( size > PREVIEW_SIZE_LAST ) + { + size = PREVIEW_SIZE_LAST; + } + + _size = size; + + if ( ratio > PREVIEW_MAX_RATIO ) + { + ratio = PREVIEW_MAX_RATIO; + } + + _ratio = ratio; + _border = border; + + queue_draw(); +} + +Preview::Preview() + : _r(0x80), + _g(0x80), + _b(0xcc), + _scaledW(0), + _scaledH(0), + _hot(false), + _within(false), + _takesFocus(false), + _view(VIEW_TYPE_LIST), + _size(PREVIEW_SIZE_SMALL), + _ratio(100), + _border(BORDER_NONE), + _previewPixbuf(nullptr), + _scaled(nullptr), + _linked(PREVIEW_LINK_NONE) +{ + set_can_focus(true); + set_receives_default(true); + + set_sensitive(true); + + add_events(Gdk::BUTTON_PRESS_MASK + |Gdk::BUTTON_RELEASE_MASK + |Gdk::KEY_PRESS_MASK + |Gdk::KEY_RELEASE_MASK + |Gdk::FOCUS_CHANGE_MASK + |Gdk::ENTER_NOTIFY_MASK + |Gdk::LEAVE_NOTIFY_MASK ); +} + +} // namespace Widget +} // 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/widget/preview.h b/src/ui/widget/preview.h new file mode 100644 index 0000000..d352a0d --- /dev/null +++ b/src/ui/widget/preview.h @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Eek Preview Stuffs. + * + * The Initial Developer of the Original Code is + * Jon A. Cruz. + * Portions created by the Initial Developer are Copyright (C) 2005-2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#ifndef SEEN_EEK_PREVIEW_H +#define SEEN_EEK_PREVIEW_H + +#include <gtkmm/drawingarea.h> + +/** + * @file + * Generic implementation of an object that can be shown by a preview. + */ + +namespace Inkscape { +namespace UI { +namespace Widget { + +enum PreviewStyle { + PREVIEW_STYLE_ICON = 0, + PREVIEW_STYLE_PREVIEW, + PREVIEW_STYLE_NAME, + PREVIEW_STYLE_BLURB, + PREVIEW_STYLE_ICON_NAME, + PREVIEW_STYLE_ICON_BLURB, + PREVIEW_STYLE_PREVIEW_NAME, + PREVIEW_STYLE_PREVIEW_BLURB +}; + +enum ViewType { + VIEW_TYPE_LIST = 0, + VIEW_TYPE_GRID +}; + +enum PreviewSize { + PREVIEW_SIZE_TINY = 0, + PREVIEW_SIZE_SMALL, + PREVIEW_SIZE_MEDIUM, + PREVIEW_SIZE_BIG, + PREVIEW_SIZE_BIGGER, + PREVIEW_SIZE_HUGE +}; + +enum LinkType { + PREVIEW_LINK_NONE = 0, + PREVIEW_LINK_IN = 1, + PREVIEW_LINK_OUT = 2, + PREVIEW_LINK_OTHER = 4, + PREVIEW_FILL = 8, + PREVIEW_STROKE = 16, + PREVIEW_LINK_ALL = 31 +}; + +enum BorderStyle { + BORDER_NONE = 0, + BORDER_SOLID, + BORDER_WIDE, + BORDER_SOLID_LAST_ROW, +}; + +class Preview : public Gtk::DrawingArea { +private: + int _scaledW; + int _scaledH; + + int _r; + int _g; + int _b; + + bool _hot; + bool _within; + bool _takesFocus; ///< flag to grab focus when clicked + ViewType _view; + PreviewSize _size; + unsigned int _ratio; + LinkType _linked; + unsigned int _border; + bool _freesize = false; + + Glib::RefPtr<Gdk::Pixbuf> _previewPixbuf; + Glib::RefPtr<Gdk::Pixbuf> _scaled; + + // signals + sigc::signal<void> _signal_clicked; + sigc::signal<void, int> _signal_alt_clicked; + + void size_request(GtkRequisition *req) const; + +protected: + void get_preferred_width_vfunc(int &minimal_width, int &natural_width) const override; + void get_preferred_height_vfunc(int &minimal_height, int &natural_height) const override; + bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override; + bool on_button_press_event(GdkEventButton *button_event) override; + bool on_button_release_event(GdkEventButton *button_event) override; + bool on_enter_notify_event(GdkEventCrossing* event ) override; + bool on_leave_notify_event(GdkEventCrossing* event ) override; + +public: + Preview(); + bool get_focus_on_click() const {return _takesFocus;} + void set_focus_on_click(bool focus_on_click) {_takesFocus = focus_on_click;} + LinkType get_linked() const; + void set_linked(LinkType link); + void set_details(ViewType view, + PreviewSize size, + guint ratio, + guint border); + void set_color(int r, int g, int b); + void set_pixbuf(const Glib::RefPtr<Gdk::Pixbuf> &pixbuf); + void set_freesize(bool enable); + static void set_size_mappings(guint count, GtkIconSize const* sizes); + + decltype(_signal_clicked) signal_clicked() {return _signal_clicked;} + decltype(_signal_alt_clicked) signal_alt_clicked() {return _signal_alt_clicked;} +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif /* SEEN_EEK_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/widget/random.cpp b/src/ui/widget/random.cpp new file mode 100644 index 0000000..495a778 --- /dev/null +++ b/src/ui/widget/random.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Carl Hetherington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "random.h" +#include "ui/icon-loader.h" +#include <glibmm/i18n.h> + +#include <gtkmm/button.h> +#include <gtkmm/image.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Scalar(label, tooltip, suffix, icon, mnemonic) +{ + startseed = 0; + addReseedButton(); +} + +Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip, + unsigned digits, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Scalar(label, tooltip, digits, suffix, icon, mnemonic) +{ + startseed = 0; + addReseedButton(); +} + +Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::RefPtr<Gtk::Adjustment> &adjust, + unsigned digits, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Scalar(label, tooltip, adjust, digits, suffix, icon, mnemonic) +{ + startseed = 0; + addReseedButton(); +} + +long Random::getStartSeed() const +{ + return startseed; +} + +void Random::setStartSeed(long newseed) +{ + startseed = newseed; +} + +void Random::addReseedButton() +{ + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("randomize", Gtk::ICON_SIZE_BUTTON)); + 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, &Random::onReseedButtonClick)); + pButton->set_tooltip_text(_("Reseed the random number generator; this creates a different sequence of random numbers.")); + + pack_start(*pButton, Gtk::PACK_SHRINK, 0); +} + +void +Random::onReseedButtonClick() +{ + startseed = g_random_int(); + signal_reseeded.emit(); +} + +} // namespace Widget +} // 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/widget/random.h b/src/ui/widget/random.h new file mode 100644 index 0000000..2648cb2 --- /dev/null +++ b/src/ui/widget/random.h @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * 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_WIDGET_RANDOM_H +#define INKSCAPE_UI_WIDGET_RANDOM_H + +#include "scalar.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A labelled text box, with spin buttons and optional + * icon or suffix, for entering arbitrary number values. It adds an extra + * number called "startseed", that is not UI edittable, but should be put in SVG. + * This does NOT generate a random number, but provides merely the saving of + * the startseed value. + */ +class Random : public Scalar +{ +public: + + /** + * Construct a Random scalar Widget. + * + * @param label Label. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Random(Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Construct a Random Scalar Widget. + * + * @param label Label. + * @param digits Number of decimal digits to display. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Random(Glib::ustring const &label, + Glib::ustring const &tooltip, + unsigned digits, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Construct a Random Scalar Widget. + * + * @param label Label. + * @param adjust Adjustment to use for the SpinButton. + * @param digits Number of decimal digits to display (defaults to 0). + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to true). + */ + Random(Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::RefPtr<Gtk::Adjustment> &adjust, + unsigned digits = 0, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Gets the startseed. + */ + long getStartSeed() const; + + /** + * Sets the startseed number. + */ + void setStartSeed(long newseed); + + sigc::signal <void> signal_reseeded; + +protected: + long startseed; + +private: + + /** + * Add reseed button to the widget. + */ + void addReseedButton(); + + void onReseedButtonClick(); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_RANDOM_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/widget/registered-enums.h b/src/ui/widget/registered-enums.h new file mode 100644 index 0000000..b0cc199 --- /dev/null +++ b/src/ui/widget/registered-enums.h @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_REGISTERED_ENUMS_H +#define INKSCAPE_UI_WIDGET_REGISTERED_ENUMS_H + +#include "ui/widget/combo-enums.h" +#include "ui/widget/registered-widget.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Simplified management of enumerations in the UI as combobox. + */ +template<typename E> class RegisteredEnum : public RegisteredWidget< LabelledComboBoxEnum<E> > +{ +public: + ~RegisteredEnum() override { + _changed_connection.disconnect(); + } + + RegisteredEnum ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + const Util::EnumDataConverter<E>& c, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr, + bool sorted = true ) + : RegisteredWidget< LabelledComboBoxEnum<E> >(label, tip, c, (const Glib::ustring &)"", (const Glib::ustring &)"", true, sorted) + { + RegisteredWidget< LabelledComboBoxEnum<E> >::init_parent(key, wr, repr_in, doc_in); + _changed_connection = combobox()->signal_changed().connect (sigc::mem_fun (*this, &RegisteredEnum::on_changed)); + } + + void set_active_by_id (E id) { + combobox()->set_active_by_id(id); + }; + + void set_active_by_key (const Glib::ustring& key) { + combobox()->set_active_by_key(key); + } + + inline const Util::EnumData<E>* get_active_data() { + combobox()->get_active_data(); + } + + ComboBoxEnum<E> * combobox() { + return LabelledComboBoxEnum<E>::getCombobox(); + } + + sigc::connection _changed_connection; + +protected: + void on_changed() { + if (combobox()->setProgrammatically) { + combobox()->setProgrammatically = false; + return; + } + + if (RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->isUpdating()) + return; + + RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->setUpdating (true); + + const Util::EnumData<E>* data = combobox()->get_active_data(); + if (data) { + RegisteredWidget< LabelledComboBoxEnum<E> >::write_to_xml(data->key.c_str()); + } + + RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->setUpdating (false); + } +}; + +} +} +} + +#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/widget/registered-widget.cpp b/src/ui/widget/registered-widget.cpp new file mode 100644 index 0000000..aaea668 --- /dev/null +++ b/src/ui/widget/registered-widget.cpp @@ -0,0 +1,843 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * 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) + * Abhishek Sharma + * + * Copyright (C) 2000 - 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "registered-widget.h" + +#include <gtkmm/radiobutton.h> + +#include "object/sp-root.h" + +#include "svg/svg-color.h" +#include "svg/stringstream.h" + +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/*######################################### + * Registered CHECKBUTTON + */ + +RegisteredCheckButton::~RegisteredCheckButton() +{ + _toggled_connection.disconnect(); +} + +RegisteredCheckButton::RegisteredCheckButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right, Inkscape::XML::Node* repr_in, SPDocument *doc_in, char const *active_str, char const *inactive_str) + : RegisteredWidget<Gtk::CheckButton>() + , _active_str(active_str) + , _inactive_str(inactive_str) +{ + init_parent(key, wr, repr_in, doc_in); + + setProgrammatically = false; + + set_tooltip_text (tip); + Gtk::Label *l = new Gtk::Label(); + l->set_markup(label); + l->set_use_underline (true); + add (*manage (l)); + + if(right) set_halign(Gtk::ALIGN_END); + else set_halign(Gtk::ALIGN_START); + + set_valign(Gtk::ALIGN_CENTER); + _toggled_connection = signal_toggled().connect (sigc::mem_fun (*this, &RegisteredCheckButton::on_toggled)); +} + +void +RegisteredCheckButton::setActive (bool b) +{ + setProgrammatically = true; + set_active (b); + //The slave button is greyed out if the master button is unchecked + for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) { + (*i)->set_sensitive(b); + } + setProgrammatically = false; +} + +void +RegisteredCheckButton::on_toggled() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) + return; + _wr->setUpdating (true); + + write_to_xml(get_active() ? _active_str : _inactive_str); + //The slave button is greyed out if the master button is unchecked + for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) { + (*i)->set_sensitive(get_active()); + } + + _wr->setUpdating (false); +} + +/*######################################### + * Registered TOGGLEBUTTON + */ + +RegisteredToggleButton::~RegisteredToggleButton() +{ + _toggled_connection.disconnect(); +} + +RegisteredToggleButton::RegisteredToggleButton (const Glib::ustring& /*label*/, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right, Inkscape::XML::Node* repr_in, SPDocument *doc_in, char const *icon_active, char const *icon_inactive) + : RegisteredWidget<Gtk::ToggleButton>() +{ + init_parent(key, wr, repr_in, doc_in); + setProgrammatically = false; + set_tooltip_text (tip); + + if(right) set_halign(Gtk::ALIGN_END); + else set_halign(Gtk::ALIGN_START); + + set_valign(Gtk::ALIGN_CENTER); + _toggled_connection = signal_toggled().connect (sigc::mem_fun (*this, &RegisteredToggleButton::on_toggled)); +} + +void +RegisteredToggleButton::setActive (bool b) +{ + setProgrammatically = true; + set_active (b); + //The slave button is greyed out if the master button is untoggled + for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) { + (*i)->set_sensitive(b); + } + setProgrammatically = false; +} + +void +RegisteredToggleButton::on_toggled() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) + return; + _wr->setUpdating (true); + + write_to_xml(get_active() ? "true" : "false"); + //The slave button is greyed out if the master button is untoggled + for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) { + (*i)->set_sensitive(get_active()); + } + + _wr->setUpdating (false); +} + +/*######################################### + * Registered UNITMENU + */ + +RegisteredUnitMenu::~RegisteredUnitMenu() +{ + _changed_connection.disconnect(); +} + +RegisteredUnitMenu::RegisteredUnitMenu (const Glib::ustring& label, const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in) + : RegisteredWidget<Labelled> (label, "" /*tooltip*/, new UnitMenu()) +{ + init_parent(key, wr, repr_in, doc_in); + + getUnitMenu()->setUnitType (UNIT_TYPE_LINEAR); + _changed_connection = getUnitMenu()->signal_changed().connect (sigc::mem_fun (*this, &RegisteredUnitMenu::on_changed)); +} + +void +RegisteredUnitMenu::setUnit (Glib::ustring unit) +{ + getUnitMenu()->setUnit(unit); +} + +void +RegisteredUnitMenu::on_changed() +{ + if (_wr->isUpdating()) + return; + + Inkscape::SVGOStringStream os; + os << getUnitMenu()->getUnitAbbr(); + + _wr->setUpdating (true); + + write_to_xml(os.str().c_str()); + + _wr->setUpdating (false); +} + + +/*######################################### + * Registered SCALARUNIT + */ + +RegisteredScalarUnit::~RegisteredScalarUnit() +{ + _value_changed_connection.disconnect(); +} + +RegisteredScalarUnit::RegisteredScalarUnit (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, const RegisteredUnitMenu &rum, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in, RSU_UserUnits user_units) + : RegisteredWidget<ScalarUnit>(label, tip, UNIT_TYPE_LINEAR, "", "", rum.getUnitMenu()), + _um(nullptr) +{ + init_parent(key, wr, repr_in, doc_in); + + setProgrammatically = false; + + initScalar (-1e6, 1e6); + setUnit (rum.getUnitMenu()->getUnitAbbr()); + setDigits (2); + _um = rum.getUnitMenu(); + _user_units = user_units; + _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredScalarUnit::on_value_changed)); +} + + +void +RegisteredScalarUnit::on_value_changed() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + Inkscape::SVGOStringStream os; + if (_user_units != RSU_none) { + // Output length in 'user units', taking into account scale in 'x' or 'y'. + double scale = 1.0; + if (doc) { + SPRoot *root = doc->getRoot(); + if (root->viewBox_set) { + // check to see if scaling is uniform + if(Geom::are_near((root->viewBox.width() * root->height.computed) / (root->width.computed * root->viewBox.height()), 1.0, Geom::EPSILON)) { + scale = (root->viewBox.width() / root->width.computed + root->viewBox.height() / root->height.computed)/2.0; + } else if (_user_units == RSU_x) { + scale = root->viewBox.width() / root->width.computed; + } else { + scale = root->viewBox.height() / root->height.computed; + } + } + } + os << getValue("px") * scale; + } else { + // Output using unit identifiers. + os << getValue(""); + if (_um) + os << _um->getUnitAbbr(); + } + + write_to_xml(os.str().c_str()); + _wr->setUpdating (false); +} + + +/*######################################### + * Registered SCALAR + */ + +RegisteredScalar::~RegisteredScalar() +{ + _value_changed_connection.disconnect(); +} + +RegisteredScalar::RegisteredScalar ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument * doc_in ) + : RegisteredWidget<Scalar>(label, tip) +{ + init_parent(key, wr, repr_in, doc_in); + + setProgrammatically = false; + setRange (-1e6, 1e6); + setDigits (2); + setIncrements(0.1, 1.0); + _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredScalar::on_value_changed)); +} + +void +RegisteredScalar::on_value_changed() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + if (_wr->isUpdating()) { + return; + } + _wr->setUpdating (true); + + Inkscape::SVGOStringStream os; + //Force exact 0 if decimals over to 6 + double val = getValue() < 1e-6 && getValue() > -1e-6?0.0:getValue(); + os << val; + //TODO: Test is ok remove this sensitives + //also removed in registered text and in registered random + //set_sensitive(false); + write_to_xml(os.str().c_str()); + //set_sensitive(true); + _wr->setUpdating (false); +} + + +/*######################################### + * Registered TEXT + */ + +RegisteredText::~RegisteredText() +{ + _activate_connection.disconnect(); +} + +RegisteredText::RegisteredText ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument * doc_in ) + : RegisteredWidget<Text>(label, tip) +{ + init_parent(key, wr, repr_in, doc_in); + + setProgrammatically = false; + _activate_connection = signal_activate().connect (sigc::mem_fun (*this, &RegisteredText::on_activate)); +} + +void +RegisteredText::on_activate() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) { + return; + } + _wr->setUpdating (true); + Glib::ustring str(getText()); + Inkscape::SVGOStringStream os; + os << str; + write_to_xml(os.str().c_str()); + _wr->setUpdating (false); +} + + +/*######################################### + * Registered COLORPICKER + */ + +RegisteredColorPicker::RegisteredColorPicker(const Glib::ustring& label, + const Glib::ustring& title, + const Glib::ustring& tip, + const Glib::ustring& ckey, + const Glib::ustring& akey, + Registry& wr, + Inkscape::XML::Node* repr_in, + SPDocument *doc_in) + : RegisteredWidget<LabelledColorPicker> (label, title, tip, 0, true) +{ + init_parent("", wr, repr_in, doc_in); + + _ckey = ckey; + _akey = akey; + _changed_connection = connectChanged (sigc::mem_fun (*this, &RegisteredColorPicker::on_changed)); +} + +RegisteredColorPicker::~RegisteredColorPicker() +{ + _changed_connection.disconnect(); +} + +void +RegisteredColorPicker::setRgba32 (guint32 rgba) +{ + LabelledColorPicker::setRgba32 (rgba); +} + +void +RegisteredColorPicker::closeWindow() +{ + LabelledColorPicker::closeWindow(); +} + +void +RegisteredColorPicker::on_changed (guint32 rgba) +{ + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + // Use local repr here. When repr is specified, use that one, but + // if repr==NULL, get the repr of namedview of active desktop. + Inkscape::XML::Node *local_repr = repr; + SPDocument *local_doc = doc; + if (!local_repr) { + SPDesktop *dt = _wr->desktop(); + if (!dt) { + _wr->setUpdating(false); + return; + } + local_repr = dt->getNamedView()->getRepr(); + local_doc = dt->getDocument(); + } + gchar c[32]; + if (_akey == _ckey + "_opacity_LPE") { //For LPE parameter we want stored with alpha + sprintf(c, "#%08x", rgba); + } else { + sp_svg_write_color(c, sizeof(c), rgba); + } + bool saved = DocumentUndo::getUndoSensitive(local_doc); + DocumentUndo::setUndoSensitive(local_doc, false); + local_repr->setAttribute(_ckey, c); + local_repr->setAttributeCssDouble(_akey.c_str(), (rgba & 0xff) / 255.0); + DocumentUndo::setUndoSensitive(local_doc, saved); + + local_doc->setModifiedSinceSave(); + DocumentUndo::done(local_doc, "registered-widget.cpp: RegisteredColorPicker::on_changed", ""); // TODO Fix description. + + _wr->setUpdating (false); +} + + +/*######################################### + * Registered SUFFIXEDINTEGER + */ + +RegisteredSuffixedInteger::~RegisteredSuffixedInteger() +{ + _changed_connection.disconnect(); +} + +RegisteredSuffixedInteger::RegisteredSuffixedInteger (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& suffix, const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in) + : RegisteredWidget<Scalar>(label, tip, 0, suffix), + setProgrammatically(false) +{ + init_parent(key, wr, repr_in, doc_in); + + setRange (0, 1e6); + setDigits (0); + setIncrements(1, 10); + + _changed_connection = signal_value_changed().connect (sigc::mem_fun(*this, &RegisteredSuffixedInteger::on_value_changed)); +} + +void +RegisteredSuffixedInteger::on_value_changed() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + Inkscape::SVGOStringStream os; + os << getValue(); + + write_to_xml(os.str().c_str()); + + _wr->setUpdating (false); +} + + +/*######################################### + * Registered RADIOBUTTONPAIR + */ + +RegisteredRadioButtonPair::~RegisteredRadioButtonPair() +{ + _changed_connection.disconnect(); +} + +RegisteredRadioButtonPair::RegisteredRadioButtonPair (const Glib::ustring& label, + const Glib::ustring& label1, const Glib::ustring& label2, + const Glib::ustring& tip1, const Glib::ustring& tip2, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in) + : RegisteredWidget<Gtk::Box>(), + _rb1(nullptr), + _rb2(nullptr) +{ + init_parent(key, wr, repr_in, doc_in); + + setProgrammatically = false; + + set_orientation(Gtk::ORIENTATION_HORIZONTAL); + add(*Gtk::manage(new Gtk::Label(label))); + _rb1 = Gtk::manage(new Gtk::RadioButton(label1, true)); + add (*_rb1); + Gtk::RadioButtonGroup group = _rb1->get_group(); + _rb2 = Gtk::manage(new Gtk::RadioButton(group, label2, true)); + add (*_rb2); + _rb2->set_active(); + _rb1->set_tooltip_text(tip1); + _rb2->set_tooltip_text(tip2); + _changed_connection = _rb1->signal_toggled().connect (sigc::mem_fun (*this, &RegisteredRadioButtonPair::on_value_changed)); +} + +void +RegisteredRadioButtonPair::setValue (bool second) +{ + if (!_rb1 || !_rb2) + return; + + setProgrammatically = true; + if (second) { + _rb2->set_active(); + } else { + _rb1->set_active(); + } +} + +void +RegisteredRadioButtonPair::on_value_changed() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + bool second = _rb2->get_active(); + write_to_xml(second ? "true" : "false"); + + _wr->setUpdating (false); +} + + +/*######################################### + * Registered POINT + */ + +RegisteredPoint::~RegisteredPoint() +{ + _value_x_changed_connection.disconnect(); + _value_y_changed_connection.disconnect(); +} + +RegisteredPoint::RegisteredPoint ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument* doc_in ) + : RegisteredWidget<Point> (label, tip) +{ + init_parent(key, wr, repr_in, doc_in); + + setRange (-1e6, 1e6); + setDigits (2); + setIncrements(0.1, 1.0); + _value_x_changed_connection = signal_x_value_changed().connect (sigc::mem_fun (*this, &RegisteredPoint::on_value_changed)); + _value_y_changed_connection = signal_y_value_changed().connect (sigc::mem_fun (*this, &RegisteredPoint::on_value_changed)); +} + +void +RegisteredPoint::on_value_changed() +{ + if (setProgrammatically()) { + clearProgrammatically(); + return; + } + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + Inkscape::SVGOStringStream os; + os << getXValue() << "," << getYValue(); + + write_to_xml(os.str().c_str()); + + _wr->setUpdating (false); +} + +/*######################################### + * Registered TRANSFORMEDPOINT + */ + +RegisteredTransformedPoint::~RegisteredTransformedPoint() +{ + _value_x_changed_connection.disconnect(); + _value_y_changed_connection.disconnect(); +} + +RegisteredTransformedPoint::RegisteredTransformedPoint ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument* doc_in ) + : RegisteredWidget<Point> (label, tip), + to_svg(Geom::identity()) +{ + init_parent(key, wr, repr_in, doc_in); + + setRange (-1e6, 1e6); + setDigits (2); + setIncrements(0.1, 1.0); + _value_x_changed_connection = signal_x_value_changed().connect (sigc::mem_fun (*this, &RegisteredTransformedPoint::on_value_changed)); + _value_y_changed_connection = signal_y_value_changed().connect (sigc::mem_fun (*this, &RegisteredTransformedPoint::on_value_changed)); +} + +void +RegisteredTransformedPoint::setValue(Geom::Point const & p) +{ + Geom::Point new_p = p * to_svg.inverse(); + Point::setValue(new_p); // the Point widget should display things in canvas coordinates +} + +void +RegisteredTransformedPoint::setTransform(Geom::Affine const & canvas_to_svg) +{ + // check if matrix is singular / has inverse + if ( ! canvas_to_svg.isSingular() ) { + to_svg = canvas_to_svg; + } else { + // set back to default + to_svg = Geom::identity(); + } +} + +void +RegisteredTransformedPoint::on_value_changed() +{ + if (setProgrammatically()) { + clearProgrammatically(); + return; + } + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + Geom::Point pos = getValue() * to_svg; + + Inkscape::SVGOStringStream os; + os << pos; + + write_to_xml(os.str().c_str()); + + _wr->setUpdating (false); +} + +/*######################################### + * Registered TRANSFORMEDPOINT + */ + +RegisteredVector::~RegisteredVector() +{ + _value_x_changed_connection.disconnect(); + _value_y_changed_connection.disconnect(); +} + +RegisteredVector::RegisteredVector ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument* doc_in ) + : RegisteredWidget<Point> (label, tip), + _polar_coords(false) +{ + init_parent(key, wr, repr_in, doc_in); + + setRange (-1e6, 1e6); + setDigits (2); + setIncrements(0.1, 1.0); + _value_x_changed_connection = signal_x_value_changed().connect (sigc::mem_fun (*this, &RegisteredVector::on_value_changed)); + _value_y_changed_connection = signal_y_value_changed().connect (sigc::mem_fun (*this, &RegisteredVector::on_value_changed)); +} + +void +RegisteredVector::setValue(Geom::Point const & p) +{ + if (!_polar_coords) { + Point::setValue(p); + } else { + Geom::Point polar; + polar[Geom::X] = atan2(p) *180/M_PI; + polar[Geom::Y] = p.length(); + Point::setValue(polar); + } +} + +void +RegisteredVector::setValue(Geom::Point const & p, Geom::Point const & origin) +{ + RegisteredVector::setValue(p); + _origin = origin; +} + +void RegisteredVector::setPolarCoords(bool polar_coords) +{ + _polar_coords = polar_coords; + if (polar_coords) { + xwidget.setLabelText(_("Angle:")); + ywidget.setLabelText(_("Distance:")); + } else { + xwidget.setLabelText(_("X:")); + ywidget.setLabelText(_("Y:")); + } +} + +void +RegisteredVector::on_value_changed() +{ + if (setProgrammatically()) { + clearProgrammatically(); + return; + } + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + Geom::Point origin = _origin; + Geom::Point vector = getValue(); + if (_polar_coords) { + vector = Geom::Point::polar(vector[Geom::X]*M_PI/180, vector[Geom::Y]); + } + + Inkscape::SVGOStringStream os; + os << origin << " , " << vector; + + write_to_xml(os.str().c_str()); + + _wr->setUpdating (false); +} + +/*######################################### + * Registered RANDOM + */ + +RegisteredRandom::~RegisteredRandom() +{ + _value_changed_connection.disconnect(); + _reseeded_connection.disconnect(); +} + +RegisteredRandom::RegisteredRandom ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument * doc_in ) + : RegisteredWidget<Random> (label, tip) +{ + init_parent(key, wr, repr_in, doc_in); + + setProgrammatically = false; + setRange (-1e6, 1e6); + setDigits (2); + setIncrements(0.1, 1.0); + _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredRandom::on_value_changed)); + _reseeded_connection = signal_reseeded.connect(sigc::mem_fun(*this, &RegisteredRandom::on_value_changed)); +} + +void +RegisteredRandom::setValue (double val, long startseed) +{ + Scalar::setValue (val); + setStartSeed(startseed); +} + +void +RegisteredRandom::on_value_changed() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) { + return; + } + _wr->setUpdating (true); + + Inkscape::SVGOStringStream os; + //Force exact 0 if decimals over to 6 + double val = getValue() < 1e-6 && getValue() > -1e-6?0.0:getValue(); + os << val << ';' << getStartSeed(); + write_to_xml(os.str().c_str()); + _wr->setUpdating (false); +} + +/*######################################### + * Registered FONT-BUTTON + */ + +RegisteredFontButton::~RegisteredFontButton() +{ + _signal_font_set.disconnect(); +} + +RegisteredFontButton::RegisteredFontButton ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument* doc_in ) + : RegisteredWidget<FontButton>(label, tip) +{ + init_parent(key, wr, repr_in, doc_in); + _signal_font_set = signal_font_value_changed().connect (sigc::mem_fun (*this, &RegisteredFontButton::on_value_changed)); +} + +void +RegisteredFontButton::setValue (Glib::ustring fontspec) +{ + FontButton::setValue(fontspec); +} + +void +RegisteredFontButton::on_value_changed() +{ + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + Inkscape::SVGOStringStream os; + os << getValue(); + + write_to_xml(os.str().c_str()); + + _wr->setUpdating (false); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/registered-widget.h b/src/ui/widget/registered-widget.h new file mode 100644 index 0000000..f843540 --- /dev/null +++ b/src/ui/widget/registered-widget.h @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * Johan Engelen <j.b.c.engelen@utwente.nl> + * Abhishek Sharma + * + * Copyright (C) 2005-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * + * Used by Live Path Effects (see src/live_effects/parameter/) and Document Properties dialog. + * + */ + +#ifndef INKSCAPE_UI_WIDGET_REGISTERED_WIDGET__H_ +#define INKSCAPE_UI_WIDGET_REGISTERED_WIDGET__H_ + +#include <2geom/affine.h> +#include "xml/node.h" +#include "registry.h" + +#include "ui/widget/scalar.h" +#include "ui/widget/scalar-unit.h" +#include "ui/widget/point.h" +#include "ui/widget/text.h" +#include "ui/widget/random.h" +#include "ui/widget/unit-menu.h" +#include "ui/widget/font-button.h" +#include "ui/widget/color-picker.h" +#include "inkscape.h" + +#include "document.h" +#include "document-undo.h" +#include "desktop.h" +#include "object/sp-namedview.h" + +#include <gtkmm/checkbutton.h> + +class SPDocument; + +namespace Gtk { + class HScale; + class RadioButton; + class SpinButton; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Registry; + +template <class W> +class RegisteredWidget : public W { +public: + void set_undo_parameters(Glib::ustring _event_description, Glib::ustring _icon_name) + { + icon_name = _icon_name; + event_description = _event_description; + write_undo = true; + } + void set_xml_target(Inkscape::XML::Node *xml_node, SPDocument *document) + { + repr = xml_node; + doc = document; + } + + bool is_updating() {if (_wr) return _wr->isUpdating(); else return false;} + +protected: + RegisteredWidget() : W() {} + template< typename A > + explicit RegisteredWidget( A& a ): W( a ) {} + template< typename A, typename B > + RegisteredWidget( A& a, B& b ): W( a, b ) {} + template< typename A, typename B, typename C > + RegisteredWidget( A& a, B& b, C* c ): W( a, b, c ) {} + template< typename A, typename B, typename C > + RegisteredWidget( A& a, B& b, C& c ): W( a, b, c ) {} + template< typename A, typename B, typename C, typename D > + RegisteredWidget( A& a, B& b, C c, D d ): W( a, b, c, d ) {} + template< typename A, typename B, typename C, typename D, typename E > + RegisteredWidget( A& a, B& b, C& c, D d, E e ): W( a, b, c, d, e ) {} + template< typename A, typename B, typename C, typename D, typename E , typename F> + RegisteredWidget( A& a, B& b, C c, D& d, E& e, F* f): W( a, b, c, d, e, f) {} + template< typename A, typename B, typename C, typename D, typename E , typename F, typename G> + RegisteredWidget( A& a, B& b, C& c, D& d, E& e, F f, G& g): W( a, b, c, d, e, f, g) {} + + ~RegisteredWidget() override = default;; + + void init_parent(const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in) + { + _wr = ≀ + _key = key; + repr = repr_in; + doc = doc_in; + if (repr && !doc) // doc cannot be NULL when repr is not NULL + g_warning("Initialization of registered widget using defined repr but with doc==NULL"); + } + + void write_to_xml(const char * svgstr) + { + // Use local repr here. When repr is specified, use that one, but + // if repr==NULL, get the repr of namedview of active desktop. + Inkscape::XML::Node *local_repr = repr; + SPDocument *local_doc = doc; + if (!local_repr) { + SPDesktop* dt = _wr->desktop(); + if (!dt) { + return; + } + local_repr = reinterpret_cast<SPObject *>(dt->getNamedView())->getRepr(); + local_doc = dt->getDocument(); + } + + bool saved = DocumentUndo::getUndoSensitive(local_doc); + DocumentUndo::setUndoSensitive(local_doc, false); + const char * svgstr_old = local_repr->attribute(_key.c_str()); + if (!write_undo) { + local_repr->setAttribute(_key, svgstr); + } + DocumentUndo::setUndoSensitive(local_doc, saved); + if (svgstr_old && svgstr && strcmp(svgstr_old,svgstr)) { + local_doc->setModifiedSinceSave(); + } + + if (write_undo) { + local_repr->setAttribute(_key, svgstr); + DocumentUndo::done(local_doc, event_description, icon_name); + } + } + + Registry * _wr = nullptr; + Glib::ustring _key; + Inkscape::XML::Node * repr = nullptr; + SPDocument * doc = nullptr; + Glib::ustring event_description; + Glib::ustring icon_name; // Used by History dialog. + bool write_undo = false; +}; + +//####################################################### + +class RegisteredCheckButton : public RegisteredWidget<Gtk::CheckButton> { +public: + ~RegisteredCheckButton() override; + RegisteredCheckButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right=false, Inkscape::XML::Node* repr_in=nullptr, SPDocument *doc_in=nullptr, char const *active_str = "true", char const *inactive_str = "false"); + + void setActive (bool); + + std::list<Gtk::Widget*> _slavewidgets; + + // a slave button is only sensitive when the master button is active + // i.e. a slave button is greyed-out when the master button is not checked + + void setSlaveWidgets(std::list<Gtk::Widget*> const &btns) { + _slavewidgets = btns; + } + + bool setProgrammatically; // true if the value was set by setActive, not changed by the user; + // if a callback checks it, it must reset it back to false + +protected: + char const *_active_str, *_inactive_str; + sigc::connection _toggled_connection; + void on_toggled() override; +}; + +class RegisteredToggleButton : public RegisteredWidget<Gtk::ToggleButton> { +public: + ~RegisteredToggleButton() override; + RegisteredToggleButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right=true, Inkscape::XML::Node* repr_in=nullptr, SPDocument *doc_in=nullptr, char const *icon_active = "true", char const *icon_inactive = "false"); + + void setActive (bool); + + std::list<Gtk::Widget*> _slavewidgets; + + // a slave button is only sensitive when the master button is active + // i.e. a slave button is greyed-out when the master button is not checked + + void setSlaveWidgets(std::list<Gtk::Widget*> const &btns) { + _slavewidgets = btns; + } + + bool setProgrammatically; // true if the value was set by setActive, not changed by the user; + // if a callback checks it, it must reset it back to false + +protected: + sigc::connection _toggled_connection; + void on_toggled() override; +}; + +class RegisteredUnitMenu : public RegisteredWidget<Labelled> { +public: + ~RegisteredUnitMenu() override; + RegisteredUnitMenu ( const Glib::ustring& label, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + + void setUnit (const Glib::ustring); + Unit const * getUnit() const { return static_cast<UnitMenu*>(_widget)->getUnit(); }; + UnitMenu* getUnitMenu() const { return static_cast<UnitMenu*>(_widget); }; + sigc::connection _changed_connection; + +protected: + void on_changed(); +}; + +// Allow RegisteredScalarUnit to output lengths in 'user units' (which may have direction dependent +// scale factors). +enum RSU_UserUnits { + RSU_none, + RSU_x, + RSU_y +}; + +class RegisteredScalarUnit : public RegisteredWidget<ScalarUnit> { +public: + ~RegisteredScalarUnit() override; + RegisteredScalarUnit ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + const RegisteredUnitMenu &rum, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr, + RSU_UserUnits _user_units = RSU_none ); + +protected: + sigc::connection _value_changed_connection; + UnitMenu *_um; + void on_value_changed(); + RSU_UserUnits _user_units; +}; + +class RegisteredScalar : public RegisteredWidget<Scalar> { +public: + ~RegisteredScalar() override; + RegisteredScalar (const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); +protected: + sigc::connection _value_changed_connection; + void on_value_changed(); +}; + +class RegisteredText : public RegisteredWidget<Text> { +public: + ~RegisteredText() override; + RegisteredText (const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + +protected: + sigc::connection _activate_connection; + void on_activate(); +}; + +class RegisteredColorPicker : public RegisteredWidget<LabelledColorPicker> { +public: + ~RegisteredColorPicker() override; + + RegisteredColorPicker (const Glib::ustring& label, + const Glib::ustring& title, + const Glib::ustring& tip, + const Glib::ustring& ckey, + const Glib::ustring& akey, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr); + + void setRgba32 (guint32); + void closeWindow(); + +protected: + Glib::ustring _ckey, _akey; + void on_changed (guint32); + sigc::connection _changed_connection; +}; + +class RegisteredSuffixedInteger : public RegisteredWidget<Scalar> { +public: + ~RegisteredSuffixedInteger() override; + RegisteredSuffixedInteger ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& suffix, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + + bool setProgrammatically; // true if the value was set by setValue, not changed by the user; + // if a callback checks it, it must reset it back to false + +protected: + sigc::connection _changed_connection; + void on_value_changed(); +}; + +class RegisteredRadioButtonPair : public RegisteredWidget<Gtk::Box> { +public: + ~RegisteredRadioButtonPair() override; + RegisteredRadioButtonPair ( const Glib::ustring& label, + const Glib::ustring& label1, + const Glib::ustring& label2, + const Glib::ustring& tip1, + const Glib::ustring& tip2, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + + void setValue (bool second); + + bool setProgrammatically; // true if the value was set by setValue, not changed by the user; + // if a callback checks it, it must reset it back to false +protected: + Gtk::RadioButton *_rb1, *_rb2; + sigc::connection _changed_connection; + void on_value_changed(); +}; + +class RegisteredPoint : public RegisteredWidget<Point> { +public: + ~RegisteredPoint() override; + RegisteredPoint ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + +protected: + sigc::connection _value_x_changed_connection; + sigc::connection _value_y_changed_connection; + void on_value_changed(); +}; + + +class RegisteredTransformedPoint : public RegisteredWidget<Point> { +public: + ~RegisteredTransformedPoint() override; + RegisteredTransformedPoint ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + + // redefine setValue, because transform must be applied + void setValue(Geom::Point const & p); + + void setTransform(Geom::Affine const & canvas_to_svg); + +protected: + sigc::connection _value_x_changed_connection; + sigc::connection _value_y_changed_connection; + void on_value_changed(); + + Geom::Affine to_svg; +}; + + +class RegisteredVector : public RegisteredWidget<Point> { +public: + ~RegisteredVector() override; + RegisteredVector (const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + + // redefine setValue, because transform must be applied + void setValue(Geom::Point const & p); + void setValue(Geom::Point const & p, Geom::Point const & origin); + + /** + * Changes the widgets text to polar coordinates. The SVG output will still be a normal cartesian vector. + * Careful: when calling getValue(), the return value's X-coord will be the angle, Y-value will be the distance/length. + * After changing the coords type (polar/non-polar), the value has to be reset (setValue). + */ + void setPolarCoords(bool polar_coords = true); + +protected: + sigc::connection _value_x_changed_connection; + sigc::connection _value_y_changed_connection; + void on_value_changed(); + + Geom::Point _origin; + bool _polar_coords; +}; + + +class RegisteredRandom : public RegisteredWidget<Random> { +public: + ~RegisteredRandom() override; + RegisteredRandom ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr); + + void setValue (double val, long startseed); + +protected: + sigc::connection _value_changed_connection; + sigc::connection _reseeded_connection; + void on_value_changed(); +}; + +class RegisteredFontButton : public RegisteredWidget<FontButton> { +public: + ~RegisteredFontButton() override; + RegisteredFontButton ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr); + + void setValue (Glib::ustring fontspec); + +protected: + sigc::connection _signal_font_set; + void on_value_changed(); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_REGISTERED_WIDGET__H_ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/registry.cpp b/src/ui/widget/registry.cpp new file mode 100644 index 0000000..4bc8e00 --- /dev/null +++ b/src/ui/widget/registry.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "registry.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +//=================================================== + +//--------------------------------------------------- + +Registry::Registry() : _updating(false) {} + +Registry::~Registry() = default; + +bool +Registry::isUpdating() +{ + return _updating; +} + +void +Registry::setUpdating (bool upd) +{ + _updating = upd; +} + +void Registry::setDesktop(SPDesktop *desktop) +{ // + _desktop = 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/widget/registry.h b/src/ui/widget/registry.h new file mode 100644 index 0000000..e6a190d --- /dev/null +++ b/src/ui/widget/registry.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_UI_WIDGET_REGISTRY__H +#define INKSCAPE_UI_WIDGET_REGISTRY__H + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Registry { +public: + Registry(); + ~Registry(); + + bool isUpdating(); + void setUpdating (bool); + + SPDesktop *desktop() const { return _desktop; } + void setDesktop(SPDesktop *desktop); + +protected: + bool _updating; + + SPDesktop *_desktop = nullptr; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Widget + +#endif // INKSCAPE_UI_WIDGET_REGISTRY__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/widget/rendering-options.cpp b/src/ui/widget/rendering-options.cpp new file mode 100644 index 0000000..4640b1e --- /dev/null +++ b/src/ui/widget/rendering-options.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Kees Cook <kees@outflux.net> + * + * Copyright (C) 2007 Kees Cook + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> + +#include "preferences.h" +#include "rendering-options.h" +#include "util/units.h" +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +void RenderingOptions::_toggled() +{ + _frame_bitmap.set_sensitive(as_bitmap()); +} + +RenderingOptions::RenderingOptions () : + Gtk::Box (Gtk::ORIENTATION_VERTICAL), + _frame_backends ( Glib::ustring(_("Backend")) ), + _radio_vector ( Glib::ustring(_("Vector")) ), + _radio_bitmap ( Glib::ustring(_("Bitmap")) ), + _frame_bitmap ( Glib::ustring(_("Bitmap options")) ), + _dpi( _("DPI"), + Glib::ustring(_("Preferred resolution of rendering, " + "in dots per inch.")), + 1, + Glib::ustring(""), Glib::ustring(""), + false) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // set up tooltips + _radio_vector.set_tooltip_text( + _("Render using Cairo vector operations. " + "The resulting image is usually smaller in file " + "size and can be arbitrarily scaled, but some " + "filter effects will not be correctly rendered.")); + _radio_bitmap.set_tooltip_text( + _("Render everything as bitmap. The resulting image " + "is usually larger in file size and cannot be " + "arbitrarily scaled without quality loss, but all " + "objects will be rendered exactly as displayed.")); + + set_border_width(2); + + Gtk::RadioButtonGroup group = _radio_vector.get_group (); + _radio_bitmap.set_group (group); + _radio_bitmap.signal_toggled().connect(sigc::mem_fun(*this, &RenderingOptions::_toggled)); + + // default to vector operations + if (prefs->getBool("/dialogs/printing/asbitmap", false)) { + _radio_bitmap.set_active(); + } else { + _radio_vector.set_active(); + } + + // configure default DPI + _dpi.setRange(Inkscape::Util::Quantity::convert(1, "in", "pt"),2400.0); + _dpi.setValue(prefs->getDouble("/dialogs/printing/dpi", + Inkscape::Util::Quantity::convert(1, "in", "pt"))); + _dpi.setIncrements(1.0,10.0); + _dpi.setDigits(0); + _dpi.update(); + + // fill frames + Gtk::Box *box_vector = Gtk::manage( new Gtk::Box (Gtk::ORIENTATION_VERTICAL) ); + box_vector->set_border_width (2); + box_vector->add (_radio_vector); + box_vector->add (_radio_bitmap); + _frame_backends.add (*box_vector); + + Gtk::Box *box_bitmap = Gtk::manage( new Gtk::Box (Gtk::ORIENTATION_HORIZONTAL) ); + box_bitmap->set_border_width (2); + box_bitmap->add (_dpi); + _frame_bitmap.add (*box_bitmap); + + // fill up container + add (_frame_backends); + add (_frame_bitmap); + + // initialize states + _toggled(); + + show_all_children (); +} + +bool +RenderingOptions::as_bitmap () +{ + return _radio_bitmap.get_active(); +} + +double +RenderingOptions::bitmap_dpi () +{ + return _dpi.getValue(); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode: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/widget/rendering-options.h b/src/ui/widget/rendering-options.h new file mode 100644 index 0000000..65d96f4 --- /dev/null +++ b/src/ui/widget/rendering-options.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Kees Cook <kees@outflux.net> + * + * Copyright (C) 2007 Kees Cook + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_RENDERING_OPTIONS_H +#define INKSCAPE_UI_WIDGET_RENDERING_OPTIONS_H + +#include "scalar.h" + +#include <gtkmm/frame.h> +#include <gtkmm/radiobutton.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A container for selecting rendering options. + */ +class RenderingOptions : public Gtk::Box +{ +public: + + /** + * Construct a Rendering Options widget. + */ + RenderingOptions(); + + bool as_bitmap(); // should we render as a bitmap? + double bitmap_dpi(); // at what DPI should we render the bitmap? + +protected: + // Radio buttons to select desired rendering + Gtk::Frame _frame_backends; + Gtk::RadioButton _radio_vector; + Gtk::RadioButton _radio_bitmap; + + // Bitmap options + Gtk::Frame _frame_bitmap; + Scalar _dpi; // DPI of bitmap to render + + // callback for bitmap button + void _toggled(); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_RENDERING_OPTIONS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode: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/widget/rotateable.cpp b/src/ui/widget/rotateable.cpp new file mode 100644 index 0000000..7f32823 --- /dev/null +++ b/src/ui/widget/rotateable.cpp @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * buliabyak@gmail.com + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include <gtkmm/eventbox.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" +#include "rotateable.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +Rotateable::Rotateable(): + axis(-M_PI/4), + maxdecl(M_PI/4) +{ + dragging = false; + working = false; + scrolling = false; + modifier = 0; + current_axis = axis; + + signal_button_press_event().connect(sigc::mem_fun(*this, &Rotateable::on_click)); + signal_motion_notify_event().connect(sigc::mem_fun(*this, &Rotateable::on_motion)); + signal_button_release_event().connect(sigc::mem_fun(*this, &Rotateable::on_release)); + gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK); + signal_scroll_event().connect(sigc::mem_fun(*this, &Rotateable::on_scroll)); + +} + +bool Rotateable::on_click(GdkEventButton *event) { + if (event->button == 1) { + drag_started_x = event->x; + drag_started_y = event->y; + modifier = get_single_modifier(modifier, event->state); + dragging = true; + working = false; + current_axis = axis; + return true; + } + return false; +} + +guint Rotateable::get_single_modifier(guint old, guint state) { + + if (old == 0 || old == 3) { + if (state & GDK_CONTROL_MASK) + return 1; // ctrl + if (state & GDK_SHIFT_MASK) + return 2; // shift + if (state & GDK_MOD1_MASK) + return 3; // alt + return 0; + } else { + if (!(state & GDK_CONTROL_MASK) && !(state & GDK_SHIFT_MASK)) { + if (state & GDK_MOD1_MASK) + return 3; // alt + else + return 0; // none + } + if (old == 1) { + if (state & GDK_SHIFT_MASK && !(state & GDK_CONTROL_MASK)) + return 2; // shift + if (state & GDK_MOD1_MASK && !(state & GDK_CONTROL_MASK)) + return 3; // alt + return 1; + } + if (old == 2) { + if (state & GDK_CONTROL_MASK && !(state & GDK_SHIFT_MASK)) + return 1; // ctrl + if (state & GDK_MOD1_MASK && !(state & GDK_SHIFT_MASK)) + return 3; // alt + return 2; + } + return old; + } +} + + +bool Rotateable::on_motion(GdkEventMotion *event) { + if (dragging) { + double dist = Geom::L2(Geom::Point(event->x, event->y) - Geom::Point(drag_started_x, drag_started_y)); + double angle = atan2(event->y - drag_started_y, event->x - drag_started_x); + if (dist > 20) { + working = true; + double force = CLAMP (-(angle - current_axis)/maxdecl, -1, 1); + if (fabs(force) < 0.002) + force = 0; // snap to zero + if (modifier != get_single_modifier(modifier, event->state)) { + // user has switched modifiers in mid drag, close past drag and start a new + // one, redefining axis temporarily + do_release(force, modifier); + current_axis = angle; + modifier = get_single_modifier(modifier, event->state); + } else { + do_motion(force, modifier); + } + } + return true; + } + return false; +} + + +bool Rotateable::on_release(GdkEventButton *event) { + if (dragging && working) { + double angle = atan2(event->y - drag_started_y, event->x - drag_started_x); + double force = CLAMP(-(angle - current_axis) / maxdecl, -1, 1); + if (fabs(force) < 0.002) + force = 0; // snap to zero + do_release(force, modifier); + current_axis = axis; + dragging = false; + working = false; + return true; + } + dragging = false; + working = false; + return false; +} + +bool Rotateable::on_scroll(GdkEventScroll* event) +{ + double change = 0.0; + + if (event->direction == GDK_SCROLL_UP) { + change = 1.0; + } else if (event->direction == GDK_SCROLL_DOWN) { + change = -1.0; + } else if (event->direction == GDK_SCROLL_SMOOTH) { + double delta_y_clamped = CLAMP(event->delta_y, -1.0, 1.0); // values > 1 result in excessive changes + change = 1.0 * -delta_y_clamped; + } else { + return FALSE; + } + + drag_started_x = event->x; + drag_started_y = event->y; + modifier = get_single_modifier(modifier, event->state); + dragging = false; + working = false; + scrolling = true; + current_axis = axis; + + do_scroll(change, modifier); + + dragging = false; + working = false; + scrolling = false; + + return TRUE; +} + +Rotateable::~Rotateable() = default; + + + +} // namespace Widget +} // 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/widget/rotateable.h b/src/ui/widget/rotateable.h new file mode 100644 index 0000000..c174a09 --- /dev/null +++ b/src/ui/widget/rotateable.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * buliabyak@gmail.com + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_ROTATEABLE_H +#define INKSCAPE_UI_ROTATEABLE_H + +#include <gtkmm/box.h> +#include <gtkmm/eventbox.h> +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Widget adjustable by dragging it to rotate away from a zero-change axis. + */ +class Rotateable: public Gtk::EventBox +{ +public: + Rotateable(); + + ~Rotateable() override; + + bool on_click(GdkEventButton *event); + bool on_motion(GdkEventMotion *event); + bool on_release(GdkEventButton *event); + bool on_scroll(GdkEventScroll* event); + + double axis; + double current_axis; + double maxdecl; + bool scrolling; + +private: + double drag_started_x; + double drag_started_y; + guint modifier; + bool dragging; + bool working; + + guint get_single_modifier(guint old, guint state); + + virtual void do_motion (double /*by*/, guint /*state*/) {} + virtual void do_release (double /*by*/, guint /*state*/) {} + virtual void do_scroll (double /*by*/, guint /*state*/) {} +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_ROTATEABLE_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/widget/scalar-unit.cpp b/src/ui/widget/scalar-unit.cpp new file mode 100644 index 0000000..9db6b79 --- /dev/null +++ b/src/ui/widget/scalar-unit.cpp @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Bryce Harrington <bryce@bryceharrington.org> + * Derek P. Moore <derekm@hackunix.org> + * buliabyak@gmail.com + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "scalar-unit.h" +#include "spinbutton.h" + +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Widget { + +ScalarUnit::ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip, + UnitType unit_type, + Glib::ustring const &suffix, + Glib::ustring const &icon, + UnitMenu *unit_menu, + bool mnemonic) + : Scalar(label, tooltip, suffix, icon, mnemonic), + _unit_menu(unit_menu), + _hundred_percent(0), + _absolute_is_increment(false), + _percentage_is_increment(false) +{ + if (_unit_menu == nullptr) { + _unit_menu = new UnitMenu(); + g_assert(_unit_menu); + _unit_menu->setUnitType(unit_type); + + remove(*_widget); + Gtk::Box *widget_holder = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 6); + widget_holder->pack_start(*_widget, Gtk::PACK_SHRINK); + widget_holder->pack_start(*Gtk::manage(_unit_menu), Gtk::PACK_SHRINK); + pack_start(*Gtk::manage(widget_holder), Gtk::PACK_SHRINK); + } + _unit_menu->signal_changed() + .connect_notify(sigc::mem_fun(*this, &ScalarUnit::on_unit_changed)); + + static_cast<SpinButton*>(_widget)->setUnitMenu(_unit_menu); + + lastUnits = _unit_menu->getUnitAbbr(); +} + +ScalarUnit::ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip, + ScalarUnit &take_unitmenu, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Scalar(label, tooltip, suffix, icon, mnemonic), + _unit_menu(take_unitmenu._unit_menu), + _hundred_percent(0), + _absolute_is_increment(false), + _percentage_is_increment(false) +{ + _unit_menu->signal_changed() + .connect_notify(sigc::mem_fun(*this, &ScalarUnit::on_unit_changed)); + + static_cast<SpinButton*>(_widget)->setUnitMenu(_unit_menu); + + lastUnits = _unit_menu->getUnitAbbr(); +} + + +void ScalarUnit::initScalar(double min_value, double max_value) +{ + g_assert(_unit_menu != nullptr); + Scalar::setDigits(_unit_menu->getDefaultDigits()); + Scalar::setIncrements(_unit_menu->getDefaultStep(), + _unit_menu->getDefaultPage()); + Scalar::setRange(min_value, max_value); +} + +bool ScalarUnit::setUnit(Glib::ustring const &unit) +{ + g_assert(_unit_menu != nullptr); + // First set the unit + if (!_unit_menu->setUnit(unit)) { + return false; + } + lastUnits = unit; + return true; +} + +void ScalarUnit::setUnitType(UnitType unit_type) +{ + g_assert(_unit_menu != nullptr); + _unit_menu->setUnitType(unit_type); + lastUnits = _unit_menu->getUnitAbbr(); +} + +void ScalarUnit::resetUnitType(UnitType unit_type) +{ + g_assert(_unit_menu != nullptr); + _unit_menu->resetUnitType(unit_type); + lastUnits = _unit_menu->getUnitAbbr(); +} + +Unit const * ScalarUnit::getUnit() const +{ + g_assert(_unit_menu != nullptr); + return _unit_menu->getUnit(); +} + +UnitType ScalarUnit::getUnitType() const +{ + g_assert(_unit_menu); + return _unit_menu->getUnitType(); +} + +void ScalarUnit::setValue(double number, Glib::ustring const &units) +{ + g_assert(_unit_menu != nullptr); + _unit_menu->setUnit(units); + Scalar::setValue(number); +} + +void ScalarUnit::setValueKeepUnit(double number, Glib::ustring const &units) +{ + g_assert(_unit_menu != nullptr); + if (units == "") { + // set the value in the default units + Scalar::setValue(number); + } else { + double conversion = _unit_menu->getConversion(units); + Scalar::setValue(number / conversion); + } +} + +void ScalarUnit::setValue(double number) +{ + Scalar::setValue(number); +} + +double ScalarUnit::getValue(Glib::ustring const &unit_name) const +{ + g_assert(_unit_menu != nullptr); + if (unit_name == "") { + // Return the value in the default units + return Scalar::getValue(); + } else { + double conversion = _unit_menu->getConversion(unit_name); + return conversion * Scalar::getValue(); + } +} + +void ScalarUnit::grabFocusAndSelectEntry() +{ + _widget->grab_focus(); + static_cast<SpinButton*>(_widget)->select_region(0, 20); +} + +void ScalarUnit::setAlignment(double xalign) +{ + xalign = std::clamp(xalign,0.0,1.0); + static_cast<Gtk::Entry*>(_widget)->set_alignment(xalign); +} + +void ScalarUnit::setHundredPercent(double number) +{ + _hundred_percent = number; +} + +void ScalarUnit::setAbsoluteIsIncrement(bool value) +{ + _absolute_is_increment = value; +} + +void ScalarUnit::setPercentageIsIncrement(bool value) +{ + _percentage_is_increment = value; +} + +double ScalarUnit::PercentageToAbsolute(double value) +{ + // convert from percent to absolute + double convertedVal = 0; + double hundred_converted = _hundred_percent / _unit_menu->getConversion("px"); // _hundred_percent is in px + if (_percentage_is_increment) + value += 100; + convertedVal = 0.01 * hundred_converted * value; + if (_absolute_is_increment) + convertedVal -= hundred_converted; + + return convertedVal; +} + +double ScalarUnit::AbsoluteToPercentage(double value) +{ + double convertedVal = 0; + // convert from absolute to percent + if (_hundred_percent == 0) { + if (_percentage_is_increment) + convertedVal = 0; + else + convertedVal = 100; + } else { + double hundred_converted = _hundred_percent / _unit_menu->getConversion("px", lastUnits); // _hundred_percent is in px + if (_absolute_is_increment) + value += hundred_converted; + convertedVal = 100 * value / hundred_converted; + if (_percentage_is_increment) + convertedVal -= 100; + } + + return convertedVal; +} + +double ScalarUnit::getAsPercentage() +{ + double convertedVal = AbsoluteToPercentage(Scalar::getValue()); + return convertedVal; +} + + +void ScalarUnit::setFromPercentage(double value) +{ + double absolute = PercentageToAbsolute(value); + Scalar::setValue(absolute); +} + + +void ScalarUnit::on_unit_changed() +{ + g_assert(_unit_menu != nullptr); + + Glib::ustring abbr = _unit_menu->getUnitAbbr(); + + if (_suffix) { + _suffix->set_label(abbr); + } + + Inkscape::Util::Unit const *new_unit = unit_table.getUnit(abbr); + Inkscape::Util::Unit const *old_unit = unit_table.getUnit(lastUnits); + + double convertedVal = 0; + if (old_unit->type == UNIT_TYPE_DIMENSIONLESS && new_unit->type == UNIT_TYPE_LINEAR) { + convertedVal = PercentageToAbsolute(Scalar::getValue()); + } else if (old_unit->type == UNIT_TYPE_LINEAR && new_unit->type == UNIT_TYPE_DIMENSIONLESS) { + convertedVal = AbsoluteToPercentage(Scalar::getValue()); + } else { + double conversion = _unit_menu->getConversion(lastUnits); + convertedVal = Scalar::getValue() / conversion; + } + Scalar::setValue(convertedVal); + + lastUnits = abbr; +} + +} // namespace Widget +} // 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/widget/scalar-unit.h b/src/ui/widget/scalar-unit.h new file mode 100644 index 0000000..3d7b77d --- /dev/null +++ b/src/ui/widget/scalar-unit.h @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Bryce Harrington <bryce@bryceharrington.org> + * Derek P. Moore <derekm@hackunix.org> + * buliabyak@gmail.com + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_SCALAR_UNIT_H +#define INKSCAPE_UI_WIDGET_SCALAR_UNIT_H + +#include "scalar.h" +#include "unit-menu.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A labelled text box, with spin buttons and optional icon or suffix, for + * entering the values of various unit types. + * + * A ScalarUnit is a control for entering, viewing, or manipulating + * numbers with units. This differs from ordinary numbers like 2 or + * 3.14 because the number portion of a scalar *only* has meaning + * when considered with its unit type. For instance, 12 m and 12 in + * have very different actual values, but 1 m and 100 cm have the same + * value. The ScalarUnit allows us to abstract the presentation of + * the scalar to the user from the internal representations used by + * the program. + */ +class ScalarUnit : public Scalar +{ +public: + /** + * Construct a ScalarUnit. + * + * @param label Label. + * @param unit_type Unit type (defaults to UNIT_TYPE_LINEAR). + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param unit_menu UnitMenu drop down; if not specified, one will be created + * and displayed after the widget (defaults to NULL). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to true). + */ + ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip, + UnitType unit_type = UNIT_TYPE_LINEAR, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + UnitMenu *unit_menu = nullptr, + bool mnemonic = true); + + /** + * Construct a ScalarUnit. + * + * @param label Label. + * @param tooltip Tooltip text. + * @param take_unitmenu Use the unitmenu from this parameter. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to true). + */ + ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip, + ScalarUnit &take_unitmenu, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Initializes the scalar based on the settings in _unit_menu. + * Requires that _unit_menu has already been initialized. + */ + void initScalar(double min_value, double max_value); + + /** + * Gets the object for the currently selected unit. + */ + Unit const * getUnit() const; + + /** + * Gets the UnitType ID for the unit. + */ + UnitType getUnitType() const; + + /** + * Returns the value in the given unit system. + */ + double getValue(Glib::ustring const &units) const; + + /** + * Sets the unit for the ScalarUnit widget. + */ + bool setUnit(Glib::ustring const &units); + + /** + * Adds the unit type to the ScalarUnit widget. + */ + void setUnitType(UnitType unit_type); + + /** + * Resets the unit type for the ScalarUnit widget. + */ + void resetUnitType(UnitType unit_type); + + /** + * allow align text in entry. + */ + void setAlignment(double xalign); + + /** + * Sets the number and unit system. + */ + void setValue(double number, Glib::ustring const &units); + + /** + * Convert and sets the number only and keeps the current unit. + */ + void setValueKeepUnit(double number, Glib::ustring const &units); + + /** + * Sets the number only. + */ + void setValue(double number); + + /** + * Grab focus, and select the text that is in the entry field. + */ + void grabFocusAndSelectEntry(); + + void setHundredPercent(double number); + + void setAbsoluteIsIncrement(bool value); + + void setPercentageIsIncrement(bool value); + + /** + * Convert value from % to absolute, using _hundred_percent and *_is_increment flags. + */ + double PercentageToAbsolute(double value); + + /** + * Convert value from absolute to %, using _hundred_percent and *_is_increment flags. + */ + double AbsoluteToPercentage(double value); + + /** + * Assuming the current unit is absolute, get the corresponding % value. + */ + double getAsPercentage(); + + /** + * Assuming the current unit is absolute, set the value corresponding to a given %. + */ + void setFromPercentage(double value); + + /** + * Signal handler for updating the value and suffix label when unit is changed. + */ + void on_unit_changed(); + +protected: + UnitMenu *_unit_menu; + + double _hundred_percent; // the length that corresponds to 100%, in px, for %-to/from-absolute conversions + + bool _absolute_is_increment; // if true, 120% with _hundred_percent=100px gets converted to/from 20px; otherwise, to/from 120px + bool _percentage_is_increment; // if true, 120px with _hundred_percent=100px gets converted to/from 20%; otherwise, to/from 120% + // if both are true, 20px is converted to/from 20% if _hundred_percent=100px + + Glib::ustring lastUnits; // previously selected unit, for conversions +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_SCALAR_UNIT_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/widget/scalar.cpp b/src/ui/widget/scalar.cpp new file mode 100644 index 0000000..d6766fe --- /dev/null +++ b/src/ui/widget/scalar.cpp @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * Bryce Harrington <bryce@bryceharrington.org> + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) 2004-2011 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "scalar.h" +#include "spinbutton.h" +#include <gtkmm/scale.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new SpinButton(), suffix, icon, mnemonic), + setProgrammatically(false) +{ +} + +Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip, + unsigned digits, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new SpinButton(0.0, digits), suffix, icon, mnemonic), + setProgrammatically(false) +{ +} + +Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::RefPtr<Gtk::Adjustment> &adjust, + unsigned digits, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new SpinButton(adjust, 0.0, digits), suffix, icon, mnemonic), + setProgrammatically(false) +{ +} + +unsigned Scalar::getDigits() const +{ + g_assert(_widget != nullptr); + return static_cast<SpinButton*>(_widget)->get_digits(); +} + +double Scalar::getStep() const +{ + g_assert(_widget != nullptr); + double step, page; + static_cast<SpinButton*>(_widget)->get_increments(step, page); + return step; +} + +double Scalar::getPage() const +{ + g_assert(_widget != nullptr); + double step, page; + static_cast<SpinButton*>(_widget)->get_increments(step, page); + return page; +} + +double Scalar::getRangeMin() const +{ + g_assert(_widget != nullptr); + double min, max; + static_cast<SpinButton*>(_widget)->get_range(min, max); + return min; +} + +double Scalar::getRangeMax() const +{ + g_assert(_widget != nullptr); + double min, max; + static_cast<SpinButton*>(_widget)->get_range(min, max); + return max; +} + +double Scalar::getValue() const +{ + g_assert(_widget != nullptr); + return static_cast<SpinButton*>(_widget)->get_value(); +} + +int Scalar::getValueAsInt() const +{ + g_assert(_widget != nullptr); + return static_cast<SpinButton*>(_widget)->get_value_as_int(); +} + + +void Scalar::setDigits(unsigned digits) +{ + g_assert(_widget != nullptr); + static_cast<SpinButton*>(_widget)->set_digits(digits); +} + +void Scalar::setIncrements(double step, double /*page*/) +{ + g_assert(_widget != nullptr); + static_cast<SpinButton*>(_widget)->set_increments(step, 0); +} + +void Scalar::setRange(double min, double max) +{ + g_assert(_widget != nullptr); + static_cast<SpinButton*>(_widget)->set_range(min, max); +} + +void Scalar::setValue(double value, bool setProg) +{ + g_assert(_widget != nullptr); + if (setProg) { + setProgrammatically = true; // callback is supposed to reset back, if it cares + } + static_cast<SpinButton*>(_widget)->set_value(value); +} + +void Scalar::setWidthChars(unsigned chars) +{ + g_assert(_widget != NULL); + static_cast<SpinButton*>(_widget)->set_width_chars(chars); +} + +void Scalar::update() +{ + g_assert(_widget != nullptr); + static_cast<SpinButton*>(_widget)->update(); +} + +void Scalar::addSlider() +{ + auto scale = new Gtk::Scale(static_cast<SpinButton*>(_widget)->get_adjustment()); + scale->set_draw_value(false); + pack_start(*manage (scale)); +} + +Glib::SignalProxy0<void> Scalar::signal_value_changed() +{ + return static_cast<SpinButton*>(_widget)->signal_value_changed(); +} + +Glib::SignalProxy1<bool, GdkEventButton*> Scalar::signal_button_release_event() +{ + return static_cast<SpinButton*>(_widget)->signal_button_release_event(); +} + +void Scalar::hide_label() { + if (auto label = const_cast<Gtk::Label*>(getLabel())) { + label->hide(); + label->set_no_show_all(); + label->set_hexpand(true); + } + if (_widget) { + remove(*_widget); + _widget->set_hexpand(); + this->pack_end(*_widget); + } +} + + +} // namespace Widget +} // 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/widget/scalar.h b/src/ui/widget/scalar.h new file mode 100644 index 0000000..0ef8d35 --- /dev/null +++ b/src/ui/widget/scalar.h @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Carl Hetherington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_SCALAR_H +#define INKSCAPE_UI_WIDGET_SCALAR_H + +#include "labelled.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A labelled text box, with spin buttons and optional + * icon or suffix, for entering arbitrary number values. + */ +class Scalar : public Labelled +{ +public: + /** + * Construct a Scalar Widget. + * + * @param label Label. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Scalar(Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Construct a Scalar Widget. + * + * @param label Label. + * @param digits Number of decimal digits to display. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Scalar(Glib::ustring const &label, + Glib::ustring const &tooltip, + unsigned digits, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Construct a Scalar Widget. + * + * @param label Label. + * @param adjust Adjustment to use for the SpinButton. + * @param digits Number of decimal digits to display (defaults to 0). + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to true). + */ + Scalar(Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::RefPtr<Gtk::Adjustment> &adjust, + unsigned digits = 0, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Fetches the precision of the spin button. + */ + unsigned getDigits() const; + + /** + * Gets the current step increment used by the spin button. + */ + double getStep() const; + + /** + * Gets the current page increment used by the spin button. + */ + double getPage() const; + + /** + * Gets the minimum range value allowed for the spin button. + */ + double getRangeMin() const; + + /** + * Gets the maximum range value allowed for the spin button. + */ + double getRangeMax() const; + + bool getSnapToTicks() const; + + /** + * Get the value in the spin_button. + */ + double getValue() const; + + /** + * Get the value spin_button represented as an integer. + */ + int getValueAsInt() const; + + /** + * Sets the precision to be displayed by the spin button. + */ + void setDigits(unsigned digits); + + /** + * Sets the step and page increments for the spin button. + * @todo Remove the second parameter - deprecated + */ + void setIncrements(double step, double page); + + /** + * Sets the minimum and maximum range allowed for the spin button. + */ + void setRange(double min, double max); + + /** + * Sets the value of the spin button. + */ + void setValue(double value, bool setProg = true); + + /** + * Sets the width of the spin button by number of characters. + */ + void setWidthChars(unsigned chars); + + /** + * Manually forces an update of the spin button. + */ + void update(); + + /** + * Adds a slider (HScale) to the left of the spinbox. + */ + void addSlider(); + + /** + * Signal raised when the spin button's value changes. + */ + Glib::SignalProxy0<void> signal_value_changed(); + + /** + * Signal raised when the spin button's pressed. + */ + Glib::SignalProxy1<bool, GdkEventButton*> signal_button_release_event(); + + /** + * true if the value was set by setValue, not changed by the user; + * if a callback checks it, it must reset it back to false. + */ + bool setProgrammatically; + + // permanently hide label part + void hide_label(); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_SCALAR_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/widget/scroll-utils.cpp b/src/ui/widget/scroll-utils.cpp new file mode 100644 index 0000000..5822a15 --- /dev/null +++ b/src/ui/widget/scroll-utils.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Thomas Holder + * + * Copyright (C) 2020 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "scroll-utils.h" + +#include <gtkmm/scrolledwindow.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Get the first ancestor which is scrollable. + */ +Gtk::Widget *get_scrollable_ancestor(Gtk::Widget *widget) +{ + auto parent = widget->get_parent(); + if (!parent) { + return nullptr; + } + if (auto scrollable = dynamic_cast<Gtk::ScrolledWindow *>(parent)) { + return scrollable; + } + return get_scrollable_ancestor(parent); +} + +/** + * Return true if scrolling is allowed. + * + * Scrolling is allowed for any of: + * - Shift modifier is pressed + * - Widget has focus + * - Widget has no scrollable ancestor + */ +bool scrolling_allowed(Gtk::Widget *widget, GdkEventScroll *event) +{ + bool const shift = event && (event->state & GDK_SHIFT_MASK); + return shift || // + widget->has_focus() || // + get_scrollable_ancestor(widget) == nullptr; +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/scroll-utils.h b/src/ui/widget/scroll-utils.h new file mode 100644 index 0000000..14b45de --- /dev/null +++ b/src/ui/widget/scroll-utils.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_UI_WIDGET_SCROLL_UTILS_H +#define SEEN_INKSCAPE_UI_WIDGET_SCROLL_UTILS_H + +/* Authors: + * Thomas Holder + * + * Copyright (C) 2020 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdk/gdk.h> + +namespace Gtk { +class Widget; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +Gtk::Widget *get_scrollable_ancestor(Gtk::Widget *widget); + +bool scrolling_allowed(Gtk::Widget *widget, GdkEventScroll *event = nullptr); + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif +// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/scrollprotected.h b/src/ui/widget/scrollprotected.h new file mode 100644 index 0000000..c060398 --- /dev/null +++ b/src/ui/widget/scrollprotected.h @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_UI_WIDGET_SCROLLPROTECTED_H +#define SEEN_INKSCAPE_UI_WIDGET_SCROLLPROTECTED_H + +/* Authors: + * Thomas Holder + * Anshudhar Kumar Singh <anshudhar2001@gmail.com> + * + * Copyright (C) 2020-2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> + +#include "scroll-utils.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A class decorator which blocks the scroll event if the widget does not have + * focus and any ancestor is a scrollable window, and SHIFT is not pressed. + * + * For custom scroll event handlers, derived classes must implement + * on_safe_scroll_event instead of on_scroll_event. Directly connecting to + * signal_scroll_event() will bypass the scroll protection. + * + * @tparam Base A subclass of Gtk::Widget + */ +template <typename Base> +class ScrollProtected : public Base +{ +public: + using Base::Base; + using typename Base::BaseObjectType; + ScrollProtected() + : Base() + {} + ScrollProtected(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade) + : Base(cobject){}; + ~ScrollProtected() override{}; + +protected: + /** + * Event handler for "safe" scroll events which are only triggered if: + * - the widget has focus + * - or the widget has no scrolled window ancestor + * - or the Shift key is pressed + */ + virtual bool on_safe_scroll_event(GdkEventScroll *event) + { // + return Base::on_scroll_event(event); + } + + bool on_scroll_event(GdkEventScroll *event) final + { + if (!scrolling_allowed(this, event)) { + return false; + } + return on_safe_scroll_event(event); + } +}; + +/** + * A class decorator for scroll widgets like scrolled window to transfer scroll to + * any ancestor which is is a scrollable window when scroll reached end. + * + * For custom scroll event handlers, derived classes must implement + * on_safe_scroll_event instead of on_scroll_event. Directly connecting to + * signal_scroll_event() will bypass the scroll protection. + * + * @tparam Base A subclass of Gtk::Widget + */ +template <typename Base> +class ScrollTransfer : public Base +{ +public: + using Base::Base; + using typename Base::BaseObjectType; + ScrollTransfer() + : Base() + {} + ScrollTransfer(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade) + : Base(cobject){}; + ~ScrollTransfer() override{}; +protected: + /** + * Event handler for "safe" scroll events + */ + virtual bool on_safe_scroll_event(GdkEventScroll *event) + { // + return Base::on_scroll_event(event); + } + + bool on_scroll_event(GdkEventScroll *event) final + { + auto scrollable = dynamic_cast<Gtk::Widget *>(Inkscape::UI::Widget::get_scrollable_ancestor(this)); + auto adj = this->get_vadjustment(); + auto before = adj->get_value(); + bool result = on_safe_scroll_event(event); + auto after = adj->get_value(); + if (scrollable && before == after) { + return false; + } + + return result; + } +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif +// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/selected-style.cpp b/src/ui/widget/selected-style.cpp new file mode 100644 index 0000000..433112c --- /dev/null +++ b/src/ui/widget/selected-style.cpp @@ -0,0 +1,1435 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * buliabyak@gmail.com + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2005 author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "selected-style.h" + +#include <vector> + +#include <gtkmm/separatormenuitem.h> + + +#include "desktop-style.h" +#include "document-undo.h" +#include "gradient-chemistry.h" +#include "message-context.h" +#include "selection.h" + +#include "include/gtkmm_version.h" + +#include "object/sp-hatch.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-namedview.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "style.h" + +#include "svg/css-ostringstream.h" +#include "svg/svg-color.h" + +#include "ui/cursor-utils.h" +#include "ui/dialog/dialog-container.h" +#include "ui/dialog/dialog-base.h" +#include "ui/dialog/fill-and-stroke.h" +#include "ui/icon-names.h" +#include "ui/tools/tool-base.h" +#include "ui/widget/canvas.h" // Forced redraws. +#include "ui/widget/color-preview.h" +#include "ui/widget/gradient-image.h" + +#include "widgets/ege-paint-def.h" +#include "widgets/spw-utilities.h" + +using Inkscape::Util::unit_table; + +static gdouble const _sw_presets[] = { 32 , 16 , 10 , 8 , 6 , 4 , 3 , 2 , 1.5 , 1 , 0.75 , 0.5 , 0.25 , 0.1 }; +static gchar const *const _sw_presets_str[] = {"32", "16", "10", "8", "6", "4", "3", "2", "1.5", "1", "0.75", "0.5", "0.25", "0.1"}; + +static void +ss_selection_changed (Inkscape::Selection *, gpointer data) +{ + Inkscape::UI::Widget::SelectedStyle *ss = (Inkscape::UI::Widget::SelectedStyle *) data; + ss->update(); +} + +static void +ss_selection_modified( Inkscape::Selection *selection, guint flags, gpointer data ) +{ + // Don't update the style when dragging or doing non-style related changes + if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) { + ss_selection_changed (selection, data); + } +} + +static void +ss_subselection_changed( gpointer /*dragger*/, gpointer data ) +{ + ss_selection_changed (nullptr, data); +} + +namespace { + +void clearTooltip( Gtk::Widget &widget ) +{ + widget.set_tooltip_text(""); + widget.set_has_tooltip(false); +} + +} // namespace + +namespace Inkscape { +namespace UI { +namespace Widget { + + +struct DropTracker { + SelectedStyle* parent; + int item; +}; + +/* Drag and Drop */ +enum ui_drop_target_info { + APP_OSWB_COLOR +}; + +//TODO: warning: deprecated conversion from string constant to ‘gchar*’ +// +//Turn out to be warnings that we should probably leave in place. The +// pointers/types used need to be read-only. So until we correct the using +// code, those warnings are actually desired. They say "Hey! Fix this". We +// definitely don't want to hide/ignore them. --JonCruz +static const GtkTargetEntry ui_drop_target_entries [] = { + {g_strdup("application/x-oswb-color"), 0, APP_OSWB_COLOR} +}; + +static guint nui_drop_target_entries = G_N_ELEMENTS(ui_drop_target_entries); + +/* convenience function */ +static Dialog::FillAndStroke *get_fill_and_stroke_panel(SPDesktop *desktop); + +SelectedStyle::SelectedStyle(bool /*layout*/) + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) + , current_stroke_width(0) + , _sw_unit(nullptr) + , _desktop(nullptr) + , _table() + , _fill_label(_("Fill:")) + , _stroke_label(_("Stroke:")) + , _opacity_label(_("O:")) + , _fill_place(this, SS_FILL) + , _stroke_place(this, SS_STROKE) + , _fill_flag_place() + , _stroke_flag_place() + , _opacity_place() + , _opacity_adjustment(Gtk::Adjustment::create(100, 0.0, 100, 1.0, 10.0)) + , _opacity_sb(0.02, 0) + , _fill(Gtk::ORIENTATION_HORIZONTAL, 1) + , _stroke(Gtk::ORIENTATION_HORIZONTAL) + , _stroke_width_place(this) + , _stroke_width("") + , _fill_empty_space("") + , _opacity_blocked(false) +{ + set_name("SelectedStyle"); + _drop[0] = _drop[1] = nullptr; + _dropEnabled[0] = _dropEnabled[1] = false; + + _fill_label.set_halign(Gtk::ALIGN_END); + _fill_label.set_valign(Gtk::ALIGN_CENTER); + _fill_label.set_margin_top(0); + _fill_label.set_margin_bottom(0); + _stroke_label.set_halign(Gtk::ALIGN_END); + _stroke_label.set_valign(Gtk::ALIGN_CENTER); + _stroke_label.set_margin_top(0); + _stroke_label.set_margin_bottom(0); + _opacity_label.set_halign(Gtk::ALIGN_START); + _opacity_label.set_valign(Gtk::ALIGN_CENTER); + _opacity_label.set_margin_top(0); + _opacity_label.set_margin_bottom(0); + _stroke_width.set_name("monoStrokeWidth"); + _fill_empty_space.set_name("fillEmptySpace"); + + _fill_label.set_margin_start(0); + _fill_label.set_margin_end(0); + _stroke_label.set_margin_start(0); + _stroke_label.set_margin_end(0); + _opacity_label.set_margin_start(0); + _opacity_label.set_margin_end(0); + + _table.set_column_spacing(2); + _table.set_row_spacing(0); + + for (int i = SS_FILL; i <= SS_STROKE; i++) { + + _na[i].set_markup (_("N/A")); + _na[i].show_all(); + __na[i] = (_("Nothing selected")); + + if (i == SS_FILL) { + _none[i].set_markup (C_("Fill", "<i>None</i>")); + } else { + _none[i].set_markup (C_("Stroke", "<i>None</i>")); + } + _none[i].show_all(); + __none[i] = (i == SS_FILL)? (C_("Fill and stroke", "No fill, middle-click for black fill")) : (C_("Fill and stroke", "No stroke, middle-click for black stroke")); + + _pattern[i].set_markup (_("Pattern")); + _pattern[i].show_all(); + __pattern[i] = (i == SS_FILL)? (_("Pattern (fill)")) : (_("Pattern (stroke)")); + + _hatch[i].set_markup(_("Hatch")); + _hatch[i].show_all(); + __hatch[i] = (i == SS_FILL) ? (_("Hatch (fill)")) : (_("Hatch (stroke)")); + + _lgradient[i].set_markup (_("<b>L</b>")); + _lgradient[i].show_all(); + __lgradient[i] = (i == SS_FILL)? (_("Linear gradient (fill)")) : (_("Linear gradient (stroke)")); + + _gradient_preview_l[i] = Gtk::manage(new GradientImage(nullptr)); + _gradient_box_l[i].set_orientation(Gtk::ORIENTATION_HORIZONTAL); + _gradient_box_l[i].pack_start(_lgradient[i]); + _gradient_box_l[i].pack_start(*_gradient_preview_l[i]); + _gradient_box_l[i].show_all(); + + _rgradient[i].set_markup (_("<b>R</b>")); + _rgradient[i].show_all(); + __rgradient[i] = (i == SS_FILL)? (_("Radial gradient (fill)")) : (_("Radial gradient (stroke)")); + + _gradient_preview_r[i] = Gtk::manage(new GradientImage(nullptr)); + _gradient_box_r[i].set_orientation(Gtk::ORIENTATION_HORIZONTAL); + _gradient_box_r[i].pack_start(_rgradient[i]); + _gradient_box_r[i].pack_start(*_gradient_preview_r[i]); + _gradient_box_r[i].show_all(); + +#ifdef WITH_MESH + _mgradient[i].set_markup (_("<b>M</b>")); + _mgradient[i].show_all(); + __mgradient[i] = (i == SS_FILL)? (_("Mesh gradient (fill)")) : (_("Mesh gradient (stroke)")); + + _gradient_preview_m[i] = Gtk::manage(new GradientImage(nullptr)); + _gradient_box_m[i].set_orientation(Gtk::ORIENTATION_HORIZONTAL); + _gradient_box_m[i].pack_start(_mgradient[i]); + _gradient_box_m[i].pack_start(*_gradient_preview_m[i]); + _gradient_box_m[i].show_all(); +#endif + + _many[i].set_markup (_("≠")); + _many[i].show_all(); + __many[i] = (i == SS_FILL)? (_("Different fills")) : (_("Different strokes")); + + _unset[i].set_markup (_("<b>Unset</b>")); + _unset[i].show_all(); + __unset[i] = (i == SS_FILL)? (_("Unset fill")) : (_("Unset stroke")); + + _color_preview[i] = new Inkscape::UI::Widget::ColorPreview (0); + __color[i] = (i == SS_FILL)? (_("Flat color (fill)")) : (_("Flat color (stroke)")); + + // TRANSLATORS: A means "Averaged" + _averaged[i].set_markup (_("<b>a</b>")); + _averaged[i].show_all(); + __averaged[i] = (i == SS_FILL)? (_("Fill is averaged over selected objects")) : (_("Stroke is averaged over selected objects")); + + // TRANSLATORS: M means "Multiple" + _multiple[i].set_markup (_("<b>m</b>")); + _multiple[i].show_all(); + __multiple[i] = (i == SS_FILL)? (_("Multiple selected objects have the same fill")) : (_("Multiple selected objects have the same stroke")); + + _popup_edit[i].add(*(new Gtk::Label((i == SS_FILL)? _("Edit fill...") : _("Edit stroke..."), Gtk::ALIGN_START))); + _popup_edit[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_edit : &SelectedStyle::on_stroke_edit )); + + _popup_lastused[i].add(*(new Gtk::Label(_("Last set color"), Gtk::ALIGN_START))); + _popup_lastused[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_lastused : &SelectedStyle::on_stroke_lastused )); + + _popup_lastselected[i].add(*(new Gtk::Label(_("Last selected color"), Gtk::ALIGN_START))); + _popup_lastselected[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_lastselected : &SelectedStyle::on_stroke_lastselected )); + + _popup_invert[i].add(*(new Gtk::Label(_("Invert"), Gtk::ALIGN_START))); + _popup_invert[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_invert : &SelectedStyle::on_stroke_invert )); + + _popup_white[i].add(*(new Gtk::Label(_("White"), Gtk::ALIGN_START))); + _popup_white[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_white : &SelectedStyle::on_stroke_white )); + + _popup_black[i].add(*(new Gtk::Label(_("Black"), Gtk::ALIGN_START))); + _popup_black[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_black : &SelectedStyle::on_stroke_black )); + + _popup_copy[i].add(*(new Gtk::Label(_("Copy color"), Gtk::ALIGN_START))); + _popup_copy[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_copy : &SelectedStyle::on_stroke_copy )); + + _popup_paste[i].add(*(new Gtk::Label(_("Paste color"), Gtk::ALIGN_START))); + _popup_paste[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_paste : &SelectedStyle::on_stroke_paste )); + + _popup_swap[i].add(*(new Gtk::Label(_("Swap fill and stroke"), Gtk::ALIGN_START))); + _popup_swap[i].signal_activate().connect(sigc::mem_fun(*this, + &SelectedStyle::on_fillstroke_swap)); + + _popup_opaque[i].add(*(new Gtk::Label((i == SS_FILL)? _("Make fill opaque") : _("Make stroke opaque"), Gtk::ALIGN_START))); + _popup_opaque[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_opaque : &SelectedStyle::on_stroke_opaque )); + + //TRANSLATORS COMMENT: unset is a verb here + _popup_unset[i].add(*(new Gtk::Label((i == SS_FILL)? _("Unset fill") : _("Unset stroke"), Gtk::ALIGN_START))); + _popup_unset[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_unset : &SelectedStyle::on_stroke_unset )); + + _popup_remove[i].add(*(new Gtk::Label((i == SS_FILL)? _("Remove fill") : _("Remove stroke"), Gtk::ALIGN_START))); + _popup_remove[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_remove : &SelectedStyle::on_stroke_remove )); + + _popup[i].attach(_popup_edit[i], 0,1, 0,1); + _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 1,2); + _popup[i].attach(_popup_lastused[i], 0,1, 2,3); + _popup[i].attach(_popup_lastselected[i], 0,1, 3,4); + _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 4,5); + _popup[i].attach(_popup_invert[i], 0,1, 5,6); + _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 6,7); + _popup[i].attach(_popup_white[i], 0,1, 7,8); + _popup[i].attach(_popup_black[i], 0,1, 8,9); + _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 9,10); + _popup[i].attach(_popup_copy[i], 0,1, 10,11); + _popup_copy[i].set_sensitive(false); + _popup[i].attach(_popup_paste[i], 0,1, 11,12); + _popup[i].attach(_popup_swap[i], 0,1, 12,13); + _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 13,14); + _popup[i].attach(_popup_opaque[i], 0,1, 14,15); + _popup[i].attach(_popup_unset[i], 0,1, 15,16); + _popup[i].attach(_popup_remove[i], 0,1, 16,17); + _popup[i].show_all(); + + _mode[i] = SS_NA; + } + + { + int row = 0; + + Inkscape::Util::UnitTable::UnitMap m = unit_table.units(Inkscape::Util::UNIT_TYPE_LINEAR); + Inkscape::Util::UnitTable::UnitMap::iterator iter = m.begin(); + while(iter != m.end()) { + Gtk::RadioMenuItem *mi = Gtk::manage(new Gtk::RadioMenuItem(_sw_group)); + mi->add(*(new Gtk::Label(iter->first, Gtk::ALIGN_START))); + _unit_mis.push_back(mi); + Inkscape::Util::Unit const *u = unit_table.getUnit(iter->first); + mi->signal_activate().connect(sigc::bind<Inkscape::Util::Unit const *>(sigc::mem_fun(*this, &SelectedStyle::on_popup_units), u)); + _popup_sw.attach(*mi, 0,1, row, row+1); + row++; + ++iter; + } + + _popup_sw.attach(*(new Gtk::SeparatorMenuItem()), 0,1, row, row+1); + row++; + + for (guint i = 0; i < G_N_ELEMENTS(_sw_presets_str); ++i) { + Gtk::MenuItem *mi = Gtk::manage(new Gtk::MenuItem()); + mi->add(*(new Gtk::Label(_sw_presets_str[i], Gtk::ALIGN_START))); + mi->signal_activate().connect(sigc::bind<int>(sigc::mem_fun(*this, &SelectedStyle::on_popup_preset), i)); + _popup_sw.attach(*mi, 0,1, row, row+1); + row++; + } + + _popup_sw.attach(*(new Gtk::SeparatorMenuItem()), 0,1, row, row+1); + row++; + + _popup_sw_remove.add(*(new Gtk::Label(_("Remove"), Gtk::ALIGN_START))); + _popup_sw_remove.signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::on_stroke_remove)); + _popup_sw.attach(_popup_sw_remove, 0,1, row, row+1); + row++; + + _popup_sw.show_all(); + } + // fill row + _fill_flag_place.set_size_request(SELECTED_STYLE_FLAG_WIDTH , -1); + + _fill_place.add(_na[SS_FILL]); + _fill_place.set_tooltip_text(__na[SS_FILL]); + _fill.set_size_request(SELECTED_STYLE_PLACE_WIDTH, -1); + _fill.pack_start(_fill_place, Gtk::PACK_EXPAND_WIDGET); + + _fill_empty_space.set_size_request(SELECTED_STYLE_STROKE_WIDTH); + + // stroke row + _stroke_flag_place.set_size_request(SELECTED_STYLE_FLAG_WIDTH, -1); + + _stroke_place.add(_na[SS_STROKE]); + _stroke_place.set_tooltip_text(__na[SS_STROKE]); + _stroke.set_size_request(SELECTED_STYLE_PLACE_WIDTH, -1); + _stroke.pack_start(_stroke_place, Gtk::PACK_EXPAND_WIDGET); + + _stroke_width_place.add(_stroke_width); + _stroke_width_place.set_size_request(SELECTED_STYLE_STROKE_WIDTH); + + // opacity selector + _opacity_place.add(_opacity_label); + + _opacity_sb.set_adjustment(_opacity_adjustment); + _opacity_sb.set_size_request (SELECTED_STYLE_SB_WIDTH, -1); + _opacity_sb.set_sensitive (false); + + // arrange in table + _table.attach(_fill_label, 0, 0, 1, 1); + _table.attach(_stroke_label, 0, 1, 1, 1); + + _table.attach(_fill_flag_place, 1, 0, 1, 1); + _table.attach(_stroke_flag_place, 1, 1, 1, 1); + + _table.attach(_fill, 2, 0, 1, 1); + _table.attach(_stroke, 2, 1, 1, 1); + + _table.attach(_fill_empty_space, 3, 0, 1, 1); + _table.attach(_stroke_width_place, 3, 1, 1, 1); + + _table.attach(_opacity_place, 4, 0, 1, 2); + _table.attach(_opacity_sb, 5, 0, 1, 2); + + pack_start(_table, true, true, 2); + + set_size_request (SELECTED_STYLE_WIDTH, -1); + + _drop[SS_FILL] = new DropTracker(); + ((DropTracker*)_drop[SS_FILL])->parent = this; + ((DropTracker*)_drop[SS_FILL])->item = SS_FILL; + + _drop[SS_STROKE] = new DropTracker(); + ((DropTracker*)_drop[SS_STROKE])->parent = this; + ((DropTracker*)_drop[SS_STROKE])->item = SS_STROKE; + + g_signal_connect(_stroke_place.gobj(), + "drag_data_received", + G_CALLBACK(dragDataReceived), + _drop[SS_STROKE]); + + g_signal_connect(_fill_place.gobj(), + "drag_data_received", + G_CALLBACK(dragDataReceived), + _drop[SS_FILL]); + + _fill_place.signal_button_release_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_fill_click)); + _stroke_place.signal_button_release_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_stroke_click)); + _opacity_place.signal_button_press_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_opacity_click)); + _stroke_width_place.signal_button_press_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_sw_click)); + _stroke_width_place.signal_button_release_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_sw_click)); + _opacity_sb.signal_populate_popup().connect(sigc::mem_fun(*this, &SelectedStyle::on_opacity_menu)); + _opacity_sb.signal_value_changed().connect(sigc::mem_fun(*this, &SelectedStyle::on_opacity_changed)); +} + +SelectedStyle::~SelectedStyle() +{ + selection_changed_connection->disconnect(); + delete selection_changed_connection; + selection_modified_connection->disconnect(); + delete selection_modified_connection; + subselection_changed_connection->disconnect(); + delete subselection_changed_connection; + _unit_mis.clear(); + + _fill_place.remove(); + _stroke_place.remove(); + + for (int i = SS_FILL; i <= SS_STROKE; i++) { + delete _color_preview[i]; + } + + delete (DropTracker*)_drop[SS_FILL]; + delete (DropTracker*)_drop[SS_STROKE]; +} + +void +SelectedStyle::setDesktop(SPDesktop *desktop) +{ + _desktop = desktop; + + Inkscape::Selection *selection = desktop->getSelection(); + + selection_changed_connection = new sigc::connection (selection->connectChanged( + sigc::bind ( + sigc::ptr_fun(&ss_selection_changed), + this ) + )); + selection_modified_connection = new sigc::connection (selection->connectModified( + sigc::bind ( + sigc::ptr_fun(&ss_selection_modified), + this ) + )); + subselection_changed_connection = new sigc::connection (desktop->connectToolSubselectionChanged( + sigc::bind ( + sigc::ptr_fun(&ss_subselection_changed), + this ) + )); + + _sw_unit = desktop->getNamedView()->display_units; + + // Set the doc default unit active in the units list + for ( auto mi:_unit_mis ) { + if (mi && mi->get_label() == _sw_unit->abbr) { + mi->set_active(); + break; + } + } +} + +void SelectedStyle::dragDataReceived( GtkWidget */*widget*/, + GdkDragContext */*drag_context*/, + gint /*x*/, gint /*y*/, + GtkSelectionData *data, + guint /*info*/, + guint /*event_time*/, + gpointer user_data ) +{ + DropTracker* tracker = (DropTracker*)user_data; + + // copied from drag-and-drop.cpp, case APP_OSWB_COLOR + bool worked = false; + Glib::ustring colorspec; + if (gtk_selection_data_get_format(data) == 8) { + ege::PaintDef color; + worked = color.fromMIMEData("application/x-oswb-color", + reinterpret_cast<char const *>(gtk_selection_data_get_data(data)), + gtk_selection_data_get_length(data), + gtk_selection_data_get_format(data)); + if (worked) { + if (color.getType() == ege::PaintDef::CLEAR) { + colorspec = ""; // TODO check if this is sufficient + } else if (color.getType() == ege::PaintDef::NONE) { + colorspec = "none"; + } else { + unsigned int r = color.getR(); + unsigned int g = color.getG(); + unsigned int b = color.getB(); + + gchar* tmp = g_strdup_printf("#%02x%02x%02x", r, g, b); + colorspec = tmp; + g_free(tmp); + } + } + } + if (worked) { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, (tracker->item == SS_FILL) ? "fill":"stroke", colorspec.c_str()); + + sp_desktop_set_style(tracker->parent->_desktop, css); + sp_repr_css_attr_unref(css); + DocumentUndo::done(tracker->parent->_desktop->getDocument(), _("Drop color"), ""); + } +} + +void SelectedStyle::on_fill_remove() { + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "fill", "none"); + sp_desktop_set_style (_desktop, css, true, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Remove fill"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_stroke_remove() { + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "stroke", "none"); + sp_desktop_set_style (_desktop, css, true, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Remove stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_fill_unset() { + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_unset_property (css, "fill"); + sp_desktop_set_style (_desktop, css, true, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Unset fill"), INKSCAPE_ICON("dialog-fill-and-stroke")); + +} + +void SelectedStyle::on_stroke_unset() { + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_unset_property (css, "stroke"); + sp_repr_css_unset_property (css, "stroke-opacity"); + sp_repr_css_unset_property (css, "stroke-width"); + sp_repr_css_unset_property (css, "stroke-miterlimit"); + sp_repr_css_unset_property (css, "stroke-linejoin"); + sp_repr_css_unset_property (css, "stroke-linecap"); + sp_repr_css_unset_property (css, "stroke-dashoffset"); + sp_repr_css_unset_property (css, "stroke-dasharray"); + sp_desktop_set_style (_desktop, css, true, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Unset stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_fill_opaque() { + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "fill-opacity", "1"); + sp_desktop_set_style (_desktop, css, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Make fill opaque"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_stroke_opaque() { + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "stroke-opacity", "1"); + sp_desktop_set_style (_desktop, css, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Make fill opaque"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_fill_lastused() { + SPCSSAttr *css = sp_repr_css_attr_new (); + guint32 color = sp_desktop_get_color(_desktop, true); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), color); + sp_repr_css_set_property (css, "fill", c); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Apply last set color to fill"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_stroke_lastused() { + SPCSSAttr *css = sp_repr_css_attr_new (); + guint32 color = sp_desktop_get_color(_desktop, false); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), color); + sp_repr_css_set_property (css, "stroke", c); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Apply last set color to stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_fill_lastselected() { + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), _lastselected[SS_FILL]); + sp_repr_css_set_property (css, "fill", c); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Apply last selected color to fill"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_stroke_lastselected() { + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), _lastselected[SS_STROKE]); + sp_repr_css_set_property (css, "stroke", c); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Apply last selected color to stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_fill_invert() { + SPCSSAttr *css = sp_repr_css_attr_new (); + guint32 color = _thisselected[SS_FILL]; + gchar c[64]; + if (_mode[SS_FILL] == SS_LGRADIENT || _mode[SS_FILL] == SS_RGRADIENT) { + sp_gradient_invert_selected_gradients(_desktop, Inkscape::FOR_FILL); + return; + + } + + if (_mode[SS_FILL] != SS_COLOR) return; + sp_svg_write_color (c, sizeof(c), + SP_RGBA32_U_COMPOSE( + (255 - SP_RGBA32_R_U(color)), + (255 - SP_RGBA32_G_U(color)), + (255 - SP_RGBA32_B_U(color)), + SP_RGBA32_A_U(color) + ) + ); + sp_repr_css_set_property (css, "fill", c); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Invert fill"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_stroke_invert() { + SPCSSAttr *css = sp_repr_css_attr_new (); + guint32 color = _thisselected[SS_STROKE]; + gchar c[64]; + if (_mode[SS_STROKE] == SS_LGRADIENT || _mode[SS_STROKE] == SS_RGRADIENT) { + sp_gradient_invert_selected_gradients(_desktop, Inkscape::FOR_STROKE); + return; + } + if (_mode[SS_STROKE] != SS_COLOR) return; + sp_svg_write_color (c, sizeof(c), + SP_RGBA32_U_COMPOSE( + (255 - SP_RGBA32_R_U(color)), + (255 - SP_RGBA32_G_U(color)), + (255 - SP_RGBA32_B_U(color)), + SP_RGBA32_A_U(color) + ) + ); + sp_repr_css_set_property (css, "stroke", c); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Invert stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_fill_white() { + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), 0xffffffff); + sp_repr_css_set_property (css, "fill", c); + sp_repr_css_set_property (css, "fill-opacity", "1"); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("White fill"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_stroke_white() { + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), 0xffffffff); + sp_repr_css_set_property (css, "stroke", c); + sp_repr_css_set_property (css, "stroke-opacity", "1"); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("White stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_fill_black() { + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), 0x000000ff); + sp_repr_css_set_property (css, "fill", c); + sp_repr_css_set_property (css, "fill-opacity", "1.0"); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Black fill"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_stroke_black() { + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), 0x000000ff); + sp_repr_css_set_property (css, "stroke", c); + sp_repr_css_set_property (css, "stroke-opacity", "1.0"); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Black stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); +} + +void SelectedStyle::on_fill_copy() { + if (_mode[SS_FILL] == SS_COLOR) { + gchar c[64]; + sp_svg_write_color (c, sizeof(c), _thisselected[SS_FILL]); + Glib::ustring text; + text += c; + if (!text.empty()) { + Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get(); + refClipboard->set_text(text); + } + } +} + +void SelectedStyle::on_stroke_copy() { + if (_mode[SS_STROKE] == SS_COLOR) { + gchar c[64]; + sp_svg_write_color (c, sizeof(c), _thisselected[SS_STROKE]); + Glib::ustring text; + text += c; + if (!text.empty()) { + Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get(); + refClipboard->set_text(text); + } + } +} + +void SelectedStyle::on_fill_paste() { + Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get(); + Glib::ustring const text = refClipboard->wait_for_text(); + + if (!text.empty()) { + guint32 color = sp_svg_read_color(text.c_str(), 0x000000ff); // impossible value, as SVG color cannot have opacity + if (color == 0x000000ff) // failed to parse color string + return; + + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "fill", text.c_str()); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Paste fill"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } +} + +void SelectedStyle::on_stroke_paste() { + Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get(); + Glib::ustring const text = refClipboard->wait_for_text(); + + if (!text.empty()) { + guint32 color = sp_svg_read_color(text.c_str(), 0x000000ff); // impossible value, as SVG color cannot have opacity + if (color == 0x000000ff) // failed to parse color string + return; + + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "stroke", text.c_str()); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Paste stroke"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } +} + +void SelectedStyle::on_fillstroke_swap() { + _desktop->getSelection()->swapFillStroke(); +} + +void SelectedStyle::on_fill_edit() { + if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop)) + fs->showPageFill(); +} + +void SelectedStyle::on_stroke_edit() { + if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop)) + fs->showPageStrokePaint(); +} + +bool +SelectedStyle::on_fill_click(GdkEventButton *event) +{ + if (event->button == 1) { // click, open fill&stroke + + if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop)) + fs->showPageFill(); + + } else if (event->button == 3) { // right-click, popup menu + _popup[SS_FILL].popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } else if (event->button == 2) { // middle click, toggle none/lastcolor + if (_mode[SS_FILL] == SS_NONE) { + on_fill_lastused(); + } else { + on_fill_remove(); + } + } + return true; +} + +bool +SelectedStyle::on_stroke_click(GdkEventButton *event) +{ + if (event->button == 1) { // click, open fill&stroke + if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop)) + fs->showPageStrokePaint(); + } else if (event->button == 3) { // right-click, popup menu + _popup[SS_STROKE].popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } else if (event->button == 2) { // middle click, toggle none/lastcolor + if (_mode[SS_STROKE] == SS_NONE) { + on_stroke_lastused(); + } else { + on_stroke_remove(); + } + } + return true; +} + +bool +SelectedStyle::on_sw_click(GdkEventButton *event) +{ + if (event->button == 1) { // click, open fill&stroke + if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop)) + fs->showPageStrokeStyle(); + } else if (event->button == 3) { // right-click, popup menu + _popup_sw.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } else if (event->button == 2) { // middle click, toggle none/lastwidth? + // + } + return true; +} + +bool +SelectedStyle::on_opacity_click(GdkEventButton *event) +{ + if (event->button == 2) { // middle click + const char* opacity = _opacity_sb.get_value() < 50? "0.5" : (_opacity_sb.get_value() == 100? "0" : "1"); + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "opacity", opacity); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Change opacity"), INKSCAPE_ICON("dialog-fill-and-stroke")); + return true; + } + + return false; +} + +void SelectedStyle::on_popup_units(Inkscape::Util::Unit const *unit) { + _sw_unit = unit; + update(); +} + +void SelectedStyle::on_popup_preset(int i) { + SPCSSAttr *css = sp_repr_css_attr_new (); + gdouble w; + if (_sw_unit) { + w = Inkscape::Util::Quantity::convert(_sw_presets[i], _sw_unit, "px"); + } else { + w = _sw_presets[i]; + } + Inkscape::CSSOStringStream os; + os << w; + sp_repr_css_set_property (css, "stroke-width", os.str().c_str()); + // FIXME: update dash patterns! + sp_desktop_set_style (_desktop, css, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), _("Change stroke width"), INKSCAPE_ICON("swatches")); +} + +void +SelectedStyle::update() +{ + if (_desktop == nullptr) + return; + + // create temporary style + SPStyle query(_desktop->getDocument()); + + for (int i = SS_FILL; i <= SS_STROKE; i++) { + Gtk::EventBox *place = (i == SS_FILL)? &_fill_place : &_stroke_place; + Gtk::EventBox *flag_place = (i == SS_FILL)? &_fill_flag_place : &_stroke_flag_place; + + place->remove(); + flag_place->remove(); + + clearTooltip(*place); + clearTooltip(*flag_place); + + _mode[i] = SS_NA; + _paintserver_id[i].clear(); + + _popup_copy[i].set_sensitive(false); + + // query style from desktop. This returns a result flag and fills query with the style of subselection, if any, or selection + int result = sp_desktop_query_style (_desktop, &query, + (i == SS_FILL)? QUERY_STYLE_PROPERTY_FILL : QUERY_STYLE_PROPERTY_STROKE); + switch (result) { + case QUERY_STYLE_NOTHING: + place->add(_na[i]); + place->set_tooltip_text(__na[i]); + _mode[i] = SS_NA; + if ( _dropEnabled[i] ) { + gtk_drag_dest_unset( GTK_WIDGET((i==SS_FILL) ? _fill_place.gobj():_stroke_place.gobj()) ); + _dropEnabled[i] = false; + } + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: + case QUERY_STYLE_MULTIPLE_SAME: + if ( !_dropEnabled[i] ) { + gtk_drag_dest_set( GTK_WIDGET( (i==SS_FILL) ? _fill_place.gobj():_stroke_place.gobj()), + GTK_DEST_DEFAULT_ALL, + ui_drop_target_entries, + nui_drop_target_entries, + GdkDragAction(GDK_ACTION_COPY | GDK_ACTION_MOVE) ); + _dropEnabled[i] = true; + } + SPIPaint *paint; + if (i == SS_FILL) { + paint = &(query.fill); + } else { + paint = &(query.stroke); + } + if (paint->set && paint->isPaintserver()) { + SPPaintServer *server = (i == SS_FILL)? SP_STYLE_FILL_SERVER (&query) : SP_STYLE_STROKE_SERVER (&query); + if ( server ) { + Inkscape::XML::Node *srepr = server->getRepr(); + _paintserver_id[i] += "url(#"; + _paintserver_id[i] += srepr->attribute("id"); + _paintserver_id[i] += ")"; + + if (SP_IS_LINEARGRADIENT(server)) { + SPGradient *vector = SP_GRADIENT(server)->getVector(); + _gradient_preview_l[i]->set_gradient(vector); + place->add(_gradient_box_l[i]); + place->set_tooltip_text(__lgradient[i]); + _mode[i] = SS_LGRADIENT; + } else if (SP_IS_RADIALGRADIENT(server)) { + SPGradient *vector = SP_GRADIENT(server)->getVector(); + _gradient_preview_r[i]->set_gradient(vector); + place->add(_gradient_box_r[i]); + place->set_tooltip_text(__rgradient[i]); + _mode[i] = SS_RGRADIENT; +#ifdef WITH_MESH + } else if (SP_IS_MESHGRADIENT(server)) { + SPGradient *array = SP_GRADIENT(server)->getArray(); + _gradient_preview_m[i]->set_gradient(array); + place->add(_gradient_box_m[i]); + place->set_tooltip_text(__mgradient[i]); + _mode[i] = SS_MGRADIENT; +#endif + } else if (SP_IS_PATTERN(server)) { + place->add(_pattern[i]); + place->set_tooltip_text(__pattern[i]); + _mode[i] = SS_PATTERN; + } else if (SP_IS_HATCH(server)) { + place->add(_hatch[i]); + place->set_tooltip_text(__hatch[i]); + _mode[i] = SS_HATCH; + } + } else { + g_warning ("file %s: line %d: Unknown paint server", __FILE__, __LINE__); + } + } else if (paint->set && paint->isColor()) { + guint32 color = paint->value.color.toRGBA32( + SP_SCALE24_TO_FLOAT ((i == SS_FILL)? query.fill_opacity.value : query.stroke_opacity.value)); + _lastselected[i] = _thisselected[i]; + _thisselected[i] = color; // include opacity + ((Inkscape::UI::Widget::ColorPreview*)_color_preview[i])->setRgba32 (color); + _color_preview[i]->show_all(); + place->add(*_color_preview[i]); + gchar c_string[64]; + g_snprintf (c_string, 64, "%06x/%.3g", color >> 8, SP_RGBA32_A_F(color)); + place->set_tooltip_text(__color[i] + ": " + c_string + _(", drag to adjust, middle-click to remove")); + _mode[i] = SS_COLOR; + _popup_copy[i].set_sensitive(true); + + } else if (paint->set && paint->isNone()) { + place->add(_none[i]); + place->set_tooltip_text(__none[i]); + _mode[i] = SS_NONE; + } else if (!paint->set) { + place->add(_unset[i]); + place->set_tooltip_text(__unset[i]); + _mode[i] = SS_UNSET; + } + if (result == QUERY_STYLE_MULTIPLE_AVERAGED) { + flag_place->add(_averaged[i]); + flag_place->set_tooltip_text(__averaged[i]); + } else if (result == QUERY_STYLE_MULTIPLE_SAME) { + flag_place->add(_multiple[i]); + flag_place->set_tooltip_text(__multiple[i]); + } + break; + case QUERY_STYLE_MULTIPLE_DIFFERENT: + place->add(_many[i]); + place->set_tooltip_text(__many[i]); + _mode[i] = SS_MANY; + break; + default: + break; + } + } + +// Now query opacity + clearTooltip(_opacity_place); + clearTooltip(_opacity_sb); + + int result = sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_MASTEROPACITY); + + switch (result) { + case QUERY_STYLE_NOTHING: + _opacity_place.set_tooltip_text(_("Nothing selected")); + _opacity_sb.set_tooltip_text(_("Nothing selected")); + _opacity_sb.set_sensitive(false); + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: + case QUERY_STYLE_MULTIPLE_SAME: + _opacity_place.set_tooltip_text(_("Opacity (%)")); + _opacity_sb.set_tooltip_text(_("Opacity (%)")); + if (_opacity_blocked) break; + _opacity_blocked = true; + _opacity_sb.set_sensitive(true); + _opacity_adjustment->set_value(SP_SCALE24_TO_FLOAT(query.opacity.value) * 100); + _opacity_blocked = false; + break; + } + +// Now query stroke_width + int result_sw = sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_STROKEWIDTH); + switch (result_sw) { + case QUERY_STYLE_NOTHING: + _stroke_width.set_markup(""); + current_stroke_width = 0; + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: + case QUERY_STYLE_MULTIPLE_SAME: + { + if (query.stroke_extensions.hairline) { + _stroke_width.set_markup(_("Hairline")); + auto str = Glib::ustring::compose(_("Stroke width: %1"), _("Hairline")); + _stroke_width_place.set_tooltip_text(str); + } else { + double w; + if (_sw_unit) { + w = Inkscape::Util::Quantity::convert(query.stroke_width.computed, "px", _sw_unit); + } else { + w = query.stroke_width.computed; + } + current_stroke_width = w; + + { + gchar *str = g_strdup_printf(" %#.3g", w); + if (str[strlen(str) - 1] == ',' || str[strlen(str) - 1] == '.') { + str[strlen(str)-1] = '\0'; + } + _stroke_width.set_markup(str); + g_free (str); + } + { + gchar *str = g_strdup_printf(_("Stroke width: %.5g%s%s"), + w, + _sw_unit? _sw_unit->abbr.c_str() : "px", + (result_sw == QUERY_STYLE_MULTIPLE_AVERAGED)? + _(" (averaged)") : ""); + _stroke_width_place.set_tooltip_text(str); + g_free (str); + } + } + break; + } + default: + break; + } +} + +void SelectedStyle::opacity_0() {_opacity_sb.set_value(0);} +void SelectedStyle::opacity_025() {_opacity_sb.set_value(25);} +void SelectedStyle::opacity_05() {_opacity_sb.set_value(50);} +void SelectedStyle::opacity_075() {_opacity_sb.set_value(75);} +void SelectedStyle::opacity_1() {_opacity_sb.set_value(100);} + +void SelectedStyle::on_opacity_menu (Gtk::Menu *menu) { + + std::vector<Gtk::Widget *> children = menu->get_children(); + for (auto iter : children) { + menu->remove(*iter); + } + + { + Gtk::MenuItem *item = new Gtk::MenuItem; + item->add(*(new Gtk::Label(_("0 (transparent)"), Gtk::ALIGN_START, Gtk::ALIGN_START))); + item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_0 )); + menu->add(*item); + } + { + Gtk::MenuItem *item = new Gtk::MenuItem; + item->add(*(new Gtk::Label("25%", Gtk::ALIGN_START, Gtk::ALIGN_START))); + item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_025 )); + menu->add(*item); + } + { + Gtk::MenuItem *item = new Gtk::MenuItem; + item->add(*(new Gtk::Label("50%", Gtk::ALIGN_START, Gtk::ALIGN_START))); + item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_05 )); + menu->add(*item); + } + { + Gtk::MenuItem *item = new Gtk::MenuItem; + item->add(*(new Gtk::Label("75%", Gtk::ALIGN_START, Gtk::ALIGN_START))); + item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_075 )); + menu->add(*item); + } + { + Gtk::MenuItem *item = new Gtk::MenuItem; + item->add(*(new Gtk::Label(_("100% (opaque)"), Gtk::ALIGN_START, Gtk::ALIGN_START))); + item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_1 )); + menu->add(*item); + } + + menu->show_all(); +} + +void SelectedStyle::on_opacity_changed () +{ + g_return_if_fail(_desktop); // TODO this shouldn't happen! + if (_opacity_blocked) + return; + _opacity_blocked = true; + SPCSSAttr *css = sp_repr_css_attr_new (); + Inkscape::CSSOStringStream os; + os << CLAMP ((_opacity_adjustment->get_value() / 100), 0.0, 1.0); + sp_repr_css_set_property (css, "opacity", os.str().c_str()); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::maybeDone(_desktop->getDocument(), "fillstroke:opacity", _("Change opacity"), INKSCAPE_ICON("dialog-fill-and-stroke")); + _opacity_blocked = false; +} + +/* ============================================= RotateableSwatch */ + +RotateableSwatch::RotateableSwatch(SelectedStyle *parent, guint mode) + : fillstroke(mode) + , parent(parent) +{ +} + +RotateableSwatch::~RotateableSwatch() = default; + +double +RotateableSwatch::color_adjust(float *hsla, double by, guint32 cc, guint modifier) +{ + SPColor::rgb_to_hsl_floatv (hsla, SP_RGBA32_R_F(cc), SP_RGBA32_G_F(cc), SP_RGBA32_B_F(cc)); + hsla[3] = SP_RGBA32_A_F(cc); + double diff = 0; + if (modifier == 2) { // saturation + double old = hsla[1]; + if (by > 0) { + hsla[1] += by * (1 - hsla[1]); + } else { + hsla[1] += by * (hsla[1]); + } + diff = hsla[1] - old; + } else if (modifier == 1) { // lightness + double old = hsla[2]; + if (by > 0) { + hsla[2] += by * (1 - hsla[2]); + } else { + hsla[2] += by * (hsla[2]); + } + diff = hsla[2] - old; + } else if (modifier == 3) { // alpha + double old = hsla[3]; + hsla[3] += by/2; + if (hsla[3] < 0) { + hsla[3] = 0; + } else if (hsla[3] > 1) { + hsla[3] = 1; + } + diff = hsla[3] - old; + } else { // hue + double old = hsla[0]; + hsla[0] += by/2; + while (hsla[0] < 0) + hsla[0] += 1; + while (hsla[0] > 1) + hsla[0] -= 1; + diff = hsla[0] - old; + } + + float rgb[3]; + SPColor::hsl_to_rgb_floatv (rgb, hsla[0], hsla[1], hsla[2]); + + gchar c[64]; + sp_svg_write_color (c, sizeof(c), + SP_RGBA32_U_COMPOSE( + (SP_COLOR_F_TO_U(rgb[0])), + (SP_COLOR_F_TO_U(rgb[1])), + (SP_COLOR_F_TO_U(rgb[2])), + 0xff + ) + ); + + SPCSSAttr *css = sp_repr_css_attr_new (); + + if (modifier == 3) { // alpha + Inkscape::CSSOStringStream osalpha; + osalpha << hsla[3]; + sp_repr_css_set_property(css, (fillstroke == SS_FILL) ? "fill-opacity" : "stroke-opacity", osalpha.str().c_str()); + } else { + sp_repr_css_set_property (css, (fillstroke == SS_FILL) ? "fill" : "stroke", c); + } + sp_desktop_set_style (parent->getDesktop(), css); + sp_repr_css_attr_unref (css); + return diff; +} + +void +RotateableSwatch::do_motion(double by, guint modifier) { + if (parent->_mode[fillstroke] != SS_COLOR) + return; + + if (!scrolling && !cr_set) { + + std::string cursor_filename = "adjust_hue.svg"; + if (modifier == 2) { + cursor_filename = "adjust_saturation.svg"; + } else if (modifier == 1) { + cursor_filename = "adjust_lightness.svg"; + } else if (modifier == 3) { + cursor_filename = "adjust_alpha.svg"; + } + + auto window = get_window(); + auto cursor = load_svg_cursor(get_display(), window, cursor_filename); + get_window()->set_cursor(cursor); + } + + guint32 cc; + if (!startcolor_set) { + cc = startcolor = parent->_thisselected[fillstroke]; + startcolor_set = true; + } else { + cc = startcolor; + } + + float hsla[4]; + double diff = 0; + + diff = color_adjust(hsla, by, cc, modifier); + + if (modifier == 3) { // alpha + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust alpha")), INKSCAPE_ICON("dialog-fill-and-stroke")); + double ch = hsla[3]; + parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>alpha</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Ctrl</b> to adjust lightness, with <b>Shift</b> to adjust saturation, without modifiers to adjust hue"), ch - diff, ch, diff); + + } else if (modifier == 2) { // saturation + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust saturation")), INKSCAPE_ICON("dialog-fill-and-stroke")); + double ch = hsla[1]; + parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>saturation</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Ctrl</b> to adjust lightness, with <b>Alt</b> to adjust alpha, without modifiers to adjust hue"), ch - diff, ch, diff); + + } else if (modifier == 1) { // lightness + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust lightness")), INKSCAPE_ICON("dialog-fill-and-stroke")); + double ch = hsla[2]; + parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>lightness</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Shift</b> to adjust saturation, with <b>Alt</b> to adjust alpha, without modifiers to adjust hue"), ch - diff, ch, diff); + + } else { // hue + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust hue")), INKSCAPE_ICON("dialog-fill-and-stroke")); + double ch = hsla[0]; + parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>hue</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Shift</b> to adjust saturation, with <b>Alt</b> to adjust alpha, with <b>Ctrl</b> to adjust lightness"), ch - diff, ch, diff); + } +} + + +void +RotateableSwatch::do_scroll(double by, guint modifier) { + do_motion(by/30.0, modifier); + do_release(by/30.0, modifier); +} + +void +RotateableSwatch::do_release(double by, guint modifier) { + if (parent->_mode[fillstroke] != SS_COLOR) + return; + + float hsla[4]; + color_adjust(hsla, by, startcolor, modifier); + + if (cr_set) { + get_window()->set_cursor(); // Use parent window cursor. + cr_set = false; + } + + if (modifier == 3) { // alpha + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, ("Adjust alpha"), INKSCAPE_ICON("dialog-fill-and-stroke")); + + } else if (modifier == 2) { // saturation + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, ("Adjust saturation"), INKSCAPE_ICON("dialog-fill-and-stroke")); + + } else if (modifier == 1) { // lightness + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, ("Adjust lightness"), INKSCAPE_ICON("dialog-fill-and-stroke")); + + } else { // hue + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, ("Adjust hue"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } + + if (!strcmp(undokey, "ssrot1")) { + undokey = "ssrot2"; + } else { + undokey = "ssrot1"; + } + + parent->getDesktop()->event_context->message_context->clear(); + startcolor_set = false; +} + +/* ============================================= RotateableStrokeWidth */ + +RotateableStrokeWidth::RotateableStrokeWidth(SelectedStyle *parent) : + parent(parent), + startvalue(0), + startvalue_set(false), + undokey("swrot1") +{ +} + +RotateableStrokeWidth::~RotateableStrokeWidth() = default; + +double +RotateableStrokeWidth::value_adjust(double current, double by, guint /*modifier*/, bool final) +{ + double newval; + // by is -1..1 + double max_f = 50; // maximum width is (current * max_f), minimum - zero + newval = current * (std::exp(std::log(max_f-1) * (by+1)) - 1) / (max_f-2); + + SPCSSAttr *css = sp_repr_css_attr_new (); + if (final && newval < 1e-6) { + // if dragged into zero and this is the final adjust on mouse release, delete stroke; + // if it's not final, leave it a chance to increase again (which is not possible with "none") + sp_repr_css_set_property (css, "stroke", "none"); + } else { + newval = Inkscape::Util::Quantity::convert(newval, parent->_sw_unit, "px"); + Inkscape::CSSOStringStream os; + os << newval; + sp_repr_css_set_property (css, "stroke-width", os.str().c_str()); + } + + sp_desktop_set_style (parent->getDesktop(), css); + sp_repr_css_attr_unref (css); + return newval - current; +} + +void +RotateableStrokeWidth::do_motion(double by, guint modifier) { + + // if this is the first motion after a mouse grab, remember the current width + if (!startvalue_set) { + startvalue = parent->current_stroke_width; + // if it's 0, adjusting (which uses multiplication) will not be able to change it, so we + // cheat and provide a non-zero value + if (startvalue == 0) + startvalue = 1; + startvalue_set = true; + } + + if (modifier == 3) { // Alt, do nothing + } else { + double diff = value_adjust(startvalue, by, modifier, false); + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust stroke width")), INKSCAPE_ICON("dialog-fill-and-stroke")); + parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>stroke width</b>: was %.3g, now <b>%.3g</b> (diff %.3g)"), startvalue, startvalue + diff, diff); + } +} + +void +RotateableStrokeWidth::do_release(double by, guint modifier) { + + if (modifier == 3) { // do nothing + + } else { + value_adjust(startvalue, by, modifier, true); + startvalue_set = false; + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust stroke width")), INKSCAPE_ICON("dialog-fill-and-stroke")); + } + + if (!strcmp(undokey, "swrot1")) { + undokey = "swrot2"; + } else { + undokey = "swrot1"; + } + parent->getDesktop()->event_context->message_context->clear(); +} + +void +RotateableStrokeWidth::do_scroll(double by, guint modifier) { + do_motion(by/10.0, modifier); + startvalue_set = false; +} + +Dialog::FillAndStroke *get_fill_and_stroke_panel(SPDesktop *desktop) +{ + desktop->getContainer()->new_dialog("FillStroke"); + return dynamic_cast<Dialog::FillAndStroke *>(desktop->getContainer()->get_dialog("FillStroke")); +} + +} // namespace Widget +} // 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/widget/selected-style.h b/src/ui/widget/selected-style.h new file mode 100644 index 0000000..0ad002b --- /dev/null +++ b/src/ui/widget/selected-style.h @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * buliabyak@gmail.com + * scislac@users.sf.net + * + * Copyright (C) 2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_CURRENT_STYLE_H +#define INKSCAPE_UI_CURRENT_STYLE_H + +#include <gtkmm/box.h> +#include <gtkmm/grid.h> + +#include <gtkmm/label.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/enums.h> +#include <gtkmm/menu.h> +#include <gtkmm/menuitem.h> +#include <gtkmm/adjustment.h> +#include <gtkmm/radiobuttongroup.h> +#include <gtkmm/radiomenuitem.h> +#include "ui/widget/spinbutton.h" + +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "rotateable.h" + +constexpr int SELECTED_STYLE_SB_WIDTH = 48; +constexpr int SELECTED_STYLE_PLACE_WIDTH = 50; +constexpr int SELECTED_STYLE_STROKE_WIDTH = 40; +constexpr int SELECTED_STYLE_FLAG_WIDTH = 12; +constexpr int SELECTED_STYLE_WIDTH = 250; + +class SPDesktop; + +namespace Inkscape { + +namespace Util { + class Unit; +} + +namespace UI { +namespace Widget { + +enum { + SS_NA, + SS_NONE, + SS_UNSET, + SS_PATTERN, + SS_LGRADIENT, + SS_RGRADIENT, +#ifdef WITH_MESH + SS_MGRADIENT, +#endif + SS_MANY, + SS_COLOR, + SS_HATCH +}; + +enum { + SS_FILL, + SS_STROKE +}; + +class GradientImage; +class SelectedStyle; + +class RotateableSwatch : public Rotateable { + public: + RotateableSwatch(SelectedStyle *parent, guint mode); + ~RotateableSwatch() override; + + double color_adjust (float *hsl, double by, guint32 cc, guint state); + + void do_motion (double by, guint state) override; + void do_release (double by, guint state) override; + void do_scroll (double by, guint state) override; + +private: + guint fillstroke; + + SelectedStyle *parent; + + guint32 startcolor = 0; + bool startcolor_set = false; + + gchar const *undokey = "ssrot1"; + + bool cr_set = false; +}; + +class RotateableStrokeWidth : public Rotateable { + public: + RotateableStrokeWidth(SelectedStyle *parent); + ~RotateableStrokeWidth() override; + + double value_adjust(double current, double by, guint modifier, bool final); + void do_motion (double by, guint state) override; + void do_release (double by, guint state) override; + void do_scroll (double by, guint state) override; + +private: + SelectedStyle *parent; + + double startvalue; + bool startvalue_set; + + gchar const *undokey; +}; + +/** + * Selected style indicator (fill, stroke, opacity). + */ +class SelectedStyle : public Gtk::Box +{ +public: + SelectedStyle(bool layout = true); + + ~SelectedStyle() override; + + void setDesktop(SPDesktop *desktop); + SPDesktop *getDesktop() {return _desktop;} + void update(); + + guint32 _lastselected[2]; + guint32 _thisselected[2]; + + guint _mode[2]; + + double current_stroke_width; + Inkscape::Util::Unit const *_sw_unit; // points to object in UnitTable, do not delete + +protected: + SPDesktop *_desktop; + + Gtk::Grid _table; + + Gtk::Label _fill_label; + Gtk::Label _stroke_label; + Gtk::Label _opacity_label; + + RotateableSwatch _fill_place; + RotateableSwatch _stroke_place; + + Gtk::EventBox _fill_flag_place; + Gtk::EventBox _stroke_flag_place; + + Gtk::EventBox _opacity_place; + Glib::RefPtr<Gtk::Adjustment> _opacity_adjustment; + Inkscape::UI::Widget::SpinButton _opacity_sb; + + Gtk::Label _na[2]; + Glib::ustring __na[2]; + + Gtk::Label _none[2]; + Glib::ustring __none[2]; + + Gtk::Label _pattern[2]; + Glib::ustring __pattern[2]; + + Gtk::Label _hatch[2]; + Glib::ustring __hatch[2]; + + Gtk::Label _lgradient[2]; + Glib::ustring __lgradient[2]; + + GradientImage *_gradient_preview_l[2]; + Gtk::Box _gradient_box_l[2]; + + Gtk::Label _rgradient[2]; + Glib::ustring __rgradient[2]; + + GradientImage *_gradient_preview_r[2]; + Gtk::Box _gradient_box_r[2]; + +#ifdef WITH_MESH + Gtk::Label _mgradient[2]; + Glib::ustring __mgradient[2]; + + GradientImage *_gradient_preview_m[2]; + Gtk::Box _gradient_box_m[2]; +#endif + + Gtk::Label _many[2]; + Glib::ustring __many[2]; + + Gtk::Label _unset[2]; + Glib::ustring __unset[2]; + + Gtk::Widget *_color_preview[2]; + Glib::ustring __color[2]; + + Gtk::Label _averaged[2]; + Glib::ustring __averaged[2]; + Gtk::Label _multiple[2]; + Glib::ustring __multiple[2]; + + Gtk::Box _fill; + Gtk::Box _stroke; + RotateableStrokeWidth _stroke_width_place; + Gtk::Label _stroke_width; + Gtk::Label _fill_empty_space; + + Glib::ustring _paintserver_id[2]; + + sigc::connection *selection_changed_connection; + sigc::connection *selection_modified_connection; + sigc::connection *subselection_changed_connection; + + static void dragDataReceived( GtkWidget *widget, + GdkDragContext *drag_context, + gint x, gint y, + GtkSelectionData *data, + guint info, + guint event_time, + gpointer user_data ); + + bool on_fill_click(GdkEventButton *event); + bool on_stroke_click(GdkEventButton *event); + bool on_opacity_click(GdkEventButton *event); + bool on_sw_click(GdkEventButton *event); + + bool _opacity_blocked; + void on_opacity_changed(); + void on_opacity_menu(Gtk::Menu *menu); + void opacity_0(); + void opacity_025(); + void opacity_05(); + void opacity_075(); + void opacity_1(); + + void on_fill_remove(); + void on_stroke_remove(); + void on_fill_lastused(); + void on_stroke_lastused(); + void on_fill_lastselected(); + void on_stroke_lastselected(); + void on_fill_unset(); + void on_stroke_unset(); + void on_fill_edit(); + void on_stroke_edit(); + void on_fillstroke_swap(); + void on_fill_invert(); + void on_stroke_invert(); + void on_fill_white(); + void on_stroke_white(); + void on_fill_black(); + void on_stroke_black(); + void on_fill_copy(); + void on_stroke_copy(); + void on_fill_paste(); + void on_stroke_paste(); + void on_fill_opaque(); + void on_stroke_opaque(); + + Gtk::Menu _popup[2]; + Gtk::MenuItem _popup_edit[2]; + Gtk::MenuItem _popup_lastused[2]; + Gtk::MenuItem _popup_lastselected[2]; + Gtk::MenuItem _popup_invert[2]; + Gtk::MenuItem _popup_white[2]; + Gtk::MenuItem _popup_black[2]; + Gtk::MenuItem _popup_copy[2]; + Gtk::MenuItem _popup_paste[2]; + Gtk::MenuItem _popup_swap[2]; + Gtk::MenuItem _popup_opaque[2]; + Gtk::MenuItem _popup_unset[2]; + Gtk::MenuItem _popup_remove[2]; + + Gtk::Menu _popup_sw; + Gtk::RadioButtonGroup _sw_group; + std::vector<Gtk::RadioMenuItem*> _unit_mis; + void on_popup_units(Inkscape::Util::Unit const *u); + void on_popup_preset(int i); + Gtk::MenuItem _popup_sw_remove; + + void *_drop[2]; + bool _dropEnabled[2]; +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_BUTTON_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/widget/shapeicon.cpp b/src/ui/widget/shapeicon.cpp new file mode 100644 index 0000000..6b7a9d9 --- /dev/null +++ b/src/ui/widget/shapeicon.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2020 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "color.h" +#include "ui/widget/shapeicon.h" +#include "ui/icon-loader.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/* + * This is a type of CellRenderer which you might expect to inherit from the + * pixbuf CellRenderer, but we actually need to write a Cairo surface directly + * in order to maintain HiDPI sharpness in icons. Upstream Gtk have made it clear + * that CellRenderers are going away in Gtk4 so they aren't interested in fixing + * rendering problems like the one in CellRendererPixbuf. + * + * See: https://gitlab.gnome.org/GNOME/gtk/-/issues/613 + */ + +void CellRendererItemIcon::render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags) +{ + std::string shape_type = _property_shape_type.get_value(); + std::string highlight = SPColor(_property_color.get_value()).toString(); + std::string cache_id = shape_type + "-" + highlight; + + // if the icon isn't cached, render it to a pixbuf + int scale = widget.get_scale_factor(); + if ( !_icon_cache[cache_id] ) { + _icon_cache[cache_id] = sp_get_shape_icon(shape_type, Gdk::RGBA(highlight), _size, scale); + } + g_return_if_fail(_icon_cache[cache_id]); + + // Center the icon in the cell area + int x = cell_area.get_x() + int((cell_area.get_width() - _size) * 0.5); + int y = cell_area.get_y() + int((cell_area.get_height() - _size) * 0.5); + + // Paint the pixbuf to a cairo surface to get HiDPI support + paint_icon(cr, widget, _icon_cache[cache_id], x, y); + + // Create an overlay icon + int clipmask = _property_clipmask.get_value(); + if (clipmask > 0) { + if (!_clip_overlay) { + _clip_overlay = sp_get_icon_pixbuf("overlay-clip", Gtk::ICON_SIZE_MENU, scale); + } + if (!_mask_overlay) { + _mask_overlay = sp_get_icon_pixbuf("overlay-mask", Gtk::ICON_SIZE_MENU, scale); + } + + if (clipmask == OVERLAY_CLIP && _clip_overlay) { + paint_icon(cr, widget, _clip_overlay, x, y); + } + if (clipmask == OVERLAY_MASK && _mask_overlay) { + paint_icon(cr, widget, _mask_overlay, x, y); + } + } + +} + +void CellRendererItemIcon::paint_icon(const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + Glib::RefPtr<Gdk::Pixbuf> pixbuf, + int x, int y) +{ + cairo_surface_t *surface = gdk_cairo_surface_create_from_pixbuf( + pixbuf->gobj(), 0, widget.get_window()->gobj()); + if (!surface) return; + cairo_set_source_surface(cr->cobj(), surface, x, y); + cr->set_operator(Cairo::OPERATOR_ATOP); + cr->rectangle(x, y, _size, _size); + cr->fill(); + cairo_surface_destroy(surface); // free! +} + +void CellRendererItemIcon::get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const +{ + min_h = _size; + nat_h = _size + 4; +} + +void CellRendererItemIcon::get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const +{ + min_w = _size; + nat_w = _size + 4; +} + + +} // namespace Widget +} // 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/widget/shapeicon.h b/src/ui/widget/shapeicon.h new file mode 100644 index 0000000..b5ca033 --- /dev/null +++ b/src/ui/widget/shapeicon.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __UI_DIALOG_SHAPEICON_H__ +#define __UI_DIALOG_SHAPEICON_H__ +/* + * Authors: + * Martin Owens + * + * Copyright (C) 2020 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/iconinfo.h> +#include <gtkmm/cellrenderer.h> +#include <gtkmm/widget.h> +#include <glibmm/property.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +// Object overlay states usually modify the icon and indicate +// That there may be non-item children under this item (e.g. clip) +using OverlayState = int; +enum OverlayStates : OverlayState { + OVERLAY_NONE = 0, // Nothing special about the object. + OVERLAY_CLIP = 1, // Object has a clip + OVERLAY_MASK = 2 // Object has a mask +}; + +/* Custom cell renderer for type icon */ +class CellRendererItemIcon : public Gtk::CellRenderer { +public: + + CellRendererItemIcon() : + Glib::ObjectBase(typeid(CellRenderer)), + Gtk::CellRenderer(), + _property_shape_type(*this, "shape_type", "unknown"), + _property_color(*this, "color", 0), + _property_clipmask(*this, "clipmask", 0), + _clip_overlay(nullptr), + _mask_overlay(nullptr) + { + Gtk::IconSize::lookup(Gtk::ICON_SIZE_MENU, _size, _size); + } + ~CellRendererItemIcon() override = default; + + Glib::PropertyProxy<std::string> property_shape_type() { + return _property_shape_type.get_proxy(); + } + Glib::PropertyProxy<unsigned int> property_color() { + return _property_color.get_proxy(); + } + Glib::PropertyProxy<unsigned int> property_clipmask() { + return _property_clipmask.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; + void paint_icon(const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + Glib::RefPtr<Gdk::Pixbuf> pixbuf, + int x, int y); + + void get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const override; + void get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const override; + +private: + + int _size; + Glib::Property<std::string> _property_shape_type; + Glib::Property<unsigned int> _property_color; + Glib::Property<unsigned int> _property_clipmask; + std::map<const std::string, Glib::RefPtr<Gdk::Pixbuf> > _icon_cache; + + // Overlay indicators + Glib::RefPtr<Gdk::Pixbuf> _mask_overlay; + Glib::RefPtr<Gdk::Pixbuf> _clip_overlay; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + + +#endif /* __UI_DIALOG_SHAPEICON_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/widget/spin-button-tool-item.cpp b/src/ui/widget/spin-button-tool-item.cpp new file mode 100644 index 0000000..08ba38b --- /dev/null +++ b/src/ui/widget/spin-button-tool-item.cpp @@ -0,0 +1,607 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "spin-button-tool-item.h" + +#include <algorithm> +#include <gtkmm/box.h> +#include <gtkmm/image.h> +#include <gtkmm/radiomenuitem.h> +#include <gtkmm/toolbar.h> + +#include <cmath> +#include <utility> + +#include "spinbutton.h" +#include "ui/icon-loader.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * \brief Handler for the button's "focus-in-event" signal + * + * \param focus_event The event that triggered the signal + * + * \detail This just logs the current value of the spin-button + * and sets the _transfer_focus flag + */ +bool +SpinButtonToolItem::on_btn_focus_in_event(GdkEventFocus * /* focus_event */) +{ + _last_val = _btn->get_value(); + _transfer_focus = true; + + return false; // Event not consumed +} + +/** + * \brief Handler for the button's "focus-out-event" signal + * + * \param focus_event The event that triggered the signal + * + * \detail This just unsets the _transfer_focus flag + */ +bool +SpinButtonToolItem::on_btn_focus_out_event(GdkEventFocus * /* focus_event */) +{ + _transfer_focus = false; + + return false; // Event not consumed +} + +/** + * \brief Handler for the button's "key-press-event" signal + * + * \param key_event The event that triggered the signal + * + * \detail If the ESC key was pressed, restore the last value and defocus. + * If the Enter key was pressed, just defocus. + */ +bool +SpinButtonToolItem::on_btn_key_press_event(GdkEventKey *key_event) +{ + bool was_consumed = false; // Whether event has been consumed or not + auto display = Gdk::Display::get_default(); + auto keymap = display->get_keymap(); + guint key = 0; + gdk_keymap_translate_keyboard_state(keymap, key_event->hardware_keycode, + static_cast<GdkModifierType>(key_event->state), + 0, &key, 0, 0, 0); + + auto val = _btn->get_value(); + + switch(key) { + case GDK_KEY_Escape: + { + _transfer_focus = true; + _btn->set_value(_last_val); + defocus(); + was_consumed = true; + } + break; + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + { + _transfer_focus = true; + defocus(); + was_consumed = true; + } + break; + + case GDK_KEY_Tab: + { + _transfer_focus = false; + was_consumed = process_tab(1); + } + break; + + case GDK_KEY_ISO_Left_Tab: + { + _transfer_focus = false; + was_consumed = process_tab(-1); + } + break; + + // TODO: Enable variable step-size if this is ever used + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + { + _transfer_focus = false; + _btn->set_value(val+1); + was_consumed=true; + } + break; + + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + { + _transfer_focus = false; + _btn->set_value(val-1); + was_consumed=true; + } + break; + + case GDK_KEY_Page_Up: + case GDK_KEY_KP_Page_Up: + { + _transfer_focus = false; + _btn->set_value(val+10); + was_consumed=true; + } + break; + + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Down: + { + _transfer_focus = false; + _btn->set_value(val-10); + was_consumed=true; + } + break; + + case GDK_KEY_z: + case GDK_KEY_Z: + { + _transfer_focus = false; + _btn->set_value(_last_val); + was_consumed = true; + } + break; + } + + return was_consumed; +} + +/** + * \brief Shift focus to a different widget + * + * \details This only has an effect if the _transfer_focus flag and the _focus_widget are set + */ +void +SpinButtonToolItem::defocus() +{ + if(_transfer_focus && _focus_widget) { + _focus_widget->grab_focus(); + } +} + +/** + * \brief Move focus to another spinbutton in the toolbar + * + * \param increment[in] The number of places to shift within the toolbar + */ +bool +SpinButtonToolItem::process_tab(int increment) +{ + // If the increment is zero, do nothing + if(increment == 0) return true; + + // Here, we're working through the widget hierarchy: + // Toolbar + // |- ToolItem (*this) + // |-> Box + // |-> SpinButton (*_btn) + // + // Our aim is to find the next/previous spin-button within a toolitem in our toolbar + + bool handled = false; + + // We only bother doing this if the current item is actually in a toolbar! + auto toolbar = dynamic_cast<Gtk::Toolbar *>(get_parent()); + + if (toolbar) { + // Get the index of the current item within the toolbar and the total number of items + auto my_index = toolbar->get_item_index(*this); + auto n_items = toolbar->get_n_items(); + + auto test_index = my_index + increment; // The index of the item we want to check + + // Loop through tool items as long as we're within the limits of the toolbar and + // we haven't yet found our new item to focus on + while(test_index > 0 && test_index <= n_items && !handled) { + + auto tool_item = toolbar->get_nth_item(test_index); + + if(tool_item) { + // There are now two options that we support: + if (auto sb_tool_item = dynamic_cast<SpinButtonToolItem *>(tool_item)) { + // (1) The tool item is a SpinButtonToolItem, in which case, we just pass + // focus to its spin-button + sb_tool_item->grab_button_focus(); + handled = true; + } + else if(dynamic_cast<Gtk::SpinButton *>(tool_item->get_child())) { + // (2) The tool item contains a plain Gtk::SpinButton, in which case we + // pass focus directly to it + tool_item->get_child()->grab_focus(); + } + } + + test_index += increment; + } + } + + return handled; +} + +/** + * \brief Handler for toggle events on numeric menu items + * + * \details Sets the adjustment to the desired value + */ +void +SpinButtonToolItem::on_numeric_menu_item_toggled(double value, Gtk::RadioMenuItem* button) +{ + // Called both when Radio button is deactivated and activated. Only set when activated. + if (button->get_active()) { + auto adj = _btn->get_adjustment(); + adj->set_value(value); + } +} + +Gtk::RadioMenuItem * +SpinButtonToolItem::create_numeric_menu_item(Gtk::RadioButtonGroup *group, + double value, + const Glib::ustring& label, + bool enable) +{ + // Represent the value as a string + std::ostringstream ss; + ss << value; + + Glib::ustring item_label = ss.str(); + + // Append the label if specified + if (!label.empty()) { + item_label += ": " + label; + } + + auto numeric_option = Gtk::manage(new Gtk::RadioMenuItem(*group, item_label)); + if (enable) { + numeric_option->set_active(); // Do before connecting toggled_handler. + } + + // Set the adjustment value in response to changes in the selected item + auto toggled_handler = sigc::bind(sigc::mem_fun(*this, &SpinButtonToolItem::on_numeric_menu_item_toggled), value, numeric_option); + numeric_option->signal_toggled().connect(toggled_handler); + + return numeric_option; +} + +/** + * \brief Create a menu containing fixed numeric options for the adjustment + * + * \details Each of these values represents a snap-point for the adjustment's value + */ +Gtk::Menu * +SpinButtonToolItem::create_numeric_menu() +{ + auto numeric_menu = Gtk::manage(new Gtk::Menu()); + + Gtk::RadioMenuItem::Group group; + + // Get values for the adjustment + auto adj = _btn->get_adjustment(); + auto adj_value = round_to_precision(adj->get_value()); + auto lower = round_to_precision(adj->get_lower()); + auto upper = round_to_precision(adj->get_upper()); + auto page = adj->get_page_increment(); + + // Start by setting some fixed values based on the adjustment's + // parameters. + NumericMenuData values; + + // first add all custom items (necessary) + for (auto custom_data : _custom_menu_data) { + if (custom_data.first >= lower && custom_data.first <= upper) { + values.emplace(custom_data); + } + } + + values.emplace(adj_value, ""); + + // for quick page changes using mouse, step can changes can be done with +/- buttons on + // SpinButton + values.emplace(::fmin(adj_value + page, upper), ""); + values.emplace(::fmax(adj_value - page, lower), ""); + + // add upper/lower limits to options + if (_show_upper_limit) { + values.emplace(upper, ""); + } + if (_show_lower_limit) { + values.emplace(lower, ""); + } + + auto add_item = [&numeric_menu, this, &group, adj_value](ValueLabel value){ + bool enable = (adj_value == value.first); + auto numeric_menu_item = create_numeric_menu_item(&group, value.first, value.second, enable); + numeric_menu->append(*numeric_menu_item); + }; + + if (_sort_decreasing) { + std::for_each(values.crbegin(), values.crend(), add_item); + } else { + std::for_each(values.cbegin(), values.cend(), add_item); + } + + return numeric_menu; +} + +/** + * \brief Create a menu-item in response to the "create-menu-proxy" signal + * + * \detail This is an override for the default Gtk::ToolItem handler so + * we don't need to explicitly connect this to the signal. It + * runs if the toolitem is unable to fit on the toolbar, and + * must be represented by a menu item instead. + */ +bool +SpinButtonToolItem::on_create_menu_proxy() +{ + // The main menu-item. It just contains the label that normally appears + // next to the spin-button, and an indicator for a sub-menu. + auto menu_item = Gtk::manage(new Gtk::MenuItem(_label_text)); + auto numeric_menu = create_numeric_menu(); + menu_item->set_submenu(*numeric_menu); + + set_proxy_menu_item(_name, *menu_item); + + return true; // Finished handling the event +} + +/** + * \brief Create a new SpinButtonToolItem + * + * \param[in] name A unique ID for this tool-item (not translatable) + * \param[in] label_text The text to display in the toolbar + * \param[in] adjustment The Gtk::Adjustment to attach to the spinbutton + * \param[in] climb_rate The climb rate for the spin button (default = 0) + * \param[in] digits Number of decimal places to display + */ +SpinButtonToolItem::SpinButtonToolItem(const Glib::ustring name, + const Glib::ustring& label_text, + Glib::RefPtr<Gtk::Adjustment>& adjustment, + double climb_rate, + int digits) + : _btn(Gtk::manage(new SpinButton(adjustment, climb_rate, digits))), + _name(std::move(name)), + _label_text(label_text), + _digits(digits) +{ + set_margin_start(3); + set_margin_end(3); + set_name(_name); + + // Handle popup menu + _btn->signal_popup_menu().connect(sigc::mem_fun(*this, &SpinButtonToolItem::on_popup_menu), false); + + // Handle button events + auto btn_focus_in_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_focus_in_event); + _btn->signal_focus_in_event().connect(btn_focus_in_event_cb, false); + + auto btn_focus_out_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_focus_out_event); + _btn->signal_focus_out_event().connect(btn_focus_out_event_cb, false); + + auto btn_key_press_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_key_press_event); + _btn->signal_key_press_event().connect(btn_key_press_event_cb, false); + + auto btn_button_press_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_button_press_event); + _btn->signal_button_press_event().connect(btn_button_press_event_cb, false); + + _btn->add_events(Gdk::KEY_PRESS_MASK); + + // Create a label + _label = Gtk::manage(new Gtk::Label(label_text)); + + // Arrange the widgets in a horizontal box + _hbox = Gtk::manage(new Gtk::Box()); + _hbox->set_spacing(3); + _hbox->pack_start(*_label); + _hbox->pack_start(*_btn); + add(*_hbox); + show_all(); +} + +void +SpinButtonToolItem::set_icon(const Glib::ustring& icon_name) +{ + _hbox->remove(*_label); + _icon = Gtk::manage(sp_get_icon_image(icon_name, Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + if(_icon) { + _hbox->pack_start(*_icon); + _hbox->reorder_child(*_icon, 0); + } + + show_all(); +} + +bool +SpinButtonToolItem::on_btn_button_press_event(const GdkEventButton *button_event) +{ + if (gdk_event_triggers_context_menu(reinterpret_cast<const GdkEvent *>(button_event)) && + button_event->type == GDK_BUTTON_PRESS) { + do_popup_menu(button_event); + return true; + } + + return false; +} + +void +SpinButtonToolItem::do_popup_menu(const GdkEventButton *button_event) +{ + auto menu = create_numeric_menu(); + menu->attach_to_widget(*_btn); + menu->show_all(); + menu->popup_at_pointer(reinterpret_cast<const GdkEvent *>(button_event)); +} + +/** + * \brief Create a popup menu + */ +bool +SpinButtonToolItem::on_popup_menu() +{ + do_popup_menu(nullptr); + return true; +} + +/** + * \brief Transfers focus to the child spinbutton by default + */ +void +SpinButtonToolItem::on_grab_focus() +{ + grab_button_focus(); +} + +/** + * \brief Set the tooltip to display on this (and all child widgets) + * + * \param[in] text The tooltip to display + */ +void +SpinButtonToolItem::set_all_tooltip_text(const Glib::ustring& text) +{ + set_tooltip_text(text); + _btn->set_tooltip_text(text); +} + +/** + * \brief Set the widget that focus moves to when this one loses focus + * + * \param widget The widget that will gain focus + */ +void +SpinButtonToolItem::set_focus_widget(Gtk::Widget *widget) +{ + _focus_widget = widget; +} + +/** + * \brief Grab focus on the spin-button widget + */ +void +SpinButtonToolItem::grab_button_focus() +{ + _btn->grab_focus(); +} + +/** + * \brief A wrapper of Geom::decimal_round to remember precision + */ +double +SpinButtonToolItem::round_to_precision(double value) { + return Geom::decimal_round(value, _digits); +} + +/** + * \brief [discouraged] Set numeric data option in Radio menu. + * + * \param[in] values values to provide as options + * \param[in] labels label to show for the value at same index in values. + * + * \detail Use is advised only when there are no labels. + * This is discouraged in favor of other overloads of the function, due to error prone + * usage. Using two vectors for related data, undermining encapsulation. + */ +void +SpinButtonToolItem::set_custom_numeric_menu_data(const std::vector<double>& values, + const std::vector<Glib::ustring>& labels) +{ + + if (values.size() != labels.size() && !labels.empty()) { + g_warning("Cannot add custom menu items. Value and label arrays are different sizes"); + return; + } + + _custom_menu_data.clear(); + + if (labels.empty()) { + for (const auto &value : values) { + _custom_menu_data.emplace(round_to_precision(value), ""); + } + return; + } + + int i = 0; + for (const auto &value : values) { + _custom_menu_data.emplace(round_to_precision(value), labels[i++]); + } +} + +/** + * \brief Set numeric data options for Radio menu (densely labeled data). + * + * \param[in] value_labels value and labels to provide as options + * + * \detail Should be used when most of the values have an associated label (densely labeled data) + * + */ +void +SpinButtonToolItem::set_custom_numeric_menu_data(const std::vector<ValueLabel>& value_labels) { + _custom_menu_data.clear(); + for(const auto& value_label : value_labels) { + _custom_menu_data.emplace(round_to_precision(value_label.first), value_label.second); + } +} + + +/** + * \brief Set numeric data options for Radio menu (sparsely labeled data). + * + * \param[in] values values without labels + * \param[in] sparse_labels value and labels to provide as options + * + * \detail Should be used when very few values have an associated label (sparsely labeled data). + * Duplicate values in vector and map are acceptable but, values labels in map are + * preferred. Avoid using duplicate values intentionally though. + * + */ +void +SpinButtonToolItem::set_custom_numeric_menu_data(const std::vector<double> &values, + const std::unordered_map<double, Glib::ustring> &sparse_labels) +{ + _custom_menu_data.clear(); + + for(const auto& value_label : sparse_labels) { + _custom_menu_data.emplace(round_to_precision(value_label.first), value_label.second); + } + + for(const auto& value : values) { + _custom_menu_data.emplace(round_to_precision(value), ""); + } + +} + + +void SpinButtonToolItem::show_upper_limit(bool show) { _show_upper_limit = show; } + +void SpinButtonToolItem::show_lower_limit(bool show) { _show_lower_limit = show; } + +void SpinButtonToolItem::show_limits(bool show) { _show_upper_limit = _show_lower_limit = show; } + +void SpinButtonToolItem::sort_decreasing(bool decreasing) { _sort_decreasing = decreasing; } + +Glib::RefPtr<Gtk::Adjustment> +SpinButtonToolItem::get_adjustment() +{ + return _btn->get_adjustment(); +} +} // namespace Widget +} // 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/widget/spin-button-tool-item.h b/src/ui/widget/spin-button-tool-item.h new file mode 100644 index 0000000..73caf14 --- /dev/null +++ b/src/ui/widget/spin-button-tool-item.h @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SPIN_BUTTON_TOOL_ITEM_H +#define SEEN_SPIN_BUTTON_TOOL_ITEM_H + +#include <gtkmm/toolitem.h> +#include <unordered_map> +#include <utility> + +#include "2geom/math-utils.h" + +namespace Gtk { +class Box; +class RadioButtonGroup; +class RadioMenuItem; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class SpinButton; + +/** + * \brief A spin-button with a label that can be added to a toolbar + */ +class SpinButtonToolItem : public Gtk::ToolItem +{ +private: + using ValueLabel = std::pair<double, Glib::ustring>; + using NumericMenuData = std::map<double, Glib::ustring>; + + Glib::ustring _name; ///< A unique ID for the widget (NOT translatable) + SpinButton *_btn; ///< The spin-button within the widget + Glib::ustring _label_text; ///< A string to use in labels for the widget (translatable) + double _last_val = 0.0; ///< The last value of the adjustment + bool _transfer_focus = false; ///< Whether or not to transfer focus + + Gtk::Box *_hbox; ///< Horizontal box, to store widgets + Gtk::Widget *_label; ///< A text label to describe the setting + Gtk::Widget *_icon; ///< An icon to describe the setting + + /** A widget that grabs focus when this one loses it */ + Gtk::Widget * _focus_widget = nullptr; + + // Custom values and labels to add to the numeric popup-menu + NumericMenuData _custom_menu_data; + + // To show or not to show upper/lower limit of the adjustment + bool _show_upper_limit = false; + bool _show_lower_limit = false; + + // sort in decreasing order + bool _sort_decreasing = false; + + // digits of adjustment + int _digits; + + // just a wrapper for Geom::decimal_round to simplify calls + double round_to_precision(double value); + + // Event handlers + bool on_btn_focus_in_event(GdkEventFocus *focus_event); + bool on_btn_focus_out_event(GdkEventFocus *focus_event); + bool on_btn_key_press_event(GdkEventKey *key_event); + bool on_btn_button_press_event(const GdkEventButton *button_event); + bool on_popup_menu(); + void do_popup_menu(const GdkEventButton *button_event); + + void defocus(); + bool process_tab(int direction); + + void on_numeric_menu_item_toggled(double value, Gtk::RadioMenuItem* button); + + Gtk::Menu * create_numeric_menu(); + + Gtk::RadioMenuItem * create_numeric_menu_item(Gtk::RadioButtonGroup *group, + double value, + const Glib::ustring& label = "", + bool enable = false); + +protected: + bool on_create_menu_proxy() override; + void on_grab_focus() override; + +public: + SpinButtonToolItem(const Glib::ustring name, + const Glib::ustring& label_text, + Glib::RefPtr<Gtk::Adjustment>& adjustment, + double climb_rate = 0.1, + int digits = 3); + + void set_all_tooltip_text(const Glib::ustring& text); + void set_focus_widget(Gtk::Widget *widget); + void grab_button_focus(); + + void set_custom_numeric_menu_data(const std::vector<double>& values, + const std::vector<Glib::ustring>& labels = std::vector<Glib::ustring>()); + + void set_custom_numeric_menu_data(const std::vector<ValueLabel> &value_labels); + + void set_custom_numeric_menu_data(const std::vector<double>& values, + const std::unordered_map<double, Glib::ustring>& sparse_labels); + + Glib::RefPtr<Gtk::Adjustment> get_adjustment(); + void set_icon(const Glib::ustring& icon_name); + + // display limits + void show_upper_limit(bool show = true); + void show_lower_limit(bool show = true); + void show_limits (bool show = true); + + // sorting order + void sort_decreasing(bool decreasing = true); + + SpinButton *get_spin_button() { return _btn; }; +}; +} // namespace Widget +} // namespace UI +} // namespace Inkscape +#endif // SEEN_SPIN_BUTTON_TOOL_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/widget/spin-scale.cpp b/src/ui/widget/spin-scale.cpp new file mode 100644 index 0000000..21aa525 --- /dev/null +++ b/src/ui/widget/spin-scale.cpp @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * + * Copyright (C) 2007 Nicholas Bishop <nicholasbishop@gmail.com> + * 2008 Felipe C. da S. Sanches <juca@members.fsf.org> + * 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * Derived from and replaces SpinSlider + */ + +#include "spin-scale.h" + +#include <glibmm/i18n.h> +#include <glibmm/stringutils.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +SpinScale::SpinScale(const Glib::ustring label, double value, + double lower, double upper, + double step_increment, double page_increment, int digits, + const SPAttr a, const Glib::ustring tip_text) + : AttrWidget(a, value) + , _inkspinscale(value, lower, upper, step_increment, page_increment, 0) +{ + set_name("SpinScale"); + + _inkspinscale.set_label (label); + _inkspinscale.set_digits (digits); + _inkspinscale.set_tooltip_text (tip_text); + + _adjustment = _inkspinscale.get_adjustment(); + + signal_value_changed().connect(signal_attr_changed().make_slot()); + + pack_start(_inkspinscale); + + show_all_children(); +} + +SpinScale::SpinScale(const Glib::ustring label, + Glib::RefPtr<Gtk::Adjustment> adjustment, int digits, + const SPAttr a, const Glib::ustring tip_text) + : AttrWidget(a, 0.0) + , _inkspinscale(adjustment) +{ + set_name("SpinScale"); + + _inkspinscale.set_label (label); + _inkspinscale.set_digits (digits); + _inkspinscale.set_tooltip_text (tip_text); + + _adjustment = _inkspinscale.get_adjustment(); + + signal_value_changed().connect(signal_attr_changed().make_slot()); + + pack_start(_inkspinscale); + + show_all_children(); +} + +Glib::ustring SpinScale::get_as_attribute() const +{ + const double val = _adjustment->get_value(); + + if( _inkspinscale.get_digits() == 0) + return Glib::Ascii::dtostr((int)val); + else + return Glib::Ascii::dtostr(val); +} + +void SpinScale::set_from_attribute(SPObject* o) +{ + const gchar* val = attribute_value(o); + if (val) + _adjustment->set_value(Glib::Ascii::strtod(val)); + else + _adjustment->set_value(get_default()->as_double()); +} + +Glib::SignalProxy0<void> SpinScale::signal_value_changed() +{ + return _adjustment->signal_value_changed(); +} + +double SpinScale::get_value() const +{ + return _adjustment->get_value(); +} + +void SpinScale::set_value(const double val) +{ + _adjustment->set_value(val); +} + +void SpinScale::set_focuswidget(GtkWidget *widget) +{ + _inkspinscale.set_focus_widget(widget); +} + +const decltype(SpinScale::_adjustment) SpinScale::get_adjustment() const +{ + return _adjustment; +} + +decltype(SpinScale::_adjustment) SpinScale::get_adjustment() +{ + return _adjustment; +} + + +DualSpinScale::DualSpinScale(const Glib::ustring label1, const Glib::ustring label2, + double value, double lower, double upper, + double step_increment, double page_increment, int digits, + const SPAttr a, + const Glib::ustring tip_text1, const Glib::ustring tip_text2) + : AttrWidget(a), + _s1(label1, value, lower, upper, step_increment, page_increment, digits, SPAttr::INVALID, tip_text1), + _s2(label2, value, lower, upper, step_increment, page_increment, digits, SPAttr::INVALID, tip_text2), + //TRANSLATORS: "Link" means to _link_ two sliders together + _link(C_("Sliders", "Link")) +{ + set_name("DualSpinScale"); + signal_value_changed().connect(signal_attr_changed().make_slot()); + + _s1.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot()); + _s2.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot()); + _s1.get_adjustment()->signal_value_changed().connect(sigc::mem_fun(*this, &DualSpinScale::update_linked)); + + _link.signal_toggled().connect(sigc::mem_fun(*this, &DualSpinScale::link_toggled)); + + Gtk::Box* vb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + vb->add(_s1); + vb->add(_s2); + pack_start(*vb); + pack_start(_link, false, false); + _link.set_active(true); + + show_all(); +} + +Glib::ustring DualSpinScale::get_as_attribute() const +{ + if(_link.get_active()) + return _s1.get_as_attribute(); + else + return _s1.get_as_attribute() + " " + _s2.get_as_attribute(); +} + +void DualSpinScale::set_from_attribute(SPObject* o) +{ + const gchar* val = attribute_value(o); + if(val) { + // Split val into parts + gchar** toks = g_strsplit(val, " ", 2); + + if(toks) { + double v1 = 0.0, v2 = 0.0; + if(toks[0]) + v1 = v2 = Glib::Ascii::strtod(toks[0]); + if(toks[1]) + v2 = Glib::Ascii::strtod(toks[1]); + + _link.set_active(toks[1] == nullptr); + + _s1.get_adjustment()->set_value(v1); + _s2.get_adjustment()->set_value(v2); + + g_strfreev(toks); + } + } +} + +sigc::signal<void>& DualSpinScale::signal_value_changed() +{ + return _signal_value_changed; +} + +const SpinScale& DualSpinScale::get_SpinScale1() const +{ + return _s1; +} + +SpinScale& DualSpinScale::get_SpinScale1() +{ + return _s1; +} + +const SpinScale& DualSpinScale::get_SpinScale2() const +{ + return _s2; +} + +SpinScale& DualSpinScale::get_SpinScale2() +{ + return _s2; +} + +void DualSpinScale::link_toggled() +{ + _s2.set_sensitive(!_link.get_active()); + update_linked(); +} + +void DualSpinScale::update_linked() +{ + if(_link.get_active()) + _s2.set_value(_s1.get_value()); +} + + +} // namespace Widget +} // 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/widget/spin-scale.h b/src/ui/widget/spin-scale.h new file mode 100644 index 0000000..b2e478d --- /dev/null +++ b/src/ui/widget/spin-scale.h @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * + * Copyright (C) 2007 Nicholas Bishop <nicholasbishop@gmail.com> + * 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * Derived from and replaces SpinSlider + */ + +#ifndef INKSCAPE_UI_WIDGET_SPIN_SCALE_H +#define INKSCAPE_UI_WIDGET_SPIN_SCALE_H + +#include <gtkmm/adjustment.h> +#include <gtkmm/box.h> +#include <gtkmm/togglebutton.h> +#include "attr-widget.h" +#include "ink-spinscale.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Wrap the InkSpinScale class and attach an attribute. + * A combo widget with label, scale slider, spinbutton, and adjustment; + */ +class SpinScale : public Gtk::Box, public AttrWidget +{ + +public: + SpinScale(const Glib::ustring label, double value, + double lower, double upper, + double step_increment, double page_increment, int digits, + const SPAttr a = SPAttr::INVALID, const Glib::ustring tip_text = ""); + + // Used by extensions + SpinScale(const Glib::ustring label, + Glib::RefPtr<Gtk::Adjustment> adjustment, int digits, + const SPAttr a = SPAttr::INVALID, const Glib::ustring tip_text = ""); + + Glib::ustring get_as_attribute() const override; + void set_from_attribute(SPObject*) override; + + // Shortcuts to _adjustment + Glib::SignalProxy0<void> signal_value_changed(); + double get_value() const; + void set_value(const double); + void set_focuswidget(GtkWidget *widget); + +private: + Glib::RefPtr<Gtk::Adjustment> _adjustment; + InkSpinScale _inkspinscale; + +public: + const decltype(_adjustment) get_adjustment() const; + decltype(_adjustment) get_adjustment(); +}; + + +/** + * Contains two SpinScales for controlling number-opt-number attributes. + * + * @see SpinScale + */ +class DualSpinScale : public Gtk::Box, public AttrWidget +{ +public: + DualSpinScale(const Glib::ustring label1, const Glib::ustring label2, + double value, double lower, double upper, + double step_increment, double page_increment, int digits, + const SPAttr a, + const Glib::ustring tip_text1, const Glib::ustring tip_text2); + + Glib::ustring get_as_attribute() const override; + void set_from_attribute(SPObject*) override; + + sigc::signal<void>& signal_value_changed(); + + const SpinScale& get_SpinScale1() const; + SpinScale& get_SpinScale1(); + + const SpinScale& get_SpinScale2() const; + SpinScale& get_SpinScale2(); + + //void remove_scale(); +private: + void link_toggled(); + void update_linked(); + sigc::signal<void> _signal_value_changed; + SpinScale _s1, _s2; + Gtk::ToggleButton _link; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_SPIN_SCALE_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/widget/spinbutton.cpp b/src/ui/widget/spinbutton.cpp new file mode 100644 index 0000000..423ffe5 --- /dev/null +++ b/src/ui/widget/spinbutton.cpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Johan B. C. Engelen + * + * Copyright (C) 2011 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spinbutton.h" + +#include "scroll-utils.h" +#include "unit-menu.h" +#include "unit-tracker.h" +#include "util/expression-evaluator.h" +#include "ui/tools/tool-base.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +int SpinButton::on_input(double* newvalue) +{ + if (_dont_evaluate) return false; + + try { + Inkscape::Util::EvaluatorQuantity result; + if (_unit_menu || _unit_tracker) { + Unit const *unit = nullptr; + if (_unit_menu) { + unit = _unit_menu->getUnit(); + } else { + unit = _unit_tracker->getActiveUnit(); + } + Inkscape::Util::ExpressionEvaluator eval = Inkscape::Util::ExpressionEvaluator(get_text().c_str(), unit); + result = eval.evaluate(); + // check if output dimension corresponds to input unit + if (result.dimension != (unit->isAbsolute() ? 1 : 0) ) { + throw Inkscape::Util::EvaluatorException("Input dimensions do not match with parameter dimensions.",""); + } + } else { + Inkscape::Util::ExpressionEvaluator eval = Inkscape::Util::ExpressionEvaluator(get_text().c_str(), nullptr); + result = eval.evaluate(); + } + *newvalue = result.value; + } + catch(Inkscape::Util::EvaluatorException &e) { + g_message ("%s", e.what()); + + return false; + } + + return true; +} + +bool SpinButton::on_focus_in_event(GdkEventFocus *event) +{ + _on_focus_in_value = get_value(); + return parent_type::on_focus_in_event(event); +} + +bool SpinButton::on_key_press_event(GdkEventKey* event) +{ + switch (Inkscape::UI::Tools::get_latin_keyval (event)) { + case GDK_KEY_Escape: // defocus + undo(); + defocus(); + return true; // I consumed the event + break; + case GDK_KEY_Return: // defocus + case GDK_KEY_KP_Enter: + defocus(); + break; + case GDK_KEY_Tab: + case GDK_KEY_ISO_Left_Tab: + // set the flag meaning "do not leave toolbar when changing value" + _stay = true; + break; + case GDK_KEY_z: + case GDK_KEY_Z: + _stay = true; + if (event->state & GDK_CONTROL_MASK) { + undo(); + return true; // I consumed the event + } + break; + default: + break; + } + + return parent_type::on_key_press_event(event); +} + +void SpinButton::undo() +{ + set_value(_on_focus_in_value); +} + +void SpinButton::defocus() +{ + // defocus spinbutton by moving focus to the canvas, unless "stay" is on + if (_stay) { + _stay = false; + } else { + Gtk::Widget *widget = _defocus_widget ? _defocus_widget : get_scrollable_ancestor(this); + if (widget) { + widget->grab_focus(); + } + } +} + +} // namespace Widget +} // 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/widget/spinbutton.h b/src/ui/widget/spinbutton.h new file mode 100644 index 0000000..e8e5628 --- /dev/null +++ b/src/ui/widget/spinbutton.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Johan B. C. Engelen + * + * Copyright (C) 2011 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_SPINBUTTON_H +#define INKSCAPE_UI_WIDGET_SPINBUTTON_H + +#include <gtkmm/spinbutton.h> + +#include "scrollprotected.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +class UnitMenu; +class UnitTracker; + +/** + * SpinButton widget, that allows entry of simple math expressions (also units, when linked with UnitMenu), + * and allows entry of both '.' and ',' for the decimal, even when in numeric mode. + * + * Calling "set_numeric()" effectively disables the expression parsing. If no unit menu is linked, all unitlike characters are ignored. + */ +class SpinButton : public ScrollProtected<Gtk::SpinButton> +{ + using parent_type = ScrollProtected<Gtk::SpinButton>; + +public: + using parent_type::parent_type; + + void setUnitMenu(UnitMenu* unit_menu) { _unit_menu = unit_menu; }; + + void addUnitTracker(UnitTracker* ut) { _unit_tracker = ut; }; + + // TODO: Might be better to just have a default value and a reset() method? + inline void set_zeroable(const bool zeroable = true) { _zeroable = zeroable; } + inline void set_oneable(const bool oneable = true) { _oneable = oneable; } + + inline bool get_zeroable() const { return _zeroable; } + inline bool get_oneable() const { return _oneable; } + + void defocus(); + +protected: + UnitMenu *_unit_menu = nullptr; ///< Linked unit menu for unit conversion in entered expressions. + UnitTracker *_unit_tracker = nullptr; ///< Linked unit tracker for unit conversion in entered expressions. + double _on_focus_in_value = 0.; + Gtk::Widget *_defocus_widget = nullptr; ///< Widget that should grab focus when the spinbutton defocuses + + bool _zeroable = false; ///< Reset-value should be zero + bool _oneable = false; ///< Reset-value should be one + + bool _stay = false; ///< Whether to ignore defocusing + bool _dont_evaluate = false; ///< Don't attempt to evaluate expressions + + /** + * This callback function should try to convert the entered text to a number and write it to newvalue. + * It calls a method to evaluate the (potential) mathematical expression. + * + * @retval false No conversion done, continue with default handler. + * @retval true Conversion successful, don't call default handler. + */ + int on_input(double* newvalue) override; + + /** + * When focus is obtained, save the value to enable undo later. + * @retval false continue with default handler. + * @retval true don't call default handler. + */ + bool on_focus_in_event(GdkEventFocus *) override; + + /** + * Handle specific keypress events, like Ctrl+Z. + * + * @retval false continue with default handler. + * @retval true don't call default handler. + */ + bool on_key_press_event(GdkEventKey *) override; + + /** + * Undo the editing, by resetting the value upon when the spinbutton got focus. + */ + void undo(); + + public: + inline void set_defocus_widget(const decltype(_defocus_widget) widget) { _defocus_widget = widget; } + inline void set_dont_evaluate(bool flag) { _dont_evaluate = flag; } +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_SPINBUTTON_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/widget/stroke-style.cpp b/src/ui/widget/stroke-style.cpp new file mode 100644 index 0000000..53b50dd --- /dev/null +++ b/src/ui/widget/stroke-style.cpp @@ -0,0 +1,1232 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Bryce Harrington <brycehar@bryceharrington.org> + * bulia byak <buliabyak@users.sf.net> + * Maximilian Albert <maximilian.albert@gmail.com> + * Josh Andler <scislac@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2001-2005 authors + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2004 John Cliff + * Copyright (C) 2008 Maximilian Albert (gtkmm-ification) + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noSP_SS_VERBOSE + +#include "stroke-style.h" + +#include "object/sp-marker.h" +#include "object/sp-namedview.h" +#include "object/sp-rect.h" +#include "object/sp-stop.h" +#include "object/sp-text.h" + +#include "svg/svg-color.h" + +#include "ui/icon-loader.h" +#include "ui/widget/dash-selector.h" +#include "ui/widget/marker-combo-box.h" +#include "ui/widget/unit-menu.h" +#include "ui/tools/marker-tool.h" +#include "ui/dialog/dialog-base.h" + +#include "actions/actions-tools.h" + +#include "widgets/style-utils.h" + +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +/** + * Extract the actual name of the link + * e.g. get mTriangle from url(#mTriangle). + * \return Buffer containing the actual name, allocated from GLib; + * the caller should free the buffer when they no longer need it. + */ +SPObject* getMarkerObj(gchar const *n, SPDocument *doc) +{ + gchar const *p = n; + while (*p != '\0' && *p != '#') { + p++; + } + + if (*p == '\0' || p[1] == '\0') { + return nullptr; + } + + p++; + int c = 0; + while (p[c] != '\0' && p[c] != ')') { + c++; + } + + if (p[c] == '\0') { + return nullptr; + } + + gchar* b = g_strdup(p); + b[c] = '\0'; + + // FIXME: get the document from the object and let the caller pass it in + SPObject *marker = doc->getObjectById(b); + + g_free(b); + return marker; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Construct a stroke-style radio button with a given icon + * + * \param[in] grp The Gtk::RadioButtonGroup to which to add the new button + * \param[in] icon The icon to use for the button + * \param[in] button_type The type of stroke-style radio button (join/cap) + * \param[in] stroke_style The style attribute to associate with the button + */ +StrokeStyle::StrokeStyleButton::StrokeStyleButton(Gtk::RadioButtonGroup &grp, + char const *icon, + StrokeStyleButtonType button_type, + gchar const *stroke_style) + : + Gtk::RadioButton(grp), + button_type(button_type), + stroke_style(stroke_style) +{ + show(); + set_mode(false); + + auto px = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_LARGE_TOOLBAR)); + g_assert(px != nullptr); + px->show(); + add(*px); +} + +std::vector<double> parse_pattern(const Glib::ustring& input) { + std::vector<double> output; + if (input.empty()) return output; + + std::istringstream stream(input.c_str()); + while (stream) { + double val; + stream >> val; + if (stream) { + output.push_back(val); + } + } + + return output; +} + +StrokeStyle::StrokeStyle() : + Gtk::Box(), + miterLimitSpin(), + widthSpin(), + unitSelector(), + joinMiter(), + joinRound(), + joinBevel(), + capButt(), + capRound(), + capSquare(), + dashSelector(), + update(false), + desktop(nullptr), + startMarkerConn(), + midMarkerConn(), + endMarkerConn(), + _old_unit(nullptr) +{ + set_name("StrokeSelector"); + table = Gtk::manage(new Gtk::Grid()); + table->set_border_width(4); + table->set_row_spacing(4); + table->set_hexpand(false); + table->set_halign(Gtk::ALIGN_CENTER); + table->show(); + add(*table); + + Gtk::Box *hb; + gint i = 0; + + //spw_label(t, C_("Stroke width", "_Width:"), 0, i); + + hb = spw_hbox(table, 3, 1, i); + +// TODO: when this is gtkmmified, use an Inkscape::UI::Widget::ScalarUnit instead of the separate +// spinbutton and unit selector for stroke width. In sp_stroke_style_line_update, use +// setHundredPercent to remember the averaged width corresponding to 100%. Then the +// stroke_width_set_unit will be removed (because ScalarUnit takes care of conversions itself) + widthAdj = new Glib::RefPtr<Gtk::Adjustment>(Gtk::Adjustment::create(1.0, 0.0, 1000.0, 0.1, 10.0, 0.0)); + widthSpin = new Inkscape::UI::Widget::SpinButton(*widthAdj, 0.1, 3); + widthSpin->set_tooltip_text(_("Stroke width")); + widthSpin->show(); + spw_label(table, C_("Stroke width", "_Width:"), 0, i, widthSpin); + + sp_dialog_defocus_on_enter_cpp(widthSpin); + + hb->pack_start(*widthSpin, false, false, 0); + unitSelector = Gtk::manage(new Inkscape::UI::Widget::UnitMenu()); + unitSelector->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR); + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + unitSelector->addUnit(*unit_table.getUnit("%")); + unitSelector->append("hairline", _("Hairline")); + _old_unit = unitSelector->getUnit(); + if (desktop) { + unitSelector->setUnit(desktop->getNamedView()->display_units->abbr); + _old_unit = desktop->getNamedView()->display_units; + } + widthSpin->setUnitMenu(unitSelector); + unitSelector->signal_changed().connect(sigc::mem_fun(*this, &StrokeStyle::unitChangedCB)); + unitSelector->show(); + + hb->pack_start(*unitSelector, FALSE, FALSE, 0); + (*widthAdj)->signal_value_changed().connect(sigc::mem_fun(*this, &StrokeStyle::setStrokeWidth)); + + i++; + + /* Dash */ + spw_label(table, _("Dashes:"), 0, i, nullptr); //no mnemonic for now + //decide what to do: + // implement a set_mnemonic_source function in the + // Inkscape::UI::Widget::DashSelector class, so that we do not have to + // expose any of the underlying widgets? + dashSelector = Gtk::manage(new Inkscape::UI::Widget::DashSelector); + _pattern = Gtk::make_managed<Gtk::Entry>(); + + dashSelector->show(); + dashSelector->set_hexpand(); + dashSelector->set_halign(Gtk::ALIGN_FILL); + dashSelector->set_valign(Gtk::ALIGN_CENTER); + table->attach(*dashSelector, 1, i, 3, 1); + dashSelector->changed_signal.connect(sigc::mem_fun(*this, &StrokeStyle::setStrokeDash)); + + i++; + + table->attach(*_pattern, 1, i, 4, 1); + _pattern_label = spw_label(table, _("_Pattern:"), 0, i, _pattern); + _pattern_label->set_tooltip_text(_("Repeating \"dash gap ...\" pattern")); + _pattern->set_no_show_all(); + _pattern_label->set_no_show_all(); + _pattern->signal_changed().connect([=](){ + if (update || _editing_pattern) return; + + auto pat = parse_pattern(_pattern->get_text()); + _editing_pattern = true; + update = true; + dashSelector->set_dash(pat, dashSelector->get_offset()); + update = false; + setStrokeDash(); + _editing_pattern = false; + }); + update_pattern(0, nullptr); + + i++; + + /* Drop down marker selectors*/ + // TRANSLATORS: Path markers are an SVG feature that allows you to attach arbitrary shapes + // (arrowheads, bullets, faces, whatever) to the start, end, or middle nodes of a path. + + spw_label(table, _("Markers:"), 0, i, nullptr); + + hb = spw_hbox(table, 1, 1, i); + i++; + + startMarkerCombo = Gtk::manage(new MarkerComboBox("marker-start", SP_MARKER_LOC_START)); + startMarkerCombo->set_tooltip_text(_("Start Markers are drawn on the first node of a path or shape")); + startMarkerConn = startMarkerCombo->signal_changed().connect([=]() { markerSelectCB(startMarkerCombo, SP_MARKER_LOC_START); }); + startMarkerCombo->edit_signal.connect([=] { enterEditMarkerMode(SP_MARKER_LOC_START); }); + startMarkerCombo->show(); + + hb->pack_start(*startMarkerCombo, true, true, 0); + + midMarkerCombo = Gtk::manage(new MarkerComboBox("marker-mid", SP_MARKER_LOC_MID)); + midMarkerCombo->set_tooltip_text(_("Mid Markers are drawn on every node of a path or shape except the first and last nodes")); + midMarkerConn = midMarkerCombo->signal_changed().connect([=]() { markerSelectCB(midMarkerCombo, SP_MARKER_LOC_MID); }); + midMarkerCombo->edit_signal.connect([=] { enterEditMarkerMode(SP_MARKER_LOC_MID); }); + midMarkerCombo->show(); + + hb->pack_start(*midMarkerCombo, true, true, 0); + + endMarkerCombo = Gtk::manage(new MarkerComboBox("marker-end", SP_MARKER_LOC_END)); + endMarkerCombo->set_tooltip_text(_("End Markers are drawn on the last node of a path or shape")); + endMarkerConn = endMarkerCombo->signal_changed().connect([=]() { markerSelectCB(endMarkerCombo, SP_MARKER_LOC_END); }); + endMarkerCombo->edit_signal.connect([=] { enterEditMarkerMode(SP_MARKER_LOC_END); }); + endMarkerCombo->show(); + + hb->pack_start(*endMarkerCombo, true, true, 0); + i++; + + /* Join type */ + // TRANSLATORS: The line join style specifies the shape to be used at the + // corners of paths. It can be "miter", "round" or "bevel". + spw_label(table, _("Join:"), 0, i, nullptr); + + hb = spw_hbox(table, 3, 1, i); + + Gtk::RadioButtonGroup joinGrp; + + joinBevel = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-bevel"), + hb, STROKE_STYLE_BUTTON_JOIN, "bevel"); + + // TRANSLATORS: Bevel join: joining lines with a blunted (flattened) corner. + // For an example, draw a triangle with a large stroke width and modify the + // "Join" option (in the Fill and Stroke dialog). + joinBevel->set_tooltip_text(_("Bevel join")); + + joinRound = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-round"), + hb, STROKE_STYLE_BUTTON_JOIN, "round"); + + // TRANSLATORS: Round join: joining lines with a rounded corner. + // For an example, draw a triangle with a large stroke width and modify the + // "Join" option (in the Fill and Stroke dialog). + joinRound->set_tooltip_text(_("Round join")); + + joinMiter = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-miter"), + hb, STROKE_STYLE_BUTTON_JOIN, "miter"); + + // TRANSLATORS: Miter join: joining lines with a sharp (pointed) corner. + // For an example, draw a triangle with a large stroke width and modify the + // "Join" option (in the Fill and Stroke dialog). + joinMiter->set_tooltip_text(_("Miter join")); + + /* Miterlimit */ + // TRANSLATORS: Miter limit: only for "miter join", this limits the length + // of the sharp "spike" when the lines connect at too sharp an angle. + // When two line segments meet at a sharp angle, a miter join results in a + // spike that extends well beyond the connection point. The purpose of the + // miter limit is to cut off such spikes (i.e. convert them into bevels) + // when they become too long. + //spw_label(t, _("Miter _limit:"), 0, i); + miterLimitAdj = new Glib::RefPtr<Gtk::Adjustment>(Gtk::Adjustment::create(4.0, 0.0, 100000.0, 0.1, 10.0, 0.0)); + miterLimitSpin = new Inkscape::UI::Widget::SpinButton(*miterLimitAdj, 0.1, 2); + miterLimitSpin->set_tooltip_text(_("Maximum length of the miter (in units of stroke width)")); + miterLimitSpin->set_width_chars(6); + miterLimitSpin->show(); + sp_dialog_defocus_on_enter_cpp(miterLimitSpin); + + hb->pack_start(*miterLimitSpin, false, false, 0); + (*miterLimitAdj)->signal_value_changed().connect(sigc::mem_fun(*this, &StrokeStyle::setStrokeMiter)); + i++; + + /* Cap type */ + // TRANSLATORS: cap type specifies the shape for the ends of lines + //spw_label(t, _("_Cap:"), 0, i); + spw_label(table, _("Cap:"), 0, i, nullptr); + + hb = spw_hbox(table, 3, 1, i); + + Gtk::RadioButtonGroup capGrp; + + capButt = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-butt"), + hb, STROKE_STYLE_BUTTON_CAP, "butt"); + + // TRANSLATORS: Butt cap: the line shape does not extend beyond the end point + // of the line; the ends of the line are square + capButt->set_tooltip_text(_("Butt cap")); + + capRound = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-round"), + hb, STROKE_STYLE_BUTTON_CAP, "round"); + + // TRANSLATORS: Round cap: the line shape extends beyond the end point of the + // line; the ends of the line are rounded + capRound->set_tooltip_text(_("Round cap")); + + capSquare = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-square"), + hb, STROKE_STYLE_BUTTON_CAP, "square"); + + // TRANSLATORS: Square cap: the line shape extends beyond the end point of the + // line; the ends of the line are square + capSquare->set_tooltip_text(_("Square cap")); + + i++; + + /* Paint order */ + // TRANSLATORS: Paint order determines the order the 'fill', 'stroke', and 'markers are painted. + spw_label(table, _("Order:"), 0, i, nullptr); + + hb = spw_hbox(table, 4, 1, i); + + Gtk::RadioButtonGroup paintOrderGrp; + + paintOrderFSM = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-fsm"), + hb, STROKE_STYLE_BUTTON_ORDER, "normal"); + paintOrderFSM->set_tooltip_text(_("Fill, Stroke, Markers")); + + paintOrderSFM = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-sfm"), + hb, STROKE_STYLE_BUTTON_ORDER, "stroke fill markers"); + paintOrderSFM->set_tooltip_text(_("Stroke, Fill, Markers")); + + paintOrderFMS = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-fms"), + hb, STROKE_STYLE_BUTTON_ORDER, "fill markers stroke"); + paintOrderFMS->set_tooltip_text(_("Fill, Markers, Stroke")); + + i++; + + hb = spw_hbox(table, 4, 1, i); + + paintOrderMFS = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-mfs"), + hb, STROKE_STYLE_BUTTON_ORDER, "markers fill stroke"); + paintOrderMFS->set_tooltip_text(_("Markers, Fill, Stroke")); + + paintOrderSMF = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-smf"), + hb, STROKE_STYLE_BUTTON_ORDER, "stroke markers fill"); + paintOrderSMF->set_tooltip_text(_("Stroke, Markers, Fill")); + + paintOrderMSF = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-msf"), + hb, STROKE_STYLE_BUTTON_ORDER, "markers stroke fill"); + paintOrderMSF->set_tooltip_text(_("Markers, Stroke, Fill")); + + i++; +} + +StrokeStyle::~StrokeStyle() +{ +} + +void StrokeStyle::setDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + + if (this->desktop) { + _document_replaced_connection.disconnect(); + } + this->desktop = desktop; + + if (!desktop) { + return; + } + + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(this, &StrokeStyle::_handleDocumentReplaced)); + + _handleDocumentReplaced(nullptr, desktop->getDocument()); + + updateLine(); + } +} + +void StrokeStyle::_handleDocumentReplaced(SPDesktop *, SPDocument *document) +{ + for (MarkerComboBox *combo : { startMarkerCombo, midMarkerCombo, endMarkerCombo }) { + combo->setDocument(document); + } +} + + +/** + * Helper function for creating stroke-style radio buttons. + * + * \param[in] grp The Gtk::RadioButtonGroup in which to add the button + * \param[in] icon The icon for the button + * \param[in] hb The Gtk::Box container in which to add the button + * \param[in] button_type The type (join/cap) for the button + * \param[in] stroke_style The style attribute to associate with the button + * + * \details After instantiating the button, it is added to a container box and + * a handler for the toggle event is connected. + */ +StrokeStyle::StrokeStyleButton * +StrokeStyle::makeRadioButton(Gtk::RadioButtonGroup &grp, + char const *icon, + Gtk::Box *hb, + StrokeStyleButtonType button_type, + gchar const *stroke_style) +{ + g_assert(icon != nullptr); + g_assert(hb != nullptr); + + StrokeStyleButton *tb = new StrokeStyleButton(grp, icon, button_type, stroke_style); + + hb->pack_start(*tb, false, false, 0); + + tb->signal_toggled().connect(sigc::bind<StrokeStyleButton *, StrokeStyle *>( + sigc::ptr_fun(&StrokeStyle::buttonToggledCB), tb, this)); + + return tb; +} + +void StrokeStyle::enterEditMarkerMode(SPMarkerLoc _editMarkerMode) +{ + SPDesktop *desktop = this->desktop; + + if (desktop) { + set_active_tool(desktop, "Marker"); + Inkscape::UI::Tools::MarkerTool *mt = dynamic_cast<Inkscape::UI::Tools::MarkerTool*>(desktop->event_context); + + if(mt) { + mt->editMarkerMode = _editMarkerMode; + mt->selection_changed(desktop->getSelection()); + } + } +} + + +bool StrokeStyle::areMarkersBeingUpdated() +{ + return startMarkerCombo->in_update() || midMarkerCombo->in_update() || endMarkerCombo->in_update(); +} + +/** + * Handles when user selects one of the markers from the marker combobox. + * Gets the marker uri string and applies it to all selected + * items in the current desktop. + */ +void StrokeStyle::markerSelectCB(MarkerComboBox *marker_combo, SPMarkerLoc const which) +{ + if (update || areMarkersBeingUpdated()) { + return; + } + + SPDocument *document = desktop->getDocument(); + if (!document) { + return; + } + + // Get marker ID; could be empty (to remove marker) + std::string marker = marker_combo->get_active_marker_uri(); + + update = true; + + SPCSSAttr *css = sp_repr_css_attr_new(); + gchar const *combo_id = marker_combo->get_id(); + sp_repr_css_set_property(css, combo_id, marker.c_str()); + + Inkscape::Selection *selection = desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (!SP_IS_SHAPE(item)) { + continue; + } + Inkscape::XML::Node *selrepr = item->getRepr(); + if (selrepr) { + sp_repr_css_change_recursive(selrepr, css, "style"); + } + + item->requestModified(SP_OBJECT_MODIFIED_FLAG); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + + DocumentUndo::done(document, _("Set markers"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } + + /* edit marker mode - update */ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + if (desktop) { + Inkscape::UI::Tools::MarkerTool *mt = dynamic_cast<Inkscape::UI::Tools::MarkerTool*>(desktop->event_context); + + if(mt) { + mt->editMarkerMode = which; + mt->selection_changed(desktop->getSelection()); + } + } + + sp_repr_css_attr_unref(css); + css = nullptr; + + update = false; +}; + +/** + * Callback for when UnitMenu widget is modified. + * Triggers update action. + */ +void StrokeStyle::unitChangedCB() +{ + Inkscape::Util::Unit const *new_unit = unitSelector->getUnit(); + + if (_old_unit == new_unit) + return; + + // If the unit selector is set to hairline, don't do the normal conversion. + if (isHairlineSelected()) { + // Force update in setStrokeWidth + _old_unit = new_unit; + _last_width = -1; + setStrokeWidth(); + return; + } + + if (new_unit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) { + // Prevent update in setStrokeWidth + _last_width = 100.0; + widthSpin->set_value(100); + } else { + // Remove the non-scaling-stroke effect and the hairline extensions + if (!update) { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_unset_property(css, "vector-effect"); + sp_repr_css_unset_property(css, "-inkscape-stroke"); + sp_desktop_set_style(desktop, css); + sp_repr_css_attr_unref(css); + css = nullptr; + DocumentUndo::done(desktop->getDocument(), _("Remove hairline stroke"), + INKSCAPE_ICON("dialog-fill-and-stroke")); + } + if (_old_unit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) { + // Prevent update of unit (inf-loop) in updateLine + _old_unit = new_unit; + // Going from % to any other unit means our widthSpin is completely invalid. + updateLine(); + } else { + // Scale the value and record the old_unit + widthSpin->set_value(Inkscape::Util::Quantity::convert(widthSpin->get_value(), _old_unit, new_unit)); + } + } + _old_unit = new_unit; +} + +/** + * Callback for when stroke style widget is modified. + * Triggers update action. + */ +void +StrokeStyle::selectionModifiedCB(guint flags) +{ + // We care deeply about only updating when the style is updated + // if we update on other flags, we slow inkscape down when dragging + if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) { + updateLine(); + } +} + +/** + * Callback for when stroke style widget is changed. + * Triggers update action. + */ +void +StrokeStyle::selectionChangedCB() +{ + updateLine(); +} + +/** + * Get a dash array and offset from the style. + * + * Both values are de-scaled by the style's width if needed. + */ +std::vector<double> +StrokeStyle::getDashFromStyle(SPStyle *style, double &offset) +{ + auto prefs = Inkscape::Preferences::get(); + + std::vector<double> ret; + size_t len = style->stroke_dasharray.values.size(); + + double scaledash = 1.0; + if (prefs->getBool("/options/dash/scale", true) && style->stroke_width.computed) { + scaledash = style->stroke_width.computed; + } + + offset = style->stroke_dashoffset.value / scaledash; + for (unsigned i = 0; i < len; i++) { + ret.push_back(style->stroke_dasharray.values[i].value / scaledash); + } + return ret; +} + +/** + * Sets selector widgets' dash style from an SPStyle object. + */ +void +StrokeStyle::setDashSelectorFromStyle(Inkscape::UI::Widget::DashSelector *dsel, SPStyle *style) +{ + double offset = 0; + auto d = getDashFromStyle(style, offset); + if (!d.empty()) { + dsel->set_dash(d, offset); + update_pattern(d.size(), d.data()); + } else { + dsel->set_dash(std::vector<double>(), 0.0); + update_pattern(0, nullptr); + } +} + +void StrokeStyle::update_pattern(int ndash, const double* pattern) { + if (_editing_pattern || _pattern->has_focus()) return; + + std::ostringstream ost; + for (int i = 0; i < ndash; ++i) { + ost << pattern[i] << ' '; + } + _pattern->set_text(ost.str().c_str()); + if (ndash > 0) { + _pattern_label->show(); + _pattern->show(); + } + else { + _pattern_label->hide(); + _pattern->hide(); + } +} + +/** + * Sets the join type for a line, and updates the stroke style widget's buttons + */ +void +StrokeStyle::setJoinType (unsigned const jointype) +{ + Gtk::RadioButton *tb = nullptr; + switch (jointype) { + case SP_STROKE_LINEJOIN_MITER: + tb = joinMiter; + break; + case SP_STROKE_LINEJOIN_ROUND: + tb = joinRound; + break; + case SP_STROKE_LINEJOIN_BEVEL: + tb = joinBevel; + break; + default: + // Should not happen + std::cerr << "StrokeStyle::setJoinType(): Invalid value: " << jointype << std::endl; + tb = joinMiter; + break; + } + setJoinButtons(tb); +} + +/** + * Sets the cap type for a line, and updates the stroke style widget's buttons + */ +void +StrokeStyle::setCapType (unsigned const captype) +{ + Gtk::RadioButton *tb = nullptr; + switch (captype) { + case SP_STROKE_LINECAP_BUTT: + tb = capButt; + break; + case SP_STROKE_LINECAP_ROUND: + tb = capRound; + break; + case SP_STROKE_LINECAP_SQUARE: + tb = capSquare; + break; + default: + // Should not happen + std::cerr << "StrokeStyle::setCapType(): Invalid value: " << captype << std::endl; + tb = capButt; + break; + } + setCapButtons(tb); +} + +/** + * Sets the cap type for a line, and updates the stroke style widget's buttons + */ +void +StrokeStyle::setPaintOrder (gchar const *paint_order) +{ + Gtk::RadioButton *tb = paintOrderFSM; + + SPIPaintOrder temp; + temp.read( paint_order ); + + if (temp.layer[0] != SP_CSS_PAINT_ORDER_NORMAL) { + + if (temp.layer[0] == SP_CSS_PAINT_ORDER_FILL) { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) { + tb = paintOrderFSM; + } else { + tb = paintOrderFMS; + } + } else if (temp.layer[0] == SP_CSS_PAINT_ORDER_STROKE) { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_FILL) { + tb = paintOrderSFM; + } else { + tb = paintOrderSMF; + } + } else { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) { + tb = paintOrderMSF; + } else { + tb = paintOrderMFS; + } + } + + } + setPaintOrderButtons(tb); +} + +/** + * Callback for when stroke style widget is updated, including markers, cap type, + * join type, etc. + */ +void +StrokeStyle::updateLine() +{ + if (update) { + return; + } + + auto *widg = get_parent()->get_parent()->get_parent()->get_parent(); + auto dialogbase = dynamic_cast<Inkscape::UI::Dialog::DialogBase*>(widg); + if (dialogbase && !dialogbase->getShowing()) { + return; + } + + update = true; + + Inkscape::Selection *sel = desktop ? desktop->getSelection() : nullptr; + + if (!sel || sel->isEmpty()) { + // Nothing selected, grey-out all controls in the stroke-style dialog + table->set_sensitive(false); + + update = false; + + return; + } + + FillOrStroke kind = STROKE; + + // create temporary style + SPStyle query(SP_ACTIVE_DOCUMENT); + // query into it + int result_sw = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKEWIDTH); + int result_ml = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKEMITERLIMIT); + int result_cap = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKECAP); + int result_join = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKEJOIN); + int result_order = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_PAINTORDER); + + SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL); + + { + table->set_sensitive(true); + widthSpin->set_sensitive(true); + + if (result_sw == QUERY_STYLE_MULTIPLE_AVERAGED) { + unitSelector->setUnit("%"); + } else if (query.stroke_extensions.hairline) { + unitSelector->set_active_id("hairline"); + } else { + // same width, or only one object; no sense to keep percent, switch to absolute + Inkscape::Util::Unit const *tempunit = unitSelector->getUnit(); + if (tempunit->type != Inkscape::Util::UNIT_TYPE_LINEAR) { + unitSelector->setUnit(desktop->getNamedView()->display_units->abbr); + } + } + + Inkscape::Util::Unit const *unit = unitSelector->getUnit(); + + if (query.stroke_extensions.hairline) { + widthSpin->set_sensitive(false); + (*widthAdj)->set_value(1); + } else if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) { + double avgwidth = Inkscape::Util::Quantity::convert(query.stroke_width.computed, "px", unit); + (*widthAdj)->set_value(avgwidth); + } else { + (*widthAdj)->set_value(100); + } + + // if none of the selected objects has a stroke, than quite some controls should be disabled + // These options should also be disabled for hairlines, since they don't make sense for + // 0-width lines. + // The markers might still be shown though, so marker and stroke-width widgets stay enabled + bool is_enabled = (result_sw != QUERY_STYLE_NOTHING) && !targPaint.isNoneSet() + && !query.stroke_extensions.hairline; + joinMiter->set_sensitive(is_enabled); + joinRound->set_sensitive(is_enabled); + joinBevel->set_sensitive(is_enabled); + + miterLimitSpin->set_sensitive(is_enabled); + + capButt->set_sensitive(is_enabled); + capRound->set_sensitive(is_enabled); + capSquare->set_sensitive(is_enabled); + + dashSelector->set_sensitive(is_enabled); + _pattern->set_sensitive(is_enabled); + } + + if (result_ml != QUERY_STYLE_NOTHING) + (*miterLimitAdj)->set_value(query.stroke_miterlimit.value); // TODO: reflect averagedness? + + using Inkscape::is_query_style_updateable; + if (! is_query_style_updateable(result_join)) { + setJoinType(query.stroke_linejoin.value); + } else { + setJoinButtons(nullptr); + } + + if (! is_query_style_updateable(result_cap)) { + setCapType (query.stroke_linecap.value); + } else { + setCapButtons(nullptr); + } + + if (! is_query_style_updateable(result_order)) { + setPaintOrder (query.paint_order.value); + } else { + setPaintOrder (nullptr); + } + + std::vector<SPItem*> const objects(sel->items().begin(), sel->items().end()); + if (objects.size()) { + SPObject *const object = objects[0]; + SPStyle *const style = object->style; + /* Markers */ + updateAllMarkers(objects, true); // FIXME: make this desktop query too + + /* Dash */ + setDashSelectorFromStyle(dashSelector, style); // FIXME: make this desktop query too + } + table->set_sensitive(true); + + update = false; +} + +/** + * Sets a line's dash properties in a CSS style object. + */ +void +StrokeStyle::setScaledDash(SPCSSAttr *css, + int ndash, const double *dash, double offset, + double scale) +{ + if (ndash > 0) { + Inkscape::CSSOStringStream osarray; + for (int i = 0; i < ndash; i++) { + osarray << dash[i] * scale; + if (i < (ndash - 1)) { + osarray << ","; + } + } + sp_repr_css_set_property(css, "stroke-dasharray", osarray.str().c_str()); + + Inkscape::CSSOStringStream osoffset; + osoffset << offset * scale; + sp_repr_css_set_property(css, "stroke-dashoffset", osoffset.str().c_str()); + } else { + sp_repr_css_set_property(css, "stroke-dasharray", "none"); + sp_repr_css_set_property(css, "stroke-dashoffset", nullptr); + } +} + +static inline double calcScaleLineWidth(const double width_typed, SPItem *const item, Inkscape::Util::Unit const *const unit) +{ + if (unit->abbr == "%") { + auto scale = item->i2doc_affine().descrim();; + const gdouble old_w = item->style->stroke_width.computed; + return (old_w * width_typed / 100) * scale; + } else if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) { + return Inkscape::Util::Quantity::convert(width_typed, unit, "px"); + } + return width_typed; +} + +/** + * Set the stroke width and adjust the dash pattern if needed. + */ +void StrokeStyle::setStrokeWidth() +{ + double width_typed = (*widthAdj)->get_value(); + + // Don't change the selection if an update is happening, + // but also store the value for later comparison. + if (update || fabs(_last_width - width_typed) < 1E-6) { + _last_width = width_typed; + return; + } + update = true; + + auto prefs = Inkscape::Preferences::get(); + auto unit = unitSelector->getUnit(); + + SPCSSAttr *css = sp_repr_css_attr_new(); + if (isHairlineSelected()) { + /* For renderers that don't understand -inkscape-stroke:hairline, fall back to 1px non-scaling */ + width_typed = 1; + sp_repr_css_set_property(css, "vector-effect", "non-scaling-stroke"); + sp_repr_css_set_property(css, "-inkscape-stroke", "hairline"); + } else { + sp_repr_css_unset_property(css, "vector-effect"); + sp_repr_css_unset_property(css, "-inkscape-stroke"); + } + + for (auto item : desktop->getSelection()->items()) { + const double width = calcScaleLineWidth(width_typed, item, unit); + sp_repr_css_set_property_double(css, "stroke-width", width); + + if (prefs->getBool("/options/dash/scale", true)) { + // This will read the old stroke-width to un-scale the pattern. + double offset = 0; + auto dash = getDashFromStyle(item->style, offset); + setScaledDash(css, dash.size(), dash.data(), offset, width); + } + sp_desktop_apply_css_recursive (item, css, true); + } + sp_desktop_set_style (desktop, css, false); + + sp_repr_css_attr_unref(css); + DocumentUndo::done(desktop->getDocument(), _("Set stroke width"), + INKSCAPE_ICON("dialog-fill-and-stroke")); + + if (unit->abbr == "%") { + // reset to 100 percent + _last_width = 100.0; + (*widthAdj)->set_value(100.0); + } else { + _last_width = width_typed; + } + update = false; +} + +/** + * Set the stroke dash pattern, scale to the existing width if needed + */ +void StrokeStyle::setStrokeDash() +{ + if (update) return; + update = true; + + auto document = desktop->getDocument(); + auto prefs = Inkscape::Preferences::get(); + + double offset = 0; + const auto& dash = dashSelector->get_dash(&offset); + update_pattern(dash.size(), dash.data()); + + SPCSSAttr *css = sp_repr_css_attr_new(); + for (auto item : desktop->getSelection()->items()) { + double scale = item->i2doc_affine().descrim(); + if(prefs->getBool("/options/dash/scale", true)) { + scale = item->style->stroke_width.computed * scale; + } + + setScaledDash(css, dash.size(), dash.data(), offset, scale); + sp_desktop_apply_css_recursive (item, css, true); + } + sp_desktop_set_style (desktop, css, false); + + sp_repr_css_attr_unref(css); + DocumentUndo::done(document, _("Set stroke dash"), + INKSCAPE_ICON("dialog-fill-and-stroke")); + update = false; +} + +/** + * Set the Miter Limit value only. + */ +void StrokeStyle::setStrokeMiter() +{ + if (update) return; + update = true; + + SPCSSAttr *css = sp_repr_css_attr_new(); + auto value = (*miterLimitAdj)->get_value(); + sp_repr_css_set_property_double(css, "stroke-miterlimit", value); + + for (auto item : desktop->getSelection()->items()) { + sp_desktop_apply_css_recursive(item, css, true); + } + sp_desktop_set_style (desktop, css, false); + sp_repr_css_attr_unref(css); + DocumentUndo::done(desktop->getDocument(), _("Set stroke miter"), + INKSCAPE_ICON("dialog-fill-and-stroke")); + update = false; +} + +/** + * Returns whether the currently selected stroke width is "hairline" + * + */ +bool +StrokeStyle::isHairlineSelected() const +{ + return unitSelector->get_active_id() == "hairline"; +} + + +/** + * This routine handles toggle events for buttons in the stroke style dialog. + * + * When activated, this routine gets the data for the various widgets, and then + * calls the respective routines to update css properties, etc. + * + */ +void StrokeStyle::buttonToggledCB(StrokeStyleButton *tb, StrokeStyle *spw) +{ + if (spw->update) { + return; + } + + if (tb->get_active()) { + if (tb->get_button_type() == STROKE_STYLE_BUTTON_JOIN) { + spw->miterLimitSpin->set_sensitive(!strcmp(tb->get_stroke_style(), "miter")); + } + + /* TODO: Create some standardized method */ + SPCSSAttr *css = sp_repr_css_attr_new(); + + switch (tb->get_button_type()) { + case STROKE_STYLE_BUTTON_JOIN: + sp_repr_css_set_property(css, "stroke-linejoin", tb->get_stroke_style()); + sp_desktop_set_style (spw->desktop, css); + spw->setJoinButtons(tb); + break; + case STROKE_STYLE_BUTTON_CAP: + sp_repr_css_set_property(css, "stroke-linecap", tb->get_stroke_style()); + sp_desktop_set_style (spw->desktop, css); + spw->setCapButtons(tb); + break; + case STROKE_STYLE_BUTTON_ORDER: + sp_repr_css_set_property(css, "paint-order", tb->get_stroke_style()); + sp_desktop_set_style (spw->desktop, css); + //spw->setPaintButtons(tb); + } + + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(spw->desktop->getDocument(), _("Set stroke style"), INKSCAPE_ICON("dialog-fill-and-stroke")); + } +} + +/** + * Updates the join style toggle buttons + */ +void +StrokeStyle::setJoinButtons(Gtk::ToggleButton *active) +{ + joinMiter->set_active(active == joinMiter); + miterLimitSpin->set_sensitive(active == joinMiter && !isHairlineSelected()); + joinRound->set_active(active == joinRound); + joinBevel->set_active(active == joinBevel); +} + +/** + * Updates the cap style toggle buttons + */ +void +StrokeStyle::setCapButtons(Gtk::ToggleButton *active) +{ + capButt->set_active(active == capButt); + capRound->set_active(active == capRound); + capSquare->set_active(active == capSquare); +} + + +/** + * Updates the paint order style toggle buttons + */ +void +StrokeStyle::setPaintOrderButtons(Gtk::ToggleButton *active) +{ + paintOrderFSM->set_active(active == paintOrderFSM); + paintOrderSFM->set_active(active == paintOrderSFM); + paintOrderFMS->set_active(active == paintOrderFMS); + paintOrderMFS->set_active(active == paintOrderMFS); + paintOrderSMF->set_active(active == paintOrderSMF); + paintOrderMSF->set_active(active == paintOrderMSF); +} + + +/** + * Recursively builds a simple list from an arbitrarily complex selection + * of items and grouped items + */ +static void buildGroupedItemList(SPObject *element, std::vector<SPObject*> &simple_list) +{ + if (SP_IS_GROUP(element)) { + for (SPObject *i = element->firstChild(); i; i = i->getNext()) { + buildGroupedItemList(i, simple_list); + } + } else { + simple_list.push_back(element); + } +} + + +/** + * Updates the marker combobox to highlight the appropriate marker and scroll to + * that marker. + */ +void +StrokeStyle::updateAllMarkers(std::vector<SPItem*> const &objects, bool skip_undo) +{ + struct { MarkerComboBox *key; int loc; } const keyloc[] = { + { startMarkerCombo, SP_MARKER_LOC_START }, + { midMarkerCombo, SP_MARKER_LOC_MID }, + { endMarkerCombo, SP_MARKER_LOC_END } + }; + + bool all_texts = true; + + auto simplified_list = std::vector<SPObject *>(); + for (SPItem *item : objects) { + buildGroupedItemList(item, simplified_list); + } + + for (SPObject *object : simplified_list) { + if (!SP_IS_TEXT(object)) { + all_texts = false; + break; + } + } + + // We show markers of the last object in the list only + // FIXME: use the first in the list that has the marker of each type, if any + + for (auto const &markertype : keyloc) { + // For all three marker types, + + // find the corresponding combobox item + MarkerComboBox *combo = markertype.key; + + // Quit if we're in update state + if (combo->in_update()) { + return; + } + + // Per SVG spec, text objects cannot have markers; disable combobox if only texts are selected + // They should also be disabled for hairlines, since scaling against a 0-width line doesn't + // make sense. + combo->set_sensitive(!all_texts && !isHairlineSelected()); + + SPObject *marker = nullptr; + + if (!all_texts && !isHairlineSelected()) { + for (SPObject *object : simplified_list) { + char const *value = object->style->marker_ptrs[markertype.loc]->value(); + + // If the object has this type of markers, + if (value == nullptr) + continue; + + // Extract the name of the marker that the object uses + marker = getMarkerObj(value, object->document); + } + } + + // Scroll the combobox to that marker + combo->set_current(marker); + } + +} + +} // namespace Widget +} // 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/widget/stroke-style.h b/src/ui/widget/stroke-style.h new file mode 100644 index 0000000..0cc29d5 --- /dev/null +++ b/src/ui/widget/stroke-style.h @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Widgets used in the stroke style dialog. + */ +/* Author: + * Lauris Kaplinski <lauris@ximian.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +// WHOA! talk about header bloat! + +#ifndef SEEN_DIALOGS_STROKE_STYLE_H +#define SEEN_DIALOGS_STROKE_STYLE_H + +#include <glibmm/i18n.h> +#include <gtkmm/grid.h> +#include <gtkmm/radiobutton.h> + + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "fill-style.h" // to get sp_fill_style_widget_set_desktop +#include "gradient-chemistry.h" +#include "inkscape.h" +#include "path-prefix.h" +#include "preferences.h" +#include "selection.h" +#include "style.h" + +#include "display/drawing.h" + +#include "helper/stock-items.h" + +#include "io/sys.h" + +#include "svg/css-ostringstream.h" + +#include "ui/cache/svg_preview_cache.h" +#include "ui/dialog-events.h" +#include "ui/icon-names.h" +#include "ui/widget/spinbutton.h" + +#include "widgets/spw-utilities.h" + + +namespace Gtk { +class Widget; +class Container; +} + +namespace Inkscape { + namespace Util { + class Unit; + } + namespace UI { + namespace Widget { + class DashSelector; + class MarkerComboBox; + class UnitMenu; + } + } +} + +struct { gchar const *key; gint value; } const SPMarkerNames[] = { + {"marker-all", SP_MARKER_LOC}, + {"marker-start", SP_MARKER_LOC_START}, + {"marker-mid", SP_MARKER_LOC_MID}, + {"marker-end", SP_MARKER_LOC_END}, + {"", SP_MARKER_LOC_QTY}, + {nullptr, -1} +}; + + +SPObject *getMarkerObj(gchar const *n, SPDocument *doc); + +namespace Inkscape { +namespace UI { +namespace Widget { +class StrokeStyleButton; + +class StrokeStyle : public Gtk::Box +{ +public: + StrokeStyle(); + ~StrokeStyle() override; + void setDesktop(SPDesktop *desktop); + void updateLine(); + void selectionModifiedCB(guint flags); + void selectionChangedCB(); +private: + /** List of valid types for the stroke-style radio-button widget */ + enum StrokeStyleButtonType { + STROKE_STYLE_BUTTON_JOIN, ///< A button to set the line-join style + STROKE_STYLE_BUTTON_CAP, ///< A button to set the line-cap style + STROKE_STYLE_BUTTON_ORDER ///< A button to set the paint-order style + }; + + /** + * A custom radio-button for setting the stroke style. It can be configured + * to set either the join or cap style by setting the button_type field. + */ + class StrokeStyleButton : public Gtk::RadioButton { + public: + StrokeStyleButton(Gtk::RadioButtonGroup &grp, + char const *icon, + StrokeStyleButtonType button_type, + gchar const *stroke_style); + + /** Get the type (line/cap) of the stroke-style button */ + inline StrokeStyleButtonType get_button_type() {return button_type;} + + /** Get the stroke style attribute associated with the button */ + inline gchar const * get_stroke_style() {return stroke_style;} + + private: + StrokeStyleButtonType button_type; ///< The type (line/cap) of the button + gchar const *stroke_style; ///< The stroke style associated with the button + }; + + std::vector<double> getDashFromStyle(SPStyle *style, double &offset); + + void updateAllMarkers(std::vector<SPItem*> const &objects, bool skip_undo = false); + void setDashSelectorFromStyle(Inkscape::UI::Widget::DashSelector *dsel, SPStyle *style); + void setJoinType (unsigned const jointype); + void setCapType (unsigned const captype); + void setPaintOrder (gchar const *paint_order); + void setJoinButtons(Gtk::ToggleButton *active); + void setCapButtons(Gtk::ToggleButton *active); + void setPaintOrderButtons(Gtk::ToggleButton *active); + void setStrokeWidth(); + void setStrokeDash(); + void setStrokeMiter(); + void setScaledDash(SPCSSAttr *css, int ndash, const double *dash, double offset, double scale); + bool isHairlineSelected() const; + + StrokeStyleButton * makeRadioButton(Gtk::RadioButtonGroup &grp, + char const *icon, + Gtk::Box *hb, + StrokeStyleButtonType button_type, + gchar const *stroke_style); + + // Callback functions + void unitChangedCB(); + bool areMarkersBeingUpdated(); + void markerSelectCB(MarkerComboBox *marker_combo, SPMarkerLoc const which); + static void buttonToggledCB(StrokeStyleButton *tb, StrokeStyle *spw); + + + MarkerComboBox *startMarkerCombo; + MarkerComboBox *midMarkerCombo; + MarkerComboBox *endMarkerCombo; + Gtk::Grid *table; + Glib::RefPtr<Gtk::Adjustment> *widthAdj; + Glib::RefPtr<Gtk::Adjustment> *miterLimitAdj; + Inkscape::UI::Widget::SpinButton *miterLimitSpin; + Inkscape::UI::Widget::SpinButton *widthSpin; + Inkscape::UI::Widget::UnitMenu *unitSelector; + //Gtk::ToggleButton *hairline; + StrokeStyleButton *joinMiter; + StrokeStyleButton *joinRound; + StrokeStyleButton *joinBevel; + StrokeStyleButton *capButt; + StrokeStyleButton *capRound; + StrokeStyleButton *capSquare; + StrokeStyleButton *paintOrderFSM; + StrokeStyleButton *paintOrderSFM; + StrokeStyleButton *paintOrderFMS; + StrokeStyleButton *paintOrderMFS; + StrokeStyleButton *paintOrderSMF; + StrokeStyleButton *paintOrderMSF; + Inkscape::UI::Widget::DashSelector *dashSelector; + Gtk::Entry* _pattern = nullptr; + Gtk::Label* _pattern_label = nullptr; + void update_pattern(int ndash, const double* pattern); + bool _editing_pattern = false; + + gboolean update; + double _last_width = 0.0; + SPDesktop *desktop; + sigc::connection startMarkerConn; + sigc::connection midMarkerConn; + sigc::connection endMarkerConn; + + Inkscape::Util::Unit const *_old_unit; + + void _handleDocumentReplaced(SPDesktop *, SPDocument *); + void enterEditMarkerMode(SPMarkerLoc editMarkerMode); + sigc::connection _document_replaced_connection; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_DIALOGS_STROKE_STYLE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/style-subject.cpp b/src/ui/widget/style-subject.cpp new file mode 100644 index 0000000..110f6ff --- /dev/null +++ b/src/ui/widget/style-subject.cpp @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 MenTaLguY <mental@rydia.net> + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "style-subject.h" + +#include "desktop.h" +#include "desktop-style.h" +#include "layer-manager.h" +#include "selection.h" + +#include "xml/sp-css-attr.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +StyleSubject::StyleSubject() { +} + +StyleSubject::~StyleSubject() { + setDesktop(nullptr); +} + +void StyleSubject::setDesktop(SPDesktop *desktop) { + if (desktop != _desktop) { + _desktop = desktop; + _afterDesktopSwitch(desktop); + if (_desktop) { + _emitChanged(); // This updates the widgets. + } + } +} + +StyleSubject::Selection::Selection() = default; + +StyleSubject::Selection::~Selection() = default; + +Inkscape::Selection *StyleSubject::Selection::_getSelection() const { + SPDesktop *desktop = getDesktop(); + if (desktop) { + return desktop->getSelection(); + } else { + return nullptr; + } +} + +std::vector<SPObject*> StyleSubject::Selection::list() { + Inkscape::Selection *selection = _getSelection(); + if(selection) { + return std::vector<SPObject *>(selection->objects().begin(), selection->objects().end()); + } + + return std::vector<SPObject*>(); +} + +Geom::OptRect StyleSubject::Selection::getBounds(SPItem::BBoxType type) { + Inkscape::Selection *selection = _getSelection(); + if (selection) { + return selection->bounds(type); + } else { + return Geom::OptRect(); + } +} + +int StyleSubject::Selection::queryStyle(SPStyle *query, int property) { + SPDesktop *desktop = getDesktop(); + if (desktop) { + return sp_desktop_query_style(desktop, query, property); + } else { + return QUERY_STYLE_NOTHING; + } +} + +void StyleSubject::Selection::_afterDesktopSwitch(SPDesktop *desktop) { + _sel_changed.disconnect(); + _subsel_changed.disconnect(); + _sel_modified.disconnect(); + if (desktop) { + _subsel_changed = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &Selection::_emitChanged))); + Inkscape::Selection *selection = desktop->getSelection(); + if (selection) { + _sel_changed = selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &Selection::_emitChanged))); + _sel_modified = selection->connectModified(sigc::mem_fun(*this, &Selection::_emitModified)); + } + } +} + +void StyleSubject::Selection::setCSS(SPCSSAttr *css) { + SPDesktop *desktop = getDesktop(); + if (desktop) { + sp_desktop_set_style(desktop, css); + } +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode: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/widget/style-subject.h b/src/ui/widget/style-subject.h new file mode 100644 index 0000000..d88c6da --- /dev/null +++ b/src/ui/widget/style-subject.h @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Abstraction for different style widget operands. Used by ObjectCompositeSettings in Layers and + * Fill and Stroke dialogs. Dialog is responsible for keeping desktop pointer valid. + * + * This class is due to the need to differentiate between layers and objects but a layer is just a + * a group object with an extra tag. There should be no need to differentiate between the two. + * To do: remove this class and intergrate the functionality into ObjectCompositeSettings. + */ +/* + * Copyright (C) 2007 MenTaLguY <mental@rydia.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INKSCAPE_UI_WIDGET_STYLE_SUBJECT_H +#define SEEN_INKSCAPE_UI_WIDGET_STYLE_SUBJECT_H + +#include <optional> +#include <2geom/rect.h> +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "object/sp-item.h" +#include "object/sp-tag.h" +#include "object/sp-tag-use.h" +#include "object/sp-tag-use-reference.h" + +class SPDesktop; +class SPObject; +class SPCSSAttr; +class SPStyle; + +namespace Inkscape { +class Selection; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class StyleSubject { +public: + class Selection; + class CurrentLayer; + + + StyleSubject(); + virtual ~StyleSubject(); + + void setDesktop(SPDesktop *desktop); + SPDesktop *getDesktop() const { return _desktop; } + + virtual Geom::OptRect getBounds(SPItem::BBoxType type) = 0; + virtual int queryStyle(SPStyle *query, int property) = 0; + virtual void setCSS(SPCSSAttr *css) = 0; + virtual std::vector<SPObject*> list(){return std::vector<SPObject*>();}; + + sigc::connection connectChanged(sigc::signal<void>::slot_type slot) { + return _changed_signal.connect(slot); + } + +protected: + virtual void _afterDesktopSwitch(SPDesktop */*desktop*/) {} + void _emitChanged() { _changed_signal.emit(); } + void _emitModified(Inkscape::Selection* selection, guint flags) { + // Do not say this object has styles unless it's style has been modified + if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) { + _emitChanged(); + } + } + +private: + sigc::signal<void> _changed_signal; + SPDesktop *_desktop = nullptr; +}; + +class StyleSubject::Selection : public StyleSubject { +public: + Selection(); + ~Selection() override; + + Geom::OptRect getBounds(SPItem::BBoxType type) override; + int queryStyle(SPStyle *query, int property) override; + void setCSS(SPCSSAttr *css) override; + std::vector<SPObject*> list() override; + +protected: + void _afterDesktopSwitch(SPDesktop *desktop) override; + +private: + Inkscape::Selection *_getSelection() const; + + sigc::connection _sel_changed; + sigc::connection _subsel_changed; + sigc::connection _sel_modified; +}; + +} +} +} + +#endif // SEEN_INKSCAPE_UI_WIDGET_STYLE_SUBJECT_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/widget/style-swatch.cpp b/src/ui/widget/style-swatch.cpp new file mode 100644 index 0000000..a79f9d9 --- /dev/null +++ b/src/ui/widget/style-swatch.cpp @@ -0,0 +1,386 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Static style swatch (fill, stroke, opacity). + */ +/* Authors: + * buliabyak@gmail.com + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2005-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "style-swatch.h" + +#include <glibmm/i18n.h> +#include <gtkmm/grid.h> + +#include "inkscape.h" +#include "style.h" + +#include "actions/actions-tools.h" // Open tool preferences. + +#include "object/sp-linear-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" + +#include "ui/widget/color-preview.h" +#include "util/units.h" + +#include "widgets/spw-utilities.h" + +#include "xml/sp-css-attr.h" +#include "xml/attribute-record.h" + +enum { + SS_FILL, + SS_STROKE +}; + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Watches whether the tool uses the current style. + */ +class StyleSwatch::ToolObserver : public Inkscape::Preferences::Observer { +public: + ToolObserver(Glib::ustring const &path, StyleSwatch &ss) : + Observer(path), + _style_swatch(ss) + {} + void notify(Inkscape::Preferences::Entry const &val) override; +private: + StyleSwatch &_style_swatch; +}; + +/** + * Watches for changes in the observed style pref. + */ +class StyleSwatch::StyleObserver : public Inkscape::Preferences::Observer { +public: + StyleObserver(Glib::ustring const &path, StyleSwatch &ss) : + Observer(path), + _style_swatch(ss) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->notify(prefs->getEntry(path)); + } + void notify(Inkscape::Preferences::Entry const &val) override { + SPCSSAttr *css = val.getInheritedStyle(); + _style_swatch.setStyle(css); + sp_repr_css_attr_unref(css); + } +private: + StyleSwatch &_style_swatch; +}; + +void StyleSwatch::ToolObserver::notify(Inkscape::Preferences::Entry const &val) +{ + bool usecurrent = val.getBool(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (_style_swatch._style_obs) delete _style_swatch._style_obs; + + if (usecurrent) { + _style_swatch._style_obs = new StyleObserver("/desktop/style", _style_swatch); + + // If desktop's last-set style is empty, a tool uses its own fixed style even if set to use + // last-set (so long as it's empty). To correctly show this, we get the tool's style + // if the desktop's style is empty. + SPCSSAttr *css = prefs->getStyle("/desktop/style"); + const auto & al = css->attributeList(); + if (al.empty()) { + SPCSSAttr *css2 = prefs->getInheritedStyle(_style_swatch._tool_path + "/style"); + _style_swatch.setStyle(css2); + sp_repr_css_attr_unref(css2); + } + sp_repr_css_attr_unref(css); + } else { + _style_swatch._style_obs = new StyleObserver(_style_swatch._tool_path + "/style", _style_swatch); + } + prefs->addObserver(*_style_swatch._style_obs); +} + +StyleSwatch::StyleSwatch(SPCSSAttr *css, gchar const *main_tip) + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL), + _desktop(nullptr), + _css(nullptr), + _tool_obs(nullptr), + _style_obs(nullptr), + _table(Gtk::manage(new Gtk::Grid())), + _sw_unit(nullptr), + _stroke(Gtk::ORIENTATION_HORIZONTAL) +{ + set_name("StyleSwatch"); + _label[SS_FILL].set_markup(_("Fill:")); + _label[SS_STROKE].set_markup(_("Stroke:")); + + for (int i = SS_FILL; i <= SS_STROKE; i++) { + _label[i].set_halign(Gtk::ALIGN_START); + _label[i].set_valign(Gtk::ALIGN_CENTER); + _label[i].set_margin_top(0); + _label[i].set_margin_bottom(0); + _label[i].set_margin_start(0); + _label[i].set_margin_end(0); + + _color_preview[i] = new Inkscape::UI::Widget::ColorPreview (0); + } + + _opacity_value.set_halign(Gtk::ALIGN_START); + _opacity_value.set_valign(Gtk::ALIGN_CENTER); + _opacity_value.set_margin_top(0); + _opacity_value.set_margin_bottom(0); + _opacity_value.set_margin_start(0); + _opacity_value.set_margin_end(0); + + _table->set_column_spacing(2); + _table->set_row_spacing(0); + + _stroke.pack_start(_place[SS_STROKE]); + _stroke_width_place.add(_stroke_width); + _stroke.pack_start(_stroke_width_place, Gtk::PACK_SHRINK); + + _opacity_place.add(_opacity_value); + + _table->attach(_label[SS_FILL], 0, 0, 1, 1); + _table->attach(_label[SS_STROKE], 0, 1, 1, 1); + _table->attach(_place[SS_FILL], 1, 0, 1, 1); + _table->attach(_stroke, 1, 1, 1, 1); + _table->attach(_empty_space, 2, 0, 1, 2); + _table->attach(_opacity_place, 2, 0, 1, 2); + _swatch.add(*_table); + pack_start(_swatch, true, true, 0); + + set_size_request (STYLE_SWATCH_WIDTH, -1); + + setStyle (css); + + _swatch.signal_button_press_event().connect(sigc::mem_fun(*this, &StyleSwatch::on_click)); + + if (main_tip) + { + _swatch.set_tooltip_text(main_tip); + } +} + +void StyleSwatch::setToolName(const Glib::ustring& tool_name) { + _tool_name = tool_name; +} + +void StyleSwatch::setDesktop(SPDesktop *desktop) { + _desktop = desktop; +} + +bool +StyleSwatch::on_click(GdkEventButton */*event*/) +{ + if (_desktop && !_tool_name.empty()) { + auto win = _desktop->getInkscapeWindow(); + open_tool_preferences(win, _tool_name); + return true; + } + return false; +} + +StyleSwatch::~StyleSwatch() +{ + if (_css) + sp_repr_css_attr_unref (_css); + + for (int i = SS_FILL; i <= SS_STROKE; i++) { + delete _color_preview[i]; + } + + if (_style_obs) delete _style_obs; + if (_tool_obs) delete _tool_obs; +} + +void +StyleSwatch::setWatchedTool(const char *path, bool synthesize) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (_tool_obs) { + delete _tool_obs; + _tool_obs = nullptr; + } + + if (path) { + _tool_path = path; + _tool_obs = new ToolObserver(_tool_path + "/usecurrent", *this); + prefs->addObserver(*_tool_obs); + } else { + _tool_path = ""; + } + + // hack until there is a real synthesize events function for prefs, + // which shouldn't be hard to write once there is sufficient need for it + if (synthesize && _tool_obs) { + _tool_obs->notify(prefs->getEntry(_tool_path + "/usecurrent")); + } +} + + +void StyleSwatch::setStyle(SPCSSAttr *css) +{ + if (_css) + sp_repr_css_attr_unref (_css); + + if (!css) + return; + + _css = sp_repr_css_attr_new(); + sp_repr_css_merge(_css, css); + + Glib::ustring css_string; + sp_repr_css_write_string (_css, css_string); + + SPStyle style(_desktop ? _desktop->getDocument() : nullptr); + if (!css_string.empty()) { + style.mergeString(css_string.c_str()); + } + setStyle (&style); +} + +void StyleSwatch::setStyle(SPStyle *query) +{ + _place[SS_FILL].remove(); + _place[SS_STROKE].remove(); + + bool has_stroke = true; + + for (int i = SS_FILL; i <= SS_STROKE; i++) { + Gtk::EventBox *place = &(_place[i]); + + SPIPaint *paint; + if (i == SS_FILL) { + paint = &(query->fill); + } else { + paint = &(query->stroke); + } + + if (paint->set && paint->isPaintserver()) { + SPPaintServer *server = (i == SS_FILL)? SP_STYLE_FILL_SERVER (query) : SP_STYLE_STROKE_SERVER (query); + + if (SP_IS_LINEARGRADIENT (server)) { + _value[i].set_markup(_("L Gradient")); + place->add(_value[i]); + place->set_tooltip_text((i == SS_FILL)? (_("Linear gradient (fill)")) : (_("Linear gradient (stroke)"))); + } else if (SP_IS_RADIALGRADIENT (server)) { + _value[i].set_markup(_("R Gradient")); + place->add(_value[i]); + place->set_tooltip_text((i == SS_FILL)? (_("Radial gradient (fill)")) : (_("Radial gradient (stroke)"))); + } else if (SP_IS_PATTERN (server)) { + _value[i].set_markup(_("Pattern")); + place->add(_value[i]); + place->set_tooltip_text((i == SS_FILL)? (_("Pattern (fill)")) : (_("Pattern (stroke)"))); + } + + } else if (paint->set && paint->isColor()) { + guint32 color = paint->value.color.toRGBA32( SP_SCALE24_TO_FLOAT ((i == SS_FILL)? query->fill_opacity.value : query->stroke_opacity.value) ); + ((Inkscape::UI::Widget::ColorPreview*)_color_preview[i])->setRgba32 (color); + _color_preview[i]->show_all(); + place->add(*_color_preview[i]); + gchar *tip; + if (i == SS_FILL) { + tip = g_strdup_printf (_("Fill: %06x/%.3g"), color >> 8, SP_RGBA32_A_F(color)); + } else { + tip = g_strdup_printf (_("Stroke: %06x/%.3g"), color >> 8, SP_RGBA32_A_F(color)); + } + place->set_tooltip_text(tip); + g_free (tip); + } else if (paint->set && paint->isNone()) { + _value[i].set_markup(C_("Fill and stroke", "<i>None</i>")); + place->add(_value[i]); + place->set_tooltip_text((i == SS_FILL)? (C_("Fill and stroke", "No fill")) : (C_("Fill and stroke", "No stroke"))); + if (i == SS_STROKE) has_stroke = false; + } else if (!paint->set) { + _value[i].set_markup(_("<b>Unset</b>")); + place->add(_value[i]); + place->set_tooltip_text((i == SS_FILL)? (_("Unset fill")) : (_("Unset stroke"))); + if (i == SS_STROKE) has_stroke = false; + } + } + +// Now query stroke_width + if (has_stroke) { + if (query->stroke_extensions.hairline) { + Glib::ustring swidth = "<small>"; + swidth += _("Hairline"); + swidth += "</small>"; + _stroke_width.set_markup(swidth.c_str()); + auto str = Glib::ustring::compose(_("Stroke width: %1"), _("Hairline")); + _stroke_width_place.set_tooltip_text(str); + } else { + double w; + if (_sw_unit) { + w = Inkscape::Util::Quantity::convert(query->stroke_width.computed, "px", _sw_unit); + } else { + w = query->stroke_width.computed; + } + + { + gchar *str = g_strdup_printf(" %.3g", w); + Glib::ustring swidth = "<small>"; + swidth += str; + swidth += "</small>"; + _stroke_width.set_markup(swidth.c_str()); + g_free (str); + } + { + gchar *str = g_strdup_printf(_("Stroke width: %.5g%s"), + w, + _sw_unit? _sw_unit->abbr.c_str() : "px"); + _stroke_width_place.set_tooltip_text(str); + g_free (str); + } + } + } else { + _stroke_width_place.set_tooltip_text(""); + _stroke_width.set_markup(""); + _stroke_width.set_has_tooltip(false); + } + + gdouble op = SP_SCALE24_TO_FLOAT(query->opacity.value); + if (op != 1) { + { + gchar *str; + str = g_strdup_printf(_("O: %2.0f"), (op*100.0)); + Glib::ustring opacity = "<small>"; + opacity += str; + opacity += "</small>"; + _opacity_value.set_markup (opacity.c_str()); + g_free (str); + } + { + gchar *str = g_strdup_printf(_("Opacity: %2.1f %%"), (op*100.0)); + _opacity_place.set_tooltip_text(str); + g_free (str); + } + } else { + _opacity_place.set_tooltip_text(""); + _opacity_value.set_markup(""); + _opacity_value.set_has_tooltip(false); + } + + show_all(); +} + +} // namespace Widget +} // 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/widget/style-swatch.h b/src/ui/widget/style-swatch.h new file mode 100644 index 0000000..31165f5 --- /dev/null +++ b/src/ui/widget/style-swatch.h @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Static style swatch (fill, stroke, opacity) + */ +/* Authors: + * buliabyak@gmail.com + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright (C) 2005-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_CURRENT_STYLE_H +#define INKSCAPE_UI_CURRENT_STYLE_H + +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/enums.h> + +#include "desktop.h" +#include "preferences.h" + +constexpr int STYLE_SWATCH_WIDTH = 135; + +class SPStyle; +class SPCSSAttr; + +namespace Gtk { +class Grid; +} + +namespace Inkscape { + +namespace Util { + class Unit; +} + +namespace UI { +namespace Widget { + +class StyleSwatch : public Gtk::Box +{ +public: + StyleSwatch (SPCSSAttr *attr, gchar const *main_tip); + + ~StyleSwatch() override; + + void setStyle(SPStyle *style); + void setStyle(SPCSSAttr *attr); + SPCSSAttr *getStyle(); + + void setWatchedTool (const char *path, bool synthesize); + void setToolName(const Glib::ustring& tool_name); + void setDesktop(SPDesktop *desktop); + bool on_click(GdkEventButton *event); + +private: + class ToolObserver; + class StyleObserver; + + SPDesktop *_desktop; + Glib::ustring _tool_name; + SPCSSAttr *_css; + ToolObserver *_tool_obs; + StyleObserver *_style_obs; + Glib::ustring _tool_path; + + Gtk::EventBox _swatch; + + Gtk::Grid *_table; + + Gtk::Label _label[2]; + Gtk::Box _empty_space; + Gtk::EventBox _place[2]; + Gtk::EventBox _opacity_place; + Gtk::Label _value[2]; + Gtk::Label _opacity_value; + Gtk::Widget *_color_preview[2]; + Glib::ustring __color[2]; + Gtk::Box _stroke; + Gtk::EventBox _stroke_width_place; + Gtk::Label _stroke_width; + + Inkscape::Util::Unit *_sw_unit; + +friend class ToolObserver; +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_BUTTON_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/widget/swatch-selector.cpp b/src/ui/widget/swatch-selector.cpp new file mode 100644 index 0000000..edff1d5 --- /dev/null +++ b/src/ui/widget/swatch-selector.cpp @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "swatch-selector.h" + +#include <glibmm/i18n.h> + +#include "document-undo.h" +#include "document.h" +#include "gradient-chemistry.h" + +#include "object/sp-stop.h" + +#include "svg/css-ostringstream.h" +#include "svg/svg-color.h" + +#include "ui/icon-names.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/gradient-selector.h" + +#include "xml/node.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +SwatchSelector::SwatchSelector() : + Gtk::Box(Gtk::ORIENTATION_VERTICAL), + _gsel(nullptr), + _updating_color(false) +{ + using Inkscape::UI::Widget::ColorNotebook; + + _gsel = Gtk::manage(new GradientSelector()); + _gsel->setMode(GradientSelector::MODE_SWATCH); + + _gsel->show(); + + pack_start(*_gsel); + + auto color_selector = Gtk::manage(new ColorNotebook(_selected_color)); + color_selector->set_label(_("Swatch color")); + color_selector->show(); + pack_start(*color_selector); + + //_selected_color.signal_grabbed.connect(sigc::mem_fun(this, &SwatchSelector::_grabbedCb)); + _selected_color.signal_dragged.connect(sigc::mem_fun(this, &SwatchSelector::_changedCb)); + _selected_color.signal_released.connect(sigc::mem_fun(this, &SwatchSelector::_changedCb)); + // signal_changed doesn't get called if updating shape with colour. + _selected_color.signal_changed.connect(sigc::mem_fun(this, &SwatchSelector::_changedCb)); +} + +SwatchSelector::~SwatchSelector() +{ + _gsel = nullptr; +} + +GradientSelector *SwatchSelector::getGradientSelector() +{ + return _gsel; +} + +void SwatchSelector::_changedCb() +{ + if (_updating_color) { + return; + } + // TODO might have to block cycles + + if (_gsel && _gsel->getVector()) { + SPGradient *gradient = _gsel->getVector(); + SPGradient *ngr = sp_gradient_ensure_vector_normalized(gradient); + if (ngr != gradient) { + /* Our master gradient has changed */ + // TODO replace with proper - sp_gradient_vector_widget_load_gradient(GTK_WIDGET(swsel->_gsel), ngr); + } + + ngr->ensureVector(); + + + SPStop* stop = ngr->getFirstStop(); + if (stop) { + SPColor color = _selected_color.color(); + gfloat alpha = _selected_color.alpha(); + guint32 rgb = color.toRGBA32( 0x00 ); + + // TODO replace with generic shared code that also handles icc-color + Inkscape::CSSOStringStream os; + gchar c[64]; + sp_svg_write_color(c, sizeof(c), rgb); + os << "stop-color:" << c << ";stop-opacity:" << static_cast<gdouble>(alpha) <<";"; + stop->setAttribute("style", os.str()); + + DocumentUndo::done(ngr->document, _("Change swatch color"), INKSCAPE_ICON("color-gradient")); + } + } +} + +void SwatchSelector::connectchangedHandler( GCallback handler, void *data ) +{ + GObject* obj = G_OBJECT(_gsel); + g_signal_connect( obj, "changed", handler, data ); +} + +void SwatchSelector::setVector(SPDocument */*doc*/, SPGradient *vector) +{ + //GtkVBox * box = gobj(); + _gsel->setVector((vector) ? vector->document : nullptr, vector); + + if ( vector && vector->isSolid() ) { + SPStop* stop = vector->getFirstStop(); + + guint32 const colorVal = stop->get_rgba32(); + _updating_color = true; + _selected_color.setValue(colorVal); + _updating_color = false; + // gtk_widget_show_all( GTK_WIDGET(_csel) ); + } else { + //gtk_widget_hide( GTK_WIDGET(_csel) ); + } + +/* +*/ +} + +} // namespace Widget +} // 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/widget/swatch-selector.h b/src/ui/widget/swatch-selector.h new file mode 100644 index 0000000..67486a5 --- /dev/null +++ b/src/ui/widget/swatch-selector.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_SWATCH_SELECTOR_H +#define SEEN_SP_SWATCH_SELECTOR_H + +#include <gtkmm/box.h> +#include "ui/selected-color.h" + +class SPDocument; +class SPGradient; +struct SPColorSelector; + +namespace Inkscape { +namespace UI { +namespace Widget { +class GradientSelector; + +class SwatchSelector : public Gtk::Box +{ +public: + SwatchSelector(); + ~SwatchSelector() override; + + void connectchangedHandler( GCallback handler, void *data ); + + void setVector(SPDocument *doc, SPGradient *vector); + + GradientSelector *getGradientSelector(); + +private: + void _grabbedCb(); + void _draggedCb(); + void _releasedCb(); + void _changedCb(); + + GradientSelector *_gsel; + Inkscape::UI::SelectedColor _selected_color; + bool _updating_color; +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_SP_SWATCH_SELECTOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/ui/widget/text.cpp b/src/ui/widget/text.cpp new file mode 100644 index 0000000..656ec45 --- /dev/null +++ b/src/ui/widget/text.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Maximilian Albert <maximilian.albert@gmail.com> + * + * Copyright (C) 2004 Carl Hetherington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "text.h" +#include <gtkmm/entry.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +Text::Text(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new Gtk::Entry(), suffix, icon, mnemonic), + setProgrammatically(false) +{ +} + +Glib::ustring const Text::getText() const +{ + g_assert(_widget != nullptr); + return static_cast<Gtk::Entry*>(_widget)->get_text(); +} + +void Text::setText(Glib::ustring const text) +{ + g_assert(_widget != nullptr); + setProgrammatically = true; // callback is supposed to reset back, if it cares + static_cast<Gtk::Entry*>(_widget)->set_text(text); // FIXME: set correctly +} + +Glib::SignalProxy0<void> Text::signal_activate() +{ + return static_cast<Gtk::Entry*>(_widget)->signal_activate(); +} + + +} // namespace Widget +} // 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/widget/text.h b/src/ui/widget/text.h new file mode 100644 index 0000000..87c9357 --- /dev/null +++ b/src/ui/widget/text.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Maximilian Albert <maximilian.albert@gmail.com> + * + * Copyright (C) 2004 Carl Hetherington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_TEXT_H +#define INKSCAPE_UI_WIDGET_TEXT_H + +#include "labelled.h" + + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A labelled text box, with optional icon or suffix, for entering arbitrary number values. + */ +class Text : public Labelled +{ +public: + + /** + * Construct a Text Widget. + * + * @param label Label. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Text(Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Get the text in the entry. + */ + Glib::ustring const getText() const; + + /** + * Sets the text of the text entry. + */ + void setText(Glib::ustring const text); + + void update(); + + /** + * Signal raised when the spin button's value changes. + */ + Glib::SignalProxy0<void> signal_activate(); + + bool setProgrammatically; // true if the value was set by setValue, not changed by the user; + // if a callback checks it, it must reset it back to false +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_TEXT_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/widget/tolerance-slider.cpp b/src/ui/widget/tolerance-slider.cpp new file mode 100644 index 0000000..817e783 --- /dev/null +++ b/src/ui/widget/tolerance-slider.cpp @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * Abhishek Sharma + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tolerance-slider.h" + +#include "registry.h" + +#include <gtkmm/adjustment.h> +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/scale.h> + +#include "inkscape.h" +#include "document.h" +#include "document-undo.h" +#include "desktop.h" + +#include "object/sp-namedview.h" + +#include "svg/stringstream.h" + +#include "xml/repr.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +//=================================================== + +//--------------------------------------------------- + + + +//==================================================== + +ToleranceSlider::ToleranceSlider(const Glib::ustring& label1, const Glib::ustring& label2, const Glib::ustring& label3, const Glib::ustring& tip1, const Glib::ustring& tip2, const Glib::ustring& tip3, const Glib::ustring& key, Registry& wr) +: _vbox(nullptr) +{ + init(label1, label2, label3, tip1, tip2, tip3, key, wr); +} + +ToleranceSlider::~ToleranceSlider() +{ + if (_vbox) delete _vbox; + _scale_changed_connection.disconnect(); +} + +void ToleranceSlider::init (const Glib::ustring& label1, const Glib::ustring& label2, const Glib::ustring& label3, const Glib::ustring& tip1, const Glib::ustring& tip2, const Glib::ustring& tip3, const Glib::ustring& key, Registry& wr) +{ + // hbox = label + slider + // + // e.g. + // + // snap distance |-------X---| 37 + + // vbox = checkbutton + // + + // hbox + + _vbox = new Gtk::Box(Gtk::ORIENTATION_VERTICAL); + _hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + + Gtk::Label *theLabel1 = Gtk::manage(new Gtk::Label(label1)); + theLabel1->set_use_underline(); + theLabel1->set_halign(Gtk::ALIGN_START); + theLabel1->set_valign(Gtk::ALIGN_CENTER); + // align the label with the checkbox text above by indenting 22 px. + _hbox->pack_start(*theLabel1, Gtk::PACK_EXPAND_WIDGET, 22); + + _hscale = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL)); + _hscale->set_range(1.0, 51.0); + + theLabel1->set_mnemonic_widget (*_hscale); + _hscale->set_draw_value (true); + _hscale->set_value_pos (Gtk::POS_RIGHT); + _hscale->set_size_request (100, -1); + _old_val = 10; + _hscale->set_value (_old_val); + _hscale->set_tooltip_text (tip1); + _hbox->add (*_hscale); + + + Gtk::Label *theLabel2 = Gtk::manage(new Gtk::Label(label2)); + theLabel2->set_use_underline(); + Gtk::Label *theLabel3 = Gtk::manage(new Gtk::Label(label3)); + theLabel3->set_use_underline(); + _button1 = Gtk::manage(new Gtk::RadioButton); + _radio_button_group = _button1->get_group(); + _button2 = Gtk::manage(new Gtk::RadioButton); + _button2->set_group(_radio_button_group); + _button1->set_tooltip_text (tip2); + _button2->set_tooltip_text (tip3); + _button1->add (*theLabel3); + _button1->set_halign(Gtk::ALIGN_START); + _button1->set_valign(Gtk::ALIGN_CENTER); + _button2->add (*theLabel2); + _button2->set_halign(Gtk::ALIGN_START); + _button2->set_valign(Gtk::ALIGN_CENTER); + + _vbox->add (*_button1); + _vbox->add (*_button2); + // Here we need some extra pixels to get the vertical spacing right. Why? + _vbox->pack_end(*_hbox, true, true, 3); // add 3 px. + _key = key; + _scale_changed_connection = _hscale->signal_value_changed().connect (sigc::mem_fun (*this, &ToleranceSlider::on_scale_changed)); + _btn_toggled_connection = _button2->signal_toggled().connect (sigc::mem_fun (*this, &ToleranceSlider::on_toggled)); + _wr = ≀ + _vbox->show_all_children(); +} + +void ToleranceSlider::setValue (double val) +{ + auto adj = _hscale->get_adjustment(); + + adj->set_lower (1.0); + adj->set_upper (51.0); + adj->set_step_increment (1.0); + + if (val > 9999.9) // magic value 10000.0 + { + _button1->set_active (true); + _button2->set_active (false); + _hbox->set_sensitive (false); + val = 50.0; + } + else + { + _button1->set_active (false); + _button2->set_active (true); + _hbox->set_sensitive (true); + } + _hscale->set_value (val); + _hbox->show_all(); +} + +void ToleranceSlider::setLimits (double theMin, double theMax) +{ + _hscale->set_range (theMin, theMax); + _hscale->get_adjustment()->set_step_increment (1); +} + +void ToleranceSlider::on_scale_changed() +{ + update (_hscale->get_value()); +} + +void ToleranceSlider::on_toggled() +{ + if (!_button2->get_active()) + { + _old_val = _hscale->get_value(); + _hbox->set_sensitive (false); + _hbox->show_all(); + setValue (10000.0); + update (10000.0); + } + else + { + _hbox->set_sensitive (true); + _hbox->show_all(); + setValue (_old_val); + update (_old_val); + } +} + +void ToleranceSlider::update (double val) +{ + if (_wr->isUpdating()) + return; + + SPDesktop *dt = _wr->desktop(); + if (!dt) + return; + + Inkscape::SVGOStringStream os; + os << val; + + _wr->setUpdating (true); + + SPDocument *doc = dt->getDocument(); + bool saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); + Inkscape::XML::Node *repr = dt->getNamedView()->getRepr(); + repr->setAttribute(_key, os.str()); + DocumentUndo::setUndoSensitive(doc, saved); + + doc->setModifiedSinceSave(); + + _wr->setUpdating (false); +} + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/tolerance-slider.h b/src/ui/widget/tolerance-slider.h new file mode 100644 index 0000000..cb12116 --- /dev/null +++ b/src/ui/widget/tolerance-slider.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_TOLERANCE_SLIDER__H_ +#define INKSCAPE_UI_WIDGET_TOLERANCE_SLIDER__H_ + +#include <gtkmm/radiobuttongroup.h> + +namespace Gtk { +class RadioButton; +class Scale; +class Box; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Registry; + +/** + * Implementation of tolerance slider widget. + * This widget is part of the Document properties dialog. + */ +class ToleranceSlider { +public: + ToleranceSlider(const Glib::ustring& label1, + const Glib::ustring& label2, + const Glib::ustring& label3, + const Glib::ustring& tip1, + const Glib::ustring& tip2, + const Glib::ustring& tip3, + const Glib::ustring& key, + Registry& wr); + ~ToleranceSlider(); + void setValue (double); + void setLimits (double, double); + Gtk::Box* _vbox; +private: + void init (const Glib::ustring& label1, + const Glib::ustring& label2, + const Glib::ustring& label3, + const Glib::ustring& tip1, + const Glib::ustring& tip2, + const Glib::ustring& tip3, + const Glib::ustring& key, + Registry& wr); + +protected: + void on_scale_changed(); + void on_toggled(); + void update (double val); + Gtk::Box *_hbox; + Gtk::Scale *_hscale; + Gtk::RadioButtonGroup _radio_button_group; + Gtk::RadioButton *_button1; + Gtk::RadioButton *_button2; + Registry *_wr; + Glib::ustring _key; + sigc::connection _scale_changed_connection; + sigc::connection _btn_toggled_connection; + double _old_val; +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_TOLERANCE_SLIDER__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/widget/unit-menu.cpp b/src/ui/widget/unit-menu.cpp new file mode 100644 index 0000000..a0ae163 --- /dev/null +++ b/src/ui/widget/unit-menu.cpp @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> + +#include "unit-menu.h" + +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Widget { + +UnitMenu::UnitMenu() : _type(UNIT_TYPE_NONE) +{ + set_active(0); + add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); + signal_scroll_event().connect([](GdkEventScroll*){ return false; }); +} + +UnitMenu::~UnitMenu() = default; + +bool UnitMenu::setUnitType(UnitType unit_type) +{ + // Expand the unit widget with unit entries from the unit table + UnitTable::UnitMap m = unit_table.units(unit_type); + + for (auto & i : m) { + append(i.first); + } + _type = unit_type; + set_active_text(unit_table.primary(unit_type)); + + return true; +} + +bool UnitMenu::resetUnitType(UnitType unit_type) +{ + remove_all(); + + return setUnitType(unit_type); +} + +void UnitMenu::addUnit(Unit const& u) +{ + unit_table.addUnit(u, false); + append(u.abbr); +} + +Unit const * UnitMenu::getUnit() const +{ + if (get_active_text() == "") { + g_assert(_type != UNIT_TYPE_NONE); + return unit_table.getUnit(unit_table.primary(_type)); + } + return unit_table.getUnit(get_active_text()); +} + +bool UnitMenu::setUnit(Glib::ustring const & unit) +{ + // TODO: Determine if 'unit' is available in the dropdown. + // If not, return false + + set_active_text(unit); + return true; +} + +Glib::ustring UnitMenu::getUnitAbbr() const +{ + if (get_active_text() == "") { + return ""; + } + return getUnit()->abbr; +} + +UnitType UnitMenu::getUnitType() const +{ + return getUnit()->type; +} + +double UnitMenu::getUnitFactor() const +{ + return getUnit()->factor; +} + +int UnitMenu::getDefaultDigits() const +{ + return getUnit()->defaultDigits(); +} + +double UnitMenu::getDefaultStep() const +{ + int factor_digits = -1*int(log10(getUnit()->factor)); + return pow(10.0, factor_digits); +} + +double UnitMenu::getDefaultPage() const +{ + return 10 * getDefaultStep(); +} + +double UnitMenu::getConversion(Glib::ustring const &new_unit_abbr, Glib::ustring const &old_unit_abbr) const +{ + double old_factor = getUnit()->factor; + if (old_unit_abbr != "no_unit") { + old_factor = unit_table.getUnit(old_unit_abbr)->factor; + } + Unit const * new_unit = unit_table.getUnit(new_unit_abbr); + + // Catch the case of zero or negative unit factors (error!) + if (old_factor < 0.0000001 || + new_unit->factor < 0.0000001) { + // TODO: Should we assert here? + return 0.00; + } + + return old_factor / new_unit->factor; +} + +bool UnitMenu::isAbsolute() const +{ + return getUnitType() != UNIT_TYPE_DIMENSIONLESS; +} + +bool UnitMenu::isRadial() const +{ + return getUnitType() == UNIT_TYPE_RADIAL; +} + +bool UnitMenu::on_scroll_event(GdkEventScroll *event) { return false; } + +} // namespace Widget +} // 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/widget/unit-menu.h b/src/ui/widget/unit-menu.h new file mode 100644 index 0000000..1f10cc2 --- /dev/null +++ b/src/ui/widget/unit-menu.h @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_UNIT_H +#define INKSCAPE_UI_WIDGET_UNIT_H + +#include <gtkmm/comboboxtext.h> +#include <gtkmm.h> +#include "util/units.h" + +using namespace Inkscape::Util; + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A drop down menu for choosing unit types. + */ +class UnitMenu : public Gtk::ComboBoxText +{ +public: + + /** + * Construct a UnitMenu + */ + UnitMenu(); + + /* GtkBuilder constructor */ + UnitMenu(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& refGlade):Gtk::ComboBoxText(cobject){ + UnitMenu(); + }; + + ~UnitMenu() override; + + /** + * Adds the unit type to the widget. This extracts the corresponding + * units from the unit map matching the given type, and appends them + * to the dropdown widget. It causes the primary unit for the given + * unit_type to be selected. + */ + bool setUnitType(UnitType unit_type); + + /** + * Removes all unit entries, then adds the unit type to the widget. + * This extracts the corresponding + * units from the unit map matching the given type, and appends them + * to the dropdown widget. It causes the primary unit for the given + * unit_type to be selected. + */ + bool resetUnitType(UnitType unit_type); + + /** + * Adds a unit, possibly user-defined, to the menu. + */ + void addUnit(Unit const& u); + + /** + * Sets the dropdown widget to the given unit abbreviation. + * Returns true if the unit was selectable, false if not + * (i.e., if the unit was not present in the widget). + */ + bool setUnit(Glib::ustring const &unit); + + /** + * Returns the Unit object corresponding to the current selection + * in the dropdown widget. + */ + Unit const * getUnit() const; + + /** + * Returns the abbreviated unit name of the selected unit. + */ + Glib::ustring getUnitAbbr() const; + + /** + * Returns the UnitType of the selected unit. + */ + UnitType getUnitType() const; + + /** + * Returns the unit factor for the selected unit. + */ + double getUnitFactor() const; + + /** + * Returns the recommended number of digits for displaying + * numbers of this unit type. + */ + int getDefaultDigits() const; + + /** + * Returns the recommended step size in spin buttons + * displaying units of this type. + */ + double getDefaultStep() const; + + /** + * Returns the recommended page size (when hitting pgup/pgdn) + * in spin buttons displaying units of this type. + */ + double getDefaultPage() const; + + /** + * Returns the conversion factor required to convert values + * of the currently selected unit into units of type + * new_unit_abbr. + */ + double getConversion(Glib::ustring const &new_unit_abbr, Glib::ustring const &old_unit_abbr = "no_unit") const; + + /** + * Returns true if the selected unit is not dimensionless + * (false for %, true for px, pt, cm, etc). + */ + bool isAbsolute() const; + + /** + * Returns true if the selected unit is radial (deg or rad). + */ + bool isRadial() const; + +protected: + UnitType _type; + /** + * block scroll from widget if is inside a scrolled window. + */ + bool on_scroll_event(GdkEventScroll *event) override; + + Gtk::ComboBoxText* _combo; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_UNIT_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/widget/unit-tracker.cpp b/src/ui/widget/unit-tracker.cpp new file mode 100644 index 0000000..7c52b3b --- /dev/null +++ b/src/ui/widget/unit-tracker.cpp @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::UI::Widget::UnitTracker + * Simple mediator to synchronize changes to unit menus + * + * Authors: + * Jon A. Cruz <jon@joncruz.org> + * Matthew Petroff <matthew@mpetroff.net> + * + * Copyright (C) 2007 Jon A. Cruz + * Copyright (C) 2013 Matthew Petroff + * Copyright (C) 2018 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include <iostream> + +#include "unit-tracker.h" + +#include "combo-tool-item.h" + +#define COLUMN_STRING 0 + +using Inkscape::Util::UnitTable; +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Widget { + +UnitTracker::UnitTracker(UnitType unit_type) : + _active(0), + _isUpdating(false), + _activeUnit(nullptr), + _activeUnitInitialized(false), + _store(nullptr), + _priorValues() +{ + UnitTable::UnitMap m = unit_table.units(unit_type); + + ComboToolItemColumns columns; + _store = Gtk::ListStore::create(columns); + Gtk::TreeModel::Row row; + + for (auto & m_iter : m) { + + Glib::ustring unit = m_iter.first; + + row = *(_store->append()); + row[columns.col_label ] = unit; + row[columns.col_value ] = unit; + row[columns.col_tooltip ] = (""); + row[columns.col_icon ] = "NotUsed"; + row[columns.col_sensitive] = true; + } + + // Why? + gint count = _store->children().size(); + if ((count > 0) && (_active > count)) { + _setActive(--count); + } else { + _setActive(_active); + } +} + +UnitTracker::~UnitTracker() +{ + _combo_list.clear(); + + // Unhook weak references to GtkAdjustments + for (auto i : _adjList) { + g_object_weak_unref(G_OBJECT(i), _adjustmentFinalizedCB, this); + } + _adjList.clear(); +} + +bool UnitTracker::isUpdating() const +{ + return _isUpdating; +} + +Inkscape::Util::Unit const * UnitTracker::getActiveUnit() const +{ + return _activeUnit; +} + +Glib::ustring UnitTracker::getCurrentLabel() +{ + ComboToolItemColumns columns; + return _store->children()[_active][columns.col_label]; +} + +void UnitTracker::changeLabel(Glib::ustring new_label, gint pos, bool onlylabel) +{ + ComboToolItemColumns columns; + _store->children()[pos][columns.col_label] = new_label; + if (!onlylabel) { + _store->children()[pos][columns.col_value] = new_label; + } +} + +void UnitTracker::setActiveUnit(Inkscape::Util::Unit const *unit) +{ + if (unit) { + + ComboToolItemColumns columns; + int index = 0; + for (auto& row: _store->children() ) { + Glib::ustring storedUnit = row[columns.col_value]; + if (!unit->abbr.compare (storedUnit)) { + _setActive (index); + break; + } + index++; + } + } +} + +void UnitTracker::setActiveUnitByLabel(Glib::ustring label) +{ + ComboToolItemColumns columns; + int index = 0; + for (auto &row : _store->children()) { + Glib::ustring storedUnit = row[columns.col_label]; + if (!label.compare(storedUnit)) { + _setActive(index); + break; + } + index++; + } +} + +void UnitTracker::setActiveUnitByAbbr(gchar const *abbr) +{ + Inkscape::Util::Unit const *u = unit_table.getUnit(abbr); + setActiveUnit(u); +} + +void UnitTracker::addAdjustment(GtkAdjustment *adj) +{ + if (std::find(_adjList.begin(),_adjList.end(),adj) == _adjList.end()) { + g_object_weak_ref(G_OBJECT(adj), _adjustmentFinalizedCB, this); + _adjList.push_back(adj); + } else { + std::cerr << "UnitTracker::addAjustment: Adjustment already added!" << std::endl; + } +} + +void UnitTracker::addUnit(Inkscape::Util::Unit const *u) +{ + ComboToolItemColumns columns; + + Gtk::TreeModel::Row row; + row = *(_store->append()); + row[columns.col_label ] = u ? u->abbr.c_str() : ""; + row[columns.col_value ] = u ? u->abbr.c_str() : ""; + row[columns.col_tooltip ] = (""); + row[columns.col_icon ] = "NotUsed"; + row[columns.col_sensitive] = true; +} + +void UnitTracker::prependUnit(Inkscape::Util::Unit const *u) +{ + ComboToolItemColumns columns; + + Gtk::TreeModel::Row row; + row = *(_store->prepend()); + row[columns.col_label ] = u ? u->abbr.c_str() : ""; + row[columns.col_value ] = u ? u->abbr.c_str() : ""; + row[columns.col_tooltip ] = (""); + row[columns.col_icon ] = "NotUsed"; + row[columns.col_sensitive] = true; + + /* Re-shuffle our default selection here (_active gets out of sync) */ + setActiveUnit(_activeUnit); + +} + +void UnitTracker::setFullVal(GtkAdjustment *adj, gdouble val) +{ + _priorValues[adj] = val; +} + +ComboToolItem * +UnitTracker::create_tool_item(Glib::ustring const &label, + Glib::ustring const &tooltip) +{ + auto combo = ComboToolItem::create(label, tooltip, "NotUsed", _store); + combo->set_active(_active); + combo->signal_changed().connect(sigc::mem_fun(*this, &UnitTracker::_unitChangedCB)); + combo->set_name("unit-tracker"); + combo->set_data(Glib::Quark("unit-tracker"), this); + _combo_list.push_back(combo); + return combo; +} + +void UnitTracker::_unitChangedCB(int active) +{ + _setActive(active); +} + +void UnitTracker::_adjustmentFinalizedCB(gpointer data, GObject *where_the_object_was) +{ + if (data && where_the_object_was) { + UnitTracker *self = reinterpret_cast<UnitTracker *>(data); + self->_adjustmentFinalized(where_the_object_was); + } +} + +void UnitTracker::_adjustmentFinalized(GObject *where_the_object_was) +{ + GtkAdjustment* adj = (GtkAdjustment*)(where_the_object_was); + auto it = std::find(_adjList.begin(),_adjList.end(), adj); + if (it != _adjList.end()) { + _adjList.erase(it); + } else { + g_warning("Received a finalization callback for unknown object %p", where_the_object_was); + } +} + +void UnitTracker::_setActive(gint active) +{ + if ( active != _active || !_activeUnitInitialized ) { + gint oldActive = _active; + + if (_store) { + + // Find old and new units + ComboToolItemColumns columns; + int index = 0; + Glib::ustring oldAbbr( "NotFound" ); + Glib::ustring newAbbr( "NotFound" ); + for (auto& row: _store->children() ) { + if (index == _active) { + oldAbbr = row[columns.col_value]; + } + if (index == active) { + newAbbr = row[columns.col_value]; + } + if (newAbbr != "NotFound" && oldAbbr != "NotFound") break; + ++index; + } + + if (oldAbbr != "NotFound") { + + if (newAbbr != "NotFound") { + Inkscape::Util::Unit const *oldUnit = unit_table.getUnit(oldAbbr); + Inkscape::Util::Unit const *newUnit = unit_table.getUnit(newAbbr); + _activeUnit = newUnit; + + if (!_adjList.empty()) { + _fixupAdjustments(oldUnit, newUnit); + } + } else { + std::cerr << "UnitTracker::_setActive: Did not find new unit: " << active << std::endl; + } + + } else { + std::cerr << "UnitTracker::_setActive: Did not find old unit: " << oldActive + << " new: " << active << std::endl; + } + } + _active = active; + + for (auto combo : _combo_list) { + if(combo) combo->set_active(active); + } + + _activeUnitInitialized = true; + } +} + +void UnitTracker::_fixupAdjustments(Inkscape::Util::Unit const *oldUnit, Inkscape::Util::Unit const *newUnit) +{ + _isUpdating = true; + for ( auto adj : _adjList ) { + gdouble oldVal = gtk_adjustment_get_value(adj); + gdouble val = oldVal; + + if ( (oldUnit->type != Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) + && (newUnit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) ) + { + val = newUnit->factor * 100; + _priorValues[adj] = Inkscape::Util::Quantity::convert(oldVal, oldUnit, "px"); + } else if ( (oldUnit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) + && (newUnit->type != Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) ) + { + if (_priorValues.find(adj) != _priorValues.end()) { + val = Inkscape::Util::Quantity::convert(_priorValues[adj], "px", newUnit); + } + } else { + val = Inkscape::Util::Quantity::convert(oldVal, oldUnit, newUnit); + } + + gtk_adjustment_set_value(adj, val); + } + _isUpdating = false; +} + +} // namespace Widget +} // 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 : diff --git a/src/ui/widget/unit-tracker.h b/src/ui/widget/unit-tracker.h new file mode 100644 index 0000000..243b19f --- /dev/null +++ b/src/ui/widget/unit-tracker.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::UI::Widget::UnitTracker + * Simple mediator to synchronize changes to unit menus + * + * Authors: + * Jon A. Cruz <jon@joncruz.org> + * Matthew Petroff <matthew@mpetroff.net> + * + * Copyright (C) 2007 Jon A. Cruz + * Copyright (C) 2013 Matthew Petroff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_UNIT_TRACKER_H +#define INKSCAPE_UI_WIDGET_UNIT_TRACKER_H + +#include <map> +#include <vector> + +#include <gtkmm/liststore.h> + +#include "util/units.h" + +using Inkscape::Util::Unit; +using Inkscape::Util::UnitType; + +typedef struct _GObject GObject; +typedef struct _GtkAdjustment GtkAdjustment; +typedef struct _GtkListStore GtkListStore; + +namespace Inkscape { +namespace UI { +namespace Widget { +class ComboToolItem; + +class UnitTracker { +public: + UnitTracker(UnitType unit_type); + virtual ~UnitTracker(); + + bool isUpdating() const; + + void setActiveUnit(Inkscape::Util::Unit const *unit); + void setActiveUnitByAbbr(gchar const *abbr); + void setActiveUnitByLabel(Glib::ustring label); + Inkscape::Util::Unit const * getActiveUnit() const; + + void addUnit(Inkscape::Util::Unit const *u); + void addAdjustment(GtkAdjustment *adj); + void prependUnit(Inkscape::Util::Unit const *u); + void setFullVal(GtkAdjustment *adj, gdouble val); + Glib::ustring getCurrentLabel(); + void changeLabel(Glib::ustring new_label, gint pos, bool onlylabel = false); + + ComboToolItem *create_tool_item(Glib::ustring const &label, + Glib::ustring const &tooltip); + +protected: + UnitType _type; + +private: + // Callbacks + void _unitChangedCB(int active); + static void _adjustmentFinalizedCB(gpointer data, GObject *where_the_object_was); + + void _setActive(gint index); + void _fixupAdjustments(Inkscape::Util::Unit const *oldUnit, Inkscape::Util::Unit const *newUnit); + + // Cleanup + void _adjustmentFinalized(GObject *where_the_object_was); + + gint _active; + bool _isUpdating; + Inkscape::Util::Unit const *_activeUnit; + bool _activeUnitInitialized; + + Glib::RefPtr<Gtk::ListStore> _store; + std::vector<ComboToolItem *> _combo_list; + std::vector<GtkAdjustment*> _adjList; + std::map <GtkAdjustment *, gdouble> _priorValues; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_UNIT_TRACKER_H |