// SPDX-License-Identifier: GPL-2.0-or-later #include "spin-button-tool-item.h" #include #include #include #include #include #include #include #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(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(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(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(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& 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(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(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& values, const std::vector& 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& 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 &values, const std::unordered_map &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 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 :