Scrolledwindow and image view apps

Hello everyone,

I’ve written a tiny gtk4 image viewer:

It’s on flathub if you need to check the behaviour:

I’m trying to fix mousewheel zoom. The HIG says that the mousewheel should zoom (not pan) for apps of this type:

https://developer.gnome.org/hig/guidelines/pointer-touch.html?highlight=wheel

But I can’t see a way to stop the scrolledwindow I use to hold my view widget from responding to scroll events with panning.

I attach a scroll event controller and return TRUE from the scroll handler, which I think used to stop further event processing, but it’s not working any more with current gtk4/glib.

Is there a recommended way to handle this? Do I need to make my own scrolledwindow?

John

1 Like

No replies :frowning:

OK, I guess I need to copy-paste the scrolled window sources into my code and remove the mousewheel scrolling code. This seems suboptimal!

You don’t really need the whole GtkScrolledWindow, especially if you are completely changing its behaviour.

You can create a widget that has panning and scrolling-to-zoom using controllers, and pack it into a container that allocates the entire size requested by its child, and clips its contents, using gtk_widget_set_overflow().

GtkScrolledWindow is not as complex as it used to be in GTK2 and GTK3, and even in GTK4 its complexity stems from the fact that it’s a general purpose widget meant for scrolling. If you’re removing that functionality for a special-purpose behaviour, then you don’t need most of the complexity in the first place.

Hi Emmanuele, thanks for following up.

I only need to stop the mousewheel pan code (mousewheel is used for zoom in all image view windows), so it should be a very simple change.

Would gtk consider a PR that added a boolean toggle for this? Or is there some other way to stop mousewheel panning in scrolledwindow? I imagine apps like eog will be facing a similar issue.

If you only want to intercept the scroll wheel events, then add one to the child of the scrolled window, and then stop the propagation inside the .

No, not really. It’s a scrolling widget, not a zooming one.

EOG won’t be ported to GTK4, as far as I’m aware; and its direct replacement, Loupe uses the same approach I outlined above: the child of the scrolled window (LpImage) implements the Scrollable interface and uses an EventControllerScroll (locked to a vertical scroll direction) to handle the zoom factor with mouse wheels:

            scroll_controller.connect_scroll(glib::clone!(@weak obj => @default-return gtk::Inhibit(false), move |event, _, y| {
                // use Ctrl key as modifier for vertical scrolling
                if event.current_event_state() == gdk::ModifierType::CONTROL_MASK
                    || event.current_event_state() == gdk::ModifierType::SHIFT_MASK
                {
                    // propagate event to scrolled window
                    return gtk::Inhibit(false);
                }

                // touchpads do zoom via gestures only
                if event.current_event_device().map(|x| x.source())
                    == Some(gdk::InputSource::Touchpad)
                {
                    // propagate event to scrolled window
                    return gtk::Inhibit(false);
                }

                // Use exponential scaling since zoom is always multiplicative with the existing value
                // This is the right thing since `exp(n/2)^2 == exp(n)` (two small steps are the same as one larger step)
                let (zoom_factor, animated) = match event.unit() {
                    gdk::ScrollUnit::Wheel => (f64::exp( - y * f64::ln(ZOOM_FACTOR_SCROLL_WHEEL)), y.abs() >= 1.),
                    gdk::ScrollUnit::Surface => (f64::exp( - y * f64::ln(ZOOM_FACTOR_SCROLL_SURFACE)), false),
                    unknown_unit => {
                        log::warn!("Ignoring unknown scroll unit: {unknown_unit}");
                        (1., false)
                    }
                };

                let zoom =
                    obj.imp().zoom_target.get() * zoom_factor;

                    if animated {
                        obj.zoom_to(zoom);
                    } else {
                        obj.zoom_to_full(zoom, false, false);
                    }

                // do not propagate event to scrolled window
                gtk::Inhibit(true)
            }));

            obj.add_controller(scroll_controller);

That’s what I thought should work, having used previous GTK versions, but it doesn’t seem to work with current or recent gtk4s.

I made you a tiny demo program:

/* compile with
 *  gcc -g -Wall block-scroll.c `pkg-config gtk4 --cflags --libs`
 */

#include <gtk/gtk.h>

static gboolean
scroll_cb (GtkEventControllerMotion *self,
  double dx, double dy, gpointer user_data)
{ 
  printf ("scrollcb: dx = %g, dy = %g\n", dx, dy);

  // block any further vertical scroll event handling
  return TRUE;
}
    
static void
activate (GtkApplication* app,
          gpointer        user_data)
{ 
  GtkWidget *window;
  GtkWidget *scrolled_window;
  GtkTextBuffer *text_buffer;
  GtkTextIter text_iter;
  GtkWidget *text_view;
  int i;
  GtkEventController *controller;
  
  window = gtk_application_window_new (app); 
  gtk_window_set_title (GTK_WINDOW (window), "Window");
  gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);

  scrolled_window = gtk_scrolled_window_new ();
  gtk_window_set_child (GTK_WINDOW (window), scrolled_window);

  text_buffer = gtk_text_buffer_new (NULL);
  gtk_text_buffer_get_iter_at_line (text_buffer, &text_iter, 0);
  for (i = 0; i < 100; i++)
    gtk_text_buffer_insert (text_buffer, &text_iter, "Hello world\n", -1);

  text_view = gtk_text_view_new_with_buffer (text_buffer);
  controller = gtk_event_controller_scroll_new (GTK_EVENT_CONTROLLER_SCROLL_DISCRETE | GTK_EVENT_CONTROLLER_SCROLL_VERTICAL);
  g_signal_connect (controller, "scroll", G_CALLBACK (scroll_cb), NULL);
  gtk_widget_add_controller (text_view, controller);

  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scrolled_window), text_view);

  gtk_window_present (GTK_WINDOW (window));
}

int
main (int    argc,
      char **argv)
{
  GtkApplication *app;
  int status;

  app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
  g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
  status = g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);

  return status;
}

I would have expected returning TRUE from the scroll handler to stop mousewheel scroll events in scrolledwindow, but it does not seem to. I can still use the mousewheel to scroll the text widget in the program above.

This is gtk 4.10 (the one shipped with ubuntu 23.04).

I’m pretty sure this used to work in early gtk4 versions.

Ahhhh!!! Removing GTK_EVENT_CONTROLLER_SCROLL_DISCRETE fixes it!

I assumed mousewheel events are all discrete, but obviously not. Perhaps this changed at some point in gtk4 development?

Anyway, thank you very much for helping, I’ll close.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.