// Copyright (c) 2022-2025 Sophie Herold
// Copyright (c) 2022 Christopher Davis
// Copyright (c) 2024 Lukáš Tyrychtr
// Copyright (c) 2024 Fina Wilke
//
// 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 std::cell::{OnceCell, RefCell};
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::Properties;
use gtk::CompositeTemplate;
use crate::deps::*;
use crate::util::gettext::*;
use crate::util::root::ParentWindow;
use crate::util::{self};
use crate::widgets::image::LpImage;
const FALLBACK: &str = "–";
mod imp {
use super::*;
#[derive(Debug, Default, CompositeTemplate, Properties)]
#[properties(wrapper_type = super::LpPropertiesView)]
#[template(file = "properties_view.ui")]
pub struct LpPropertiesView {
#[property(nullable, set = Self::set_image, get)]
image: RefCell>,
image_signals: OnceCell,
#[template_child]
folder: TemplateChild,
#[template_child]
folder_button: TemplateChild,
#[template_child]
uri: TemplateChild,
#[template_child]
image_size: TemplateChild,
#[template_child]
image_format: TemplateChild,
#[template_child]
file_size: TemplateChild,
#[template_child]
dates: 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]
user_comment: 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]
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);
klass.install_action_async("properties.open-folder", None, |obj, _, _| async move {
obj.imp().open_directory().await;
});
klass.install_action("properties.copy-location", None, move |obj, _, _| {
obj.imp().copy_location();
});
klass.install_action("properties.open-location", None, move |obj, _, _| {
obj.imp().open_location();
});
}
fn instance_init(obj: &glib::subclass::InitializingObject) {
obj.init_template();
}
}
impl ObjectImpl for LpPropertiesView {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
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_local(
"metadata-changed",
false,
glib::clone!(
#[weak]
obj,
#[upgrade_or_default]
move |_| {
obj.imp().update();
None
}
),
);
self.folder_button
.reset_relation(gtk::AccessibleRelation::LabelledBy);
self.folder_button
.reset_relation(gtk::AccessibleRelation::DescribedBy);
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;
};
let metadata = image.metadata();
// Folder
let parent = metadata
.host_path()
.or_else(|| file.path())
.as_ref()
.and_then(|x| x.parent())
.and_then(|p| util::get_file_display_name(&gio::File::for_path(p)));
let has_folder = Self::update_row(&self.folder, parent);
// The portal only supports opening folders of files that have a path
obj.action_set_enabled("properties.open-folder", file.path().is_some());
// Only show URI if now folder available
let uri = if !has_folder {
image.file().map(|x| x.uri())
} else {
None
};
Self::update_row(&self.uri, uri);
// Image info
Self::update_row(&self.image_size, self.image_size());
Self::update_row(&self.image_format, self.format_name());
Self::update_row(&self.file_size, metadata.file_size());
// Dates
let has_dates = [
Self::update_row(&self.file_created, metadata.file_created()),
Self::update_row(&self.file_modified, metadata.file_modified()),
]
.into_iter()
.any(|x| x);
self.dates.set_visible(has_dates);
// Details (EXIF)
let has_details = [
Self::update_row(&self.location, metadata.gps_location_place()),
Self::update_row(&self.originally_created, metadata.originally_created()),
Self::update_row(&self.user_comment, metadata.user_comment()),
Self::update_row(&self.aperture, metadata.f_number()),
Self::update_row(&self.exposure, metadata.exposure_time()),
Self::update_row(&self.iso, metadata.iso()),
Self::update_row(&self.focal_length, metadata.focal_length()),
Self::update_row(&self.maker_model, metadata.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 format_name(&self) -> Option {
let image = self.image.borrow();
let metadata = image.as_ref()?.metadata();
let mut format = metadata.format_name().unwrap_or(gettext("Unknown"));
if metadata.alpha_channel().unwrap_or(false) {
// Translators: Addition of image being transparent to format name
format.push_str(&gettext(", transparent"));
}
if metadata.grayscale().unwrap_or(false) {
// Translators: Addition of image being grayscale to format name
format.push_str(&gettext(", grayscale"));
}
if let Some(bit_depth) = metadata.bit_depth() {
// Translators: Addition of bit size to format name
format.push_str(&gettext_f(r", {}\u{202F}bit", [bit_depth.to_string()]));
}
Some(format)
}
fn image_size(&self) -> Option {
let obj = self.obj();
if let Some(image) = obj.image() {
match image.metadata().dimensions_text() {
Some(string) => Some(string.to_string()),
_ => {
let (width, height) = image.image_size();
if width > 0 && height > 0 {
Some(
// Translators: Image " x "
gettext_f(
r"{}\u{202F}\u{D7}\u{202F}{}",
[width.to_string(), height.to_string()],
),
)
} 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 {
tracing::error!("Could not open parent directory: {e}");
};
}
/// Copy GPS location
fn copy_location(&self) {
let obj = self.obj();
if let Some(location) = obj.image().and_then(|x| x.metadata().gps_location()) {
let clipboard = obj.display().clipboard();
clipboard.set_text(&location.iso_6709());
obj.window_show_toast(
&gettext("Image Coordinates Copied"),
adw::ToastPriority::Normal,
);
}
}
/// 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 {
tracing::error!("Failed to show geo URI: {err}")
}
},
);
}
}
}
}
glib::wrapper! {
pub struct LpPropertiesView(ObjectSubclass)
@extends gtk::Widget, adw::Bin,
@implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable, gtk::Accessible;
}