From c36e7f4eb6472bfa1bd986304927f15b0253d2c1 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Sat, 2 Dec 2023 22:07:33 +0100 Subject: [PATCH] code: Replace many uses of GtkExpressions Closes #276, #100 --- src/image_metadata/obj.rs | 4 + src/widgets/image.rs | 2 +- src/widgets/properties_view.rs | 445 +++++++++++++++------------------ src/widgets/properties_view.ui | 160 ++---------- src/window.rs | 99 +++++--- src/window.ui | 27 -- 6 files changed, 279 insertions(+), 458 deletions(-) diff --git a/src/image_metadata/obj.rs b/src/image_metadata/obj.rs index 8d52f12..16e7b06 100644 --- a/src/image_metadata/obj.rs +++ b/src/image_metadata/obj.rs @@ -46,6 +46,10 @@ impl LpImageMetadata { pub fn gps_location(&self) -> Option { self.imp().metadata.borrow().gps_location() } + + pub fn src(&self) -> std::cell::Ref { + self.imp().metadata.borrow() + } } impl From for LpImageMetadata { diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 540cf4e..9caab17 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -177,7 +177,7 @@ mod imp { pub(super) _vscroll_policy: PhantomData, /// Currently EXIF data - #[property(name = "metadata", get)] + #[property(name = "metadata", type=LpImageMetadata, get)] pub(super) image_metadata: RefCell, /// Image dimension details for SVGs pub(super) dimension_details: RefCell, diff --git a/src/widgets/properties_view.rs b/src/widgets/properties_view.rs index 7473faa..510d458 100644 --- a/src/widgets/properties_view.rs +++ b/src/widgets/properties_view.rs @@ -1,6 +1,5 @@ // Copyright (c) 2022-2023 Sophie Herold -// Copyright (c) 2022-2023 Christopher Davis -// Copyright (c) 2023 Julian Hofer +// Copyright (c) 2022 Christopher Davis // // 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 @@ -23,14 +22,11 @@ use crate::util; use adw::prelude::*; use adw::subclass::prelude::*; -use glib::clone; +use glib::Properties; use gtk::CompositeTemplate; -use futures::future::{AbortHandle, Abortable}; -use once_cell::sync::Lazy; -use std::cell::RefCell; +use std::cell::{OnceCell, RefCell}; -use crate::image_metadata::LpImageMetadata; use crate::widgets::image::LpImage; const FALLBACK: &str = "–"; @@ -38,16 +34,45 @@ const FALLBACK: &str = "–"; mod imp { use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, Properties)] + #[properties(wrapper_type = super::LpPropertiesView)] #[template(file = "properties_view.ui")] pub struct LpPropertiesView { - pub(super) file: RefCell>>, - pub(super) image: RefCell>>, + #[property(nullable, set = Self::set_image, get)] + image: RefCell>, + image_signals: OnceCell, - pub(super) metadata: RefCell, + #[template_child] + folder: TemplateChild, - pub(super) file_info: RefCell>, - pub(super) info_handle: RefCell>, + #[template_child] + image_size: TemplateChild, + #[template_child] + image_format: TemplateChild, + #[template_child] + file_size: TemplateChild, + + #[template_child] + file_created: TemplateChild, + #[template_child] + file_modified: TemplateChild, + + #[template_child] + details: TemplateChild, + #[template_child] + location: TemplateChild, + #[template_child] + originally_created: TemplateChild, + #[template_child] + aperture: TemplateChild, + #[template_child] + exposure: TemplateChild, + #[template_child] + iso: TemplateChild, + #[template_child] + focal_length: TemplateChild, + #[template_child] + maker_model: TemplateChild, } #[glib::object_subclass] @@ -58,18 +83,13 @@ mod imp { fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); - Self::Type::bind_template_callbacks(klass); - klass.install_action_async( - "properties.open-folder", - None, - |properties, _, _| async move { - let _ = properties.open_directory().await; - }, - ); + klass.install_action_async("properties.open-folder", None, |obj, _, _| async move { + obj.imp().open_directory().await; + }); - klass.install_action("properties.open-location", None, move |properties, _, _| { - properties.open_location(); + klass.install_action("properties.open-location", None, move |obj, _, _| { + obj.imp().open_location(); }); } @@ -79,63 +99,165 @@ mod imp { } impl ObjectImpl for LpPropertiesView { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("file") - .explicit_notify() - .build(), - glib::ParamSpecObject::builder::("file-info") - .read_only() - .explicit_notify() - .build(), - glib::ParamSpecObject::builder::("image") - .explicit_notify() - .build(), - glib::ParamSpecObject::builder::("metadata") - .explicit_notify() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - let prop_name = pspec.name(); - - match prop_name { - "file" => obj.file().to_value(), - "image" => obj.image().to_value(), - "metadata" => self.metadata.borrow().to_value(), - "file-info" => obj.file_info().to_value(), - _ => unimplemented!("Failed to get property \"{prop_name}\""), - } - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - let prop_name = pspec.name(); - - match prop_name { - "image" => obj.set_image(value.get().ok().as_ref()), - "file" => obj.set_file(value.get().ok().as_ref()), - "metadata" => obj.set_metadata(value.get().unwrap()), - _ => unimplemented!(), - } - } - fn constructed(&self) { + self.parent_constructed(); + let obj = self.obj(); - self.parent_constructed(); - obj.action_set_enabled("properties.open-folder", false); + let image_signals = self.image_signals(); + + image_signals.connect_notify_local( + Some("image-size-available"), + glib::clone!(@weak obj => move |_,_| obj.imp().update()), + ); + image_signals.connect_notify_local( + Some("metadata"), + glib::clone!(@weak obj => move |_,_| obj.imp().update()), + ); + + self.update(); + } + + fn properties() -> &'static [glib::ParamSpec] { + Self::derived_properties() + } + + fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + self.derived_set_property(id, value, pspec) + } + + fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { + self.derived_property(id, pspec) } } impl WidgetImpl for LpPropertiesView {} impl BinImpl for LpPropertiesView {} + + impl LpPropertiesView { + fn set_image(&self, image: Option) { + self.image.replace(image.clone()); + self.image_signals().set_target(image.as_ref()); + self.update(); + } + + fn image_signals(&self) -> &glib::SignalGroup { + self.image_signals + .get_or_init(glib::SignalGroup::new::) + } + + fn update(&self) { + let obj = self.obj(); + + let Some(image) = obj.image() else { + return; + }; + let Some(file) = image.file() else { + return; + }; + + // Folder + Self::update_row(&self.folder, self.folder_name()); + obj.action_set_enabled("properties.open-folder", file.path().is_some()); + + // Image info + Self::update_row(&self.image_size, self.image_size()); + Self::update_row(&self.image_format, image.format().map(|x| x.to_string())); + //Self::update_row(&self.file_size, self.image_size()); + + // Sizes + + // Details + let metadata = image.metadata(); + let meta = metadata.src(); + let has_details = [ + Self::update_row(&self.location, meta.gps_location().map(|x| x.display())), + Self::update_row(&self.originally_created, meta.originally_created()), + Self::update_row(&self.aperture, meta.f_number()), + Self::update_row(&self.exposure, meta.exposure_time()), + Self::update_row(&self.iso, meta.iso()), + Self::update_row(&self.focal_length, meta.focal_length()), + Self::update_row(&self.maker_model, meta.maker_model()), + ] + .iter() + .any(|x| *x); + self.details.set_visible(has_details); + } + + fn update_row(row: &adw::ActionRow, value: Option>) -> bool { + if let Some(value) = value { + row.set_subtitle(value.as_ref()); + row.set_visible(true); + true + } else { + row.set_subtitle(FALLBACK); + row.set_visible(false); + false + } + } + + fn folder_name(&self) -> Option { + self.file() + .and_then(|f| f.parent()) + .and_then(|p| util::get_file_display_name(&p)) + } + + fn image_size(&self) -> Option { + let obj = self.obj(); + + if let Some(image) = obj.image() { + match image.dimension_details() { + ImageDimensionDetails::Svg(string) => Some(string), + _ => { + let (width, height) = image.image_size(); + if width > 0 && height > 0 { + return Some(format!("{width}\u{202F}\u{D7}\u{202F}{height}")); + } else { + None + } + } + } + } else { + None + } + } + + fn file(&self) -> Option { + self.image.borrow().as_ref().and_then(|x| x.file()) + } + + async fn open_directory(&self) { + let obj = self.obj(); + + let launcher = gtk::FileLauncher::new(self.file().as_ref()); + let win = obj.native().and_downcast::(); + if let Err(e) = launcher.open_containing_folder_future(win.as_ref()).await { + log::error!("Could not open parent directory: {e}"); + }; + } + + /// Open GPS location in apps like Maps via `geo:` URI + fn open_location(&self) { + let obj = self.obj(); + + if let Some(uri) = obj + .image() + .and_then(|x| x.metadata().gps_location()) + .map(|x| x.geo_uri()) + { + gio::AppInfo::launch_default_for_uri_async( + &uri, + gio::AppLaunchContext::NONE, + gio::Cancellable::NONE, + |result| { + if let Err(err) = result { + log::error!("Failed to show geo URI: {err}") + } + }, + ); + } + } + } } glib::wrapper! { @@ -144,9 +266,8 @@ glib::wrapper! { @implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable; } -#[gtk::template_callbacks] impl LpPropertiesView { - fn set_file(&self, file: Option<&gio::File>) { + /* fn set_file(&self, file: Option<&gio::File>) { let imp = self.imp(); self.action_set_enabled( @@ -173,179 +294,5 @@ impl LpPropertiesView { imp.file.replace(weak); self.notify("file"); } - - fn file(&self) -> Option { - let imp = self.imp(); - imp.file.borrow().as_ref().and_then(|w| w.upgrade()) - } - - fn set_image(&self, image: Option<&LpImage>) { - let imp = self.imp(); - - imp.image.replace(image.as_ref().map(|x| x.downgrade())); - self.notify("image"); - } - - fn image(&self) -> Option { - let imp = self.imp(); - imp.image.borrow().as_ref().and_then(|w| w.upgrade()) - } - - fn file_info(&self) -> Option { - let imp = self.imp(); - imp.file_info.borrow().clone() - } - - fn build_file_info(&self, file: &gio::File) { - let imp = self.imp(); - - // We need to be able to cancel this future so that - // changing files before the metadata loads does not cause - // old metadata to be loaded. In order to make a cancellable - // future, we use `AbortHandle` and `Abortable` from the `futures` - // crate. - // - // `handle` is the `AbortHandle`, which we need to store in order - // to abort the future. `reg` is the `AbortRegistration` that we - // give to `Abortable::new()` to tie our future to the `AbortHandle`. - // - // Here we build the future: - let (handle, reg) = AbortHandle::new_pair(); - let fut = Abortable::new( - clone!(@weak self as view, @strong file => async move { - let info_result = util::query_attributes_future( - &file, - vec![ - gio::FILE_ATTRIBUTE_STANDARD_SIZE, - gio::FILE_ATTRIBUTE_TIME_CREATED, - gio::FILE_ATTRIBUTE_TIME_MODIFIED, - ], - ) - .await; - - match info_result { - Ok(info) => view.on_info_loaded(info), - Err(err) => log::warn!("Failed to load file info: {err}"), - } - }), - reg, - ); - - // ...store the handle: - imp.info_handle.replace(Some(handle)); - - // ...then spawn the future. - glib::spawn_future_local(async { - let _ = fut.await; - }); - } - - fn set_metadata(&self, metadata: LpImageMetadata) { - let imp = self.imp(); - imp.metadata.replace(metadata); - self.notify("metadata"); - } - - // This is where we handle the results of the future. - // This could technically be part of the future, but we keep - // them separate for a bit of cleanliness. - fn on_info_loaded(&self, info: gio::FileInfo) { - let imp = self.imp(); - imp.file_info.replace(Some(info)); - self.notify("file-info"); - } - - async fn open_directory(&self) -> anyhow::Result<()> { - let launcher = gtk::FileLauncher::new(self.file().as_ref()); - let win = self.native().and_downcast::(); - if let Err(e) = launcher.open_containing_folder_future(win.as_ref()).await { - log::error!("Could not open parent directory: {e}"); - }; - - Ok(()) - } - - /// Open GPS location in apps like Maps via `geo:` URI - fn open_location(&self) { - if let Some(uri) = self - .image() - .and_then(|x| x.metadata().gps_location()) - .map(|x| x.geo_uri()) - { - gio::AppInfo::launch_default_for_uri_async( - &uri, - gio::AppLaunchContext::NONE, - gio::Cancellable::NONE, - |result| { - if let Err(err) = result { - log::error!("Failed to show geo URI: {err}") - } - }, - ); - } - } - - // In the LpPropertiesView UI file we define a few `gtk::Expression`s - // that are closures. These closures take either the current `gio::File` - // or the current file's associated `gio::FileInfo` and process them - // accordingly. - // - // In this function we chain `Option`s with `and_then()` in order - // to handle optional results with a fallback, without needing to - // have multiple `match` or `if let` branches, and without needing - // to unwrap. - #[template_callback] - fn folder_name(&self, file: Option) -> String { - file.and_then(|f| f.parent()) // If the file exists, get the parent - .and_then(|p| util::get_file_display_name(&p)) // if that exists, the display name - .unwrap_or_else(|| FALLBACK.to_owned()) // and if we get nothing, - // use `FALLBACK` - } - - #[template_callback] - fn image_size(&self, _image_size: bool) -> String { - if let Some(image) = self.image() { - match image.dimension_details() { - ImageDimensionDetails::Svg(string) => string, - _ => { - let (width, height) = image.image_size(); - if width > 0 && height > 0 { - format!("{width}\u{202F}\u{D7}\u{202F}{height}") - } else { - FALLBACK.to_owned() - } - } - } - } else { - FALLBACK.to_owned() - } - } - - #[template_callback] - fn file_size(&self, info: Option) -> String { - info.map(|info| { - let size = info.size() as u64; - glib::format_size(size).to_string() - }) - .unwrap_or_else(|| FALLBACK.to_owned()) - } - - #[template_callback] - fn created_date(&self, info: Option) -> String { - info.and_then(|i| i.creation_date_time()) - .and_then(|t| util::datetime_fmt(&t)) - .unwrap_or_else(|| FALLBACK.to_owned()) - } - - #[template_callback] - fn modified_date(&self, info: Option) -> String { - info.and_then(|i| i.modification_date_time()) - .and_then(|t| util::datetime_fmt(&t)) - .unwrap_or_else(|| FALLBACK.to_owned()) - } - - #[template_callback] - fn has_content(&self, content: Option) -> bool { - content.map_or(false, |x| !x.is_empty()) - } + */ } diff --git a/src/widgets/properties_view.ui b/src/widgets/properties_view.ui index 1657388..de7499d 100644 --- a/src/widgets/properties_view.ui +++ b/src/widgets/properties_view.ui @@ -2,16 +2,6 @@