From 55811a2bb7b03182af966614eb770610f5f4b961 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Wed, 29 Nov 2023 01:49:14 +0100 Subject: [PATCH] image: Use macro for object properties --- src/decoder/tiling.rs | 2 +- src/widgets/image.rs | 419 ++++++++++++--------------------- src/widgets/image_page.rs | 17 +- src/widgets/properties_view.rs | 2 +- src/widgets/properties_view.ui | 2 +- src/window.rs | 2 +- 6 files changed, 166 insertions(+), 278 deletions(-) diff --git a/src/decoder/tiling.rs b/src/decoder/tiling.rs index 2bddc3f..8fab42e 100644 --- a/src/decoder/tiling.rs +++ b/src/decoder/tiling.rs @@ -20,7 +20,7 @@ //! //! It is not always feasible or desiareble to store the complete decoded //! image in the VRAM. [`TiledImage`] allows to compose the parts of -//! the image currently viewed, out of smaller [`Tiles`]. +//! the image currently viewed, out of smaller [`Tile`]s. //! //! This is especially important for SVGs where every the image has to //! be re-genearted for each zoom level. It can also be used to allow diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 1b8dba4..8434b6f 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -42,6 +42,7 @@ use futures::prelude::*; use once_cell::sync::Lazy; use std::cell::{Cell, OnceCell, RefCell}; +use std::marker::PhantomData; use std::sync::Arc; /// Default background color around images and behind transparent images @@ -102,34 +103,46 @@ const THUMBNAIL_SIZE: f32 = 128.; mod imp { use super::*; + use glib::Properties; - #[derive(Debug, Default)] + #[derive(Debug, Default, Properties)] + #[properties(wrapper_type = super::LpImage)] pub struct LpImage { + #[property(get, set = LpImage::set_file, explicit_notify)] pub(super) file: RefCell>, + #[property(get)] pub(super) is_deleted: Cell, + #[property(get)] pub(super) is_unsupported: Cell, + /// Set to true when image is ready for displaying + #[property(get)] + pub(super) is_loaded: Cell, + /// Set if an error has occurred, shown on error_page + #[property(get)] + pub(super) error: RefCell>, + #[property(name = "format-name", type = Option, get = Self::format_name)] + pub(super) format: RefCell>, + pub(super) background_color: RefCell>, + /// Track changes to this image pub(super) file_monitor: RefCell>, pub(super) frame_buffer: Arc>, pub(super) decoder: RefCell>>, - pub(super) format: RefCell>, - pub(super) background_color: RefCell>, - - /// Set to true when image is ready for displaying - pub(super) is_loaded: Cell, - /// Set if an error has occurred, shown on error_page - pub(super) error: RefCell>, /// Rotation final value (can differ from `rotation` during animation) pub(super) rotation_target: Cell, /// Rotated presentation of original image in degrees clockwise + #[property(get, set = Self::set_rotation, explicit_notify)] pub(super) rotation: Cell, // Animates the `rotation` property pub(super) rotation_animation: OnceCell, + /// Mirrored presentation of original image + #[property(get, set = Self::set_mirrored, explicit_notify)] pub(super) mirrored: Cell, /// Displayed zoom level + #[property(get, set = Self::set_zoom, explicit_notify)] pub(super) zoom: Cell, pub(super) zoom_animation: OnceCell, /// Targeted zoom level, might differ from `zoom` when animation is running @@ -141,20 +154,33 @@ mod imp { pub(super) zoom_vscrollbar_transition: Cell, /// Always fit image into window, causes `zoom` to change automatically + #[property(get, set)] pub(super) best_fit: Cell, /// Max zoom level is reached, stored to only send signals on change - pub(super) max_zoom: Cell, + #[property(get, set)] + pub(super) is_max_zoom: Cell, /// Horizontal scrolling - pub(super) hadjustment: RefCell>, + #[property(override_interface = gtk::Scrollable, get , set = Self::set_hadjustment)] + pub(super) hadjustment: RefCell, /// Vertical scrolling - pub(super) vadjustment: RefCell>, + #[property(override_interface = gtk::Scrollable, get , set = Self::set_vadjustment)] + pub(super) vadjustment: RefCell, + + #[property(override_interface = gtk::Scrollable, get = Self::scroll_policy, set = Self::set_ignore_scroll_policy)] + pub(super) _hscroll_policy: PhantomData, + #[property(override_interface = gtk::Scrollable, get = Self::scroll_policy, set = Self::set_ignore_scroll_policy)] + pub(super) _vscroll_policy: PhantomData, /// Currently EXIF data + #[property(name = "metadata", get)] pub(super) image_metadata: RefCell, /// Image dimension details for SVGs pub(super) dimension_details: RefCell, + #[property(get)] + _image_size_available: bool, + /// Current pointer position pub(super) pointer_position: Cell>, @@ -188,91 +214,15 @@ mod imp { impl ObjectImpl for LpImage { fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("file") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("is-deleted") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("is-loaded") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("is-unsupported") - .read_only() - .build(), - glib::ParamSpecString::builder("error").read_only().build(), - glib::ParamSpecObject::builder::("metadata") - .read_only() - .build(), - glib::ParamSpecString::builder("format-name") - .read_only() - .build(), - glib::ParamSpecDouble::builder("rotation") - .explicit_notify() - .build(), - glib::ParamSpecBoolean::builder("mirrored") - .explicit_notify() - .build(), - glib::ParamSpecDouble::builder("zoom") - .explicit_notify() - .build(), - glib::ParamSpecBoolean::builder("best-fit") - .explicit_notify() - .build(), - glib::ParamSpecBoolean::builder("is-max-zoom") - .read_only() - .build(), - glib::ParamSpecVariant::builder("image-size", glib::VariantTy::TUPLE) - .read_only() - .build(), - glib::ParamSpecOverride::for_interface::("hadjustment"), - glib::ParamSpecOverride::for_interface::("vadjustment"), - glib::ParamSpecOverride::for_interface::("hscroll-policy"), - glib::ParamSpecOverride::for_interface::("vscroll-policy"), - ] - }); - - PROPERTIES.as_ref() + Self::derived_properties() } - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - match pspec.name() { - "file" => obj.file().to_value(), - "is-deleted" => obj.is_deleted().to_value(), - "is-loaded" => obj.is_loaded().to_value(), - "is-unsupported" => obj.is_unsupported().to_value(), - "error" => obj.error().to_value(), - "metadata" => obj.metadata().to_value(), - "format-name" => obj.format_name().to_value(), - "rotation" => obj.rotation().to_value(), - "mirrored" => obj.mirrored().to_value(), - "zoom" => obj.zoom().to_value(), - "best-fit" => obj.is_best_fit().to_value(), - "is-max-zoom" => obj.is_max_zoom().to_value(), - "image-size" => obj.image_size().to_variant().to_value(), - // don't use getter functions here since they can return a fake adjustment - "hadjustment" => self.hadjustment.borrow().to_value(), - "vadjustment" => self.vadjustment.borrow().to_value(), - "hscroll-policy" | "vscroll-policy" => gtk::ScrollablePolicy::Minimum.to_value(), - name => unimplemented!("property {}", name), - } + fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + self.derived_set_property(id, value, pspec) } - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - match pspec.name() { - "rotation" => obj.set_rotation(value.get().unwrap()), - "mirrored" => obj.set_mirrored(value.get().unwrap()), - "zoom" => obj.set_zoom(value.get().unwrap()), - "best-fit" => obj.set_best_fit(value.get().unwrap()), - "hadjustment" => obj.set_hadjustment(value.get().unwrap()), - "vadjustment" => obj.set_vadjustment(value.get().unwrap()), - "hscroll-policy" | "vscroll-policy" => (), - _ => unimplemented!(), - } + fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { + self.derived_property(id, pspec) } fn constructed(&self) { @@ -559,6 +509,102 @@ mod imp { zoom_gesture.group_with(&rotation_gesture); } + + fn set_file(&self, file: &gio::File) { + let obj = self.obj(); + + if obj.file().map_or(false, |x| file.equal(&x)) { + return; + } + + self.file.replace(Some(file.clone())); + + let monitor = + file.monitor_file(gio::FileMonitorFlags::WATCH_MOVES, gio::Cancellable::NONE); + if let Ok(m) = &monitor { + m.connect_changed(glib::clone!(@weak obj => move |_, file_a, file_b, event| { + obj.file_changed(event, file_a, file_b); + })); + } + + self.file_monitor.replace(monitor.ok()); + + obj.notify_file(); + } + + fn set_mirrored(&self, mirrored: bool) { + let obj = self.obj(); + + if mirrored == obj.mirrored() { + return; + } + + self.mirrored.set(mirrored); + obj.notify_mirrored(); + obj.queue_draw(); + } + + /// Set zoom level aiming for cursor position or center if not available + /// + /// Aiming means that the scrollbars are adjust such that the same point + /// of the image remains under the cursor after changing the zoom level. + fn set_zoom(&self, zoom: f64) { + self.obj() + .set_zoom_aiming(zoom, self.pointer_position.get()) + } + + fn set_hadjustment(&self, hadjustment: Option) { + let obj = self.obj(); + + let adjustment = hadjustment.unwrap_or_default(); + + adjustment.connect_value_changed(glib::clone!(@weak obj => move |_| { + obj.request_tiles(); + obj.queue_draw(); + })); + + self.hadjustment.replace(adjustment); + obj.configure_adjustments(); + } + + fn set_vadjustment(&self, vadjustment: Option) { + let obj = self.obj(); + + let adjustment = vadjustment.unwrap_or_default(); + + adjustment.connect_value_changed(glib::clone!(@weak obj => move |_| { + obj.request_tiles(); + obj.queue_draw(); + })); + + self.vadjustment.replace(adjustment); + obj.configure_adjustments(); + } + + pub fn set_rotation(&self, rotation: f64) { + let obj = self.obj(); + + if rotation == obj.rotation() { + return; + } + + self.rotation.set(rotation); + obj.notify_rotation(); + obj.queue_draw(); + } + + pub fn set_ignore_scroll_policy(&self, scroll_policy: gtk::ScrollablePolicy) { + log::error!("Ignored setting new scroll policy {scroll_policy:?}"); + } + + pub fn scroll_policy(&self) -> gtk::ScrollablePolicy { + gtk::ScrollablePolicy::Minimum + } + + /// Image format displayable name + pub fn format_name(&self) -> Option { + self.format.borrow().as_ref().map(|x| x.to_string()) + } } impl WidgetImpl for LpImage { @@ -640,14 +686,13 @@ mod imp { ); // Apply the scrolling position to the image - if let Some(adj) = self.hadjustment.borrow().as_ref() { - let x = -(adj.value() - (adj.upper() - display_width) / 2.); - snapshot.translate(&graphene::Point::new(widget.round_f64(x) as f32, 0.)); - } - if let Some(adj) = self.vadjustment.borrow().as_ref() { - let y = -(adj.value() - (adj.upper() - display_height) / 2.); - snapshot.translate(&graphene::Point::new(0., widget.round_f64(y) as f32)); - } + let hadj: gtk::Adjustment = widget.hadjustment(); + let x = -(hadj.value() - (hadj.upper() - display_width) / 2.); + snapshot.translate(&graphene::Point::new(widget.round_f64(x) as f32, 0.)); + + let vadj = widget.vadjustment(); + let y = -(vadj.value() - (vadj.upper() - display_height) / 2.); + snapshot.translate(&graphene::Point::new(0., widget.round_f64(y) as f32)); // Centering in widget when no scrolling (black bars around image) let x = widget.round_f64(f64::max((widget_width - display_width) / 2.0, 0.)); @@ -797,14 +842,14 @@ impl LpImage { DecoderUpdate::Metadata(metadata) => { log::debug!("Received metadata"); imp.image_metadata.replace(LpImageMetadata::from(metadata)); - self.notify("metadata"); + self.notify_metadata(); self.reset_rotation(); } DecoderUpdate::Dimensions(dimension_details) => { log::debug!("Received dimensions: {:?}", self.original_dimensions()); self.imp().dimension_details.replace(dimension_details); - self.notify("image-size"); + self.notify_image_size_available(); self.configure_best_fit(); self.request_tiles(); } @@ -832,7 +877,7 @@ impl LpImage { } DecoderUpdate::Format(format) => { imp.format.replace(Some(format)); - self.notify("format-name"); + self.notify_format_name(); } DecoderUpdate::Animated => { let callback_id = self @@ -871,18 +916,6 @@ impl LpImage { glib::ControlFlow::Continue } - pub fn is_loaded(&self) -> bool { - self.imp().is_loaded.get() - } - - pub fn is_deleted(&self) -> bool { - self.imp().is_deleted.get() - } - - pub fn is_unsupported(&self) -> bool { - self.imp().is_unsupported.get() - } - /// Zoom level that makes the image fit in widget /// /// During image rotation the image does not actually fit into widget. @@ -929,29 +962,6 @@ impl LpImage { } } - pub fn file(&self) -> Option { - self.imp().file.borrow().clone() - } - - pub(super) fn set_file(&self, file: &gio::File) { - let imp = self.imp(); - - imp.file.replace(Some(file.clone())); - - let monitor = file.monitor_file(gio::FileMonitorFlags::WATCH_MOVES, gio::Cancellable::NONE); - if let Ok(m) = &monitor { - m.connect_changed( - glib::clone!(@weak self as obj => move |_, file_a, file_b, event| { - obj.file_changed(event, file_a, file_b); - }), - ); - } - - imp.file_monitor.replace(monitor.ok()); - - self.notify("file"); - } - /// File changed on drive fn file_changed( &self, @@ -990,7 +1000,7 @@ impl LpImage { | gio::FileMonitorEvent::Unmounted => { log::debug!("File no longer available: {event:?} {}", file_a.uri()); self.imp().is_deleted.set(true); - self.notify("is-deleted"); + self.notify_is_deleted(); } _ => {} } @@ -1055,34 +1065,6 @@ impl LpImage { Some(renderer.render_texture(&node, None)) } - fn mirrored(&self) -> bool { - self.imp().mirrored.get() - } - - fn set_mirrored(&self, mirrored: bool) { - if mirrored == self.mirrored() { - return; - } - - self.imp().mirrored.set(mirrored); - self.notify("mirrored"); - self.queue_draw(); - } - - pub fn rotation(&self) -> f64 { - self.imp().rotation.get() - } - - pub fn set_rotation(&self, rotation: f64) { - if rotation == self.rotation() { - return; - } - - self.imp().rotation.set(rotation); - self.notify("rotation"); - self.queue_draw(); - } - /// Set rotation and mirroring to the state would have after loading pub fn reset_rotation(&self) { let orientation = self.metadata().orientation(); @@ -1125,41 +1107,6 @@ impl LpImage { self.imp().best_fit.get() } - pub fn set_best_fit(&self, best_fit: bool) { - if best_fit == self.is_best_fit() { - return; - } - - self.imp().best_fit.set(best_fit); - self.notify("best-fit"); - } - - /// Current zoom level - pub fn zoom(&self) -> f64 { - self.imp().zoom.get() - } - - /// Set zoom level aiming for cursor position or center if not available - /// - /// Aiming means that the scrollbars are adjust such that the same point - /// of the image remains under the cursor after changing the zoom level. - fn set_zoom(&self, zoom: f64) { - self.set_zoom_aiming(zoom, self.imp().pointer_position.get()) - } - - pub fn is_max_zoom(&self) -> bool { - self.imp().max_zoom.get() - } - - fn set_max_zoom(&self, value: bool) { - if self.is_max_zoom() == value { - return; - } - - self.imp().max_zoom.set(value); - self.notify("is-max-zoom"); - } - fn applicable_zoom(&self) -> f64 { decoder::tiling::zoom_normalize(self.zoom()) / self.scale_factor() as f64 } @@ -1244,7 +1191,7 @@ impl LpImage { .set_value(self.vadjustment_corrected_for_zoom(zoom_ratio, y)); } - self.notify("zoom"); + self.notify_zoom(); self.queue_draw(); } @@ -1381,9 +1328,9 @@ impl LpImage { let max_zoom = self.max_zoom(); if zoom >= max_zoom { zoom = max_zoom; - self.set_max_zoom(true); + self.set_is_max_zoom(true); } else { - self.set_max_zoom(false); + self.set_is_max_zoom(false); } let extended_best_fit_threshold = if snap_best_fit { @@ -1516,48 +1463,6 @@ impl LpImage { adjustment.set_value(value); } - fn hadjustment(&self) -> gtk::Adjustment { - if let Some(adj) = self.imp().hadjustment.borrow().as_ref() { - adj.clone() - } else { - log::trace!("Hadjustment not set yet: Using fake object"); - gtk::Adjustment::default() - } - } - - fn set_hadjustment(&self, adjustment: Option) { - if let Some(adj) = &adjustment { - adj.connect_value_changed(glib::clone!(@weak self as obj => move |_| { - obj.request_tiles(); - obj.queue_draw(); - })); - } - - self.imp().hadjustment.replace(adjustment); - self.configure_adjustments(); - } - - fn vadjustment(&self) -> gtk::Adjustment { - if let Some(adj) = self.imp().vadjustment.borrow().as_ref() { - adj.clone() - } else { - log::trace!("Vadjustment not set yet: Using fake object"); - gtk::Adjustment::default() - } - } - - fn set_vadjustment(&self, adjustment: Option) { - if let Some(adj) = &adjustment { - adj.connect_value_changed(glib::clone!(@weak self as obj => move |_| { - obj.request_tiles(); - obj.queue_draw(); - })); - } - - self.imp().vadjustment.replace(adjustment); - self.configure_adjustments(); - } - /// Configure scrollbars for current situation fn configure_adjustments(&self) { let hadjustment = self.hadjustment(); @@ -1679,10 +1584,6 @@ impl LpImage { )); } - pub fn metadata(&self) -> LpImageMetadata { - self.imp().image_metadata.borrow().clone() - } - pub fn dimension_details(&self) -> decoder::ImageDimensionDetails { self.imp().dimension_details.borrow().clone() } @@ -1699,22 +1600,12 @@ impl LpImage { self.imp().format.borrow().clone() } - /// Image format displayable name - pub fn format_name(&self) -> Option { - self.imp().format.borrow().as_ref().map(|x| x.to_string()) - } - - /// Returns decoding error if one occured - pub fn error(&self) -> Option { - self.imp().error.borrow().clone() - } - fn set_error(&self, err: Option) { log::debug!("Decoding error: {err:?}"); self.imp() .error .replace(err.as_ref().map(|x| x.to_string())); - self.notify("error"); + self.notify_error(); if err.is_some() { self.set_loaded(false); @@ -1724,7 +1615,7 @@ impl LpImage { fn set_unsupported(&self, is_unsupported: bool) { if self.is_unsupported() != is_unsupported { self.imp().is_unsupported.set(true); - self.notify("is-unsupported"); + self.notify_is_unsupported(); if is_unsupported { self.set_loaded(false); @@ -1735,7 +1626,7 @@ impl LpImage { fn set_loaded(&self, is_loaded: bool) { if self.is_loaded() != is_loaded { self.imp().is_loaded.set(is_loaded); - self.notify("is-loaded"); + self.notify_is_loaded(); if is_loaded { self.set_error(None); diff --git a/src/widgets/image_page.rs b/src/widgets/image_page.rs index 394e204..d01e5cb 100644 --- a/src/widgets/image_page.rs +++ b/src/widgets/image_page.rs @@ -118,26 +118,23 @@ mod imp { imp.spinner.set_visible(spinner_visible); })); - obj.image().connect_notify_local( - Some("is-loaded"), - clone!(@weak obj => move |_,_| { + obj.image() + .connect_is_loaded_notify(clone!(@weak obj => move |_| { if obj.image().is_loaded() { log::debug!("Showing image"); obj.imp() .stack .set_visible_child(&*obj.imp().image_stack_page); } - }), - ); + })); - obj.image().connect_notify_local( - Some("error"), - clone!(@weak obj => move |_,_| { + obj.image() + .connect_error_notify(clone!(@weak obj => move |_| { if obj.image().error().is_some() { obj.imp().stack.set_visible_child(&*obj.imp().error_page); } - }), - ); + })); + // Do not waste CPU on spinner if it is not visible self.spinner.connect_map(|s| s.start()); self.spinner.connect_unmap(|s| s.stop()); diff --git a/src/widgets/properties_view.rs b/src/widgets/properties_view.rs index ba0c95f..72353ba 100644 --- a/src/widgets/properties_view.rs +++ b/src/widgets/properties_view.rs @@ -301,7 +301,7 @@ impl LpPropertiesView { } #[template_callback] - fn image_size(&self, _image_size: Option) -> String { + fn image_size(&self, _image_size: bool) -> String { if let Some(image) = self.image() { match image.dimension_details() { ImageDimensionDetails::Svg(string) => string, diff --git a/src/widgets/properties_view.ui b/src/widgets/properties_view.ui index eecd40d..1657388 100644 --- a/src/widgets/properties_view.ui +++ b/src/widgets/properties_view.ui @@ -59,7 +59,7 @@ True - + LpPropertiesView diff --git a/src/window.rs b/src/window.rs index 9e02540..9b4491a 100644 --- a/src/window.rs +++ b/src/window.rs @@ -319,7 +319,7 @@ mod imp { ); current_image_signals.connect_closure( - "notify::image-size", + "notify::image-size-available", false, glib::closure_local!(@watch win => move |_: &LpImage, _: &glib::ParamSpec| { win.image_size_ready();