diff options
Diffstat (limited to 'src/ui/widget/spin-button-tool-item.cpp')
-rw-r--r-- | src/ui/widget/spin-button-tool-item.cpp | 607 |
1 files changed, 607 insertions, 0 deletions
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 : |