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; }
42 set_name(
"InkSpinButton");
44 set_overflow(Gtk::Overflow::HIDDEN);
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);
54 _label.set_name(
"InkSpinButton-Label");
60 _minus.set_size_request(8, -1);
62 _value.set_single_line_mode();
63 _value.set_overflow(Gtk::Overflow::HIDDEN);
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);
72 _minus.set_icon_name(
"go-previous-symbolic");
73 _plus.set_icon_name(
"go-next-symbolic");
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);
84 static Glib::RefPtr<Gtk::CssProvider> provider;
86 provider = Gtk::CssProvider::create();
88 auto const display = Gdk::Display::get_default();
89 Gtk::StyleContext::add_provider_for_display(display, provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 10);
96 _motion = Gtk::EventControllerMotion::create();
112 _drag_value->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
116 _scroll = Gtk::EventControllerScroll::create();
120 _scroll->set_flags(Gtk::EventControllerScroll::Flags::BOTH_AXES);
127 _click_minus->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
134 _click_plus->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
137 _focus = Gtk::EventControllerFocus::create();
138 _focus->signal_enter().connect([
this]() {
141 set_focusable(false);
145 _focus->signal_leave().connect([
this]() {
146 if (
_entry.is_visible()) {
153 _entry.set_focus_on_click(
false);
154 _entry.set_focusable(
false);
158 set_focus_on_click();
160 _key_entry = Gtk::EventControllerKey::create();
161 _key_entry->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
174 auto m =
_minus.measure(Gtk::Orientation::HORIZONTAL);
176 m =
_entry.measure(Gtk::Orientation::VERTICAL);
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", {})
225 Glib::ObjectBase(
"InkSpinButton"),
231InkSpinButton::InkSpinButton(BaseObjectType* cobject,
const Glib::RefPtr<Gtk::Builder>&
builder):
232 Glib::ObjectBase(
"InkSpinButton"),
233 Gtk::Widget(cobject),
239InkSpinButton::InkSpinButton(BaseObjectType* cobject):
240 Glib::ObjectBase(
"InkSpinButton"),
241 Gtk::Widget(cobject),
247#undef INIT_PROPERTIES
249InkSpinButton::~InkSpinButton() =
default;
251Gtk::SizeRequestMode InkSpinButton::get_request_mode_vfunc()
const {
252 return Gtk::Widget::get_request_mode_vfunc();
255void InkSpinButton::measure_vfunc(Gtk::Orientation orientation,
int for_size,
int&
minimum,
int& natural,
int& minimum_baseline,
int& natural_baseline)
const {
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;
265 text = _min_size_pattern;
269 auto layout =
const_cast<InkSpinButton*
>(
this)->create_pango_layout(
"\u2009" + text +
"\u2009");
274 layout->get_pixel_size(text_width, text_height);
276 if (orientation == Gtk::Orientation::HORIZONTAL) {
277 minimum_baseline = natural_baseline = -1;
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);
285 auto btn = _enable_arrows ? _button_width : 0;
287 minimum = natural = std::max(_label_width + text_width, btn + text_width + btn);
290 minimum_baseline = natural_baseline = _baseline;
291 auto height = std::max(text_height, _entry_height);
293 natural = std::max(
static_cast<int>(1.5 * text_height), _entry_height);
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);
308 if (_label.get_visible()) {
309 Gtk::Allocation alloc;
311 alloc.set_width(_label_width);
314 _label.size_allocate(alloc, baseline);
315 left += _label_width;
316 right -= _label_width;
318 if (_minus.get_visible()) {
319 _minus.size_allocate(allocation, baseline);
320 left += allocation.get_width();
322 if (_plus.get_visible()) {
323 allocation.set_x(
width - allocation.get_width());
324 _plus.size_allocate(allocation, baseline);
325 right -= allocation.get_width();
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);
333 if (_entry.get_visible()) {
334 _entry.size_allocate(allocation, baseline);
339Glib::RefPtr<Gtk::Adjustment>& InkSpinButton::get_adjustment() {
343void InkSpinButton::set_adjustment(
const Glib::RefPtr<Gtk::Adjustment>& adjustment) {
344 if (!adjustment)
return;
346 _connection.disconnect();
347 _adjustment = adjustment;
348 _connection = _adjustment->signal_value_changed().connect([
this](){ update(); });
352void InkSpinButton::set_digits(
int digits) {
357int InkSpinButton::get_digits()
const {
358 return _digits.get_value();
361void InkSpinButton::set_range(
double min,
double max) {
362 _adjustment->set_lower(min);
363 _adjustment->set_upper(max);
366void InkSpinButton::set_step(
double step_increment) {
367 _adjustment->set_step_increment(step_increment);
370void InkSpinButton::set_prefix(
const std::string& prefix,
bool add_space) {
371 if (add_space && !prefix.empty()) {
372 _prefix.set_value(prefix +
" ");
375 _prefix.set_value(prefix);
380void InkSpinButton::set_suffix(
const std::string& suffix,
bool add_half_space) {
381 if (add_half_space && !suffix.empty()) {
383 _suffix.set_value(
"\u2009" + suffix);
386 _suffix.set_value(suffix);
391void InkSpinButton::set_has_frame(
bool frame) {
393 add_css_class(
"frame");
396 remove_css_class(
"frame");
400void InkSpinButton::set_trim_zeros(
bool trim) {
401 if (_trim_zeros != trim) {
407void InkSpinButton::set_scaling_factor(
double factor) {
408 assert(factor > 0 && factor < 1e9);
409 _fmt_scaling_factor = factor;
415 while (ret.find(
'.') != std::string::npos &&
416 (ret.substr(ret.length() - 1, 1) ==
"0" || ret.substr(ret.length() - 1, 1) ==
".")) {
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"));
425 if (value > 1e12 || value < -1e12) {
427 ss << std::scientific << std::setprecision(std::numeric_limits<double>::digits10) << value;
431 ss << std::fixed << std::setprecision(_digits.get_value()) << value;
437 auto limit = std::numeric_limits<double>::digits10;
438 if (value < 0)
limit += 1;
440 if (number.size() >
limit) {
441 number = number.substr(0,
limit);
446 auto suffix = _suffix.get_value();
447 auto prefix = _prefix.get_value();
448 if (with_prefix_suffix && (!suffix.empty() || !prefix.empty())) {
450 std::stringstream markup;
451 if (!prefix.empty()) {
452 markup <<
"<span alpha='50%'>" << Glib::Markup::escape_text(prefix) <<
"</span>";
454 markup <<
"<span>" << number <<
"</span>";
455 if (!suffix.empty()) {
456 markup <<
"<span alpha='50%'>" << Glib::Markup::escape_text(suffix) <<
"</span>";
461 return prefix + number + suffix;
468void InkSpinButton::update(
bool fire_change_notification) {
469 if (!_adjustment)
return;
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);
478 _value.set_markup(format(value,
true,
true, _trim_zeros,
false));
481 _minus.set_sensitive(_adjustment->get_value() > _adjustment->get_lower());
482 _plus.set_sensitive(_adjustment->get_value() < _adjustment->get_upper());
484 if (fire_change_notification) {
485 _signal_value_changed.emit(value / _fmt_scaling_factor);
489void InkSpinButton::set_new_value(
double new_value) {
490 _adjustment->set_value(new_value);
498void InkSpinButton::on_motion_enter(
double x,
double y) {
499 if (_focus->contains_focus())
return;
505void InkSpinButton::on_motion_leave() {
506 if (_focus->contains_focus())
return;
511 if (_entry.is_visible()) {
520void InkSpinButton::on_motion_enter_value(
double x,
double y) {
521 _old_cursor = get_cursor();
527 if (_drag_full_travel > 0) {
529 set_cursor(_current_cursor);
533 set_cursor(_current_cursor);
537void InkSpinButton::on_motion_leave_value() {
538 _current_cursor = _old_cursor;
539 set_cursor(_current_cursor);
547 if ((state & Gdk::ModifierType::CONTROL_MASK) == Gdk::ModifierType::CONTROL_MASK) {
549 }
else if ((state & Gdk::ModifierType::SHIFT_MASK) == Gdk::ModifierType::SHIFT_MASK) {
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);
560void InkSpinButton::on_drag_update_value(Gdk::EventSequence* sequence) {
561 if (_drag_full_travel <= 0)
return;
565 _drag_value->get_offset(dx, dy);
569 if (std::abs(dx) >
delta || std::abs(dy) >
delta) {
570 auto max_dist = _drag_full_travel;
571 auto range = _adjustment->get_upper() - _adjustment->get_lower();
572 auto state = _drag_value->get_current_event_state();
574 auto angle = std::atan2(dx, dy);
575 auto grow = angle > M_PI_4 || angle < -M_PI+M_PI_4;
579 set_new_value(value);
584void InkSpinButton::on_drag_end_value(Gdk::EventSequence* sequence) {
587 _drag_value->get_offset(dx, dy);
589 if (dx == 0 && !_dragged) {
596void InkSpinButton::show_arrows(
bool on) {
597 _minus.set_visible(on && _enable_arrows);
598 _plus.set_visible(on && _enable_arrows);
601void InkSpinButton::show_label(
bool on) {
602 _label.set_visible(on && _label_width > 0);
605static char const *
get_text(Gtk::Editable
const &editable) {
606 return gtk_editable_get_text(
const_cast<GtkEditable *
>(editable.gobj()));
609bool InkSpinButton::commit_entry() {
613 if (_dont_evaluate) {
614 value = std::stod(text);
616 else if (_evaluator) {
617 value = _evaluator(text);
622 _adjustment->set_value(value);
625 catch (
const std::exception& e) {
626 g_message(
"Expression error: %s", e.what());
631void InkSpinButton::exit_edit() {
638void InkSpinButton::cancel_editing() {
643inline void InkSpinButton::enter_edit() {
648 _entry.select_region(0, _entry.get_text_length());
651 Glib::signal_idle().connect_once([
this](){_entry.grab_focus();}, Glib::PRIORITY_HIGH_IDLE);
654bool InkSpinButton::defocus() {
655 if (_focus->contains_focus()) {
657 if (_defocus_widget) {
658 if (_defocus_widget->grab_focus())
return true;
660 if (_entry.child_focus(Gtk::DirectionType::TAB_FORWARD)) {
663 if (
auto root = get_root()) {
673void InkSpinButton::on_scroll_begin() {
674 if (_drag_full_travel <= 0)
return;
680bool InkSpinButton::on_scroll(
double dx,
double dy) {
681 if (_drag_full_travel <= 0)
return false;
684 auto delta = std::abs(dx) > std::abs(dy) ? -dx : dy;
685 _scroll_counter +=
delta;
690 constexpr double threshold = 1.0;
691#elif defined __APPLE__
693 constexpr double threshold = 5.0;
696 constexpr double threshold = 1.0;
698 if (std::abs(_scroll_counter) >= threshold) {
699 auto inc = std::round(_scroll_counter / threshold);
701 auto state = _scroll->get_current_event_state();
702 change_value(inc, state);
707void InkSpinButton::on_scroll_end() {
708 if (_drag_full_travel <= 0)
return;
711 set_cursor(_current_cursor);
714void InkSpinButton::set_value(
double new_value) {
715 set_new_value(new_value * _fmt_scaling_factor);
718double InkSpinButton::get_value()
const {
719 return _adjustment->get_value() / _fmt_scaling_factor;
722void InkSpinButton::change_value(
double inc, Gdk::ModifierType state) {
724 set_new_value(_adjustment->get_value() + _adjustment->get_step_increment() *
scale * inc);
729bool InkSpinButton::on_key_pressed(guint keyval, guint keycode, Gdk::ModifierType state) {
742 change_value(1, state);
746 change_value(-1, state);
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);
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);
772void InkSpinButton::on_activate() {
773 bool ok = commit_entry();
774 if (ok && _enter_exit_edit) {
781void InkSpinButton::on_changed() {
785void InkSpinButton::on_editing_done() {
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);
793 _spinning = Glib::signal_timeout().connect([=,
this]() {
794 change_value(steps, state);
796 auto active = gesture->is_active();
797 auto btn = gesture->get_current_button();
798 if (!active || !btn)
return false;
805void InkSpinButton::stop_spinning() {
806 if (_spinning) _spinning.disconnect();
809void InkSpinButton::set_drag_sensitivity(
double distance) {
813void InkSpinButton::set_label(
const std::string&
label) {
814 _label.set_text(
label);
816 _label.set_visible(
false);
820 _label.set_visible(
true);
821 auto l = _label.measure(Gtk::Orientation::HORIZONTAL);
822 _label_width = l.sizes.minimum;
826sigc::signal<void(
double)> InkSpinButton::signal_value_changed()
const {
827 return _signal_value_changed;
830void InkSpinButton::set_min_size(
const std::string& pattern) {
831 _min_size_pattern = pattern;
835void InkSpinButton::set_evaluator_function(std::function<
double(
const Glib::ustring&)> cb) {
839void InkSpinButton::set_has_arrows(
bool enable) {
840 if (_enable_arrows == enable)
return;
842 _enable_arrows = enable;
847void InkSpinButton::set_enter_exit_edit(
bool enable) {
848 _enter_exit_edit = enable;
851GType InkSpinButton::gtype = 0;
double distance(Shape const *s, Geom::Point const &p)
EvaluatorQuantity evaluate()
Evaluates the given arithmetic expression, along with an optional dimension analysis,...
TODO: insert short description here.
void containerize(Gtk::Widget &widget)
Make a custom widget implement sensible memory management for its children.
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.