From 169dd454cced7b08e8ca82534e0c719cdef01582 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Mon, 6 Feb 2023 18:40:32 +0100 Subject: [PATCH 1/4] image: Add double click for zoom/fullscreen Zooms on touchscreens and fullscreens otherwise. Using the fullscreen behavior for now, since that's what a lot of other apps do. --- data/gtk/image_page.ui | 2 +- src/widgets/image.rs | 38 +++++++++++++++++++++++++++++++++++++- src/widgets/image_page.rs | 4 ++-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/data/gtk/image_page.ui b/data/gtk/image_page.ui index 86f3a240..6ad4d93e 100644 --- a/data/gtk/image_page.ui +++ b/data/gtk/image_page.ui @@ -63,7 +63,7 @@ - + 3 diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 8109c37a..b124f01c 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -30,13 +30,22 @@ use once_cell::unsync::OnceCell; use std::cell::{Cell, RefCell}; use std::path::{Path, PathBuf}; +/// Milliseconds const ZOOM_ANIMATION_DURATION: u32 = 200; +/// Milliseconds const ROTATION_ANIMATION_DURATION: u32 = 200; +/// Relative to current zoom level const ZOOM_FACTOR_BUTTON: f64 = 1.5; +/// Relative to current zoom level const ZOOM_FACTOR_WHEEL: f64 = 1.3; -const ZOOM_FACTOR_WHEEL_HI_RES: f64 = 0.1; +/// Relative to current zoom level +const ZOOM_FACTOR_WHEEL_HI_RES: f64 = 0.001; +/// Relative to best-fit level +const ZOOM_FACTOR_DOUBLE_TAP: f64 = 2.5; + +/// Max zoom level 2000% const MAX_ZOOM_LEVEL: f64 = 20.0; mod imp { @@ -304,6 +313,33 @@ mod imp { fn connect_gestures(&self) { let obj = self.instance(); + // 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); + 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.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::new(); obj.add_controller(&drag_gesture); diff --git a/src/widgets/image_page.rs b/src/widgets/image_page.rs index 724583ed..1c1f1fed 100644 --- a/src/widgets/image_page.rs +++ b/src/widgets/image_page.rs @@ -49,7 +49,7 @@ mod imp { #[template_child] pub(super) popover: TemplateChild, #[template_child] - pub(super) click_gesture: TemplateChild, + pub(super) right_click_gesture: TemplateChild, #[template_child] pub(super) press_gesture: TemplateChild, @@ -100,7 +100,7 @@ mod imp { self.parent_constructed(); - self.click_gesture + self.right_click_gesture .connect_pressed(clone!(@weak obj => move |gesture, _, x, y| { obj.show_popover_at(x, y); gesture.set_state(gtk::EventSequenceState::Claimed); -- GitLab From 69889820cda59d6cdaaecee6767158052a417852 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Mon, 6 Feb 2023 19:14:01 +0100 Subject: [PATCH 2/4] window: Add Home/End shortcuts for first/last img This is the same as in EOG. Also add PageUp/Down for image navigation from EOG. Cleanup the navigation enums as well. --- data/gtk/help_overlay.ui | 12 ++++++++++++ src/application.rs | 6 ++++-- src/file_model.rs | 10 ++++++++++ src/util.rs | 12 ++++++++++++ src/widgets/image_view.rs | 20 ++++++++++++++++---- src/window.rs | 18 +++++++++++------- 6 files changed, 65 insertions(+), 13 deletions(-) diff --git a/data/gtk/help_overlay.ui b/data/gtk/help_overlay.ui index 9a5c3e62..dd4d60ac 100644 --- a/data/gtk/help_overlay.ui +++ b/data/gtk/help_overlay.ui @@ -102,6 +102,18 @@ Right + + + First Image + win.first + + + + + Last Image + win.last + + diff --git a/src/application.rs b/src/application.rs index 164481f7..9abda2fc 100644 --- a/src/application.rs +++ b/src/application.rs @@ -180,8 +180,10 @@ impl LpApplication { self.set_accels_for_action("win.toggle-fullscreen", &["F11"]); self.set_accels_for_action("win.set-background", &["F8"]); - self.set_accels_for_action("win.previous", &["Left"]); - self.set_accels_for_action("win.next", &["Right"]); + self.set_accels_for_action("win.previous", &["Left", "Page_Down"]); + self.set_accels_for_action("win.next", &["Right", "Page_Up"]); + self.set_accels_for_action("win.first", &["Home"]); + self.set_accels_for_action("win.last", &["End"]); self.set_accels_for_action("win.zoom-to(1.0)", &["1", "KP_1", "1", "KP_1"]); self.set_accels_for_action("win.zoom-to(2.0)", &["2", "KP_2", "2", "KP_2"]); diff --git a/src/file_model.rs b/src/file_model.rs index e701c3d6..f3b2dc0f 100644 --- a/src/file_model.rs +++ b/src/file_model.rs @@ -207,6 +207,16 @@ impl LpFileModel { .collect() } + /// Return first path + pub fn first(&self) -> Option { + self.imp().files.borrow().first().cloned() + } + + /// Returns last path + pub fn last(&self) -> Option { + self.imp().files.borrow().last().cloned() + } + /// Currently sorts by name fn sort(files: &mut IndexSet) { files.sort_by(|x, y| util::compare_by_name(x, y)); diff --git a/src/util.rs b/src/util.rs index 22a9ec37..3c94ac76 100644 --- a/src/util.rs +++ b/src/util.rs @@ -123,3 +123,15 @@ pub async fn spawn( .spawn(async_global_executor::spawn_blocking(f))? .await) } + +#[derive(Debug, Clone, Copy)] +pub enum Position { + First, + Last, +} + +#[derive(Debug, Clone, Copy)] +pub enum Direction { + Back, + Forward, +} diff --git a/src/widgets/image_view.rs b/src/widgets/image_view.rs index cb2bafe3..9ce9dfb0 100644 --- a/src/widgets/image_view.rs +++ b/src/widgets/image_view.rs @@ -21,6 +21,7 @@ use crate::deps::*; use crate::file_model::LpFileModel; use crate::i18n::*; use crate::thumbnail::Thumbnail; +use crate::util::{Direction, Position}; use crate::widgets::{LpImage, LpImagePage, LpSlidingView}; use adw::prelude::*; @@ -270,12 +271,11 @@ impl LpImageView { } /// Move forward or backwards - pub fn navigate(&self, direction: adw::NavigationDirection) { + pub fn navigate(&self, direction: Direction) { if let Some(current_path) = self.current_path() { let new_path = match direction { - adw::NavigationDirection::Forward => self.model().after(¤t_path), - adw::NavigationDirection::Back => self.model().before(¤t_path), - _ => unimplemented!("Navigation direction should only be back or forward."), + Direction::Forward => self.model().after(¤t_path), + Direction::Back => self.model().before(¤t_path), }; if let Some(new_path) = new_path { @@ -284,6 +284,18 @@ impl LpImageView { } } + /// Jump to position + pub fn jump(&self, position: Position) { + let new_path = match position { + Position::First => self.model().first(), + Position::Last => self.model().last(), + }; + + if let Some(new_path) = new_path { + self.navigate_to_path(&new_path); + } + } + /// Used for drag and drop fn navigate_to_path(&self, new_path: &Path) { let sliding_view = self.sliding_view(); diff --git a/src/window.rs b/src/window.rs index f0a79119..8bcd8ba3 100644 --- a/src/window.rs +++ b/src/window.rs @@ -30,7 +30,7 @@ use std::cell::RefCell; use std::path::{Path, PathBuf}; use crate::config; -use crate::util; +use crate::util::{self, Direction, Position}; use crate::widgets::{LpImage, LpImagePage, LpImageView, LpPropertiesView}; mod imp { @@ -99,15 +99,19 @@ mod imp { }); klass.install_action("win.next", None, move |win, _, _| { - win.imp() - .image_view - .navigate(adw::NavigationDirection::Forward); + win.imp().image_view.navigate(Direction::Forward); }); klass.install_action("win.previous", None, move |win, _, _| { - win.imp() - .image_view - .navigate(adw::NavigationDirection::Back); + win.imp().image_view.navigate(Direction::Back); + }); + + klass.install_action("win.first", None, move |win, _, _| { + win.imp().image_view.jump(Position::First); + }); + + klass.install_action("win.last", None, move |win, _, _| { + win.imp().image_view.jump(Position::Last); }); klass.install_action("win.zoom-out", None, move |win, _, _| { -- GitLab From 74a067e3b8e30c72db6106ef4df0ab3e3fedefe8 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Mon, 6 Feb 2023 21:07:23 +0100 Subject: [PATCH 3/4] window: Fix Left/Right button direction for RTL This also fixes the issue with the buttons triggering focus changes when the action is deactivated because buttons know have their own action. Fixes #44 Issue #38 --- src/application.rs | 4 ++-- src/window.rs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/application.rs b/src/application.rs index 9abda2fc..1309892d 100644 --- a/src/application.rs +++ b/src/application.rs @@ -180,8 +180,8 @@ impl LpApplication { self.set_accels_for_action("win.toggle-fullscreen", &["F11"]); self.set_accels_for_action("win.set-background", &["F8"]); - self.set_accels_for_action("win.previous", &["Left", "Page_Down"]); - self.set_accels_for_action("win.next", &["Right", "Page_Up"]); + self.set_accels_for_action("win.image-left", &["Left", "Page_Down"]); + self.set_accels_for_action("win.image-right", &["Right", "Page_Up"]); self.set_accels_for_action("win.first", &["Home"]); self.set_accels_for_action("win.last", &["End"]); diff --git a/src/window.rs b/src/window.rs index 8bcd8ba3..05324e50 100644 --- a/src/window.rs +++ b/src/window.rs @@ -106,6 +106,22 @@ mod imp { win.imp().image_view.navigate(Direction::Back); }); + klass.install_action("win.image-right", None, move |win, _, _| { + if win.direction() == gtk::TextDirection::Rtl { + win.imp().image_view.navigate(Direction::Back); + } else { + win.imp().image_view.navigate(Direction::Forward); + } + }); + + klass.install_action("win.image-left", None, move |win, _, _| { + if win.direction() == gtk::TextDirection::Rtl { + win.imp().image_view.navigate(Direction::Forward); + } else { + win.imp().image_view.navigate(Direction::Back); + } + }); + klass.install_action("win.first", None, move |win, _, _| { win.imp().image_view.jump(Position::First); }); -- GitLab From caefec74b45802df1846931a7dbda126faacb85b Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Tue, 7 Feb 2023 21:56:00 +0100 Subject: [PATCH 4/4] image: Limit rubberbanding zoom and add stiffness --- src/widgets/image.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/widgets/image.rs b/src/widgets/image.rs index b124f01c..04bb93d9 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -45,6 +45,11 @@ const ZOOM_FACTOR_WHEEL_HI_RES: f64 = 0.001; /// Relative to best-fit level const ZOOM_FACTOR_DOUBLE_TAP: f64 = 2.5; +/// Relative to best-fit and `MAX_ZOOM_LEVEL` +const ZOOM_FACTOR_MAX_RUBBERBAND: f64 = 2.; +/// Smaller values make the band feel stiffer +const RUBBERBANDING_EXPONENT: f64 = 0.4; + /// Max zoom level 2000% const MAX_ZOOM_LEVEL: f64 = 20.0; @@ -873,8 +878,29 @@ impl LpImage { } /// Set zoom level aiming for given position or center if not available - fn set_zoom_aiming(&self, zoom: f64, aiming: Option<(f64, f64)>) { - if zoom == self.zoom() || zoom <= 0. { + fn set_zoom_aiming(&self, mut zoom: f64, aiming: Option<(f64, f64)>) { + // allow some deviantion from max value for rubberbanding + if zoom > MAX_ZOOM_LEVEL { + let max_deviation = MAX_ZOOM_LEVEL * ZOOM_FACTOR_MAX_RUBBERBAND; + let deviation = zoom / MAX_ZOOM_LEVEL; + zoom = f64::min( + MAX_ZOOM_LEVEL * deviation.powf(RUBBERBANDING_EXPONENT), + max_deviation, + ); + } + + if zoom < self.zoom_level_best_fit() { + let minimum = self.zoom_level_best_fit(); + let max_deviation = minimum / ZOOM_FACTOR_MAX_RUBBERBAND; + let deviation = zoom / minimum; + zoom = f64::max( + minimum * deviation.powf(RUBBERBANDING_EXPONENT), + max_deviation, + ); + dbg!(zoom); + } + + if zoom == self.zoom() { return; } -- GitLab