From 6133274837140afcefe04c41fc3fac77153b035b Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 3 Jul 2024 12:24:35 -0600 Subject: [PATCH] Support dialog filters --- Cargo.lock | 1 + Cargo.toml | 1 + src/dialog.rs | 123 ++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 121 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97129cd..8d19665 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1156,6 +1156,7 @@ dependencies = [ "freedesktop_entry_parser", "fs_extra", "gio", + "glob", "i18n-embed", "i18n-embed-fl", "ignore", diff --git a/Cargo.toml b/Cargo.toml index bf8d29b..576d2b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ env_logger = "0.11" freedesktop_entry_parser = { version = "1.3", optional = true } fs_extra = "1.3" gio = { version = "0.19", optional = true } +glob = "0.3" ignore = "0.4" image = "0.24" once_cell = "1.19" diff --git a/src/dialog.rs b/src/dialog.rs index 6950fda..73bcd28 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -30,6 +30,7 @@ use std::{ collections::{HashMap, HashSet}, env, fmt, fs, path::PathBuf, + str::FromStr, time, }; @@ -89,6 +90,7 @@ impl DialogKind { } } +#[derive(Clone, Debug)] pub struct DialogChoiceOption { pub id: String, pub label: String, @@ -100,6 +102,7 @@ impl AsRef for DialogChoiceOption { } } +#[derive(Clone, Debug)] pub enum DialogChoice { CheckBox { id: String, @@ -114,6 +117,24 @@ pub enum DialogChoice { }, } +#[derive(Clone, Debug)] +pub enum DialogFilterPattern { + Glob(String), + Mime(String), +} + +#[derive(Clone, Debug)] +pub struct DialogFilter { + pub label: String, + pub patterns: Vec, +} + +impl AsRef for DialogFilter { + fn as_ref(&self) -> &str { + &self.label + } +} + pub struct Dialog { cosmic: Cosmic, mapper: fn(DialogMessage) -> M, @@ -196,6 +217,25 @@ impl Dialog { self.cosmic.app.choices = choices.into(); } + pub fn filters(&self) -> (&[DialogFilter], Option) { + (&self.cosmic.app.filters, self.cosmic.app.filter_selected) + } + + pub fn set_filters( + &mut self, + filters: impl Into>, + filter_selected: Option, + ) -> Command { + let mapper = self.mapper; + self.cosmic.app.filters = filters.into(); + self.cosmic.app.filter_selected = filter_selected; + self.cosmic + .app + .rescan_tab() + .map(DialogMessage) + .map(move |message| app::Message::App(mapper(message))) + } + pub fn subscription(&self) -> Subscription { self.cosmic .subscription() @@ -242,6 +282,7 @@ enum Message { Cancel, Choice(usize, usize), Filename(String), + Filter(usize), Modifiers(Modifiers), NotifyEvents(Vec), NotifyWatcher(WatcherWrapper), @@ -280,6 +321,8 @@ struct App { title: String, accept_label: String, choices: Vec, + filters: Vec, + filter_selected: Option, filename_id: widget::Id, modifiers: Modifiers, nav_model: segmented_button::SingleSelectModel, @@ -437,6 +480,8 @@ impl Application for App { title, accept_label, choices: Vec::new(), + filters: Vec::new(), + filter_selected: None, filename_id: widget::Id::unique(), modifiers: Modifiers::empty(), nav_model: nav_model.build(), @@ -535,6 +580,14 @@ impl Application for App { *filename = new_filename; } } + Message::Filter(filter_i) => { + if filter_i < self.filters.len() { + self.filter_selected = Some(filter_i); + } else { + self.filter_selected = None; + } + return self.rescan_tab(); + } Message::Modifiers(modifiers) => { self.modifiers = modifiers; } @@ -743,6 +796,59 @@ impl Application for App { return Command::batch(commands); } Message::TabRescan(mut items) => { + // Filter + if let Some(filter_i) = self.filter_selected { + if let Some(filter) = self.filters.get(filter_i) { + // Parse filters + let mut parsed_globs = Vec::new(); + let mut parsed_mimes = Vec::new(); + for pattern in filter.patterns.iter() { + match pattern { + DialogFilterPattern::Glob(value) => { + match glob::Pattern::new(value) { + Ok(glob) => parsed_globs.push(glob), + Err(err) => { + log::warn!("failed to parse glob {:?}: {}", value, err); + } + } + } + DialogFilterPattern::Mime(value) => { + match mime_guess::Mime::from_str(value) { + Ok(mime) => parsed_mimes.push(mime), + Err(err) => { + log::warn!("failed to parse mime {:?}: {}", value, err); + } + } + } + } + } + + items.retain(|item| { + if item.metadata.is_dir() { + // Directories are always shown + return true; + } + + // Check for mime type match (first because it is faster) + for mime in parsed_mimes.iter() { + if mime == &item.mime { + return true; + } + } + + // Check for glob match (last because it is slower) + for glob in parsed_globs.iter() { + if glob.matches(&item.name) { + return true; + } + } + + // No filters matched + false + }); + } + } + // Select based on filename if let DialogKind::SaveFile { filename } = &self.flags.kind { for item in items.iter_mut() { @@ -772,10 +878,19 @@ impl Application for App { .map(move |message| Message::TabMessage(message)), ); - let mut row = widget::row::with_capacity(self.choices.len() * 2 + 3) - .align_items(Alignment::Center) - .padding(space_xxs) - .spacing(space_xxs); + let mut row = widget::row::with_capacity( + if !self.filters.is_empty() { 1 } else { 0 } + self.choices.len() * 2 + 3, + ) + .align_items(Alignment::Center) + .padding(space_xxs) + .spacing(space_xxs); + if !self.filters.is_empty() { + row = row.push(widget::dropdown( + &self.filters, + self.filter_selected, + Message::Filter, + )); + } if let DialogKind::SaveFile { filename } = &self.flags.kind { row = row.push( widget::text_input("", filename)