2024-01-01 22:41:56 +01:00
|
|
|
|
// Copyright (c) 2022-2024 Sophie Herold
|
2023-12-02 22:07:33 +01:00
|
|
|
|
// Copyright (c) 2022 Christopher Davis
|
2024-07-17 19:06:34 +02:00
|
|
|
|
// Copyright (c) 2024 Lukáš Tyrychtr
|
|
|
|
|
|
// Copyright (c) 2024 Fina Wilke
|
2022-07-26 11:31:11 -04:00
|
|
|
|
//
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
2023-12-06 23:45:02 +01:00
|
|
|
|
use std::cell::{OnceCell, RefCell};
|
2022-07-26 11:31:11 -04:00
|
|
|
|
|
|
|
|
|
|
use adw::prelude::*;
|
|
|
|
|
|
use adw::subclass::prelude::*;
|
2024-02-09 22:06:24 +01:00
|
|
|
|
use glib::translate::IntoGlib;
|
2023-12-02 22:07:33 +01:00
|
|
|
|
use glib::Properties;
|
2022-07-26 11:31:11 -04:00
|
|
|
|
use gtk::CompositeTemplate;
|
|
|
|
|
|
|
2023-12-06 23:45:02 +01:00
|
|
|
|
use crate::deps::*;
|
2024-01-01 22:41:56 +01:00
|
|
|
|
use crate::util::gettext::*;
|
|
|
|
|
|
use crate::util::{self};
|
2022-11-06 01:08:43 +01:00
|
|
|
|
use crate::widgets::image::LpImage;
|
|
|
|
|
|
|
|
|
|
|
|
const FALLBACK: &str = "–";
|
2022-07-26 11:31:11 -04:00
|
|
|
|
|
|
|
|
|
|
mod imp {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
|
#[derive(Debug, Default, CompositeTemplate, Properties)]
|
|
|
|
|
|
#[properties(wrapper_type = super::LpPropertiesView)]
|
2023-11-28 21:50:51 +01:00
|
|
|
|
#[template(file = "properties_view.ui")]
|
2022-07-26 11:31:11 -04:00
|
|
|
|
pub struct LpPropertiesView {
|
2023-12-02 22:07:33 +01:00
|
|
|
|
#[property(nullable, set = Self::set_image, get)]
|
|
|
|
|
|
image: RefCell<Option<LpImage>>,
|
|
|
|
|
|
image_signals: OnceCell<glib::SignalGroup>,
|
|
|
|
|
|
|
|
|
|
|
|
#[template_child]
|
|
|
|
|
|
folder: TemplateChild<adw::ActionRow>,
|
2023-12-04 12:52:40 +01:00
|
|
|
|
#[template_child]
|
2024-06-24 15:12:14 +02:00
|
|
|
|
folder_button: TemplateChild<gtk::Button>,
|
|
|
|
|
|
#[template_child]
|
2023-12-04 12:52:40 +01:00
|
|
|
|
uri: TemplateChild<adw::ActionRow>,
|
2023-12-02 22:07:33 +01:00
|
|
|
|
|
|
|
|
|
|
#[template_child]
|
|
|
|
|
|
image_size: TemplateChild<adw::ActionRow>,
|
|
|
|
|
|
#[template_child]
|
|
|
|
|
|
image_format: TemplateChild<adw::ActionRow>,
|
|
|
|
|
|
#[template_child]
|
|
|
|
|
|
file_size: TemplateChild<adw::ActionRow>,
|
|
|
|
|
|
|
2023-12-04 12:52:40 +01:00
|
|
|
|
#[template_child]
|
|
|
|
|
|
dates: TemplateChild<adw::PreferencesGroup>,
|
2023-12-02 22:07:33 +01:00
|
|
|
|
#[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]
|
2024-11-13 17:32:27 +01:00
|
|
|
|
user_comment: TemplateChild<adw::ActionRow>,
|
|
|
|
|
|
#[template_child]
|
2023-12-02 22:07:33 +01:00
|
|
|
|
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>,
|
2022-07-26 11:31:11 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[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);
|
2022-11-06 01:08:43 +01:00
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
|
klass.install_action_async("properties.open-folder", None, |obj, _, _| async move {
|
|
|
|
|
|
obj.imp().open_directory().await;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2024-02-09 22:06:24 +01:00
|
|
|
|
klass.install_action("properties.copy-location", None, move |obj, _, _| {
|
|
|
|
|
|
obj.imp().copy_location();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
|
klass.install_action("properties.open-location", None, move |obj, _, _| {
|
|
|
|
|
|
obj.imp().open_location();
|
2022-11-06 01:08:43 +01:00
|
|
|
|
});
|
2022-07-26 11:31:11 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
|
|
|
|
|
obj.init_template();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl ObjectImpl for LpPropertiesView {
|
2023-12-02 22:07:33 +01:00
|
|
|
|
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"),
|
2024-06-18 00:37:38 +02:00
|
|
|
|
glib::clone!(
|
|
|
|
|
|
#[weak]
|
|
|
|
|
|
obj,
|
|
|
|
|
|
move |_, _| obj.imp().update()
|
|
|
|
|
|
),
|
2023-12-02 22:07:33 +01:00
|
|
|
|
);
|
2023-12-04 12:52:40 +01:00
|
|
|
|
|
|
|
|
|
|
image_signals.connect_local(
|
|
|
|
|
|
"metadata-changed",
|
|
|
|
|
|
false,
|
2024-06-18 00:37:38 +02:00
|
|
|
|
glib::clone!(
|
|
|
|
|
|
#[weak]
|
|
|
|
|
|
obj,
|
2024-06-23 22:28:16 +02:00
|
|
|
|
#[upgrade_or_default]
|
2024-06-18 00:37:38 +02:00
|
|
|
|
move |_| {
|
|
|
|
|
|
obj.imp().update();
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
2023-12-02 22:07:33 +01:00
|
|
|
|
);
|
|
|
|
|
|
|
2024-06-24 15:12:14 +02:00
|
|
|
|
self.folder_button
|
|
|
|
|
|
.reset_relation(gtk::AccessibleRelation::LabelledBy);
|
|
|
|
|
|
self.folder_button
|
|
|
|
|
|
.reset_relation(gtk::AccessibleRelation::DescribedBy);
|
|
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
|
self.update();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-07-26 11:31:11 -04:00
|
|
|
|
fn properties() -> &'static [glib::ParamSpec] {
|
2023-12-02 22:07:33 +01:00
|
|
|
|
Self::derived_properties()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
|
|
|
|
|
self.derived_set_property(id, value, pspec)
|
|
|
|
|
|
}
|
2022-07-26 11:31:11 -04:00
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
|
fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
|
|
|
|
|
self.derived_property(id, pspec)
|
2022-07-26 11:31:11 -04:00
|
|
|
|
}
|
2023-12-02 22:07:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl WidgetImpl for LpPropertiesView {}
|
|
|
|
|
|
impl BinImpl for LpPropertiesView {}
|
2022-07-26 11:31:11 -04:00
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
|
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) {
|
2023-02-12 02:07:42 +01:00
|
|
|
|
let obj = self.obj();
|
2023-12-02 22:07:33 +01:00
|
|
|
|
|
|
|
|
|
|
let Some(image) = obj.image() else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
|
|
|
|
|
let Some(file) = image.file() else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2023-12-04 12:52:40 +01:00
|
|
|
|
let metadata = image.metadata();
|
|
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
|
// Folder
|
2024-10-08 20:53:10 +02:00
|
|
|
|
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);
|
2023-12-04 12:52:40 +01:00
|
|
|
|
// The portal only supports opening folders of files that have a path
|
2023-12-02 22:07:33 +01:00
|
|
|
|
obj.action_set_enabled("properties.open-folder", file.path().is_some());
|
2023-12-04 12:52:40 +01:00
|
|
|
|
// 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);
|
2023-12-02 22:07:33 +01:00
|
|
|
|
|
|
|
|
|
|
// Image info
|
|
|
|
|
|
Self::update_row(&self.image_size, self.image_size());
|
2024-01-01 22:41:56 +01:00
|
|
|
|
Self::update_row(&self.image_format, self.format_name());
|
2023-12-04 12:52:40 +01:00
|
|
|
|
Self::update_row(&self.file_size, metadata.file_size());
|
2023-12-02 22:07:33 +01:00
|
|
|
|
|
2023-12-04 12:52:40 +01:00
|
|
|
|
// Dates
|
|
|
|
|
|
let has_dates = [
|
|
|
|
|
|
Self::update_row(&self.file_created, metadata.file_created()),
|
2023-12-04 18:42:21 +01:00
|
|
|
|
Self::update_row(&self.file_modified, metadata.file_modified()),
|
2023-12-04 12:52:40 +01:00
|
|
|
|
]
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.any(|x| x);
|
|
|
|
|
|
self.dates.set_visible(has_dates);
|
2023-12-02 22:07:33 +01:00
|
|
|
|
|
2023-12-04 12:52:40 +01:00
|
|
|
|
// Details (EXIF)
|
2023-12-02 22:07:33 +01:00
|
|
|
|
let has_details = [
|
2024-11-12 00:35:05 +01:00
|
|
|
|
Self::update_row(&self.location, metadata.gps_location_place()),
|
2023-12-04 12:52:40 +01:00
|
|
|
|
Self::update_row(&self.originally_created, metadata.originally_created()),
|
2024-11-13 17:32:27 +01:00
|
|
|
|
Self::update_row(&self.user_comment, metadata.user_comment()),
|
2023-12-04 12:52:40 +01:00
|
|
|
|
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()),
|
2023-12-02 22:07:33 +01:00
|
|
|
|
]
|
|
|
|
|
|
.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
|
2022-07-26 11:31:11 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-01 22:41:56 +01:00
|
|
|
|
fn format_name(&self) -> Option<String> {
|
|
|
|
|
|
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
|
2024-07-17 19:06:34 +02:00
|
|
|
|
format.push_str(&gettext_f(r", {}\u{202F}bit", [bit_depth.to_string()]));
|
2024-01-01 22:41:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Some(format)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
|
fn image_size(&self) -> Option<String> {
|
2023-02-12 02:07:42 +01:00
|
|
|
|
let obj = self.obj();
|
2022-07-26 11:31:11 -04:00
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
|
if let Some(image) = obj.image() {
|
2024-01-01 22:41:56 +01:00
|
|
|
|
match image.metadata().dimensions_text() {
|
|
|
|
|
|
Some(string) => Some(string.to_string()),
|
2023-12-02 22:07:33 +01:00
|
|
|
|
_ => {
|
|
|
|
|
|
let (width, height) = image.image_size();
|
|
|
|
|
|
if width > 0 && height > 0 {
|
2024-09-12 22:20:50 +02:00
|
|
|
|
Some(
|
|
|
|
|
|
// Translators: Image "<width> x <height>"
|
|
|
|
|
|
gettext_f(
|
|
|
|
|
|
r"{}\u{202F}\u{D7}\u{202F}{}",
|
|
|
|
|
|
[width.to_string(), height.to_string()],
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
2023-12-02 22:07:33 +01:00
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
2022-07-26 11:31:11 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
|
fn file(&self) -> Option<gio::File> {
|
|
|
|
|
|
self.image.borrow().as_ref().and_then(|x| x.file())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async fn open_directory(&self) {
|
2023-02-12 02:07:42 +01:00
|
|
|
|
let obj = self.obj();
|
2022-11-02 21:11:43 +01:00
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
|
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}");
|
|
|
|
|
|
};
|
2022-07-26 11:31:11 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-09 22:06:24 +01:00
|
|
|
|
/// 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();
|
2024-11-12 00:35:05 +01:00
|
|
|
|
clipboard.set_text(&location.iso_6709());
|
2024-02-09 22:06:24 +01:00
|
|
|
|
let _ = obj.activate_action(
|
|
|
|
|
|
"win.show-toast",
|
|
|
|
|
|
Some(
|
|
|
|
|
|
&(
|
|
|
|
|
|
gettext("Image Coordinates Copied"),
|
|
|
|
|
|
adw::ToastPriority::Normal.into_glib(),
|
|
|
|
|
|
)
|
|
|
|
|
|
.to_variant(),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
|
/// 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}")
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2022-07-26 11:31:11 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
glib::wrapper! {
|
|
|
|
|
|
pub struct LpPropertiesView(ObjectSubclass<imp::LpPropertiesView>)
|
|
|
|
|
|
@extends gtk::Widget, adw::Bin,
|
|
|
|
|
|
@implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
|
|
|
|
|
|
}
|