diff --git a/src/gui/window.cpp b/src/gui/window.cpp index b065922..b62f94b 100644 --- a/src/gui/window.cpp +++ b/src/gui/window.cpp @@ -43,6 +43,40 @@ Gtk::Label* sectionLabel(const char* text) { return label; } +// A GtkSpinButton takes every wheel turn over it: it grabs the focus and +// spins its value, so scrolling down a column of params stops dead at the +// first spin under the pointer and quietly changes it. This hands the wheel to +// the view instead unless the spin already has the focus -- the user clicked +// into it and means to adjust it. It runs in the capture phase, ahead of the +// spin's own controller, and so has to scroll the enclosing ScrolledWindow +// itself: stopping the event there stops it for the window as well. +void scrollPastUnlessFocused(Gtk::SpinButton& spin) { + auto scroll = Gtk::EventControllerScroll::create(); + scroll->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); + scroll->set_flags(Gtk::EventControllerScroll::Flags::VERTICAL); + auto* controller = scroll.get(); + scroll->signal_scroll().connect( + [&spin, controller](double, double dy) { + if ((spin.get_state_flags() & Gtk::StateFlags::FOCUS_WITHIN) == Gtk::StateFlags::FOCUS_WITHIN) { + return false; + } + auto* scroller = spin.get_ancestor(GTK_TYPE_SCROLLED_WINDOW); + if (scroller == nullptr) return true; + auto adjustment = static_cast(scroller)->get_vadjustment(); + // The step a ScrolledWindow itself takes for one notch of a wheel; + // a touchpad already reports its deltas in pixels. + double step = dy; + if (controller->get_unit() == Gdk::ScrollUnit::WHEEL) { + step *= std::pow(adjustment->get_page_size(), 2.0 / 3.0); + } + adjustment->set_value(std::clamp(adjustment->get_value() + step, adjustment->get_lower(), + adjustment->get_upper() - adjustment->get_page_size())); + return true; + }, + false); + spin.add_controller(scroll); +} + } // namespace EffectorWindow::EffectorWindow(const Options& initial) { @@ -281,6 +315,7 @@ Gtk::Widget& EffectorWindow::buildCanvasSection() { dpiSpin_.set_value(kDefaultDpi); for (auto* spin : {&widthSpin_, &heightSpin_, &dpiSpin_}) { spin->set_hexpand(true); + scrollPastUnlessFocused(*spin); } for (auto* spin : {&widthSpin_, &heightSpin_}) { spin->signal_value_changed().connect([this] { @@ -477,6 +512,7 @@ void EffectorWindow::buildParamWidgets() { : paramDouble(values, spec.key, spec.defaultValue)); spin->signal_value_changed().connect( sigc::mem_fun(*this, &EffectorWindow::onParamChanged)); + scrollPastUnlessFocused(*spin); widget = std::move(spin); break; }