diff --git a/Cargo.lock b/Cargo.lock index ece4594..e2a4e1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1287,9 +1287,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libgweather" @@ -1388,6 +1388,7 @@ dependencies = [ "indexmap", "kamadak-exif", "libadwaita", + "libc", "libgweather", "log", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 6a43366..cead69f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ gufo-common = { version = "0.1.0", features = ["serde"] } gvdb-macros = "0.1.6" indexmap = "2.0.0" kamadak-exif = "0.5.5" +libc = "0.2.158" libgweather = "4.4.0" log = "0.4.17" once_cell = "1.16.0" diff --git a/data/resources/icons/scalable/actions/checkmark-symbolic.svg b/data/resources/icons/scalable/actions/checkmark-symbolic.svg new file mode 100644 index 0000000..8a4eba4 --- /dev/null +++ b/data/resources/icons/scalable/actions/checkmark-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/resources/icons/scalable/actions/minus-symbolic.svg b/data/resources/icons/scalable/actions/minus-symbolic.svg new file mode 100644 index 0000000..1cf3321 --- /dev/null +++ b/data/resources/icons/scalable/actions/minus-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/resources/icons/scalable/actions/plus-symbolic.svg b/data/resources/icons/scalable/actions/plus-symbolic.svg new file mode 100644 index 0000000..e87b95d --- /dev/null +++ b/data/resources/icons/scalable/actions/plus-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/resources/icons/scalable/actions/zoom-toggle-symbolic.svg b/data/resources/icons/scalable/actions/zoom-toggle-symbolic.svg new file mode 100644 index 0000000..1a49621 --- /dev/null +++ b/data/resources/icons/scalable/actions/zoom-toggle-symbolic.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/data/resources/style.css b/data/resources/style.css index cfe1ba0..b0690ad 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -1,7 +1,13 @@ -/* Overlay controls for next, zoom, fullscreen */ -button.osd.circular { - min-width: 42px; - min-height: 42px; +/* Overlay controls for prev/next and zoom */ +.osd-box { + border-radius: 12px; + background: rgba(0, 0, 0, 0.65); + padding: 6px; +} + +.osd-box menubutton button { + padding-left: 0; + padding-right: 0; } .fullscreen revealer:not(.raised) .fullscreen-osd { @@ -65,4 +71,4 @@ dialog.error-details textview { .error-message { padding: 12px; font-size: 0.95em; -} +} \ No newline at end of file diff --git a/news.d/48.alpha/changed-zoom-overlay-buttons b/news.d/48.alpha/changed-zoom-overlay-buttons new file mode 100644 index 0000000..68f84f9 --- /dev/null +++ b/news.d/48.alpha/changed-zoom-overlay-buttons @@ -0,0 +1 @@ +The design of the overlay buttons, especially for zoom controls has been reworked. A new menu supports selecting from pre-defined zoom levels as well as entering a specific zoom level. diff --git a/src/util.rs b/src/util.rs index e64e699..99c513f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -18,6 +18,7 @@ pub mod gettext; +use std::ffi::CStr; use std::fmt::{Debug, Write}; use std::path::{Path, PathBuf}; @@ -229,3 +230,34 @@ pub fn contrast_ratio(color1: &gdk::RGBA, color2: &gdk::RGBA) -> f32 { (l1 + 0.05) / (l2 + 0.05) } + +#[derive(Default)] +pub struct LocaleSettings { + pub decimal_point: Option, + pub thousands_sep: Option, +} + +pub fn locale_settings() -> LocaleSettings { + unsafe { + let lconv = libc::localeconv(); + let mut locale_settings = LocaleSettings::default(); + + if lconv.is_null() { + return locale_settings; + } + + let lconv = *lconv; + + if !(lconv).decimal_point.is_null() { + let s = CStr::from_ptr(lconv.decimal_point); + locale_settings.decimal_point = s.to_str().map(|x| x.to_string()).ok(); + } + + if !lconv.thousands_sep.is_null() { + let s = CStr::from_ptr(lconv.thousands_sep); + locale_settings.thousands_sep = s.to_str().map(|x| x.to_string()).ok(); + } + + locale_settings + } +} diff --git a/src/widgets/image.rs b/src/widgets/image.rs index dee368e..907fd67 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -184,6 +184,7 @@ mod imp { pub(super) zoom_animation: OnceCell, /// Targeted zoom level, might differ from `zoom` when animation is /// running + #[property(get, set = Self::set_zoom_target)] pub(super) zoom_target: Cell, /// Point in image that should stay under the cursor during animation. /// The value is in image coordinates. diff --git a/src/widgets/image/zoom.rs b/src/widgets/image/zoom.rs index 492c49a..c7f976b 100644 --- a/src/widgets/image/zoom.rs +++ b/src/widgets/image/zoom.rs @@ -212,6 +212,7 @@ impl imp::LpImage { log::trace!("Setting zoom target {zoom_target}"); self.zoom_target.set(zoom_target); + self.obj().notify_zoom_target(); if self.obj().zoom() == self.zoom_target.get() { self.request_tiles(); @@ -365,11 +366,16 @@ impl LpImage { /// Zoom to specific level with animation not snapping to best-fit /// - /// Used for zooming to 100% or 200% + /// Used for zooming to 100% or 200% etc pub fn zoom_to_exact(&self, zoom: f64) { self.imp().zoom_to_full(zoom, true, false, false); } + /// Same as `zoom_to_exact` but zoom to center + pub fn zoom_to_exact_center(&self, zoom: f64) { + self.imp().zoom_to_full(zoom, true, false, true); + } + pub fn is_best_fit(&self) -> bool { self.imp().best_fit.get() } diff --git a/src/widgets/image_page.ui b/src/widgets/image_page.ui index 8668d5f..044134d 100644 --- a/src/widgets/image_page.ui +++ b/src/widgets/image_page.ui @@ -6,18 +6,10 @@ _Open With… win.open-with - _Print… win.print - - _Copy - win.copy-image -
Rotate @@ -34,6 +26,14 @@ _Set as Background… win.set-background + + _Delete + win.trash + + + _Copy + win.copy-image +
- \ No newline at end of file + diff --git a/src/window.rs b/src/window.rs index c4cb9c8..e865a44 100644 --- a/src/window.rs +++ b/src/window.rs @@ -79,9 +79,6 @@ mod imp { #[properties(wrapper_type = super::LpWindow)] #[template(file = "window.ui")] pub struct LpWindow { - // Template children are used with the - // TemplateChild wrapper, where T is the - // object type of the template child. #[template_child] pub(super) window_content: TemplateChild, #[template_child] @@ -92,6 +89,9 @@ mod imp { pub(super) properties_button: TemplateChild, #[template_child] pub(super) primary_menu: TemplateChild, + #[template_child] + pub(super) fullscreen_button: TemplateChild, + #[template_child] pub(super) toast_overlay: TemplateChild, #[template_child] @@ -375,6 +375,22 @@ mod imp { obj.schedule_hide_controls(); }); + obj.image_view() + .zoom_menu_button() + .connect_active_notify(glib::clone!( + #[weak] + obj, + move |button| { + if button.is_active() { + // Show overlay controls when zoom popover in them gets opened + obj.show_controls(); + } else { + // Schedule hide overlay controls when zoom popover gets closed + obj.schedule_hide_controls(); + } + } + )); + // Activate global shortcuts only if no dialog is open obj.connect_visible_dialog_notify(|obj| obj.update_accel_status()); obj.connect_is_active_notify(|obj| obj.update_accel_status()); @@ -499,7 +515,7 @@ impl LpWindow { } fn zoom_to_exact(&self, level: f64) { - if let Some(image) = self.imp().image_view.current_image() { + if let Some(image) = self.image_view().current_image() { image.zoom_to_exact(level); } } @@ -750,8 +766,9 @@ impl LpWindow { Action::RotateCw, Action::RotateCcw, Action::ZoomBestFit, - Action::ZoomToExact1, - Action::ZoomToExact2, + Action::ZoomToExact100, + Action::ZoomToExact200, + Action::ZoomToExact300, ]; let enabled_shown = self @@ -780,6 +797,12 @@ impl LpWindow { self.action_set_enabled(&Action::ZoomInCursor, can_zoom_in && enabled_shown); self.action_set_enabled(&Action::ZoomInCenter, can_zoom_in && enabled_shown); + self.image_view().set_zoom_toggle_state( + self.image_view() + .current_image() + .is_some_and(|image| !image.is_best_fit()), + ); + // Actions that are available if there is an current image, even if it's not // shown const ACTIONS_CURRENT: &[Action] = &[ @@ -944,9 +967,12 @@ impl LpWindow { } fn on_fullscreen_changed(&self) { - self.imp() - .image_view - .on_fullscreen_changed(self.is_fullscreen()); + let icon = if self.is_fullscreen() { + "view-restore-symbolic" + } else { + "view-fullscreen-symbolic" + }; + self.imp().fullscreen_button.set_icon_name(icon); if !self.is_fullscreen() { self.set_cursor(None); diff --git a/src/window.ui b/src/window.ui index 45ad442..7dc03da 100644 --- a/src/window.ui +++ b/src/window.ui @@ -16,10 +16,6 @@ _Open With… win.open-with - _Print… win.print @@ -40,6 +36,10 @@ _Set as Background… win.set-background + + _Delete + win.trash +
@@ -198,17 +198,10 @@ - - edit-copy-symbolic - win.copy-image - Copy to Clipboard - - - - - user-trash-symbolic - win.trash - Move to Trash + + end + win.toggle-fullscreen + Toggle Fullscreen diff --git a/src/window/actions.rs b/src/window/actions.rs index 65895cd..f007aa5 100644 --- a/src/window/actions.rs +++ b/src/window/actions.rs @@ -167,10 +167,12 @@ pub enum Action { ZoomInCursor, #[strum(to_string = "win.zoom-best-fit")] ZoomBestFit, - #[strum(to_string = "win.zoom-to-exact-1")] - ZoomToExact1, - #[strum(to_string = "win.zoom-to-exact-2")] - ZoomToExact2, + #[strum(to_string = "win.zoom-to-exact-100")] + ZoomToExact100, + #[strum(to_string = "win.zoom-to-exact-200")] + ZoomToExact200, + #[strum(to_string = "win.zoom-to-exact-300")] + ZoomToExact300, // Misc #[strum(to_string = "win.open")] Open, @@ -332,7 +334,7 @@ impl Action { ); } - Action::ZoomToExact1 => { + Action::ZoomToExact100 => { klass.install_action(&action, None, move |win, _, _| { win.zoom_to_exact(1.); }); @@ -350,7 +352,7 @@ impl Action { ); } - Action::ZoomToExact2 => { + Action::ZoomToExact200 => { klass.install_action(&action, None, move |win, _, _| { win.zoom_to_exact(2.); }); @@ -368,6 +370,24 @@ impl Action { ); } + Action::ZoomToExact300 => { + klass.install_action(&action, None, move |win, _, _| { + win.zoom_to_exact(2.); + }); + klass.add_binding_action(gdk::Key::_3, gdk::ModifierType::empty(), &action); + klass.add_binding_action(gdk::Key::KP_3, gdk::ModifierType::empty(), &action); + klass.add_binding_action( + gdk::Key::_3, + gdk::ModifierType::CONTROL_MASK, + &action, + ); + klass.add_binding_action( + gdk::Key::KP_3, + gdk::ModifierType::CONTROL_MASK, + &action, + ); + } + // Misc Action::Open => { klass.install_action_async(&action, None, |win, _, _| async move { @@ -427,12 +447,13 @@ impl Action { klass.install_action_async(&action, None, |win, _, _| async move { win.trash().await; }); - klass.add_binding_action(gdk::Key::Delete, gdk::ModifierType::empty(), &action); klass.add_binding_action( gdk::Key::KP_Delete, gdk::ModifierType::empty(), &action, ); + // The binding added last is shown in menus + klass.add_binding_action(gdk::Key::Delete, gdk::ModifierType::empty(), &action); } Action::Delete => { diff --git a/src/window/controls.rs b/src/window/controls.rs index 0cf2b40..2ab55a3 100644 --- a/src/window/controls.rs +++ b/src/window/controls.rs @@ -129,7 +129,7 @@ impl LpWindow { /// Start animation to hide controls pub fn hide_controls(&self) { - if self.are_controls_visible() { + if self.are_controls_visible() && !self.image_view().zoom_menu_button().is_active() { self.show_controls_animation().pause(); self.hide_controls_animation() .set_value_from(self.controls_opacity());