From ba4661031f284f66a2fbb5eface208f95ccf5298 Mon Sep 17 00:00:00 2001 From: Christopher Davis Date: Tue, 26 Jul 2022 11:31:11 -0400 Subject: [PATCH] Implement Properties Implement a properties sidebar. In order to prevent the UI from freezing, we use futures to load and cancel loading file information. We also make use of `gdk_pixbuf` and `svg_metadata` in order to get the file's metadata before the file fully loads, meaning there's little to no delay between changing files and seeing the metadata for large images. The properties widget uses expressions and properties to avoid glue code for setting each label, allowing us to have a clean separation between UI and data. --- Cargo.lock | 72 ++++-- Cargo.toml | 7 + data/gtk/properties_view.ui | 110 ++++++++++ data/gtk/window.ui | 73 +++++-- data/loupe.gresource.xml | 2 + data/style-hc.css | 3 + data/style.css | 12 +- src/meson.build | 1 + src/util.rs | 19 ++ src/widgets/mod.rs | 2 + src/widgets/properties_view.rs | 388 +++++++++++++++++++++++++++++++++ src/window.rs | 20 +- 12 files changed, 664 insertions(+), 45 deletions(-) create mode 100644 data/gtk/properties_view.ui create mode 100644 data/style-hc.css create mode 100644 src/widgets/properties_view.rs diff --git a/Cargo.lock b/Cargo.lock index e0cc8b6..883e891 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,6 +232,12 @@ dependencies = [ "syn", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "easy-parallel" version = "3.1.0" @@ -299,9 +305,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", @@ -314,9 +320,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -324,15 +330,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", @@ -341,9 +347,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-lite" @@ -362,9 +368,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ "proc-macro2", "quote", @@ -373,21 +379,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-channel", "futures-core", @@ -863,14 +869,17 @@ version = "0.1.0" dependencies = [ "anyhow", "ashpd", + "futures", "futures-channel", "gettext-rs", + "gio", "gtk-macros", "gtk4", "libadwaita", "log", "once_cell", "pretty_env_logger", + "svg_metadata", ] [[package]] @@ -1157,6 +1166,15 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "roxmltree" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf7d7b1ea646d380d0e8153158063a6da7efe30ddbf3184042848e3f8a6f671" +dependencies = [ + "xmlparser", +] + [[package]] name = "rustc_version" version = "0.3.3" @@ -1249,6 +1267,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "svg_metadata" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39f36262efe61096a17aa42140b0e44b9189806c3fa71ab3cff123429938eb0" +dependencies = [ + "doc-comment", + "lazy_static", + "regex", + "roxmltree", +] + [[package]] name = "syn" version = "1.0.83" @@ -1399,6 +1429,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "xmlparser" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8" + [[package]] name = "zbus" version = "2.1.1" diff --git a/Cargo.toml b/Cargo.toml index f0a26a2..60facf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ log = "0.4.11" once_cell = "1.9.0" pretty_env_logger = "0.4.0" futures-channel = "0.3.19" +futures = "0.3.21" +svg_metadata = "0.4.2" [dependencies.ashpd] version = "0.2.2" @@ -20,6 +22,11 @@ package = "gtk4" version = "0.4.4" features = ["v4_6"] +[dependencies.gio] +package = "gio" +version = "0.15.2" +features = ["v2_70"] + [dependencies.adw] package = "libadwaita" version = "0.1.0" diff --git a/data/gtk/properties_view.ui b/data/gtk/properties_view.ui new file mode 100644 index 0000000..0dec98b --- /dev/null +++ b/data/gtk/properties_view.ui @@ -0,0 +1,110 @@ + + + + diff --git a/data/gtk/window.ui b/data/gtk/window.ui index b7ba5de..a1eeb1c 100644 --- a/data/gtk/window.ui +++ b/data/gtk/window.ui @@ -18,6 +18,12 @@ False true false + + + LpWindow + properties_flap + + @@ -35,39 +41,60 @@ Primary Menu + + + help-about-symbolic + + - - status_page - True - - - Image Viewer - Click "Open File…" or drag an image here. + + end + never + True + + + + status_page + True - - center - Open File… - win.open - + + Image Viewer + Click "Open File…" or drag an image here. + + + center + Open File… + win.open + + + + + + + + + + + copy - - - - - - - copy + + + + + + + - + diff --git a/data/loupe.gresource.xml b/data/loupe.gresource.xml index e321c41..e481948 100644 --- a/data/loupe.gresource.xml +++ b/data/loupe.gresource.xml @@ -5,6 +5,8 @@ gtk/window.ui gtk/image_page.ui gtk/image_view.ui + gtk/properties_view.ui style.css + style-hc.css diff --git a/data/style-hc.css b/data/style-hc.css new file mode 100644 index 0000000..5376146 --- /dev/null +++ b/data/style-hc.css @@ -0,0 +1,3 @@ +row.property label.title { + opacity: 0.9; +} diff --git a/data/style.css b/data/style.css index 702c76d..e9db6f5 100644 --- a/data/style.css +++ b/data/style.css @@ -36,4 +36,14 @@ flap.fullscreen-widget dimming, shadow, border, outline { ); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px; -} \ No newline at end of file +} + +row.property label.title { + opacity: 0.55; + font-size: smaller; +} + +row.property label.subtitle { + opacity: 1.0; + font-size: 11pt; +} diff --git a/src/meson.build b/src/meson.build index 0299cda..11b7671 100644 --- a/src/meson.build +++ b/src/meson.build @@ -17,6 +17,7 @@ rust_sources = files( 'widgets/image_page.rs', 'widgets/image_view.rs', 'widgets/mod.rs', + 'widgets/properties_view.rs', 'application.rs', 'config.rs', 'file_model.rs', diff --git a/src/util.rs b/src/util.rs index 9d0a988..df6451b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -37,6 +37,25 @@ pub fn query_attributes(file: &gio::File, attributes: Vec<&str>) -> anyhow::Resu .context("Failed to query string") } +pub async fn query_attributes_future( + file: &gio::File, + attributes: Vec<&str>, +) -> anyhow::Result { + let mut attr_str = String::from(*attributes.get(0).context("No attributes")?); + + for attr in &attributes[1..] { + write!(attr_str, ",{}", attr)?; + } + + file.query_info_future( + &attr_str, + gio::FileQueryInfoFlags::empty(), + glib::Priority::default(), + ) + .await + .context("Failed to query attributes") +} + pub fn compare_by_name(file_a: &gio::File, file_b: &gio::File) -> std::cmp::Ordering { let name_a = get_file_display_name(file_a).unwrap_or_default(); let name_b = get_file_display_name(file_b).unwrap_or_default(); diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 076921a..c491c20 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -20,7 +20,9 @@ mod image; mod image_page; mod image_view; +mod properties_view; pub use image::LpImage; pub use image_page::LpImagePage; pub use image_view::LpImageView; +pub use properties_view::LpPropertiesView; diff --git a/src/widgets/properties_view.rs b/src/widgets/properties_view.rs new file mode 100644 index 0000000..551b278 --- /dev/null +++ b/src/widgets/properties_view.rs @@ -0,0 +1,388 @@ +// properties_view.rs +// +// Copyright $year 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 . +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::deps::*; +use crate::util; + +use adw::prelude::*; +use adw::subclass::prelude::*; +use glib::clone; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; + +use anyhow::Context; +use ashpd::desktop::open_uri; +use ashpd::WindowIdentifier; +use futures::future::{AbortHandle, Abortable}; +use once_cell::sync::Lazy; +use std::cell::RefCell; + +const MB: f64 = 1_000_000.0; +const KB: f64 = 1_000.0; +const FALLBACK: &str = "-"; + +mod imp { + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/org/gnome/Loupe/gtk/properties_view.ui")] + pub struct LpPropertiesView { + pub file: RefCell>>, + pub file_info: RefCell>, + pub dimensions: RefCell>, + + pub info_handle: RefCell>, + pub dimensions_handle: RefCell>, + } + + #[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("properties.open-folder", None, move |properties, _, _| { + properties.open_directory(); + }); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for LpPropertiesView { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ + glib::ParamSpecObject::new( + "file", + "", + "", + gio::File::static_type(), + glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, + ), + glib::ParamSpecObject::new( + "file-info", + "", + "", + gio::FileInfo::static_type(), + glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY, + ), + glib::ParamSpecString::new( + "dimensions", + "", + "", + Some(FALLBACK), + glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY, + ), + ] + }); + + PROPERTIES.as_ref() + } + + fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + let prop_name = pspec.name(); + + match prop_name { + "file" => obj.file().to_value(), + "file-info" => obj.file_info().to_value(), + "dimensions" => self.dimensions.borrow().to_value(), + _ => unimplemented!("Failed to get property \"{prop_name}\""), + } + } + + fn set_property( + &self, + obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + let prop_name = pspec.name(); + + match prop_name { + "file" => obj.set_file(&value.get().ok()), + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + obj.action_set_enabled("properties.open-folder", false); + } + } + + impl WidgetImpl for LpPropertiesView {} + impl BinImpl for LpPropertiesView {} +} + +glib::wrapper! { + pub struct LpPropertiesView(ObjectSubclass) + @extends gtk::Widget, adw::Bin, + @implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable; +} + +#[gtk::template_callbacks] +impl LpPropertiesView { + fn set_file(&self, file: &Option) { + let imp = self.imp(); + + self.action_set_enabled("properties.open-folder", file.is_some()); + + 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(); + } + + if let Some(handle) = imp.dimensions_handle.take() { + handle.abort(); + } + + self.build_file_info(file); + let weak = file.downgrade(); + imp.file.replace(Some(weak)); + self.notify("file"); + } + } + + fn file(&self) -> Option { + let imp = self.imp(); + imp.file.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(); + let ctx = glib::MainContext::default(); + + // 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, @weak file => async move { + if let Ok(info) = util::query_attributes_future( + &file, + vec![ + &gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + &gio::FILE_ATTRIBUTE_STANDARD_SIZE, + &gio::FILE_ATTRIBUTE_TIME_CREATED, + &gio::FILE_ATTRIBUTE_TIME_MODIFIED, + ], + ) + .await { + view.on_info_loaded(info, &file); + } + }), + reg, + ); + + // ...store the handle: + imp.info_handle.replace(Some(handle)); + + // ...then spawn the future. + ctx.spawn_local(async { + let _ = fut.await; + }); + } + + // 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, file: &gio::File) { + let imp = self.imp(); + self.build_dimensions(&info, file); + imp.file_info.replace(Some(info)); + self.notify("file-info"); + } + + fn build_dimensions(&self, info: &gio::FileInfo, file: &gio::File) { + let imp = self.imp(); + let ctx = glib::MainContext::default(); + + // Here we do the same thing we do for the rest of the file info. + let (handle, reg) = AbortHandle::new_pair(); + let fut = Abortable::new( + clone!(@weak self as view, @weak file, @weak info => async move { + let is_svg = info.content_type() + .map(|t| t.to_string()) + .map(|t| t == "image/svg+xml") + .unwrap_or_default(); + + let path = file.peek_path(); + if path.is_none() { + return; + } + + let dimensions = if is_svg { + parse_svg_file_future(path.unwrap()) + .await + .ok() + .and_then(|m| { + match (m.width(), m.height()) { + (Some(w), Some(h)) => Some((w, h)), + _ => None::<(f64, f64)>, + } + }) + .map(|(w, h)| format!("{w:.1} \u{D7} {h:.1}")) + } else { + gdk_pixbuf::Pixbuf::file_info_future(path.unwrap()) + .await + .ok() + .flatten() + .map(|(_, w, h)| format!("{w} \u{D7} {h}")) + }; + + view.on_dimensions_loaded(dimensions.unwrap_or_else(|| FALLBACK.to_string())); + }), + reg, + ); + + imp.dimensions_handle.replace(Some(handle)); + + ctx.spawn_local(async { + let _ = fut.await; + }); + } + + fn on_dimensions_loaded(&self, dimensions: String) { + let imp = self.imp(); + imp.dimensions.replace(Some(dimensions)); + self.notify("dimensions"); + } + + fn open_directory(&self) { + if let Some(directory) = self + .file() + .and_then(|f| f.peek_path()) + .and_then(|p| std::fs::File::open(p).ok()) + { + let ctx = glib::MainContext::default(); + + ctx.spawn_local(clone!(@weak self as view => async move { + let id = WindowIdentifier::from_native(&view.native().expect("No GtkNative for view")).await; + if let Err(e) = open_uri::open_directory(&id, &directory).await { + log::error!("Could not open parent directory: {e}"); + }; + })); + } + } + + // 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 file_type(&self, info: Option) -> String { + info.and_then(|i| i.content_type()) + .map(|t| t.to_string()) + .map(|t| { + t.rsplit('/').collect::>()[0] + .to_owned() + .to_uppercase() + }) + .unwrap_or_else(|| FALLBACK.to_owned()) + } + + #[template_callback] + fn file_size(&self, info: Option) -> String { + info.map(|info| { + let bytes = info.size() as f64; + + let (mut size, mut unit) = (bytes, "B"); + let sizes = [(MB, "MB"), (KB, "kB")]; + + for (b, u) in sizes { + if bytes > b { + let res = bytes / b; + size = res; + unit = u; + break; + } + } + + String::from(&format!("{size:.1} {unit}")) + }) + .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| t.format("%x, %X").ok()) + .map(|t| t.to_string()) + .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| t.format("%x, %X").ok()) + .map(|t| t.to_string()) + .unwrap_or_else(|| FALLBACK.to_owned()) + } +} + +async fn parse_svg_file_future(path: std::path::PathBuf) -> anyhow::Result { + let (sender, receiver) = futures_channel::oneshot::channel(); + + std::thread::spawn(move || { + let result = svg_metadata::Metadata::parse_file(path); + sender.send(result).unwrap() + }); + + receiver.await.unwrap().context("Could not load metadata") +} diff --git a/src/window.rs b/src/window.rs index 30555f3..71e7a57 100644 --- a/src/window.rs +++ b/src/window.rs @@ -30,7 +30,7 @@ use ashpd::WindowIdentifier; use crate::config; use crate::util; -use crate::widgets::LpImageView; +use crate::widgets::{LpImageView, LpPropertiesView}; mod imp { use super::*; @@ -70,6 +70,8 @@ mod imp { #[template_child] pub image_view: TemplateChild, #[template_child] + pub properties_view: TemplateChild, + #[template_child] pub drop_target: TemplateChild, } @@ -209,11 +211,11 @@ impl LpWindow { let imp = self.imp(); if fullscreen { - imp.flap.set_fold_policy(adw::FlapFoldPolicy::Always); imp.headerbar.add_css_class("osd"); + imp.properties_view.add_css_class("osd") } else { - imp.flap.set_fold_policy(adw::FlapFoldPolicy::Never); imp.headerbar.remove_css_class("osd"); + imp.properties_view.remove_css_class("osd") } self.set_fullscreened(fullscreen); @@ -382,4 +384,16 @@ impl LpWindow { file.and_then(|f| util::get_file_display_name(&f)) // If the file exists, get display name .unwrap_or_else(|| String::from("Image Viewer")) // Return that or the default if there's nothing } + + // We also have a closure that returns `adw::FlapFoldPolicy`. + // if we aren't fullscreened, or if the properties are revealed, + // we unfold the main flap. Otherwise we're always folded. + #[template_callback] + fn fold_policy(&self, fullscreened: bool, properties_revealed: bool) -> adw::FlapFoldPolicy { + if !fullscreened || properties_revealed { + adw::FlapFoldPolicy::Never + } else { + adw::FlapFoldPolicy::Always + } + } }