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