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
+
diff --git a/src/widgets/image_view.rs b/src/widgets/image_view.rs
index d0bb2f4..a9e20d0 100644
--- a/src/widgets/image_view.rs
+++ b/src/widgets/image_view.rs
@@ -45,7 +45,7 @@ use crate::decoder::DecoderError;
use crate::deps::*;
use crate::file_model::LpFileModel;
use crate::util::gettext::*;
-use crate::util::{Direction, Position};
+use crate::util::{self, Direction, Position};
use crate::widgets::{LpImage, LpImagePage, LpPrint, LpSlidingView};
// The number of pages we want to buffer
@@ -63,18 +63,38 @@ mod imp {
/// Direct child of this Adw::Bin
#[template_child]
pub(super) bin_child: TemplateChild,
+
/// overlayed controls
#[template_child]
pub(super) controls_box_start: TemplateChild,
#[template_child]
+ pub(super) zoom_toggle: TemplateChild,
+ #[template_child]
pub(super) controls_box_start_events: TemplateChild,
+
/// overlayed controls
#[template_child]
pub(super) controls_box_end: TemplateChild,
#[template_child]
pub(super) controls_box_end_events: TemplateChild,
#[template_child]
- pub(super) fullscreen_button: TemplateChild,
+ pub(super) zoom_menu_button: TemplateChild,
+ #[template_child]
+ pub(super) zoom_to_list: TemplateChild,
+ #[template_child]
+ pub(super) zoom_to_300: TemplateChild,
+ #[template_child]
+ pub(super) zoom_to_200: TemplateChild,
+ #[template_child]
+ pub(super) zoom_to_100: TemplateChild,
+ #[template_child]
+ pub(super) zoom_to_66: TemplateChild,
+ #[template_child]
+ pub(super) zoom_to_50: TemplateChild,
+ #[template_child]
+ pub(super) zoom_value: TemplateChild,
+ #[template_child]
+ pub(super) submit_zoom: TemplateChild,
#[template_child]
pub sliding_view: TemplateChild,
@@ -98,6 +118,9 @@ mod imp {
#[property(get = Self::is_next_available)]
_is_next_available: PhantomData,
+
+ #[property(get, set)]
+ zoom_toggle_state: Cell,
}
#[glib::object_subclass]
@@ -148,10 +171,10 @@ mod imp {
move || obj.target_page_reached()
));
- let signal_group = obj.current_image_signals();
+ let current_image_signal_group = obj.current_image_signals();
let view = &*obj;
- signal_group.connect_closure(
+ current_image_signal_group.connect_closure(
"notify::file",
true,
glib::closure_local!(
@@ -163,7 +186,7 @@ mod imp {
),
);
- signal_group.connect_closure(
+ current_image_signal_group.connect_closure(
"notify::is-deleted",
true,
glib::closure_local!(
@@ -175,6 +198,97 @@ mod imp {
),
);
+ current_image_signal_group.connect_closure(
+ "notify::zoom-target",
+ true,
+ glib::closure_local!(
+ #[watch]
+ view,
+ move |img: LpImage, _: glib::ParamSpec| {
+ let mut percent =
+ ((img.zoom_target() * 10_000.).round() / 100.).to_string();
+
+ if let Some(decimal) = util::locale_settings().decimal_point {
+ percent = percent.replace('.', &decimal);
+ }
+
+ view.imp()
+ .zoom_value
+ .set_text(&gettext_f("{} %", [percent]));
+ view.imp().zoom_value.select_region(0, -1);
+ }
+ ),
+ );
+
+ self.zoom_toggle.connect_clicked(clone!(
+ #[weak]
+ obj,
+ move |button| {
+ if let Some(page) = obj.current_page() {
+ if button.is_active() {
+ page.image().zoom_in_center()
+ } else {
+ page.image().zoom_best_fit()
+ };
+ }
+ }
+ ));
+
+ self.zoom_to_list.connect_row_activated(clone!(
+ #[weak]
+ obj,
+ move |_, row| {
+ let imp = obj.imp();
+ let value = if row == &*imp.zoom_to_300 {
+ 3.
+ } else if row == &*imp.zoom_to_200 {
+ 2.
+ } else if row == &*imp.zoom_to_100 {
+ 1.
+ } else if row == &*imp.zoom_to_66 {
+ 0.66
+ } else if row == &*imp.zoom_to_50 {
+ 0.5
+ } else {
+ log::error!("Activated unknown entry in zoom to popover.");
+ return;
+ };
+
+ if let Some(image) = obj.current_image() {
+ image.zoom_to_exact_center(value);
+ }
+
+ // Close popover after click
+ imp.zoom_menu_button.set_active(false);
+ }
+ ));
+
+ self.submit_zoom.connect_clicked(clone!(
+ #[weak]
+ obj,
+ move |_| {
+ if let Some(page) = obj.current_page() {
+ let mut value = obj.imp().zoom_value.text().to_string();
+ let locale_settings = util::locale_settings();
+
+ if let Some(thousands) = locale_settings.thousands_sep {
+ value = value.replace(&thousands, "");
+ }
+
+ if let Some(decimal) = locale_settings.decimal_point {
+ value = value.replace(&decimal, ".");
+ }
+
+ value = value.replace('%', "");
+ value = value.trim().to_string();
+
+ if let Ok(value) = value.parse::() {
+ page.image().zoom_to_exact(value / 100.);
+ }
+ }
+ }
+ ));
+
self.drag_source.set_exclusive(true);
self.drag_source.connect_prepare(glib::clone!(
@@ -678,16 +792,6 @@ impl LpImageView {
}
}
- pub fn on_fullscreen_changed(&self, is_fullscreened: bool) {
- let icon = if is_fullscreened {
- "view-restore-symbolic"
- } else {
- "view-fullscreen-symbolic"
- };
-
- self.imp().fullscreen_button.set_icon_name(icon);
- }
-
pub fn rotate_image(&self, angle: f64) {
if let Some(current_page) = self.current_page() {
current_page.image().rotate_by(angle);
@@ -770,4 +874,8 @@ impl LpImageView {
Ok(())
}
+
+ pub fn zoom_menu_button(&self) -> gtk::MenuButton {
+ self.imp().zoom_menu_button.clone()
+ }
}
diff --git a/src/widgets/image_view.ui b/src/widgets/image_view.ui
index 1a0d7a0..170fe5d 100644
--- a/src/widgets/image_view.ui
+++ b/src/widgets/image_view.ui
@@ -18,7 +18,10 @@
18
18
18
- 12
+ 6
+
@@ -37,8 +39,7 @@
win.next
Next Image
@@ -55,38 +56,203 @@
18
18
18
- 12
+
+
-
+
+
+ 6
+ zoom-toggle-symbolic
+
+ Toggle Zoom
-
- zoom-in-symbolic
- win.zoom-in-center
- Zoom In
+
-
-
-
- end
- win.toggle-fullscreen
- Toggle Fullscreen
-
+
+
+
+ submit_zoom
+
+
+
+ vertical
+ 6
+ 6
+ 6
+ 6
+ 6
+
+
+
+
+
+
+
+
+ start
+ 300 %
+
+
+
+
+ end
+ true
+ Ctrl+3
+
+
+
+
+
+
+
+
+
+
+
+
+
+ start
+ 200 %
+
+
+
+
+ end
+ true
+ Ctrl+2
+
+
+
+
+
+
+
+
+
+
+
+
+
+ start
+ 100 %
+
+
+
+
+ end
+ true
+ Ctrl+1
+
+
+
+
+
+
+
+
+
+
+
+ start
+ 66 %
+
+
+
+
+
+
+
+
+ start
+ 50 %
+
+
+
+
+
+
+
+
+ 12
+
+
+ True
+ number
+ 8
+ Custom Zoom Level
+
+
+
+
+ checkmark-symbolic
+ Zoom to Specified Level
+
+
+
+
+
+
+
+
+
@@ -100,4 +266,4 @@
-
\ 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());