diff --git a/Cargo.toml b/Cargo.toml index 36f2e50c50ae854a592ad536de371c747d915b4c..45fa78947c272875d80a78dfb132d8e42ac4f4ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,13 @@ name = "PhoshFileSelector" version = "0.0.5" edition = "2021" +description = "A file selector widget for Phosh using gtk-rs" +readme = "README.md" +homepage = "https://phosh.mobi" +repository = "https://gitlab.gnome.org/World/Phosh/pfs" +license = "GPL-3.0-or-later" +keywords = ["gtk", "adwaita", "phosh"] +exclude = ["/debian"] [lib] name = "pfs" diff --git a/po/POTFILES.in b/po/POTFILES.in index b56173b15df0d77c842127896f0a991625abda02..d36fa92836b1defb0af3140a861ba149d442c218 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,8 +1,10 @@ # List of source files containing translatable strings. +src/file_props.rs src/file_selector.rs src/init.rs src/places_box.rs src/util.rs src/dir-view.ui +src/file-props.ui src/file-selector.ui diff --git a/src/dir_view.rs b/src/dir_view.rs index 427a131aa1cbca0bed49839a10f801e44dbbe62a..b7b1132422da5e877bd87e1199cc335af1ad68aa 100644 --- a/src/dir_view.rs +++ b/src/dir_view.rs @@ -617,9 +617,7 @@ impl DirView { return; } - let _ = self - .upcast_ref::() - .activate_action("file-selector.accept", None); + let _ = self.activate_action("file-selector.accept", None); } #[template_callback] diff --git a/src/examples/demo/demo_window.rs b/src/examples/demo/demo_window.rs index 2079e52a336e183132c41e2e5d09ffbbca7962de..43cc3b5ebd81b6fd2794b51446b94779a356c232 100644 --- a/src/examples/demo/demo_window.rs +++ b/src/examples/demo/demo_window.rs @@ -93,7 +93,7 @@ impl PfsDemoWindow { Some(vec) => vec, }; - self.imp().selected_label.get().set_label(&uris[0]); + self.imp().selected_label.set_label(&uris[0]); } pub fn open_file(&self) { @@ -148,22 +148,22 @@ impl PfsDemoWindow { glib::g_debug!(LOG_DOMAIN, "File dialog done, result: {success:#?}"); let selected = selector.selected(); - this.imp().selected_label.get().set_label(""); - this.imp().choices_label.get().set_label(""); - this.imp().filter_label.get().set_label(""); + this.imp().selected_label.set_label(""); + this.imp().choices_label.set_label(""); + this.imp().filter_label.set_label(""); if success { let uris = match selected { None => vec!["".to_string()], Some(vec) => vec, }; - this.imp().selected_label.get().set_label(&uris[0]); + this.imp().selected_label.set_label(&uris[0]); let text = match selector.selected_choices() { Some(choices) => choices.to_string(), None => "".to_string(), }; - this.imp().choices_label.get().set_label(&text); + this.imp().choices_label.set_label(&text); let pos = selector.current_filter(); let text = match selector.filters() { @@ -178,7 +178,7 @@ impl PfsDemoWindow { }, None => "".to_string(), }; - this.imp().filter_label.get().set_label(&text); + this.imp().filter_label.set_label(&text); } } ), diff --git a/src/examples/open/pfs_open_application.rs b/src/examples/open/pfs_open_application.rs index eaade113571c62b590bff77d735c44ba5498eb75..801e87cf69ab393dff74020bdd546a56f0e5192e 100644 --- a/src/examples/open/pfs_open_application.rs +++ b/src/examples/open/pfs_open_application.rs @@ -13,6 +13,7 @@ use gtk::{gio, glib}; use std::cell::{Cell, RefCell}; use std::process::Command; +use pfs::file_props::FileProps; use pfs::file_selector::{FileSelector, FileSelectorMode}; use crate::config::LOG_DOMAIN; @@ -29,12 +30,10 @@ const FILE_MANAGER1_XML: &str = r#" - "#; @@ -51,10 +50,18 @@ struct ShowItems { _startup_id: String, } +#[derive(Debug, glib::Variant)] +struct ShowItemProperties { + uris: Vec, + _startup_id: String, +} + +#[allow(clippy::enum_variant_names)] #[derive(Debug)] enum FileManager1 { ShowFolders(ShowFolders), ShowItems(ShowItems), + ShowItemProperties(ShowItemProperties), } mod imp { @@ -70,6 +77,9 @@ mod imp { match method { "ShowFolders" => Ok(params.get::().map(Self::ShowFolders)), "ShowItems" => Ok(params.get::().map(Self::ShowItems)), + "ShowItemProperties" => Ok(params + .get::() + .map(Self::ShowItemProperties)), _ => Err(glib::Error::new( gio::DBusError::UnknownMethod, "No such method", @@ -210,7 +220,7 @@ impl PfsOpenApplication { #[weak(rename_to = this)] self, move |selector: FileSelector, success: bool| { - glib::g_debug!(LOG_DOMAIN, "File dialog done, result: {success:#?}"); + glib::g_debug!(LOG_DOMAIN, "File dialog done, result: {success}"); let imp = this.imp(); let selected = selector.selected(); @@ -251,6 +261,41 @@ impl PfsOpenApplication { } } + fn show_item_properties(&self, file: &gio::File) { + let imp = self.imp(); + let uri = file.uri(); + + glib::g_message!(LOG_DOMAIN, "Showing props for {uri}"); + + if imp.hold_count.replace(imp.hold_count.get() + 1) == 0 { + *self.imp().hold_guard.borrow_mut() = Some(self.hold()); + } + + let file_props = glib::Object::builder::() + .property("file", file) + .build(); + + file_props.connect_closure( + "done", + false, + glib::closure_local!( + #[weak(rename_to = this)] + self, + move |_props: FileProps, success: bool| { + glib::g_debug!(LOG_DOMAIN, "File props dialog done, result: {success}"); + let imp = this.imp(); + + if imp.hold_count.replace(imp.hold_count.get() - 1) == 1 { + // Drop the application ref count + this.imp().hold_guard.replace(None); + } + } + ), + ); + + file_props.present(); + } + fn register_object( &self, connection: &gio::DBusConnection, @@ -287,6 +332,17 @@ impl PfsOpenApplication { } Ok(None) } + FileManager1::ShowItemProperties(ShowItemProperties { + uris, + _startup_id, + }) => { + if let Some(app) = app { + for uri in &uris { + app.obj().show_item_properties(&gio::File::for_uri(uri)); + } + } + Ok(None) + } } } } diff --git a/src/file-props.ui b/src/file-props.ui new file mode 100644 index 0000000000000000000000000000000000000000..9b674fcd1aa462ef9bab06f2e74b9f0299421e59 --- /dev/null +++ b/src/file-props.ui @@ -0,0 +1,202 @@ + + + + + + diff --git a/src/file_props.rs b/src/file_props.rs new file mode 100644 index 0000000000000000000000000000000000000000..7a9c4fd348389d251f6bd600f1514ea818b06356 --- /dev/null +++ b/src/file_props.rs @@ -0,0 +1,331 @@ +/* + * Copyright 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +use adw::{prelude::*, subclass::prelude::*}; +use glib::subclass::Signal; +use glib::translate::*; +use glib_macros::{clone, Properties}; +use gtk::{gdk, gio, glib, CompositeTemplate}; +use std::cell::{Cell, RefCell}; +use std::sync::OnceLock; + +use crate::{config::LOG_DOMAIN, file_selector::FileSelector, file_selector::FileSelectorMode}; + +#[derive(Debug, Copy, Clone, Default, PartialEq, gio::glib::Enum)] +#[enum_type(name = "PfsFilePropsType")] +pub enum FilePropsType { + #[default] + File, + Directory, + //TODO: MountPoint, +} + +pub mod imp { + use super::*; + + #[derive(Debug, Default, CompositeTemplate, Properties)] + #[template(resource = "/mobi/phosh/FileSelector/file-props.ui")] + #[properties(wrapper_type = super::FileProps)] + pub struct FileProps { + #[template_child] + pub icon: TemplateChild, + + #[template_child] + pub type_label: TemplateChild, + + #[template_child] + pub size_label: TemplateChild, + + #[template_child] + pub access_row: TemplateChild, + + #[template_child] + pub modified_row: TemplateChild, + + #[template_child] + pub created_row: TemplateChild, + + #[template_child] + pub timestamp_group: TemplateChild, + + #[template_child] + pub toast_overlay: TemplateChild, + + // The file we show the info for + #[property(get, set, construct)] + pub file: RefCell>, + + #[property(get, explicit_notify)] + pub parent_folder: RefCell>, + + #[property(get, explicit_notify, builder(FilePropsType::default()))] + pub file_type: RefCell, + + done: Cell, + } + + #[glib::object_subclass] + impl ObjectSubclass for FileProps { + const NAME: &'static str = "PfsFileProps"; + type Type = super::FileProps; + type ParentType = adw::Window; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_instance_callbacks(); + + klass.add_binding_action( + gdk::Key::Escape, + gdk::ModifierType::NO_MODIFIER_MASK, + "window.close", + ); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for FileProps { + fn constructed(&self) { + self.parent_constructed(); + + let obj = self.obj(); + + obj.setup_fileinfo(); + } + + fn signals() -> &'static [Signal] { + static SIGNALS: OnceLock> = OnceLock::new(); + SIGNALS.get_or_init(|| { + vec![Signal::builder("done") + .param_types([bool::static_type()]) + .build()] + }) + } + } + + impl WidgetImpl for FileProps {} + impl WindowImpl for FileProps {} + impl AdwWindowImpl for FileProps {} + + impl FileProps { + pub(super) fn send_done(&self, success: bool, close: bool) { + if !self.done.get() { + glib::g_debug!(LOG_DOMAIN, "Done, success: {success}"); + self.obj().emit_by_name::<()>("done", &[&success]); + self.done.replace(true); + } + + if close { + self.obj().upcast_ref::().close(); + } + } + } +} + +glib::wrapper! { + pub struct FileProps(ObjectSubclass) + @extends adw::Window, gtk::Window, gtk::Widget, + @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; +} + +impl Default for FileProps { + fn default() -> Self { + glib::Object::new::() + } +} + +#[gtk::template_callbacks] +impl FileProps { + pub fn new() -> Self { + Self::default() + } + + fn update_info(&self, info: &gio::FileInfo) { + let imp = self.imp(); + let mut have_thumbnail = false; + let mut have_timestamp = false; + + let size = info.size(); + imp.size_label.set_label(&glib::format_size(size as u64)); + imp.size_label.set_visible(true); + + if let Some(created) = info.creation_date_time() { + if let Ok(fmt) = created.format_iso8601() { + imp.created_row.set_subtitle(&fmt); + imp.created_row.set_visible(true); + have_timestamp = true; + }; + }; + + if let Some(modified) = info.modification_date_time() { + if let Ok(fmt) = modified.format_iso8601() { + imp.modified_row.set_subtitle(&fmt); + imp.modified_row.set_visible(true); + have_timestamp = true; + }; + }; + + if let Some(access) = info.access_date_time() { + if let Ok(fmt) = access.format_iso8601() { + imp.access_row.set_subtitle(&fmt); + imp.access_row.set_visible(true); + have_timestamp = true; + }; + }; + + if have_timestamp { + imp.timestamp_group.set_visible(true); + } + + if let Some(content_type) = info.content_type() { + if content_type == "inode/directory" { + imp.file_type.replace(FilePropsType::Directory); + self.notify_file_type(); + imp.type_label.set_label(&gettextrs::gettext("Directory")); + } else { + imp.type_label.set_label(&content_type); + } + }; + + if let Some(path) = info.attribute_byte_string("thumbnail::path") { + if info.boolean("thumbnail::is-valid") { + imp.icon.set_from_file(Some(path)); + have_thumbnail = true; + imp.icon.set_pixel_size(256); + } + } + + if !have_thumbnail { + if let Some(icon) = info.icon() { + imp.icon.set_from_gicon(&icon); + imp.icon.set_pixel_size(128); + } + } + + if let Some(file) = self.file() { + if let Some(parent_folder) = file.parent() { + *imp.parent_folder.borrow_mut() = Some(parent_folder); + } else { + *imp.parent_folder.borrow_mut() = None; + } + self.notify_parent_folder(); + } + } + + fn clear_info(&self) { + let imp = self.imp(); + let unknown = gettextrs::gettext("Unknown"); + + imp.size_label.set_label(&unknown); + + imp.size_label.set_visible(false); + imp.timestamp_group.set_visible(false); + imp.created_row.set_visible(false); + imp.modified_row.set_visible(false); + imp.access_row.set_visible(false); + imp.type_label.set_label(&unknown); + imp.icon.set_icon_name(Some("image-missing-symbolic")); + imp.icon.set_pixel_size(128); + } + + fn setup_fileinfo(&self) { + let c = glib::MainContext::default(); + + /* TODO: get fileinfo and fill properties with it */ + let Some(file) = self.file() else { + return; + }; + + self.clear_info(); + let future = clone!( + #[weak(rename_to = this)] + self, + async move { + match file + .query_info_future( + &[ + "standard::content-type", + "standard::display-name", + "standard::icon", + "standard::size", + "thumbnail::*", + "time::access", + "time::created", + "time::modified", + ] + .join(","), + gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS, + glib::Priority::DEFAULT, + ) + .await + { + Ok(info) => this.update_info(&info), + Err(err) => { + let imp = this.imp(); + + let msg = gettextrs::gettext("Failed to get info for {}").replacen( + "{}", + this.file().unwrap().uri().as_str(), + 1, + ); + imp.toast_overlay.add_toast(adw::Toast::new(&msg)); + glib::g_warning!(LOG_DOMAIN, "Failed to get info: {err}"); + } + } + } + ); + c.spawn_local(future); + } + + #[template_callback] + fn on_close_requested(&self) -> bool { + self.imp().send_done(false, false); + false + } + + #[template_callback] + fn on_accept_clicked(&self) { + glib::g_debug!(LOG_DOMAIN, "Props done"); + self.imp().send_done(true, true); + } + + #[template_callback] + fn file_to_string(&self, file: Option) -> String { + let Some(file) = file else { + return "".to_string(); + }; + + let basename = file.basename().unwrap_or_default(); + basename.to_str().unwrap_or_default().to_string() + } + + #[template_callback] + fn file_type_to_size_label_visible(&self, file_type: FilePropsType) -> bool { + file_type == FilePropsType::File + } + + #[template_callback] + fn parent_folder_to_row_visible(&self) -> bool { + self.parent_folder().is_some() + } + + #[template_callback] + fn on_open_parent_folder_clicked(&self) { + let file_selector = glib::Object::builder::() + .property("accept_label", gettextrs::gettext("Done")) + .property("title", gettextrs::gettext("Browse Directory")) + .property("current-folder", self.parent_folder()) + .build(); + + file_selector.set_mode(FileSelectorMode::OpenFile); + file_selector.present(); + } +} diff --git a/src/file_selector.rs b/src/file_selector.rs index aa304ed30e79b0f1917038c5a7500ac48f65ed71..c754f214e9cf59d130d02d48633a0e34ce9a520c 100644 --- a/src/file_selector.rs +++ b/src/file_selector.rs @@ -183,7 +183,7 @@ pub mod imp { } if close { - self.obj().upcast_ref::().close(); + self.obj().close(); } } @@ -251,8 +251,7 @@ pub mod imp { fn set_choices_menu(&self, actions: gio::SimpleActionGroup, menu: &gio::Menu) { let obj = self.obj(); - obj.upcast_ref::() - .insert_action_group("custom-choices", Some(&actions)); + obj.insert_action_group("custom-choices", Some(&actions)); self.choices_menu_button.set_menu_model(Some(menu)); self.choices_menu_button.set_visible(menu.n_items() > 0); @@ -595,8 +594,7 @@ impl FileSelector { ) ); - self.upcast_ref::() - .insert_action_group("file-selector", Some(&actions)); + self.insert_action_group("file-selector", Some(&actions)); // Keep `current-filter` in sync with action let filter_action = actions.lookup_action("set-filter").unwrap(); @@ -630,7 +628,7 @@ impl FileSelector { dialog.set_response_appearance("replace", adw::ResponseAppearance::Destructive); dialog.choose( - self.upcast_ref::(), + self, None::<&gio::Cancellable>, clone!( #[weak(rename_to = this)] diff --git a/src/grid_item.rs b/src/grid_item.rs index 03279f3ef6331e2c44d6a198258f9d98bac258d6..b8f91cbf8e14cd4523808e5e778c8a7b4bfc7f3c 100644 --- a/src/grid_item.rs +++ b/src/grid_item.rs @@ -65,7 +65,7 @@ mod imp { if let Some(path) = info.attribute_byte_string("thumbnail::path") { let valid = info.boolean("thumbnail::is-valid"); if valid { - self.icon.get().set_from_file(Some(path)); + self.icon.set_from_file(Some(path)); have_thumbnail = true; } } @@ -73,13 +73,13 @@ mod imp { if !have_thumbnail { if let Some(icon) = info.icon() { - self.icon.get().set_from_gicon(&icon); + self.icon.set_from_gicon(&icon); } } } fn set_fileinfo(&self, info: gio::FileInfo) { - self.label.get().set_label(&info.display_name()); + self.label.set_label(&info.display_name()); *self.fileinfo.borrow_mut() = Some(info); self.update_image(); @@ -129,7 +129,7 @@ impl GridItem { let imp = self.imp(); if *imp.thumbnail_mode.borrow() != ThumbnailMode::Never { - imp.icon.get().set_from_file(Some(path)); + imp.icon.set_from_file(Some(path)); } } } diff --git a/src/lib.rs b/src/lib.rs index 7486c27b81e77d6e0041bc6f12074042e35b97d8..72b0f3465bf6735f21ed723c1b83049c116a8069 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ * Author: Guido Günther */ +pub mod file_props; pub mod file_selector; pub mod init; diff --git a/src/pfs.gresource.xml b/src/pfs.gresource.xml index aedc096ba8747c5e6146e2beceb7232d53efb8d4..19b1497bd4f98058185515f48f50b65cb60092d6 100644 --- a/src/pfs.gresource.xml +++ b/src/pfs.gresource.xml @@ -3,6 +3,7 @@ dir-stack.ui dir-view.ui + file-props.ui file-selector.ui grid-item.ui path-bar.ui