code: Replace many uses of GtkExpressions

Closes #276, #100
This commit is contained in:
Sophie Herold 2023-12-02 22:07:33 +01:00
parent 50450ad359
commit c36e7f4eb6
6 changed files with 279 additions and 458 deletions

View file

@ -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<Option<glib::WeakRef<gio::File>>>,
pub(super) image: RefCell<Option<glib::WeakRef<LpImage>>>,
#[property(nullable, set = Self::set_image, get)]
image: RefCell<Option<LpImage>>,
image_signals: OnceCell<glib::SignalGroup>,
pub(super) metadata: RefCell<LpImageMetadata>,
#[template_child]
folder: TemplateChild<adw::ActionRow>,
pub(super) file_info: RefCell<Option<gio::FileInfo>>,
pub(super) info_handle: RefCell<Option<AbortHandle>>,
#[template_child]
image_size: TemplateChild<adw::ActionRow>,
#[template_child]
image_format: TemplateChild<adw::ActionRow>,
#[template_child]
file_size: TemplateChild<adw::ActionRow>,
#[template_child]
file_created: TemplateChild<adw::ActionRow>,
#[template_child]
file_modified: TemplateChild<adw::ActionRow>,
#[template_child]
details: TemplateChild<adw::PreferencesGroup>,
#[template_child]
location: TemplateChild<adw::ActionRow>,
#[template_child]
originally_created: TemplateChild<adw::ActionRow>,
#[template_child]
aperture: TemplateChild<adw::ActionRow>,
#[template_child]
exposure: TemplateChild<adw::ActionRow>,
#[template_child]
iso: TemplateChild<adw::ActionRow>,
#[template_child]
focal_length: TemplateChild<adw::ActionRow>,
#[template_child]
maker_model: TemplateChild<adw::ActionRow>,
}
#[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<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<gio::File>("file")
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<gio::FileInfo>("file-info")
.read_only()
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<LpImage>("image")
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<LpImageMetadata>("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<LpImage>) {
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::<LpImage>)
}
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<impl AsRef<str>>) -> 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<String> {
self.file()
.and_then(|f| f.parent())
.and_then(|p| util::get_file_display_name(&p))
}
fn image_size(&self) -> Option<String> {
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<gio::File> {
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::<gtk::Window>();
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<gio::File> {
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<LpImage> {
let imp = self.imp();
imp.image.borrow().as_ref().and_then(|w| w.upgrade())
}
fn file_info(&self) -> Option<gio::FileInfo> {
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::<gtk::Window>();
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<gio::File>) -> 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<gio::FileInfo>) -> 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<gio::FileInfo>) -> 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<gio::FileInfo>) -> 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<String>) -> bool {
content.map_or(false, |x| !x.is_empty())
}
*/
}