Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
ink-spin-button.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2//
3// Authors: Tavmjong Bah
4// Mike Kowalski
5//
6
7#include "ink-spin-button.h"
8
9#include <cassert>
10#include <iomanip>
11
12#include "ui/containerize.h"
13#include "ui/util.h"
15
16namespace Inkscape::UI::Widget {
17
18// CSS styles for InkSpinButton
19// language=CSS
20auto ink_spinbutton_css = R"=====(
21@define-color border-color @unfocused_borders;
22@define-color bgnd-color alpha(@theme_base_color, 1.0);
23@define-color focus-color alpha(@theme_selected_bg_color, 0.5);
24/* :root { --border-color: lightgray; } - this is not working yet, so using nonstandard @define-color */
25#InkSpinButton { border: 0 solid @border-color; border-radius: 2px; background-color: @bgnd-color; }
26#InkSpinButton.frame { border: 1px solid @border-color; }
27#InkSpinButton:hover button { opacity: 1; }
28#InkSpinButton:focus-within { outline: 2px solid @focus-color; outline-offset: -2px; }
29#InkSpinButton label#InkSpinButton-Label { opacity: 0.5; margin-left: 3px; margin-right: 3px; }
30#InkSpinButton button { border: 0 solid alpha(@border-color, 0.30); border-radius: 2px; padding: 1px; min-width: 6px; min-height: 8px; -gtk-icon-size: 10px; background-image: none; }
31#InkSpinButton button.left { border-top-right-radius: 0; border-bottom-right-radius: 0; border-right-width: 1px; }
32#InkSpinButton button.right { border-top-left-radius: 0; border-bottom-left-radius: 0; border-left-width: 1px; }
33#InkSpinButton entry#InkSpinButton-Entry { border: none; border-radius: 3px; padding: 0; min-height: 13px; background-color: @bgnd-color; outline-width: 0; }
34)=====";
35constexpr int timeout_click = 500;
36constexpr int timeout_repeat = 50;
37
38static Glib::RefPtr<Gdk::Cursor> g_resizing_cursor;
39static Glib::RefPtr<Gdk::Cursor> g_text_cursor;
40
42 set_name("InkSpinButton");
43
44 set_overflow(Gtk::Overflow::HIDDEN);
45
46 _minus.set_name("InkSpinButton-Minus");
47 _minus.add_css_class("left");
48 _value.set_name("InkSpinButton-Value");
49 _plus.set_name("InkSpinButton-Plus");
50 _plus.add_css_class("right");
51 _entry.set_name("InkSpinButton-Entry");
52 _entry.set_alignment(0.5f);
53 _entry.set_max_width_chars(3); // let it shrink, we can always stretch it
54 _label.set_name("InkSpinButton-Label");
55
56 _value.set_expand();
57 _entry.set_expand();
58
59 _minus.set_margin(0);
60 _minus.set_size_request(8, -1);
61 _value.set_margin(0);
62 _value.set_single_line_mode();
63 _value.set_overflow(Gtk::Overflow::HIDDEN);
64 _plus.set_margin(0);
65 _plus.set_size_request(8, -1);
66 _minus.set_can_focus(false);
67 _plus.set_can_focus(false);
68 _label.set_can_focus(false);
69 _label.set_xalign(0.0f);
70 _label.set_visible(false);
71
72 _minus.set_icon_name("go-previous-symbolic");
73 _plus.set_icon_name("go-next-symbolic");
74
75 containerize(*this);
76 _label.insert_at_end(*this);
77 _minus.insert_at_end(*this);
78 _value.insert_at_end(*this);
79 _entry.insert_at_end(*this);
80 _plus.insert_at_end(*this);
81
82 set_focus_child(_entry);
83
84 static Glib::RefPtr<Gtk::CssProvider> provider;
85 if (!provider) {
86 provider = Gtk::CssProvider::create();
87 provider->load_from_data(ink_spinbutton_css);
88 auto const display = Gdk::Display::get_default();
89 Gtk::StyleContext::add_provider_for_display(display, provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 10);
90 }
91
92 // ------------- CONTROLLERS -------------
93
94 // This is mouse movement. Shows/hides +/- buttons.
95 // Shows/hides +/- buttons.
96 _motion = Gtk::EventControllerMotion::create();
97 _motion->signal_enter().connect(sigc::mem_fun(*this, &InkSpinButton::on_motion_enter));
98 _motion->signal_leave().connect(sigc::mem_fun(*this, &InkSpinButton::on_motion_leave));
99 add_controller(_motion);
100
101 // This is mouse movement. Sets cursor.
102 _motion_value = Gtk::EventControllerMotion::create();
103 _motion_value->signal_enter().connect(sigc::mem_fun(*this, &InkSpinButton::on_motion_enter_value));
104 _motion_value->signal_leave().connect(sigc::mem_fun(*this, &InkSpinButton::on_motion_leave_value));
105 _value.add_controller(_motion_value);
106
107 // This is mouse drag movement. Changes value.
108 _drag_value = Gtk::GestureDrag::create();
109 _drag_value->signal_begin().connect(sigc::mem_fun(*this, &InkSpinButton::on_drag_begin_value));
110 _drag_value->signal_update().connect(sigc::mem_fun(*this, &InkSpinButton::on_drag_update_value));
111 _drag_value->signal_end().connect(sigc::mem_fun(*this, &InkSpinButton::on_drag_end_value));
112 _drag_value->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
113 _value.add_controller(_drag_value);
114
115 // Changes value.
116 _scroll = Gtk::EventControllerScroll::create();
117 _scroll->signal_scroll_begin().connect(sigc::mem_fun(*this, &InkSpinButton::on_scroll_begin));
118 _scroll->signal_scroll().connect(sigc::mem_fun(*this, &InkSpinButton::on_scroll), false);
119 _scroll->signal_scroll_end().connect(sigc::mem_fun(*this, &InkSpinButton::on_scroll_end));
120 _scroll->set_flags(Gtk::EventControllerScroll::Flags::BOTH_AXES); // Mouse wheel is on y.
121 add_controller(_scroll);
122
123 _click_minus = Gtk::GestureClick::create();
124 _click_minus->signal_pressed().connect(sigc::mem_fun(*this, &InkSpinButton::on_pressed_minus));
125 _click_minus->signal_released().connect([this](int, double, double){ stop_spinning(); });
126 _click_minus->signal_unpaired_release().connect([this](auto, auto, auto, auto){ stop_spinning(); });
127 _click_minus->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); // Steal from default handler.
128 _minus.add_controller(_click_minus);
129
130 _click_plus = Gtk::GestureClick::create();
131 _click_plus->signal_pressed().connect(sigc::mem_fun(*this, &InkSpinButton::on_pressed_plus));
132 _click_plus->signal_released().connect([this](int, double, double){ stop_spinning(); });
133 _click_plus->signal_unpaired_release().connect([this](auto, auto, auto, auto){ stop_spinning(); });
134 _click_plus->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); // Steal from default handler.
135 _plus.add_controller(_click_plus);
136
137 _focus = Gtk::EventControllerFocus::create();
138 _focus->signal_enter().connect([this]() {
139 // show editable button if '*this' is focused, but not its entry
140 if (_focus->is_focus()) {
141 set_focusable(false);
142 enter_edit();
143 }
144 });
145 _focus->signal_leave().connect([this]() {
146 if (_entry.is_visible()) {
147 commit_entry();
148 }
149 exit_edit();
150 set_focusable(true);
151 });
152 add_controller(_focus);
153 _entry.set_focus_on_click(false);
154 _entry.set_focusable(false);
155 _entry.set_can_focus();
156 set_can_focus();
157 set_focusable();
158 set_focus_on_click();
159
160 _key_entry = Gtk::EventControllerKey::create();
161 _key_entry->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
162 _key_entry->signal_key_pressed().connect(sigc::mem_fun(*this, &InkSpinButton::on_key_pressed), false); // Before default handler.
163 _entry.add_controller(_key_entry);
164
165 // GTK4
166 // ------------- SIGNALS -------------
167
168 _entry.signal_activate().connect([this] { on_activate(); });
169
170 // Value (button) NOT USED, Click handled by zero length drag.
171 // m_value->signal_clicked().connect(sigc::mem_fun(*this, &SpinButton::on_value_clicked));
172
173 _minus.set_visible();
174 auto m = _minus.measure(Gtk::Orientation::HORIZONTAL);
175 _button_width = m.sizes.natural;
176 m = _entry.measure(Gtk::Orientation::VERTICAL);
177 _entry_height = m.sizes.natural;
178 _baseline = m.baselines.natural;
179
185 show_arrows(false);
186 _entry.hide();
187
188 property_label().signal_changed().connect([this] {
189 set_label(_label_text.get_value().raw());
190 });
191 set_label(_label_text.get_value());
192
193 property_digits().signal_changed().connect([this]{ queue_resize(); update(); });
194 property_has_frame().signal_changed().connect([this]{ set_has_frame(_has_frame); });
195 property_show_arrows().signal_changed().connect([this]{ set_has_arrows(_show_arrows); });
196 property_scaling_factor().signal_changed().connect([this]{ set_scaling_factor(_scaling_factor); });
197 property_step_value().signal_changed().connect([this]{ set_step(_step_value); });
198 property_min_value().signal_changed().connect([this]{ _adjustment->set_lower(_min_value); });
199 property_max_value().signal_changed().connect([this]{ _adjustment->set_upper(_max_value); });
200 property_value().signal_changed().connect([this]{ set_value(_num_value); });
201 property_prefix().signal_changed().connect([this]{ update(); });
202 property_suffix().signal_changed().connect([this]{ update(); });
203
204 _connection = _adjustment->signal_value_changed().connect([this](){ update(); });
205 update();
206}
207
208#define INIT_PROPERTIES \
209 _adjust(*this, "adjustment", Gtk::Adjustment::create(0, 0, 100, 1)), \
210 _digits(*this, "digits", 3), \
211 _num_value(*this, "value", 0.0), \
212 _min_value(*this, "min-value", 0.0), \
213 _max_value(*this, "max-value", 100.0), \
214 _step_value(*this, "step-value", 1.0), \
215 _scaling_factor(*this, "scaling-factor", 1.0), \
216 _has_frame(*this, "has-frame", true), \
217 _show_arrows(*this, "show-arrows", true), \
218 _enter_exit(*this, "enter-exit-editing", false), \
219 _label_text(*this, "label", {}), \
220 _prefix(*this, "prefix", {}), \
221 _suffix(*this, "suffix", {})
222
223
225 Glib::ObjectBase("InkSpinButton"),
226 INIT_PROPERTIES {
227
228 construct();
229}
230
231InkSpinButton::InkSpinButton(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& builder):
232 Glib::ObjectBase("InkSpinButton"),
233 Gtk::Widget(cobject),
234 INIT_PROPERTIES {
235
236 construct();
237}
238
239InkSpinButton::InkSpinButton(BaseObjectType* cobject):
240 Glib::ObjectBase("InkSpinButton"),
241 Gtk::Widget(cobject),
242 INIT_PROPERTIES {
243
244 construct();
245}
246
247#undef INIT_PROPERTIES
248
249InkSpinButton::~InkSpinButton() = default;
250
251Gtk::SizeRequestMode InkSpinButton::get_request_mode_vfunc() const {
252 return Gtk::Widget::get_request_mode_vfunc();
253}
254
255void InkSpinButton::measure_vfunc(Gtk::Orientation orientation, int for_size, int& minimum, int& natural, int& minimum_baseline, int& natural_baseline) const {
256
257 std::string text;
258 if (_min_size_pattern.empty()) {
259 auto delta = _digits.get_value() > 0 ? pow(10.0, -_digits.get_value()) : 0;
260 auto low = format(_adjustment->get_lower() + delta, true, false, true, true);
261 auto high = format(_adjustment->get_upper() - delta, true, false, true, true);
262 text = low.size() > high.size() ? low : high;
263 }
264 else {
265 text = _min_size_pattern;
266 }
267
268 // http://developer.gnome.org/pangomm/unstable/classPango_1_1Layout.html
269 auto layout = const_cast<InkSpinButton*>(this)->create_pango_layout("\u2009" + text + "\u2009");
270
271 int text_width = 0;
272 int text_height = 0;
273 // get the text dimensions
274 layout->get_pixel_size(text_width, text_height);
275
276 if (orientation == Gtk::Orientation::HORIZONTAL) {
277 minimum_baseline = natural_baseline = -1;
278 // always measure, so gtk doesn't complain
279 auto m = _minus.measure(orientation);
280 auto p = _plus.measure(orientation);
281 auto _ = _entry.measure(orientation);
282 _ = _value.measure(orientation);
283 _ = _label.measure(orientation);
284
285 auto btn = _enable_arrows ? _button_width : 0;
286 // always reserve space for inc/dec buttons and label, whichever is greater
287 minimum = natural = std::max(_label_width + text_width, btn + text_width + btn);
288 }
289 else {
290 minimum_baseline = natural_baseline = _baseline;
291 auto height = std::max(text_height, _entry_height);
292 minimum = height;
293 natural = std::max(static_cast<int>(1.5 * text_height), _entry_height);
294 }
295}
296
297void InkSpinButton::size_allocate_vfunc(int width, int height, int baseline) {
298 Gtk::Allocation allocation;
299 allocation.set_height(height);
300 allocation.set_width(_button_width);
301 allocation.set_x(0);
302 allocation.set_y(0);
303
304 int left = 0;
305 int right = width;
306
307 // either label or buttons may be visible, but not both
308 if (_label.get_visible()) {
309 Gtk::Allocation alloc;
310 alloc.set_height(height);
311 alloc.set_width(_label_width);
312 alloc.set_x(0);
313 alloc.set_y(0);
314 _label.size_allocate(alloc, baseline);
315 left += _label_width;
316 right -= _label_width;
317 }
318 if (_minus.get_visible()) {
319 _minus.size_allocate(allocation, baseline);
320 left += allocation.get_width();
321 }
322 if (_plus.get_visible()) {
323 allocation.set_x(width - allocation.get_width());
324 _plus.size_allocate(allocation, baseline);
325 right -= allocation.get_width();
326 }
327
328 allocation.set_x(left);
329 allocation.set_width(std::max(0, right - left));
330 if (_value.get_visible()) {
331 _value.size_allocate(allocation, baseline);
332 }
333 if (_entry.get_visible()) {
334 _entry.size_allocate(allocation, baseline);
335 }
336}
337
338
339Glib::RefPtr<Gtk::Adjustment>& InkSpinButton::get_adjustment() {
340 return _adjustment;
341}
342
343void InkSpinButton::set_adjustment(const Glib::RefPtr<Gtk::Adjustment>& adjustment) {
344 if (!adjustment) return;
345
346 _connection.disconnect();
347 _adjustment = adjustment;
348 _connection = _adjustment->signal_value_changed().connect([this](){ update(); });
349 update();
350}
351
352void InkSpinButton::set_digits(int digits) {
353 _digits = digits;
354 update();
355}
356
357int InkSpinButton::get_digits() const {
358 return _digits.get_value();
359}
360
361void InkSpinButton::set_range(double min, double max) {
362 _adjustment->set_lower(min);
363 _adjustment->set_upper(max);
364}
365
366void InkSpinButton::set_step(double step_increment) {
367 _adjustment->set_step_increment(step_increment);
368}
369
370void InkSpinButton::set_prefix(const std::string& prefix, bool add_space) {
371 if (add_space && !prefix.empty()) {
372 _prefix.set_value(prefix + " ");
373 }
374 else {
375 _prefix.set_value(prefix);
376 }
377 update();
378}
379
380void InkSpinButton::set_suffix(const std::string& suffix, bool add_half_space) {
381 if (add_half_space && !suffix.empty()) {
382 // thin space
383 _suffix.set_value("\u2009" + suffix);
384 }
385 else {
386 _suffix.set_value(suffix);
387 }
388 update();
389}
390
391void InkSpinButton::set_has_frame(bool frame) {
392 if (frame) {
393 add_css_class("frame");
394 }
395 else {
396 remove_css_class("frame");
397 }
398}
399
400void InkSpinButton::set_trim_zeros(bool trim) {
401 if (_trim_zeros != trim) {
402 _trim_zeros = trim;
403 update();
404 }
405}
406
407void InkSpinButton::set_scaling_factor(double factor) {
408 assert(factor > 0 && factor < 1e9);
409 _fmt_scaling_factor = factor;
410 queue_resize();
411 update();
412}
413
414static void trim_zeros(std::string& ret) {
415 while (ret.find('.') != std::string::npos &&
416 (ret.substr(ret.length() - 1, 1) == "0" || ret.substr(ret.length() - 1, 1) == ".")) {
417 ret.pop_back();
418 }
419}
420
421std::string InkSpinButton::format(double value, bool with_prefix_suffix, bool with_markup, bool trim_zeros, bool limit_size) const {
422 std::stringstream ss;
423 ss.imbue(std::locale("C"));
424 std::string number;
425 if (value > 1e12 || value < -1e12) {
426 // use scientific notation to limit size of the output number
427 ss << std::scientific << std::setprecision(std::numeric_limits<double>::digits10) << value;
428 number = ss.str();
429 }
430 else {
431 ss << std::fixed << std::setprecision(_digits.get_value()) << value;
432 number = ss.str();
433 if (trim_zeros) {
435 }
436 if (limit_size) {
437 auto limit = std::numeric_limits<double>::digits10;
438 if (value < 0) limit += 1;
439
440 if (number.size() > limit) {
441 number = number.substr(0, limit);
442 }
443 }
444 }
445
446 auto suffix = _suffix.get_value();
447 auto prefix = _prefix.get_value();
448 if (with_prefix_suffix && (!suffix.empty() || !prefix.empty())) {
449 if (with_markup) {
450 std::stringstream markup;
451 if (!prefix.empty()) {
452 markup << "<span alpha='50%'>" << Glib::Markup::escape_text(prefix) << "</span>";
453 }
454 markup << "<span>" << number << "</span>";
455 if (!suffix.empty()) {
456 markup << "<span alpha='50%'>" << Glib::Markup::escape_text(suffix) << "</span>";
457 }
458 return markup.str();
459 }
460 else {
461 return prefix + number + suffix;
462 }
463 }
464
465 return number;
466}
467
468void InkSpinButton::update(bool fire_change_notification) {
469 if (!_adjustment) return;
470
471 auto value = _adjustment->get_value();
472 auto text = format(value, false, false, _trim_zeros, false);
473 _entry.set_text(text);
474 if (_suffix.get_value().empty() && _prefix.get_value().empty()) {
475 _value.set_text(text);
476 }
477 else {
478 _value.set_markup(format(value, true, true, _trim_zeros, false));
479 }
480
481 _minus.set_sensitive(_adjustment->get_value() > _adjustment->get_lower());
482 _plus.set_sensitive(_adjustment->get_value() < _adjustment->get_upper());
483
484 if (fire_change_notification) {
485 _signal_value_changed.emit(value / _fmt_scaling_factor);
486 }
487}
488
489void InkSpinButton::set_new_value(double new_value) {
490 _adjustment->set_value(new_value);
491 //TODO: reflect new value in _num_value property while avoiding cycle updates
492}
493
494// ---------------- CONTROLLERS -----------------
495
496// ------------------ MOTION ------------------
497
498void InkSpinButton::on_motion_enter(double x, double y) {
499 if (_focus->contains_focus()) return;
500
501 show_label(false);
502 show_arrows();
503}
504
505void InkSpinButton::on_motion_leave() {
506 if (_focus->contains_focus()) return;
507
508 show_arrows(false);
509 show_label();
510
511 if (_entry.is_visible()) {
512 // We left spinbutton, save value and update.
513 commit_entry();
514 exit_edit();
515 }
516}
517
518// --------------- MOTION VALUE ---------------
519
520void InkSpinButton::on_motion_enter_value(double x, double y) {
521 _old_cursor = get_cursor();
522 if (!g_resizing_cursor) {
523 g_resizing_cursor = Gdk::Cursor::create(Glib::ustring("ew-resize"));
524 g_text_cursor = Gdk::Cursor::create(Glib::ustring("text"));
525 }
526 // if draging/scrolling adjustment is enabled, show appropriate cursor
527 if (_drag_full_travel > 0) {
528 _current_cursor = g_resizing_cursor;
529 set_cursor(_current_cursor);
530 }
531 else {
532 _current_cursor = g_text_cursor;
533 set_cursor(_current_cursor);
534 }
535}
536
537void InkSpinButton::on_motion_leave_value() {
538 _current_cursor = _old_cursor;
539 set_cursor(_current_cursor);
540}
541
542// --------------- DRAG VALUE ----------------
543
544static double get_accel_factor(Gdk::ModifierType state) {
545 double scale = 1.0;
546 // Ctrl modifier slows down, Shift speeds up
547 if ((state & Gdk::ModifierType::CONTROL_MASK) == Gdk::ModifierType::CONTROL_MASK) {
548 scale = 0.1;
549 } else if ((state & Gdk::ModifierType::SHIFT_MASK) == Gdk::ModifierType::SHIFT_MASK) {
550 scale = 10.0;
551 }
552 return scale;
553}
554
555void InkSpinButton::on_drag_begin_value(Gdk::EventSequence* sequence) {
556 _initial_value = _adjustment->get_value();
557 _drag_value->get_point(sequence, _drag_start.x,_drag_start.y);
558}
559
560void InkSpinButton::on_drag_update_value(Gdk::EventSequence* sequence) {
561 if (_drag_full_travel <= 0) return;
562
563 double dx = 0.0;
564 double dy = 0.0;
565 _drag_value->get_offset(dx, dy);
566
567 // If we don't move, then it probably was a button click.
568 auto delta = 1; // tweak this value to reject real clicks, or else we'll change value inadvertently
569 if (std::abs(dx) > delta || std::abs(dy) > delta) {
570 auto max_dist = _drag_full_travel; // distance to travel to adjust full range
571 auto range = _adjustment->get_upper() - _adjustment->get_lower();
572 auto state = _drag_value->get_current_event_state();
573 auto distance = std::hypot(dx, dy);
574 auto angle = std::atan2(dx, dy);
575 auto grow = angle > M_PI_4 || angle < -M_PI+M_PI_4;
576 if (!grow) distance = -distance;
577
578 auto value = _initial_value + get_accel_factor(state) * distance / max_dist * range + _adjustment->get_lower();
579 set_new_value(value);
580 _dragged = true;
581 }
582}
583
584void InkSpinButton::on_drag_end_value(Gdk::EventSequence* sequence) {
585 double dx = 0.0;
586 double dy = 0.0;
587 _drag_value->get_offset(dx, dy);
588
589 if (dx == 0 && !_dragged) {
590 // Must have been a click!
591 enter_edit();
592 }
593 _dragged = false;
594}
595
596void InkSpinButton::show_arrows(bool on) {
597 _minus.set_visible(on && _enable_arrows);
598 _plus.set_visible(on && _enable_arrows);
599}
600
601void InkSpinButton::show_label(bool on) {
602 _label.set_visible(on && _label_width > 0);
603}
604
605static char const *get_text(Gtk::Editable const &editable) {
606 return gtk_editable_get_text(const_cast<GtkEditable *>(editable.gobj())); // C API is const-incorrect
607}
608
609bool InkSpinButton::commit_entry() {
610 try {
611 double value = 0.0;
612 auto text = get_text(_entry);
613 if (_dont_evaluate) {
614 value = std::stod(text);
615 }
616 else if (_evaluator) {
617 value = _evaluator(text);
618 }
619 else {
621 }
622 _adjustment->set_value(value);
623 return true;
624 }
625 catch (const std::exception& e) {
626 g_message("Expression error: %s", e.what());
627 }
628 return false;
629}
630
631void InkSpinButton::exit_edit() {
632 show_arrows(false);
633 _entry.hide();
634 show_label();
635 _value.show();
636}
637
638void InkSpinButton::cancel_editing() {
639 update(false); // take current recorder value and update text/display
640 exit_edit();
641}
642
643inline void InkSpinButton::enter_edit() {
644 show_arrows(false);
645 show_label(false);
646 stop_spinning();
647 _value.hide();
648 _entry.select_region(0, _entry.get_text_length());
649 _entry.show();
650 // postpone it, it won't work immediately:
651 Glib::signal_idle().connect_once([this](){_entry.grab_focus();}, Glib::PRIORITY_HIGH_IDLE);
652}
653
654bool InkSpinButton::defocus() {
655 if (_focus->contains_focus()) {
656 // move focus away
657 if (_defocus_widget) {
658 if (_defocus_widget->grab_focus()) return true;
659 }
660 if (_entry.child_focus(Gtk::DirectionType::TAB_FORWARD)) {
661 return true;
662 }
663 if (auto root = get_root()) {
664 root->unset_focus();
665 return true;
666 }
667 }
668 return false;
669}
670
671// ------------------ SCROLL ------------------
672
673void InkSpinButton::on_scroll_begin() {
674 if (_drag_full_travel <= 0) return;
675
676 _scroll_counter = 0;
677 set_cursor("none");
678}
679
680bool InkSpinButton::on_scroll(double dx, double dy) {
681 if (_drag_full_travel <= 0) return false;
682
683 // grow direction: up or right
684 auto delta = std::abs(dx) > std::abs(dy) ? -dx : dy;
685 _scroll_counter += delta;
686 // this is a threshold to control rate at which scrolling increments/decrements current value;
687 // the larger the threshold, the slower the rate; it may need to be tweaked on different platforms
688#ifdef _WIN32
689 // default for mouse wheel on windows
690 constexpr double threshold = 1.0;
691#elif defined __APPLE__
692 // scrolling is very sensitive on macOS
693 constexpr double threshold = 5.0;
694#else
695 //todo: default for Linux
696 constexpr double threshold = 1.0;
697#endif
698 if (std::abs(_scroll_counter) >= threshold) {
699 auto inc = std::round(_scroll_counter / threshold);
700 _scroll_counter = 0;
701 auto state = _scroll->get_current_event_state();
702 change_value(inc, state);
703 }
704 return true;
705}
706
707void InkSpinButton::on_scroll_end() {
708 if (_drag_full_travel <= 0) return;
709
710 _scroll_counter = 0;
711 set_cursor(_current_cursor);
712}
713
714void InkSpinButton::set_value(double new_value) {
715 set_new_value(new_value * _fmt_scaling_factor);
716}
717
718double InkSpinButton::get_value() const {
719 return _adjustment->get_value() / _fmt_scaling_factor;
720}
721
722void InkSpinButton::change_value(double inc, Gdk::ModifierType state) {
723 double scale = get_accel_factor(state);
724 set_new_value(_adjustment->get_value() + _adjustment->get_step_increment() * scale * inc);
725}
726
727// ------------------ KEY ------------------
728
729bool InkSpinButton::on_key_pressed(guint keyval, guint keycode, Gdk::ModifierType state) {
730 switch (keyval) {
731 case GDK_KEY_Escape: // Cancel
732 // Esc pressed - cancel editing
733 cancel_editing();
734 defocus();
735 return false; // allow Esc to be handled by dialog too
736
737 // signal "activate" uses this key, so we won't see it:
738 // case GDK_KEY_Return:
739 // break;
740
741 case GDK_KEY_Up:
742 change_value(1, state);
743 return true;
744
745 case GDK_KEY_Down:
746 change_value(-1, state);
747 return true;
748
749 default:
750 break;
751 }
752
753 return false;
754}
755
756// ------------------ CLICK ------------------
757
758void InkSpinButton::on_pressed_plus(int n_press, double x, double y) {
759 auto state = _click_plus->get_current_event_state();
760 double inc = (state & Gdk::ModifierType::BUTTON3_MASK) == Gdk::ModifierType::BUTTON3_MASK ? 5 : 1;
761 change_value(inc, state);
762 start_spinning(inc, state, _click_plus);
763}
764
765void InkSpinButton::on_pressed_minus(int n_press, double x, double y) {
766 auto state = _click_minus->get_current_event_state();
767 double inc = (state & Gdk::ModifierType::BUTTON3_MASK) == Gdk::ModifierType::BUTTON3_MASK ? 5 : 1;
768 change_value(-inc, state);
769 start_spinning(-inc, state, _click_minus);
770}
771
772void InkSpinButton::on_activate() {
773 bool ok = commit_entry();
774 if (ok && _enter_exit_edit) {
775 set_focusable(true);
776 defocus();
777 exit_edit();
778 }
779}
780
781void InkSpinButton::on_changed() {
782 // NOT USED
783}
784
785void InkSpinButton::on_editing_done() {
786 // NOT USED
787}
788
789void InkSpinButton::start_spinning(double steps, Gdk::ModifierType state, Glib::RefPtr<Gtk::GestureClick>& gesture) {
790 _spinning = Glib::signal_timeout().connect([=,this]() {
791 change_value(steps, state);
792 // speed up
793 _spinning = Glib::signal_timeout().connect([=,this]() {
794 change_value(steps, state);
795 //TODO: find a way to read mouse button state
796 auto active = gesture->is_active();
797 auto btn = gesture->get_current_button();
798 if (!active || !btn) return false;
799 return true;
800 }, timeout_repeat);
801 return false;
802 }, timeout_click);
803}
804
805void InkSpinButton::stop_spinning() {
806 if (_spinning) _spinning.disconnect();
807}
808
809void InkSpinButton::set_drag_sensitivity(double distance) {
810 _drag_full_travel = distance;
811}
812
813void InkSpinButton::set_label(const std::string& label) {
814 _label.set_text(label);
815 if (label.empty()) {
816 _label.set_visible(false);
817 _label_width = 0;
818 }
819 else {
820 _label.set_visible(true);
821 auto l = _label.measure(Gtk::Orientation::HORIZONTAL);
822 _label_width = l.sizes.minimum;
823 }
824}
825
826sigc::signal<void(double)> InkSpinButton::signal_value_changed() const {
827 return _signal_value_changed;
828}
829
830void InkSpinButton::set_min_size(const std::string& pattern) {
831 _min_size_pattern = pattern;
832 queue_resize();
833}
834
835void InkSpinButton::set_evaluator_function(std::function<double(const Glib::ustring&)> cb) {
836 _evaluator = cb;
837}
838
839void InkSpinButton::set_has_arrows(bool enable) {
840 if (_enable_arrows == enable) return;
841
842 _enable_arrows = enable;
843 queue_resize();
844 show_arrows(enable);
845}
846
847void InkSpinButton::set_enter_exit_edit(bool enable) {
848 _enter_exit_edit = enable;
849}
850
851GType InkSpinButton::gtype = 0;
852
853} // namespace Inkscape::UI::Widget
double distance(Shape const *s, Geom::Point const &p)
Definition Shape.cpp:2136
double scale
Definition aa.cpp:228
Glib::RefPtr< Gtk::GestureDrag > _drag_value
bool on_scroll(double dx, double dy)
void on_drag_update_value(Gdk::EventSequence *sequence)
Glib::Property< double > _scaling_factor
void update(bool fire_change_notification=true)
void on_pressed_minus(int n_press, double x, double y)
Glib::Property< double > _max_value
Glib::PropertyProxy< double > property_max_value()
Glib::PropertyProxy< double > property_min_value()
Glib::RefPtr< Gtk::EventControllerMotion > _motion
void on_drag_end_value(Gdk::EventSequence *sequence)
Glib::PropertyProxy< double > property_value()
void on_pressed_plus(int n_press, double x, double y)
Glib::PropertyProxy< bool > property_show_arrows()
Glib::Property< double > _num_value
Glib::PropertyProxy< int > property_digits()
Glib::Property< double > _min_value
void set_step(double step_increment)
Glib::Property< Glib::ustring > _label_text
Glib::RefPtr< Gtk::Adjustment > _adjustment
Glib::PropertyProxy< bool > property_has_frame()
Glib::PropertyProxy< double > property_scaling_factor()
Glib::PropertyProxy< Glib::ustring > property_label()
Glib::RefPtr< Gtk::EventControllerScroll > _scroll
bool on_key_pressed(guint keyval, guint keycode, Gdk::ModifierType state)
Glib::RefPtr< Gtk::GestureClick > _click_minus
Glib::Property< double > _step_value
void on_drag_begin_value(Gdk::EventSequence *sequence)
Glib::RefPtr< Gtk::EventControllerKey > _key_entry
Glib::PropertyProxy< Glib::ustring > property_prefix()
Glib::RefPtr< Gtk::GestureClick > _click_plus
Glib::PropertyProxy< double > property_step_value()
Glib::RefPtr< Gtk::EventControllerFocus > _focus
void on_motion_enter_value(double x, double y)
Glib::RefPtr< Gtk::EventControllerMotion > _motion_value
void on_motion_enter(double x, double y)
Glib::PropertyProxy< Glib::ustring > property_suffix()
void set_label(const std::string &label)
EvaluatorQuantity evaluate()
Evaluates the given arithmetic expression, along with an optional dimension analysis,...
RootCluster root
TODO: insert short description here.
Glib::ustring label
Definition desktop.h:50
Custom widgets.
Definition desktop.h:126
static Glib::RefPtr< Gdk::Cursor > g_resizing_cursor
static void trim_zeros(std::string &ret)
constexpr int timeout_click
constexpr int timeout_repeat
static Glib::RefPtr< Gdk::Cursor > g_text_cursor
static double get_accel_factor(Gdk::ModifierType state)
void containerize(Gtk::Widget &widget)
Make a custom widget implement sensible memory management for its children.
int delta
int minimum
double height
double width
Glib::RefPtr< Gtk::Builder > builder
char const * get_text(Gtk::Editable const &editable)
Get the text from a GtkEditable without the temporary copy imposed by gtkmm.
Definition util.cpp:549