// Copyright (c) 2023-2024 Sophie Herold // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // SPDX-License-Identifier: GPL-3.0-or-later use super::*; impl imp::LpImage { /// To be called by constructor pub(super) fn connect_input_handling(&self) { self.connect_controllers(); self.connect_gestures(); } fn connect_controllers(&self) { let obj = self.obj(); // Needed for having the current cursor position available let motion_controller = gtk::EventControllerMotion::new(); motion_controller.connect_enter(glib::clone!(@weak obj => move |_, x, y| { obj.imp().pointer_position.set(Some((x, y))); })); motion_controller.connect_motion(glib::clone!(@weak obj => move |_, x, y| { obj.imp().pointer_position.set(Some((x, y))); })); motion_controller.connect_leave(glib::clone!(@weak obj => move |_| { obj.imp().pointer_position.set(None); })); obj.add_controller(motion_controller); // Zoom via scroll wheels etc let scroll_controller = gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::BOTH_AXES); scroll_controller.connect_scroll_end(glib::clone!(@weak obj => move |event| { // Avoid kinetc scrolling in scrolled window after zooming if event.current_event_state().contains(gdk::ModifierType::CONTROL_MASK) { obj.imp().cancel_deceleration(); } })); scroll_controller.connect_scroll(glib::clone!(@weak obj => @default-return glib::Propagation::Proceed, move |event, _, y| { let imp = obj.imp(); let state = event.current_event_state(); if event.current_event_device().map(|x| x.source()) == Some(gdk::InputSource::Touchpad) { // Touchpads do zoom via gestures, expect when Ctrl key is pressed if !state.contains(gdk::ModifierType::CONTROL_MASK) { // propagate event to scrolled window return glib::Propagation::Proceed; } } else { // use Ctrl key as modifier for vertical scrolling if state.contains(gdk::ModifierType::CONTROL_MASK) || state.contains(gdk::ModifierType::SHIFT_MASK) { // propagate event to scrolled window return glib::Propagation::Proceed; } } // 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 = imp.zoom_target.get() * zoom_factor; if animated { obj.zoom_to(zoom); } else { imp.zoom_to_full(zoom, false, false, false); } // do not propagate event to scrolled window glib::Propagation::Stop })); obj.add_controller(scroll_controller); } fn connect_gestures(&self) { let obj = self.obj(); // Double click for fullscreen (mouse/touchpad) or zoom (touch screen) let left_click_gesture = gtk::GestureClick::builder().button(1).build(); obj.add_controller(left_click_gesture.clone()); left_click_gesture.connect_pressed( glib::clone!(@weak obj => move |gesture, n_press, x, y| { // only handle double clicks if n_press != 2 { return; } if gesture.device().map(|x| x.source()) == Some(gdk::InputSource::Touchscreen) { // zoom obj.imp().pointer_position.set(Some((x, y))); if obj.is_best_fit() { // zoom in obj.zoom_to(ZOOM_FACTOR_DOUBLE_TAP * obj.imp().zoom_level_best_fit()); } else { // zoom back out obj.zoom_best_fit(); } } else { // fullscreen obj.activate_action("win.toggle-fullscreen", None).unwrap(); } }), ); // Drag for moving image around let drag_gesture = gtk::GestureDrag::builder().button(0).build(); obj.add_controller(drag_gesture.clone()); drag_gesture.connect_drag_begin(glib::clone!(@weak obj => move |gesture, _, _| { let imp = obj.imp(); // Allow only left and middle button if ![1, 2].contains(&gesture.current_button()) // Drag gesture for touchscreens is handled by ScrolledWindow || gesture.device().map(|x| x.source()) == Some(gdk::InputSource::Touchscreen) { gesture.set_state(gtk::EventSequenceState::Denied); return; } if obj.is_hscrollable() || obj.is_vscrollable() { imp.cancel_deceleration(); obj.set_cursor(gdk::Cursor::from_name("grabbing", None).as_ref()); imp.last_drag_value.set(Some((0., 0.))); } else { // let drag and drop handle the events when not scrollable gesture.set_state(gtk::EventSequenceState::Denied); } })); drag_gesture.connect_drag_update(glib::clone!(@weak obj => move |_, x1, y1| { let imp = obj.imp(); if let Some((x0, y0)) = obj.imp().last_drag_value.get() { imp.set_hadj_value(imp.hadj_value() - x1 + x0); imp.set_vadj_value(imp.vadj_value() - y1 + y0); } obj.imp().last_drag_value.set(Some((x1, y1))); })); drag_gesture.connect_drag_end(glib::clone!(@weak obj => move |_, _, _| { obj.set_cursor(None); obj.imp().last_drag_value.set(None); })); // Rotate let rotation_gesture = gtk::GestureRotate::new(); obj.add_controller(rotation_gesture.clone()); rotation_gesture .connect_begin(glib::clone!(@weak obj => move |_, _| obj.imp().cancel_deceleration())); rotation_gesture.connect_angle_changed(glib::clone!(@weak obj => move |gesture, _, _| { let angle = gesture.angle_delta().to_degrees(); // Only reset rotation if scale gesture is locked in if let Some(Gesture::Scale) = obj.imp().locked_gestured.get() { obj.imp().rotation.set(obj.imp().rotation_target.get()); return; } // Correct angle by the the angle at the moment of passing the threshold. // This stops the rotation from suddenly jumping when passing the threshold. let correction = if let Some(Gesture::Rotate(correction)) = obj.imp().locked_gestured.get() { correction } else if angle.abs() > ROTATE_GESTURE_LOCK_THRESHOLD { let correction = angle.signum() * ROTATE_GESTURE_LOCK_THRESHOLD; obj.imp() .locked_gestured .set(Some(Gesture::Rotate(correction))); correction } else { return; }; obj.set_rotation(obj.imp().rotation_target.get() + angle - correction); })); rotation_gesture.connect_end(glib::clone!(@weak obj => move |_, _| { log::debug!("Rotate gesture ended"); let angle = (obj.rotation() / 90.).round() * 90. - obj.imp().rotation_target.get(); obj.rotate_by(angle); obj.imp().locked_gestured.set(None); })); // Zoom let zoom_gesture = gtk::GestureZoom::new(); obj.add_controller(zoom_gesture.clone()); zoom_gesture.connect_begin(glib::clone!(@weak obj => move |gesture, _| { let imp = obj.imp(); imp.cancel_deceleration(); imp.zoom_gesture_center.set(gesture.bounding_box_center()); })); zoom_gesture.connect_scale_changed(glib::clone!(@weak obj => move |gesture, scale| { let imp = obj.imp(); let zoom = imp.zoom_target.get() * scale; // Move image with fingers on touchscreens if gesture.device().map(|x| x.source()) == Some(gdk::InputSource::Touchscreen) { if let p1 @ Some((x1, y1)) = gesture.bounding_box_center() { if let Some((x0, y0)) = imp.zoom_gesture_center.get() { imp.set_hadj_value(imp.hadj_value() + x0 - x1); imp.set_vadj_value(imp.vadj_value()+ y0 - y1); } else { log::warn!("Zoom bounding box center: No previous value"); } imp.zoom_gesture_center.set(p1); } } let zoom_out_threshold = 1. / ZOOM_GESTURE_LOCK_THRESHOLD; let zoom_in_threshold = ZOOM_GESTURE_LOCK_THRESHOLD; if let Some(Gesture::Rotate(_)) = imp.locked_gestured.get() { // Do not zoom when rotate is locked in return; } else if !(zoom_out_threshold..zoom_in_threshold).contains(&scale) { // Lock in scale when leaving the scale threshold imp.locked_gestured.set(Some(Gesture::Scale)); } imp.set_zoom_aiming(zoom, imp.zoom_gesture_center.get()); })); zoom_gesture.connect_end(glib::clone!(@weak obj => move |_, _| { let imp = obj.imp(); log::debug!("Zoom gesture ended"); let rotation_target = (obj.rotation() / 90.).round() * 90.; if obj.zoom() < imp.zoom_level_best_fit_for_rotation(rotation_target) { obj.zoom_to(imp.zoom_level_best_fit_for_rotation(rotation_target)); } else { // rubberband if over highest zoom level and sets `zoom_target` obj.zoom_to(obj.zoom()); }; imp.locked_gestured.set(None); })); zoom_gesture.group_with(&rotation_gesture); } /// Cancel kinetic scrolling movements, needed for some gestures /// /// If deceleration is not canceled gestures become buggy. fn cancel_deceleration(&self) { if let Some(scrolled_window) = self .obj() .parent() .and_then(|x| x.downcast::().ok()) { scrolled_window.set_kinetic_scrolling(false); scrolled_window.set_kinetic_scrolling(true); } else { log::error!("Could not find GtkScrolledWindow parent to cancel deceleration"); } } }