Loupe/src/widgets/properties_view.rs
Julian Hofer cd8188a1bb Simplify async code
- Update Cargo.lock
- Use new gtk-core API
- Remove unneeded async
- Remove unneeded clones

Part-of: <https://gitlab.gnome.org/GNOME/loupe/-/merge_requests/310>
2023-11-22 09:12:36 +01:00

349 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2022-2023 Sophie Herold
// Copyright (c) 2022-2023 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
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::decoder::ImageDimensionDetails;
use crate::deps::*;
use crate::util;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::clone;
use gtk::CompositeTemplate;
use futures::future::{AbortHandle, Abortable};
use once_cell::sync::Lazy;
use std::cell::RefCell;
use crate::image_metadata::LpImageMetadata;
use crate::widgets::image::LpImage;
const FALLBACK: &str = "–";
mod imp {
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(file = "../../data/gtk/properties_view.ui")]
pub struct LpPropertiesView {
pub(super) file: RefCell<Option<glib::WeakRef<gio::File>>>,
pub(super) image: RefCell<Option<glib::WeakRef<LpImage>>>,
pub(super) metadata: RefCell<LpImageMetadata>,
pub(super) file_info: RefCell<Option<gio::FileInfo>>,
pub(super) info_handle: RefCell<Option<AbortHandle>>,
}
#[glib::object_subclass]
impl ObjectSubclass for LpPropertiesView {
const NAME: &'static str = "LpPropertiesView";
type Type = super::LpPropertiesView;
type ParentType = adw::Bin;
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("properties.open-location", None, move |properties, _, _| {
properties.open_location();
});
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
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) {
let obj = self.obj();
self.parent_constructed();
obj.action_set_enabled("properties.open-folder", false);
}
}
impl WidgetImpl for LpPropertiesView {}
impl BinImpl for LpPropertiesView {}
}
glib::wrapper! {
pub struct LpPropertiesView(ObjectSubclass<imp::LpPropertiesView>)
@extends gtk::Widget, adw::Bin,
@implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
#[gtk::template_callbacks]
impl LpPropertiesView {
fn set_file(&self, file: Option<&gio::File>) {
let imp = self.imp();
self.action_set_enabled(
"properties.open-folder",
file.map_or(false, |x| x.is_native()),
);
if let Some(file) = file {
if let Some(current_file) = self.file() {
if current_file.equal(file) {
return;
}
}
// Cancel the file info future. See `build_file_info()` for details.
if let Some(handle) = imp.info_handle.take() {
handle.abort();
}
self.build_file_info(file);
}
let weak = file.map(|x| x.downgrade());
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: Option<glib::Variant>) -> 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())
}
}