Merge pull request #269 from pop-os/file-chooser
Improvements for file chooser
This commit is contained in:
commit
1b0dd43d3b
3 changed files with 518 additions and 465 deletions
702
Cargo.lock
generated
702
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -16,6 +16,7 @@ env_logger = "0.11"
|
||||||
freedesktop_entry_parser = { version = "1.3", optional = true }
|
freedesktop_entry_parser = { version = "1.3", optional = true }
|
||||||
fs_extra = "1.3"
|
fs_extra = "1.3"
|
||||||
gio = { version = "0.19", optional = true }
|
gio = { version = "0.19", optional = true }
|
||||||
|
glob = "0.3"
|
||||||
ignore = "0.4"
|
ignore = "0.4"
|
||||||
image = "0.24"
|
image = "0.24"
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
|
|
@ -47,16 +48,18 @@ slotmap = "1.0.7"
|
||||||
[dependencies.libcosmic]
|
[dependencies.libcosmic]
|
||||||
git = "https://github.com/pop-os/libcosmic.git"
|
git = "https://github.com/pop-os/libcosmic.git"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["a11y", "multi-window", "tokio", "winit"]
|
features = ["a11y", "multi-window", "tokio"]
|
||||||
|
|
||||||
[dependencies.smol_str]
|
[dependencies.smol_str]
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["desktop", "gvfs", "wgpu"]
|
default = ["desktop", "gvfs", "winit", "wgpu"]
|
||||||
desktop = ["libcosmic/desktop", "dep:freedesktop_entry_parser", "dep:xdg"]
|
desktop = ["libcosmic/desktop", "dep:freedesktop_entry_parser", "dep:xdg"]
|
||||||
gvfs = ["dep:gio"]
|
gvfs = ["dep:gio"]
|
||||||
|
wayland = ["libcosmic/wayland"]
|
||||||
|
winit = ["libcosmic/winit"]
|
||||||
wgpu = ["libcosmic/wgpu"]
|
wgpu = ["libcosmic/wgpu"]
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
|
|
|
||||||
274
src/dialog.rs
274
src/dialog.rs
|
|
@ -1,6 +1,10 @@
|
||||||
// Copyright 2023 System76 <inflist_o@system76.com>
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
#[cfg(feature = "winit")]
|
||||||
|
use cosmic::iced::multi_window::Application as IcedApplication;
|
||||||
|
#[cfg(feature = "wayland")]
|
||||||
|
use cosmic::iced::Application as IcedApplication;
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
app::{self, cosmic::Cosmic, message, Command, Core},
|
app::{self, cosmic::Cosmic, message, Command, Core},
|
||||||
cosmic_theme, executor,
|
cosmic_theme, executor,
|
||||||
|
|
@ -8,10 +12,9 @@ use cosmic::{
|
||||||
event,
|
event,
|
||||||
futures::{self, SinkExt},
|
futures::{self, SinkExt},
|
||||||
keyboard::{Event as KeyEvent, Modifiers},
|
keyboard::{Event as KeyEvent, Modifiers},
|
||||||
multi_window::Application as IcedApplication,
|
|
||||||
subscription::{self, Subscription},
|
subscription::{self, Subscription},
|
||||||
widget::scrollable,
|
widget::scrollable,
|
||||||
window, Event, Length, Size,
|
window, Alignment, Event, Length, Size,
|
||||||
},
|
},
|
||||||
theme,
|
theme,
|
||||||
widget::{self, menu::KeyBind, segmented_button},
|
widget::{self, menu::KeyBind, segmented_button},
|
||||||
|
|
@ -27,6 +30,7 @@ use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
env, fmt, fs,
|
env, fmt, fs,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
str::FromStr,
|
||||||
time,
|
time,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -66,6 +70,13 @@ impl DialogKind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn accept_label(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::SaveFile { .. } => fl!("save"),
|
||||||
|
_ => fl!("open"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_dir(&self) -> bool {
|
pub fn is_dir(&self) -> bool {
|
||||||
matches!(self, Self::OpenFolder | Self::OpenMultipleFolders)
|
matches!(self, Self::OpenFolder | Self::OpenMultipleFolders)
|
||||||
}
|
}
|
||||||
|
|
@ -79,6 +90,51 @@ impl DialogKind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DialogChoiceOption {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for DialogChoiceOption {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum DialogChoice {
|
||||||
|
CheckBox {
|
||||||
|
id: String,
|
||||||
|
label: String,
|
||||||
|
value: bool,
|
||||||
|
},
|
||||||
|
ComboBox {
|
||||||
|
id: String,
|
||||||
|
label: String,
|
||||||
|
options: Vec<DialogChoiceOption>,
|
||||||
|
selected: Option<usize>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum DialogFilterPattern {
|
||||||
|
Glob(String),
|
||||||
|
Mime(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DialogFilter {
|
||||||
|
pub label: String,
|
||||||
|
pub patterns: Vec<DialogFilterPattern>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for DialogFilter {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Dialog<M> {
|
pub struct Dialog<M> {
|
||||||
cosmic: Cosmic<App>,
|
cosmic: Cosmic<App>,
|
||||||
mapper: fn(DialogMessage) -> M,
|
mapper: fn(DialogMessage) -> M,
|
||||||
|
|
@ -139,6 +195,47 @@ impl<M: Send + 'static> Dialog<M> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_title(&mut self, title: impl Into<String>) -> Command<M> {
|
||||||
|
let mapper = self.mapper;
|
||||||
|
self.cosmic.app.title = title.into();
|
||||||
|
self.cosmic
|
||||||
|
.app
|
||||||
|
.update_title()
|
||||||
|
.map(DialogMessage)
|
||||||
|
.map(move |message| app::Message::App(mapper(message)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_accept_label(&mut self, accept_label: impl Into<String>) {
|
||||||
|
self.cosmic.app.accept_label = accept_label.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn choices(&self) -> &[DialogChoice] {
|
||||||
|
&self.cosmic.app.choices
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_choices(&mut self, choices: impl Into<Vec<DialogChoice>>) {
|
||||||
|
self.cosmic.app.choices = choices.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filters(&self) -> (&[DialogFilter], Option<usize>) {
|
||||||
|
(&self.cosmic.app.filters, self.cosmic.app.filter_selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_filters(
|
||||||
|
&mut self,
|
||||||
|
filters: impl Into<Vec<DialogFilter>>,
|
||||||
|
filter_selected: Option<usize>,
|
||||||
|
) -> Command<M> {
|
||||||
|
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<M> {
|
pub fn subscription(&self) -> Subscription<M> {
|
||||||
self.cosmic
|
self.cosmic
|
||||||
.subscription()
|
.subscription()
|
||||||
|
|
@ -183,7 +280,9 @@ struct Flags {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum Message {
|
enum Message {
|
||||||
Cancel,
|
Cancel,
|
||||||
|
Choice(usize, usize),
|
||||||
Filename(String),
|
Filename(String),
|
||||||
|
Filter(usize),
|
||||||
Modifiers(Modifiers),
|
Modifiers(Modifiers),
|
||||||
NotifyEvents(Vec<DebouncedEvent>),
|
NotifyEvents(Vec<DebouncedEvent>),
|
||||||
NotifyWatcher(WatcherWrapper),
|
NotifyWatcher(WatcherWrapper),
|
||||||
|
|
@ -219,6 +318,11 @@ impl PartialEq for WatcherWrapper {
|
||||||
struct App {
|
struct App {
|
||||||
core: Core,
|
core: Core,
|
||||||
flags: Flags,
|
flags: Flags,
|
||||||
|
title: String,
|
||||||
|
accept_label: String,
|
||||||
|
choices: Vec<DialogChoice>,
|
||||||
|
filters: Vec<DialogFilter>,
|
||||||
|
filter_selected: Option<usize>,
|
||||||
filename_id: widget::Id,
|
filename_id: widget::Id,
|
||||||
modifiers: Modifiers,
|
modifiers: Modifiers,
|
||||||
nav_model: segmented_button::SingleSelectModel,
|
nav_model: segmented_button::SingleSelectModel,
|
||||||
|
|
@ -248,9 +352,8 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_title(&mut self) -> Command<Message> {
|
fn update_title(&mut self) -> Command<Message> {
|
||||||
let title = self.flags.kind.title();
|
self.set_header_title(self.title.clone());
|
||||||
self.set_header_title(title.clone());
|
self.set_window_title(self.title.clone(), self.main_window_id())
|
||||||
self.set_window_title(title, self.main_window_id())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_watcher(&mut self) -> Command<Message> {
|
fn update_watcher(&mut self) -> Command<Message> {
|
||||||
|
|
@ -329,6 +432,9 @@ impl Application for App {
|
||||||
//TODO: make set_nav_bar_toggle_condensed pub
|
//TODO: make set_nav_bar_toggle_condensed pub
|
||||||
core.nav_bar_toggle_condensed();
|
core.nav_bar_toggle_condensed();
|
||||||
|
|
||||||
|
let title = flags.kind.title();
|
||||||
|
let accept_label = flags.kind.accept_label();
|
||||||
|
|
||||||
let mut nav_model = segmented_button::ModelBuilder::default();
|
let mut nav_model = segmented_button::ModelBuilder::default();
|
||||||
if let Some(dir) = dirs::home_dir() {
|
if let Some(dir) = dirs::home_dir() {
|
||||||
nav_model = nav_model.insert(move |b| {
|
nav_model = nav_model.insert(move |b| {
|
||||||
|
|
@ -371,6 +477,11 @@ impl Application for App {
|
||||||
let mut app = App {
|
let mut app = App {
|
||||||
core,
|
core,
|
||||||
flags,
|
flags,
|
||||||
|
title,
|
||||||
|
accept_label,
|
||||||
|
choices: Vec::new(),
|
||||||
|
filters: Vec::new(),
|
||||||
|
filter_selected: None,
|
||||||
filename_id: widget::Id::unique(),
|
filename_id: widget::Id::unique(),
|
||||||
modifiers: Modifiers::empty(),
|
modifiers: Modifiers::empty(),
|
||||||
nav_model: nav_model.build(),
|
nav_model: nav_model.build(),
|
||||||
|
|
@ -445,6 +556,22 @@ impl Application for App {
|
||||||
return window::close(self.main_window_id());
|
return window::close(self.main_window_id());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::Choice(choice_i, option_i) => {
|
||||||
|
if let Some(choice) = self.choices.get_mut(choice_i) {
|
||||||
|
match choice {
|
||||||
|
DialogChoice::CheckBox { value, .. } => *value = option_i > 0,
|
||||||
|
DialogChoice::ComboBox {
|
||||||
|
options, selected, ..
|
||||||
|
} => {
|
||||||
|
if option_i < options.len() {
|
||||||
|
*selected = Some(option_i);
|
||||||
|
} else {
|
||||||
|
*selected = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::Filename(new_filename) => {
|
Message::Filename(new_filename) => {
|
||||||
// Select based on filename
|
// Select based on filename
|
||||||
self.tab.select_name(&new_filename);
|
self.tab.select_name(&new_filename);
|
||||||
|
|
@ -453,6 +580,14 @@ impl Application for App {
|
||||||
*filename = new_filename;
|
*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) => {
|
Message::Modifiers(modifiers) => {
|
||||||
self.modifiers = modifiers;
|
self.modifiers = modifiers;
|
||||||
}
|
}
|
||||||
|
|
@ -661,6 +796,59 @@ impl Application for App {
|
||||||
return Command::batch(commands);
|
return Command::batch(commands);
|
||||||
}
|
}
|
||||||
Message::TabRescan(mut items) => {
|
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
|
// Select based on filename
|
||||||
if let DialogKind::SaveFile { filename } = &self.flags.kind {
|
if let DialogKind::SaveFile { filename } = &self.flags.kind {
|
||||||
for item in items.iter_mut() {
|
for item in items.iter_mut() {
|
||||||
|
|
@ -690,30 +878,58 @@ impl Application for App {
|
||||||
.map(move |message| Message::TabMessage(message)),
|
.map(move |message| Message::TabMessage(message)),
|
||||||
);
|
);
|
||||||
|
|
||||||
tab_column = tab_column.push(
|
let mut row = widget::row::with_capacity(
|
||||||
widget::row::with_children(vec![
|
if !self.filters.is_empty() { 1 } else { 0 } + self.choices.len() * 2 + 3,
|
||||||
if let DialogKind::SaveFile { filename } = &self.flags.kind {
|
)
|
||||||
widget::text_input("", filename)
|
.align_items(Alignment::Center)
|
||||||
.id(self.filename_id.clone())
|
.padding(space_xxs)
|
||||||
.on_input(Message::Filename)
|
.spacing(space_xxs);
|
||||||
.on_submit(Message::Save(false))
|
if !self.filters.is_empty() {
|
||||||
.into()
|
row = row.push(widget::dropdown(
|
||||||
} else {
|
&self.filters,
|
||||||
widget::horizontal_space(Length::Fill).into()
|
self.filter_selected,
|
||||||
},
|
Message::Filter,
|
||||||
widget::button::standard(fl!("cancel"))
|
));
|
||||||
.on_press(Message::Cancel)
|
}
|
||||||
.into(),
|
if let DialogKind::SaveFile { filename } = &self.flags.kind {
|
||||||
if self.flags.kind.save() {
|
row = row.push(
|
||||||
widget::button::suggested(fl!("save")).on_press(Message::Save(false))
|
widget::text_input("", filename)
|
||||||
} else {
|
.id(self.filename_id.clone())
|
||||||
widget::button::suggested(fl!("open")).on_press(Message::Open)
|
.on_input(Message::Filename)
|
||||||
|
.on_submit(Message::Save(false)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
row = row.push(widget::horizontal_space(Length::Fill));
|
||||||
|
}
|
||||||
|
for (choice_i, choice) in self.choices.iter().enumerate() {
|
||||||
|
match choice {
|
||||||
|
DialogChoice::CheckBox { label, value, .. } => {
|
||||||
|
row = row.push(widget::text::body(label));
|
||||||
|
row = row.push(widget::checkbox("", *value, move |checked| {
|
||||||
|
Message::Choice(choice_i, if checked { 1 } else { 0 })
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
.into(),
|
DialogChoice::ComboBox {
|
||||||
])
|
label,
|
||||||
.padding(space_xxs)
|
options,
|
||||||
.spacing(space_xxs),
|
selected,
|
||||||
);
|
..
|
||||||
|
} => {
|
||||||
|
row = row.push(widget::text::body(label));
|
||||||
|
row = row.push(widget::dropdown(options, *selected, move |option_i| {
|
||||||
|
Message::Choice(choice_i, option_i)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row = row.push(widget::button::standard(fl!("cancel")).on_press(Message::Cancel));
|
||||||
|
row = row.push(if self.flags.kind.save() {
|
||||||
|
widget::button::suggested(&self.accept_label).on_press(Message::Save(false))
|
||||||
|
} else {
|
||||||
|
widget::button::suggested(&self.accept_label).on_press(Message::Open)
|
||||||
|
});
|
||||||
|
|
||||||
|
tab_column = tab_column.push(row);
|
||||||
|
|
||||||
let content: Element<_> = tab_column.into();
|
let content: Element<_> = tab_column.into();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue