1 // SPDX-License-Identifier: GPL-2.0-or-later
2 
3 #include "spin-button-tool-item.h"
4 
5 #include <algorithm>
6 #include <gtkmm/box.h>
7 #include <gtkmm/image.h>
8 #include <gtkmm/radiomenuitem.h>
9 #include <gtkmm/toolbar.h>
10 
11 #include <cmath>
12 #include <utility>
13 
14 #include "spinbutton.h"
15 #include "ui/icon-loader.h"
16 
17 namespace Inkscape {
18 namespace UI {
19 namespace Widget {
20 
21 /**
22  * \brief Handler for the button's "focus-in-event" signal
23  *
24  * \param focus_event The event that triggered the signal
25  *
26  * \detail This just logs the current value of the spin-button
27  *         and sets the _transfer_focus flag
28  */
29 bool
on_btn_focus_in_event(GdkEventFocus *)30 SpinButtonToolItem::on_btn_focus_in_event(GdkEventFocus * /* focus_event */)
31 {
32     _last_val = _btn->get_value();
33     _transfer_focus = true;
34 
35     return false; // Event not consumed
36 }
37 
38 /**
39  * \brief Handler for the button's "focus-out-event" signal
40  *
41  * \param focus_event The event that triggered the signal
42  *
43  * \detail This just unsets the _transfer_focus flag
44  */
45 bool
on_btn_focus_out_event(GdkEventFocus *)46 SpinButtonToolItem::on_btn_focus_out_event(GdkEventFocus * /* focus_event */)
47 {
48     _transfer_focus = false;
49 
50     return false; // Event not consumed
51 }
52 
53 /**
54  * \brief Handler for the button's "key-press-event" signal
55  *
56  * \param key_event The event that triggered the signal
57  *
58  * \detail If the ESC key was pressed, restore the last value and defocus.
59  *         If the Enter key was pressed, just defocus.
60  */
61 bool
on_btn_key_press_event(GdkEventKey * key_event)62 SpinButtonToolItem::on_btn_key_press_event(GdkEventKey *key_event)
63 {
64     bool was_consumed = false; // Whether event has been consumed or not
65     auto display = Gdk::Display::get_default();
66     auto keymap  = display->get_keymap();
67     guint key = 0;
68     gdk_keymap_translate_keyboard_state(keymap, key_event->hardware_keycode,
69                                         static_cast<GdkModifierType>(key_event->state),
70                                         0, &key, 0, 0, 0);
71 
72     auto val = _btn->get_value();
73 
74     switch(key) {
75         case GDK_KEY_Escape:
76         {
77             _transfer_focus = true;
78             _btn->set_value(_last_val);
79             defocus();
80             was_consumed = true;
81         }
82         break;
83 
84         case GDK_KEY_Return:
85         case GDK_KEY_KP_Enter:
86         {
87             _transfer_focus = true;
88             defocus();
89             was_consumed = true;
90         }
91         break;
92 
93         case GDK_KEY_Tab:
94         {
95             _transfer_focus = false;
96             was_consumed = process_tab(1);
97         }
98         break;
99 
100         case GDK_KEY_ISO_Left_Tab:
101         {
102             _transfer_focus = false;
103             was_consumed = process_tab(-1);
104         }
105         break;
106 
107         // TODO: Enable variable step-size if this is ever used
108         case GDK_KEY_Up:
109         case GDK_KEY_KP_Up:
110         {
111             _transfer_focus = false;
112             _btn->set_value(val+1);
113             was_consumed=true;
114         }
115         break;
116 
117         case GDK_KEY_Down:
118         case GDK_KEY_KP_Down:
119         {
120             _transfer_focus = false;
121             _btn->set_value(val-1);
122             was_consumed=true;
123         }
124         break;
125 
126         case GDK_KEY_Page_Up:
127         case GDK_KEY_KP_Page_Up:
128         {
129             _transfer_focus = false;
130             _btn->set_value(val+10);
131             was_consumed=true;
132         }
133         break;
134 
135         case GDK_KEY_Page_Down:
136         case GDK_KEY_KP_Page_Down:
137         {
138             _transfer_focus = false;
139             _btn->set_value(val-10);
140             was_consumed=true;
141         }
142         break;
143 
144         case GDK_KEY_z:
145         case GDK_KEY_Z:
146         {
147             _transfer_focus = false;
148             _btn->set_value(_last_val);
149             was_consumed = true;
150         }
151         break;
152     }
153 
154     return was_consumed;
155 }
156 
157 /**
158  * \brief Shift focus to a different widget
159  *
160  * \details This only has an effect if the _transfer_focus flag and the _focus_widget are set
161  */
162 void
defocus()163 SpinButtonToolItem::defocus()
164 {
165     if(_transfer_focus && _focus_widget) {
166         _focus_widget->grab_focus();
167     }
168 }
169 
170 /**
171  * \brief Move focus to another spinbutton in the toolbar
172  *
173  * \param increment[in] The number of places to shift within the toolbar
174  */
175 bool
process_tab(int increment)176 SpinButtonToolItem::process_tab(int increment)
177 {
178     // If the increment is zero, do nothing
179     if(increment == 0) return true;
180 
181     // Here, we're working through the widget hierarchy:
182     // Toolbar
183     // |- ToolItem (*this)
184     //    |-> Box
185     //       |-> SpinButton (*_btn)
186     //
187     // Our aim is to find the next/previous spin-button within a toolitem in our toolbar
188 
189     bool handled = false;
190 
191     // We only bother doing this if the current item is actually in a toolbar!
192     auto toolbar = dynamic_cast<Gtk::Toolbar *>(get_parent());
193 
194     if (toolbar) {
195         // Get the index of the current item within the toolbar and the total number of items
196         auto my_index = toolbar->get_item_index(*this);
197         auto n_items  = toolbar->get_n_items();
198 
199         auto test_index = my_index + increment; // The index of the item we want to check
200 
201         // Loop through tool items as long as we're within the limits of the toolbar and
202         // we haven't yet found our new item to focus on
203         while(test_index > 0 && test_index <= n_items && !handled) {
204 
205             auto tool_item = toolbar->get_nth_item(test_index);
206 
207             if(tool_item) {
208                 // There are now two options that we support:
209                 if (auto sb_tool_item = dynamic_cast<SpinButtonToolItem *>(tool_item)) {
210                     // (1) The tool item is a SpinButtonToolItem, in which case, we just pass
211                     //     focus to its spin-button
212                     sb_tool_item->grab_button_focus();
213                     handled = true;
214                 }
215                 else if(dynamic_cast<Gtk::SpinButton *>(tool_item->get_child())) {
216                     // (2) The tool item contains a plain Gtk::SpinButton, in which case we
217                     //     pass focus directly to it
218                     tool_item->get_child()->grab_focus();
219                 }
220             }
221 
222             test_index += increment;
223         }
224     }
225 
226     return handled;
227 }
228 
229 /**
230  * \brief Handler for toggle events on numeric menu items
231  *
232  * \details Sets the adjustment to the desired value
233  */
234 void
on_numeric_menu_item_toggled(double value)235 SpinButtonToolItem::on_numeric_menu_item_toggled(double value)
236 {
237     auto adj = _btn->get_adjustment();
238     adj->set_value(value);
239 }
240 
241 Gtk::RadioMenuItem *
create_numeric_menu_item(Gtk::RadioButtonGroup * group,double value,const Glib::ustring & label)242 SpinButtonToolItem::create_numeric_menu_item(Gtk::RadioButtonGroup *group,
243                                              double                 value,
244                                              const Glib::ustring&   label)
245 {
246     // Represent the value as a string
247     std::ostringstream ss;
248     ss << value;
249 
250     // Append the label if specified
251     if (!label.empty()) {
252         ss << ": " << label;
253     }
254 
255     auto numeric_option = Gtk::manage(new Gtk::RadioMenuItem(*group, ss.str()));
256 
257     // Set the adjustment value in response to changes in the selected item
258     auto toggled_handler = sigc::bind(sigc::mem_fun(*this, &SpinButtonToolItem::on_numeric_menu_item_toggled), value);
259     numeric_option->signal_toggled().connect(toggled_handler);
260 
261     return numeric_option;
262 }
263 
264 /**
265  * \brief Create a menu containing fixed numeric options for the adjustment
266  *
267  * \details Each of these values represents a snap-point for the adjustment's value
268  */
269 Gtk::Menu *
create_numeric_menu()270 SpinButtonToolItem::create_numeric_menu()
271 {
272     auto numeric_menu = Gtk::manage(new Gtk::Menu());
273 
274     Gtk::RadioMenuItem::Group group;
275 
276     // Get values for the adjustment
277     auto adj = _btn->get_adjustment();
278     auto adj_value = round_to_precision(adj->get_value());
279     auto lower = round_to_precision(adj->get_lower());
280     auto upper = round_to_precision(adj->get_upper());
281     auto page = adj->get_page_increment();
282 
283     // Start by setting some fixed values based on the adjustment's
284     // parameters.
285     NumericMenuData values;
286 
287     // first add all custom items (necessary)
288     for (auto custom_data : _custom_menu_data) {
289         if (custom_data.first >= lower && custom_data.first <= upper) {
290             values.emplace(custom_data);
291         }
292     }
293 
294     values.emplace(adj_value, "");
295 
296     // for quick page changes using mouse, step can changes can be done with +/- buttons on
297     // SpinButton
298     values.emplace(::fmin(adj_value + page, upper), "");
299     values.emplace(::fmax(adj_value - page, lower), "");
300 
301     // add upper/lower limits to options
302     if (_show_upper_limit) {
303         values.emplace(upper, "");
304     }
305     if (_show_lower_limit) {
306         values.emplace(lower, "");
307     }
308 
309     auto add_item = [&numeric_menu, this, &group, adj_value](ValueLabel value){
310         auto numeric_menu_item = create_numeric_menu_item(&group, value.first, value.second);
311         numeric_menu->append(*numeric_menu_item);
312 
313         if (adj_value == value.first) {
314             numeric_menu_item->set_active();
315         }
316     };
317 
318     if (_sort_decreasing) {
319         std::for_each(values.crbegin(), values.crend(), add_item);
320     } else {
321         std::for_each(values.cbegin(), values.cend(), add_item);
322     }
323 
324     return numeric_menu;
325 }
326 
327 /**
328  * \brief Create a menu-item in response to the "create-menu-proxy" signal
329  *
330  * \detail This is an override for the default Gtk::ToolItem handler so
331  *         we don't need to explicitly connect this to the signal. It
332  *         runs if the toolitem is unable to fit on the toolbar, and
333  *         must be represented by a menu item instead.
334  */
335 bool
on_create_menu_proxy()336 SpinButtonToolItem::on_create_menu_proxy()
337 {
338     // The main menu-item.  It just contains the label that normally appears
339     // next to the spin-button, and an indicator for a sub-menu.
340     auto menu_item = Gtk::manage(new Gtk::MenuItem(_label_text));
341     auto numeric_menu = create_numeric_menu();
342     menu_item->set_submenu(*numeric_menu);
343 
344     set_proxy_menu_item(_name, *menu_item);
345 
346     return true; // Finished handling the event
347 }
348 
349 /**
350  * \brief Create a new SpinButtonToolItem
351  *
352  * \param[in] name       A unique ID for this tool-item (not translatable)
353  * \param[in] label_text The text to display in the toolbar
354  * \param[in] adjustment The Gtk::Adjustment to attach to the spinbutton
355  * \param[in] climb_rate The climb rate for the spin button (default = 0)
356  * \param[in] digits     Number of decimal places to display
357  */
SpinButtonToolItem(const Glib::ustring name,const Glib::ustring & label_text,Glib::RefPtr<Gtk::Adjustment> & adjustment,double climb_rate,int digits)358 SpinButtonToolItem::SpinButtonToolItem(const Glib::ustring            name,
359                                        const Glib::ustring&           label_text,
360                                        Glib::RefPtr<Gtk::Adjustment>& adjustment,
361                                        double                         climb_rate,
362                                        int                            digits)
363     : _btn(Gtk::manage(new SpinButton(adjustment, climb_rate, digits))),
364       _name(std::move(name)),
365       _label_text(label_text),
366       _digits(digits)
367 {
368     set_margin_start(3);
369     set_margin_end(3);
370     set_name(_name);
371 
372     // Handle popup menu
373     _btn->signal_popup_menu().connect(sigc::mem_fun(*this, &SpinButtonToolItem::on_popup_menu), false);
374 
375     // Handle button events
376     auto btn_focus_in_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_focus_in_event);
377     _btn->signal_focus_in_event().connect(btn_focus_in_event_cb, false);
378 
379     auto btn_focus_out_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_focus_out_event);
380     _btn->signal_focus_out_event().connect(btn_focus_out_event_cb, false);
381 
382     auto btn_key_press_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_key_press_event);
383     _btn->signal_key_press_event().connect(btn_key_press_event_cb, false);
384 
385     auto btn_button_press_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_button_press_event);
386     _btn->signal_button_press_event().connect(btn_button_press_event_cb, false);
387 
388     _btn->add_events(Gdk::KEY_PRESS_MASK);
389 
390     // Create a label
391     _label = Gtk::manage(new Gtk::Label(label_text));
392 
393     // Arrange the widgets in a horizontal box
394     _hbox = Gtk::manage(new Gtk::Box());
395     _hbox->set_spacing(3);
396     _hbox->pack_start(*_label);
397     _hbox->pack_start(*_btn);
398     add(*_hbox);
399     show_all();
400 }
401 
402 void
set_icon(const Glib::ustring & icon_name)403 SpinButtonToolItem::set_icon(const Glib::ustring& icon_name)
404 {
405     _hbox->remove(*_label);
406     _icon = Gtk::manage(sp_get_icon_image(icon_name, Gtk::ICON_SIZE_SMALL_TOOLBAR));
407 
408     if(_icon) {
409         _hbox->pack_start(*_icon);
410         _hbox->reorder_child(*_icon, 0);
411     }
412 
413     show_all();
414 }
415 
416 bool
on_btn_button_press_event(const GdkEventButton * button_event)417 SpinButtonToolItem::on_btn_button_press_event(const GdkEventButton *button_event)
418 {
419     if (gdk_event_triggers_context_menu(reinterpret_cast<const GdkEvent *>(button_event)) &&
420             button_event->type == GDK_BUTTON_PRESS) {
421         do_popup_menu(button_event);
422         return true;
423     }
424 
425     return false;
426 }
427 
428 void
do_popup_menu(const GdkEventButton * button_event)429 SpinButtonToolItem::do_popup_menu(const GdkEventButton *button_event)
430 {
431     auto menu = create_numeric_menu();
432     menu->attach_to_widget(*_btn);
433     menu->show_all();
434     menu->popup_at_pointer(reinterpret_cast<const GdkEvent *>(button_event));
435 }
436 
437 /**
438  * \brief Create a popup menu
439  */
440 bool
on_popup_menu()441 SpinButtonToolItem::on_popup_menu()
442 {
443     do_popup_menu(nullptr);
444     return true;
445 }
446 
447 /**
448  * \brief Transfers focus to the child spinbutton by default
449  */
450 void
on_grab_focus()451 SpinButtonToolItem::on_grab_focus()
452 {
453     grab_button_focus();
454 }
455 
456 /**
457  * \brief Set the tooltip to display on this (and all child widgets)
458  *
459  * \param[in] text The tooltip to display
460  */
461 void
set_all_tooltip_text(const Glib::ustring & text)462 SpinButtonToolItem::set_all_tooltip_text(const Glib::ustring& text)
463 {
464     set_tooltip_text(text);
465     _btn->set_tooltip_text(text);
466 }
467 
468 /**
469  * \brief Set the widget that focus moves to when this one loses focus
470  *
471  * \param widget The widget that will gain focus
472  */
473 void
set_focus_widget(Gtk::Widget * widget)474 SpinButtonToolItem::set_focus_widget(Gtk::Widget *widget)
475 {
476     _focus_widget = widget;
477 }
478 
479 /**
480  * \brief Grab focus on the spin-button widget
481  */
482 void
grab_button_focus()483 SpinButtonToolItem::grab_button_focus()
484 {
485     _btn->grab_focus();
486 }
487 
488 /**
489  * \brief A wrapper of Geom::decimal_round to remember precision
490  */
491 double
round_to_precision(double value)492 SpinButtonToolItem::round_to_precision(double value) {
493     return Geom::decimal_round(value, _digits);
494 }
495 
496 /**
497  * \brief     [discouraged] Set numeric data option in Radio menu.
498  *
499  * \param[in] values  values to provide as options
500  * \param[in] labels  label to show for the value at same index in values.
501  *
502  * \detail    Use is advised only when there are no labels.
503  *            This is discouraged in favor of other overloads of the function, due to error prone
504  *            usage. Using two vectors for related data, undermining encapsulation.
505  */
506 void
set_custom_numeric_menu_data(const std::vector<double> & values,const std::vector<Glib::ustring> & labels)507 SpinButtonToolItem::set_custom_numeric_menu_data(const std::vector<double>&        values,
508                                                  const std::vector<Glib::ustring>& labels)
509 {
510 
511     if (values.size() != labels.size() && !labels.empty()) {
512         g_warning("Cannot add custom menu items. Value and label arrays are different sizes");
513         return;
514     }
515 
516     _custom_menu_data.clear();
517 
518     if (labels.empty()) {
519         for (const auto &value : values) {
520             _custom_menu_data.emplace(round_to_precision(value), "");
521         }
522         return;
523     }
524 
525     int i = 0;
526     for (const auto &value : values) {
527         _custom_menu_data.emplace(round_to_precision(value), labels[i++]);
528     }
529 }
530 
531 /**
532  * \brief     Set numeric data options for Radio menu (densely labeled data).
533  *
534  * \param[in] value_labels  value and labels to provide as options
535  *
536  * \detail    Should be used when most of the values have an associated label (densely labeled data)
537  *
538  */
539 void
set_custom_numeric_menu_data(const std::vector<ValueLabel> & value_labels)540 SpinButtonToolItem::set_custom_numeric_menu_data(const std::vector<ValueLabel>& value_labels) {
541     _custom_menu_data.clear();
542     for(const auto& value_label : value_labels) {
543         _custom_menu_data.emplace(round_to_precision(value_label.first), value_label.second);
544     }
545 }
546 
547 
548 /**
549  * \brief     Set numeric data options for Radio menu (sparsely labeled data).
550  *
551  * \param[in] values         values without labels
552  * \param[in] sparse_labels  value and labels to provide as options
553  *
554  * \detail    Should be used when very few values have an associated label (sparsely labeled data).
555  *            Duplicate values in vector and map are acceptable but, values labels in map are
556  *            preferred. Avoid using duplicate values intentionally though.
557  *
558  */
559 void
set_custom_numeric_menu_data(const std::vector<double> & values,const std::unordered_map<double,Glib::ustring> & sparse_labels)560 SpinButtonToolItem::set_custom_numeric_menu_data(const std::vector<double> &values,
561                                                       const std::unordered_map<double, Glib::ustring> &sparse_labels)
562 {
563     _custom_menu_data.clear();
564 
565     for(const auto& value_label : sparse_labels) {
566         _custom_menu_data.emplace(round_to_precision(value_label.first), value_label.second);
567     }
568 
569     for(const auto& value : values) {
570         _custom_menu_data.emplace(round_to_precision(value), "");
571     }
572 
573 }
574 
575 
show_upper_limit(bool show)576 void SpinButtonToolItem::show_upper_limit(bool show) { _show_upper_limit = show; }
577 
show_lower_limit(bool show)578 void SpinButtonToolItem::show_lower_limit(bool show) { _show_lower_limit = show; }
579 
show_limits(bool show)580 void SpinButtonToolItem::show_limits(bool show) { _show_upper_limit = _show_lower_limit = show; }
581 
sort_decreasing(bool decreasing)582 void SpinButtonToolItem::sort_decreasing(bool decreasing) { _sort_decreasing = decreasing; }
583 
584 Glib::RefPtr<Gtk::Adjustment>
get_adjustment()585 SpinButtonToolItem::get_adjustment()
586 {
587     return _btn->get_adjustment();
588 }
589 } // namespace Widget
590 } // namespace UI
591 } // namespace Inkscape
592 /*
593   Local Variables:
594   mode:c++
595   c-file-style:"stroustrup"
596   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
597   indent-tabs-mode:nil
598   fill-column:99
599   End:
600 */
601 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
602