2024-06-27 09:03:44 -06:00
|
|
|
// Copyright 2023 System76 <info@system76.com>
|
2024-02-01 15:55:52 -07:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
|
|
|
|
|
|
use cosmic::{
|
2025-03-22 19:28:36 -06:00
|
|
|
app::{context_drawer, cosmic::Cosmic, Core, Task},
|
2024-09-11 09:08:20 -06:00
|
|
|
cosmic_config, cosmic_theme, executor,
|
2024-02-01 15:55:52 -07:00
|
|
|
iced::{
|
2025-03-22 19:28:36 -06:00
|
|
|
self, event,
|
2024-02-01 15:55:52 -07:00
|
|
|
futures::{self, SinkExt},
|
2024-10-02 16:07:19 -06:00
|
|
|
keyboard::{Event as KeyEvent, Key, Modifiers},
|
2025-03-22 19:28:36 -06:00
|
|
|
mouse, stream, window, Alignment, Event, Length, Point, Size, Subscription,
|
2024-02-01 15:55:52 -07:00
|
|
|
},
|
2024-02-29 12:26:45 -07:00
|
|
|
theme,
|
2024-08-20 13:26:10 -06:00
|
|
|
widget::{
|
|
|
|
|
self,
|
2025-03-19 14:44:13 -06:00
|
|
|
menu::{key_bind::Modifier, Action as MenuAction, KeyBind},
|
2024-08-20 13:26:10 -06:00
|
|
|
segmented_button,
|
|
|
|
|
},
|
2024-02-01 15:55:52 -07:00
|
|
|
Application, ApplicationExt, Element,
|
|
|
|
|
};
|
2024-03-20 11:54:37 -06:00
|
|
|
use notify_debouncer_full::{
|
|
|
|
|
new_debouncer,
|
|
|
|
|
notify::{self, RecommendedWatcher, Watcher},
|
|
|
|
|
DebouncedEvent, Debouncer, FileIdMap,
|
|
|
|
|
};
|
2024-09-03 00:09:49 +02:00
|
|
|
use recently_used_xbel::update_recently_used;
|
2024-02-28 15:46:23 -07:00
|
|
|
use std::{
|
|
|
|
|
any::TypeId,
|
2024-09-13 08:10:02 -06:00
|
|
|
collections::{HashMap, HashSet, VecDeque},
|
2024-03-20 11:54:37 -06:00
|
|
|
env, fmt, fs,
|
2024-10-10 13:27:50 -06:00
|
|
|
num::NonZeroU16,
|
2024-02-28 15:46:23 -07:00
|
|
|
path::PathBuf,
|
2024-07-03 12:24:35 -06:00
|
|
|
str::FromStr,
|
2024-10-09 15:41:10 -06:00
|
|
|
time::{self, Instant},
|
2024-02-28 15:46:23 -07:00
|
|
|
};
|
2024-02-01 15:55:52 -07:00
|
|
|
|
|
|
|
|
use crate::{
|
2024-09-20 19:36:50 -06:00
|
|
|
app::{Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind},
|
2025-04-14 08:59:32 -06:00
|
|
|
config::{Config, Favorite, IconSizes, TabConfig, TimeConfig, TIME_CONFIG_ID},
|
2024-02-01 15:55:52 -07:00
|
|
|
fl, home_dir,
|
2024-10-02 16:07:19 -06:00
|
|
|
key_bind::key_binds,
|
2024-09-11 09:08:20 -06:00
|
|
|
localize::LANGUAGE_SORTER,
|
2024-09-11 14:22:40 -06:00
|
|
|
menu,
|
2024-10-09 15:41:10 -06:00
|
|
|
mounter::{MounterItem, MounterItems, MounterKey, MounterMessage, MOUNTERS},
|
2024-03-20 16:27:00 -06:00
|
|
|
tab::{self, ItemMetadata, Location, Tab},
|
2024-02-01 15:55:52 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
2025-03-15 11:59:03 -04:00
|
|
|
pub struct DialogMessage(cosmic::Action<Message>);
|
2024-02-13 12:29:50 -07:00
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub enum DialogResult {
|
|
|
|
|
Cancel,
|
|
|
|
|
Open(Vec<PathBuf>),
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-20 11:58:39 -07:00
|
|
|
#[derive(Clone, Debug)]
|
2024-02-15 15:03:01 -07:00
|
|
|
pub enum DialogKind {
|
|
|
|
|
OpenFile,
|
|
|
|
|
OpenFolder,
|
|
|
|
|
OpenMultipleFiles,
|
|
|
|
|
OpenMultipleFolders,
|
2024-02-20 11:58:39 -07:00
|
|
|
SaveFile { filename: String },
|
2024-02-15 15:03:01 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DialogKind {
|
|
|
|
|
pub fn title(&self) -> String {
|
|
|
|
|
match self {
|
|
|
|
|
Self::OpenFile => fl!("open-file"),
|
|
|
|
|
Self::OpenFolder => fl!("open-folder"),
|
|
|
|
|
Self::OpenMultipleFiles => fl!("open-multiple-files"),
|
|
|
|
|
Self::OpenMultipleFolders => fl!("open-multiple-folders"),
|
2024-02-20 11:58:39 -07:00
|
|
|
Self::SaveFile { .. } => fl!("save-file"),
|
2024-02-15 15:03:01 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-03 09:25:23 -06:00
|
|
|
pub fn accept_label(&self) -> String {
|
|
|
|
|
match self {
|
|
|
|
|
Self::SaveFile { .. } => fl!("save"),
|
|
|
|
|
_ => fl!("open"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-26 14:11:45 -07:00
|
|
|
pub fn is_dir(&self) -> bool {
|
|
|
|
|
matches!(self, Self::OpenFolder | Self::OpenMultipleFolders)
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-15 15:03:01 -07:00
|
|
|
pub fn multiple(&self) -> bool {
|
|
|
|
|
matches!(self, Self::OpenMultipleFiles | Self::OpenMultipleFolders)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn save(&self) -> bool {
|
2024-02-20 11:58:39 -07:00
|
|
|
matches!(self, Self::SaveFile { .. })
|
2024-02-15 15:03:01 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-03 12:24:35 -06:00
|
|
|
#[derive(Clone, Debug)]
|
2024-07-03 09:25:23 -06:00
|
|
|
pub struct DialogChoiceOption {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub label: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AsRef<str> for DialogChoiceOption {
|
|
|
|
|
fn as_ref(&self) -> &str {
|
|
|
|
|
&self.label
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-03 12:24:35 -06:00
|
|
|
#[derive(Clone, Debug)]
|
2024-07-03 09:25:23 -06:00
|
|
|
pub enum DialogChoice {
|
|
|
|
|
CheckBox {
|
|
|
|
|
id: String,
|
|
|
|
|
label: String,
|
|
|
|
|
value: bool,
|
|
|
|
|
},
|
|
|
|
|
ComboBox {
|
|
|
|
|
id: String,
|
|
|
|
|
label: String,
|
|
|
|
|
options: Vec<DialogChoiceOption>,
|
|
|
|
|
selected: Option<usize>,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-03 12:24:35 -06:00
|
|
|
#[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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 14:44:13 -06:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct DialogLabelSpan {
|
|
|
|
|
pub text: String,
|
|
|
|
|
pub underline: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct DialogLabel {
|
|
|
|
|
pub spans: Vec<DialogLabelSpan>,
|
|
|
|
|
pub key_bind_opt: Option<KeyBind>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: AsRef<str>> From<T> for DialogLabel {
|
|
|
|
|
fn from(text: T) -> Self {
|
|
|
|
|
let mut spans = Vec::<DialogLabelSpan>::new();
|
|
|
|
|
let mut key_bind_opt = None;
|
|
|
|
|
let mut next_underline = false;
|
|
|
|
|
for c in text.as_ref().chars() {
|
|
|
|
|
let underline = next_underline;
|
|
|
|
|
next_underline = false;
|
|
|
|
|
|
|
|
|
|
if c == '_' {
|
|
|
|
|
if !underline {
|
|
|
|
|
next_underline = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if underline && key_bind_opt.is_none() {
|
|
|
|
|
key_bind_opt = Some(KeyBind {
|
|
|
|
|
modifiers: vec![Modifier::Alt],
|
|
|
|
|
key: Key::Character(c.to_lowercase().to_string().into()),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(span) = spans.last_mut() {
|
|
|
|
|
if underline == span.underline {
|
|
|
|
|
span.text.push(c);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
spans.push(DialogLabelSpan {
|
|
|
|
|
text: String::from(c),
|
|
|
|
|
underline,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-31 09:17:39 -06:00
|
|
|
Self {
|
2025-03-19 14:44:13 -06:00
|
|
|
spans,
|
2025-03-31 09:17:39 -06:00
|
|
|
key_bind_opt,
|
|
|
|
|
}
|
2025-03-19 14:44:13 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a, M: Clone + 'static> From<&'a DialogLabel> for Element<'a, M> {
|
|
|
|
|
fn from(label: &'a DialogLabel) -> Self {
|
|
|
|
|
let mut iced_spans = Vec::with_capacity(label.spans.len());
|
|
|
|
|
for span in label.spans.iter() {
|
|
|
|
|
iced_spans.push(cosmic::iced::widget::span(&span.text).underline(span.underline));
|
|
|
|
|
}
|
|
|
|
|
cosmic::iced::widget::rich_text(iced_spans).into()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-13 12:29:50 -07:00
|
|
|
pub struct Dialog<M> {
|
|
|
|
|
cosmic: Cosmic<App>,
|
|
|
|
|
mapper: fn(DialogMessage) -> M,
|
2024-02-15 15:03:01 -07:00
|
|
|
on_result: Box<dyn Fn(DialogResult) -> M>,
|
2024-02-13 12:29:50 -07:00
|
|
|
}
|
|
|
|
|
|
2024-02-15 15:03:01 -07:00
|
|
|
impl<M: Send + 'static> Dialog<M> {
|
2024-02-13 12:29:50 -07:00
|
|
|
pub fn new(
|
2024-02-15 15:03:01 -07:00
|
|
|
kind: DialogKind,
|
|
|
|
|
path_opt: Option<PathBuf>,
|
2024-02-13 12:29:50 -07:00
|
|
|
mapper: fn(DialogMessage) -> M,
|
2024-02-15 15:03:01 -07:00
|
|
|
on_result: impl Fn(DialogResult) -> M + 'static,
|
2024-10-21 13:51:10 -06:00
|
|
|
) -> (Self, Task<M>) {
|
2024-02-15 15:03:01 -07:00
|
|
|
//TODO: only do this once somehow?
|
|
|
|
|
crate::localize::localize();
|
|
|
|
|
|
2024-09-11 09:08:20 -06:00
|
|
|
let (config_handler, config) = Config::load();
|
|
|
|
|
|
2025-01-20 02:48:55 -05:00
|
|
|
let mut settings = window::Settings {
|
|
|
|
|
decorations: false,
|
|
|
|
|
exit_on_close_request: false,
|
|
|
|
|
min_size: Some(Size::new(360.0, 180.0)),
|
|
|
|
|
resizable: true,
|
|
|
|
|
size: Size::new(1024.0, 640.0),
|
|
|
|
|
transparent: true,
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
2024-02-13 13:12:23 -07:00
|
|
|
|
|
|
|
|
#[cfg(target_os = "linux")]
|
2024-02-13 12:49:52 -07:00
|
|
|
{
|
2024-02-13 13:12:23 -07:00
|
|
|
settings.platform_specific.application_id = App::APP_ID.to_string();
|
2024-02-13 12:49:52 -07:00
|
|
|
}
|
2024-02-13 13:12:23 -07:00
|
|
|
|
2024-10-21 14:14:43 -06:00
|
|
|
let (window_id, window_command) = window::open(settings.clone());
|
2024-02-13 12:29:50 -07:00
|
|
|
|
2024-10-21 15:52:10 -06:00
|
|
|
let mut core = Core::default();
|
2024-10-22 08:21:51 -06:00
|
|
|
core.set_main_window_id(Some(window_id));
|
2024-02-15 15:03:01 -07:00
|
|
|
let flags = Flags {
|
|
|
|
|
kind,
|
|
|
|
|
path_opt: path_opt
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|path| match fs::canonicalize(path) {
|
|
|
|
|
Ok(ok) => Some(ok),
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::warn!("failed to canonicalize {:?}: {}", path, err);
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}),
|
2024-10-21 14:14:43 -06:00
|
|
|
window_id,
|
2024-09-11 09:08:20 -06:00
|
|
|
config_handler,
|
|
|
|
|
config,
|
2024-02-15 15:03:01 -07:00
|
|
|
};
|
2024-10-21 13:51:10 -06:00
|
|
|
|
2024-10-22 08:21:51 -06:00
|
|
|
let (cosmic, cosmic_command) = Cosmic::<App>::init((core, flags));
|
2024-02-13 12:29:50 -07:00
|
|
|
(
|
|
|
|
|
Self {
|
|
|
|
|
cosmic,
|
|
|
|
|
mapper,
|
2024-02-15 15:03:01 -07:00
|
|
|
on_result: Box::new(on_result),
|
2024-02-13 12:29:50 -07:00
|
|
|
},
|
2024-10-21 14:14:43 -06:00
|
|
|
Task::batch([
|
2025-03-15 11:59:03 -04:00
|
|
|
window_command.map(|_id| cosmic::action::none()),
|
2024-10-21 14:14:43 -06:00
|
|
|
cosmic_command
|
|
|
|
|
.map(DialogMessage)
|
2025-03-15 11:59:03 -04:00
|
|
|
.map(move |message| cosmic::action::app(mapper(message))),
|
2024-10-21 14:14:43 -06:00
|
|
|
]),
|
2024-02-13 12:29:50 -07:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-21 13:51:10 -06:00
|
|
|
pub fn set_title(&mut self, title: impl Into<String>) -> Task<M> {
|
2024-07-03 09:25:23 -06:00
|
|
|
let mapper = self.mapper;
|
|
|
|
|
self.cosmic.app.title = title.into();
|
|
|
|
|
self.cosmic
|
|
|
|
|
.app
|
|
|
|
|
.update_title()
|
|
|
|
|
.map(DialogMessage)
|
2025-03-15 11:59:03 -04:00
|
|
|
.map(move |message| cosmic::action::app(mapper(message)))
|
2024-07-03 09:25:23 -06:00
|
|
|
}
|
|
|
|
|
|
2025-03-19 14:44:13 -06:00
|
|
|
pub fn set_accept_label(&mut self, accept_label: impl AsRef<str>) {
|
|
|
|
|
self.cosmic.app.accept_label = DialogLabel::from(accept_label);
|
2024-07-03 09:25:23 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-03 12:24:35 -06:00
|
|
|
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>,
|
2024-10-21 13:51:10 -06:00
|
|
|
) -> Task<M> {
|
2024-07-03 12:24:35 -06:00
|
|
|
let mapper = self.mapper;
|
|
|
|
|
self.cosmic.app.filters = filters.into();
|
|
|
|
|
self.cosmic.app.filter_selected = filter_selected;
|
|
|
|
|
self.cosmic
|
|
|
|
|
.app
|
|
|
|
|
.rescan_tab()
|
|
|
|
|
.map(DialogMessage)
|
2025-03-15 11:59:03 -04:00
|
|
|
.map(move |message| cosmic::action::app(mapper(message)))
|
2024-07-03 12:24:35 -06:00
|
|
|
}
|
|
|
|
|
|
2024-02-13 12:29:50 -07:00
|
|
|
pub fn subscription(&self) -> Subscription<M> {
|
|
|
|
|
self.cosmic
|
|
|
|
|
.subscription()
|
|
|
|
|
.map(DialogMessage)
|
2025-01-05 13:50:08 -07:00
|
|
|
.with(self.mapper)
|
|
|
|
|
.map(|(mapper, message)| mapper(message))
|
2024-02-13 12:29:50 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-21 13:51:10 -06:00
|
|
|
pub fn update(&mut self, message: DialogMessage) -> Task<M> {
|
2024-02-13 12:29:50 -07:00
|
|
|
let mapper = self.mapper;
|
|
|
|
|
let command = self
|
|
|
|
|
.cosmic
|
|
|
|
|
.update(message.0)
|
|
|
|
|
.map(DialogMessage)
|
2025-03-15 11:59:03 -04:00
|
|
|
.map(move |message| cosmic::action::app(mapper(message)));
|
2024-02-13 12:29:50 -07:00
|
|
|
if let Some(result) = self.cosmic.app.result_opt.take() {
|
2024-02-15 15:03:01 -07:00
|
|
|
let on_result_message = (self.on_result)(result);
|
2024-10-21 13:51:10 -06:00
|
|
|
Task::batch([
|
2024-02-13 12:29:50 -07:00
|
|
|
command,
|
2025-03-15 11:59:03 -04:00
|
|
|
Task::perform(async move { cosmic::action::app(on_result_message) }, |x| x),
|
2024-02-13 12:29:50 -07:00
|
|
|
])
|
|
|
|
|
} else {
|
|
|
|
|
command
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn view(&self, window_id: window::Id) -> Element<M> {
|
|
|
|
|
self.cosmic
|
|
|
|
|
.view(window_id)
|
|
|
|
|
.map(DialogMessage)
|
|
|
|
|
.map(self.mapper)
|
|
|
|
|
}
|
2024-07-03 13:14:25 -06:00
|
|
|
|
|
|
|
|
pub fn window_id(&self) -> window::Id {
|
2024-10-21 13:51:10 -06:00
|
|
|
self.cosmic.app.flags.window_id
|
2024-07-03 13:14:25 -06:00
|
|
|
}
|
2024-02-13 12:29:50 -07:00
|
|
|
}
|
|
|
|
|
|
2024-09-13 08:10:02 -06:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
enum DialogPage {
|
|
|
|
|
NewFolder { parent: PathBuf, name: String },
|
|
|
|
|
Replace { filename: String },
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-13 12:29:50 -07:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
struct Flags {
|
2024-02-15 15:03:01 -07:00
|
|
|
kind: DialogKind,
|
|
|
|
|
path_opt: Option<PathBuf>,
|
2024-02-13 12:29:50 -07:00
|
|
|
window_id: window::Id,
|
2025-01-23 14:45:03 -07:00
|
|
|
#[allow(dead_code)]
|
2024-09-11 09:08:20 -06:00
|
|
|
config_handler: Option<cosmic_config::Config>,
|
|
|
|
|
config: Config,
|
2024-02-01 17:43:41 -07:00
|
|
|
}
|
2024-02-01 15:55:52 -07:00
|
|
|
|
|
|
|
|
/// Messages that are used specifically by our [`App`].
|
|
|
|
|
#[derive(Clone, Debug)]
|
2024-02-13 12:29:50 -07:00
|
|
|
enum Message {
|
2024-09-11 14:22:40 -06:00
|
|
|
None,
|
2024-02-01 17:34:22 -07:00
|
|
|
Cancel,
|
2024-07-03 09:25:23 -06:00
|
|
|
Choice(usize, usize),
|
2024-09-11 09:08:20 -06:00
|
|
|
Config(Config),
|
2025-03-22 19:28:36 -06:00
|
|
|
CursorMoved(Point),
|
2024-09-13 08:10:02 -06:00
|
|
|
DialogCancel,
|
|
|
|
|
DialogComplete,
|
|
|
|
|
DialogUpdate(DialogPage),
|
2024-02-15 15:03:01 -07:00
|
|
|
Filename(String),
|
2024-07-03 12:24:35 -06:00
|
|
|
Filter(usize),
|
2024-10-02 16:07:19 -06:00
|
|
|
Key(Modifiers, Key),
|
2025-03-26 00:20:50 +01:00
|
|
|
ModifiersChanged(Modifiers),
|
2024-09-11 09:08:20 -06:00
|
|
|
MounterItems(MounterKey, MounterItems),
|
2024-09-13 08:10:02 -06:00
|
|
|
NewFolder,
|
2024-03-20 11:54:37 -06:00
|
|
|
NotifyEvents(Vec<DebouncedEvent>),
|
2024-02-01 15:55:52 -07:00
|
|
|
NotifyWatcher(WatcherWrapper),
|
2024-02-01 17:34:22 -07:00
|
|
|
Open,
|
2024-10-04 11:07:27 -06:00
|
|
|
Preview,
|
2024-02-26 15:15:49 -07:00
|
|
|
Save(bool),
|
2025-03-22 19:28:36 -06:00
|
|
|
ScrollTab(i16),
|
2024-09-11 13:56:35 -06:00
|
|
|
SearchActivate,
|
|
|
|
|
SearchClear,
|
|
|
|
|
SearchInput(String),
|
2025-03-15 11:59:03 -04:00
|
|
|
Surface(cosmic::surface::Action),
|
2025-01-20 02:48:55 -05:00
|
|
|
#[allow(clippy::enum_variant_names)]
|
2024-02-15 15:03:01 -07:00
|
|
|
TabMessage(tab::Message),
|
2024-10-10 13:53:01 -06:00
|
|
|
TabRescan(Location, Option<tab::Item>, Vec<tab::Item>),
|
2024-10-10 13:27:50 -06:00
|
|
|
TabView(tab::View),
|
2025-04-14 08:59:32 -06:00
|
|
|
TimeConfigChange(TimeConfig),
|
2024-10-10 13:27:50 -06:00
|
|
|
ToggleFoldersFirst,
|
|
|
|
|
ZoomDefault,
|
|
|
|
|
ZoomIn,
|
|
|
|
|
ZoomOut,
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
|
2024-09-20 19:36:50 -06:00
|
|
|
impl From<AppMessage> for Message {
|
|
|
|
|
fn from(app_message: AppMessage) -> Message {
|
|
|
|
|
match app_message {
|
2024-10-10 13:27:50 -06:00
|
|
|
AppMessage::None => Message::None,
|
2024-10-04 11:53:27 -06:00
|
|
|
AppMessage::Preview(_entity_opt) => Message::Preview,
|
2024-10-03 13:01:59 -06:00
|
|
|
AppMessage::SearchActivate => Message::SearchActivate,
|
2025-03-22 19:28:36 -06:00
|
|
|
AppMessage::ScrollTab(scroll_speed) => Message::ScrollTab(scroll_speed),
|
2024-09-20 19:36:50 -06:00
|
|
|
AppMessage::TabMessage(_entity_opt, tab_message) => Message::TabMessage(tab_message),
|
2024-10-10 13:27:50 -06:00
|
|
|
AppMessage::TabView(_entity_opt, view) => Message::TabView(view),
|
|
|
|
|
AppMessage::ToggleFoldersFirst => Message::ToggleFoldersFirst,
|
|
|
|
|
AppMessage::ZoomDefault(_entity_opt) => Message::ZoomDefault,
|
|
|
|
|
AppMessage::ZoomIn(_entity_opt) => Message::ZoomIn,
|
|
|
|
|
AppMessage::ZoomOut(_entity_opt) => Message::ZoomOut,
|
2025-01-05 12:08:00 -07:00
|
|
|
AppMessage::NewItem(_entity_opt, true) => Message::NewFolder,
|
2024-09-20 19:36:50 -06:00
|
|
|
unsupported => {
|
|
|
|
|
log::warn!("{unsupported:?} not supported in dialog mode");
|
|
|
|
|
Message::None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-11 09:08:20 -06:00
|
|
|
pub struct MounterData(MounterKey, MounterItem);
|
|
|
|
|
|
2024-02-13 12:29:50 -07:00
|
|
|
struct WatcherWrapper {
|
2024-03-20 11:54:37 -06:00
|
|
|
watcher_opt: Option<Debouncer<RecommendedWatcher, FileIdMap>>,
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Clone for WatcherWrapper {
|
|
|
|
|
fn clone(&self) -> Self {
|
|
|
|
|
Self { watcher_opt: None }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-20 11:54:37 -06:00
|
|
|
impl fmt::Debug for WatcherWrapper {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
f.debug_struct("WatcherWrapper").finish()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-01 15:55:52 -07:00
|
|
|
impl PartialEq for WatcherWrapper {
|
|
|
|
|
fn eq(&self, _other: &Self) -> bool {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The [`App`] stores application-specific state.
|
2024-02-13 12:29:50 -07:00
|
|
|
struct App {
|
2024-02-01 15:55:52 -07:00
|
|
|
core: Core,
|
2024-02-01 17:43:41 -07:00
|
|
|
flags: Flags,
|
2024-07-03 09:25:23 -06:00
|
|
|
title: String,
|
2025-03-19 14:44:13 -06:00
|
|
|
accept_label: DialogLabel,
|
2024-07-03 09:25:23 -06:00
|
|
|
choices: Vec<DialogChoice>,
|
2024-09-20 19:36:50 -06:00
|
|
|
context_page: ContextPage,
|
2024-09-13 08:10:02 -06:00
|
|
|
dialog_pages: VecDeque<DialogPage>,
|
|
|
|
|
dialog_text_input: widget::Id,
|
2024-07-03 12:24:35 -06:00
|
|
|
filters: Vec<DialogFilter>,
|
|
|
|
|
filter_selected: Option<usize>,
|
2024-02-15 15:03:01 -07:00
|
|
|
filename_id: widget::Id,
|
2024-02-13 12:29:50 -07:00
|
|
|
modifiers: Modifiers,
|
2024-09-11 09:08:20 -06:00
|
|
|
mounter_items: HashMap<MounterKey, MounterItems>,
|
2024-02-01 15:55:52 -07:00
|
|
|
nav_model: segmented_button::SingleSelectModel,
|
2024-02-13 12:29:50 -07:00
|
|
|
result_opt: Option<DialogResult>,
|
2024-09-11 13:56:35 -06:00
|
|
|
search_id: widget::Id,
|
2024-02-15 15:03:01 -07:00
|
|
|
tab: Tab,
|
2024-05-31 09:51:34 -06:00
|
|
|
key_binds: HashMap<KeyBind, Action>,
|
2024-03-20 11:54:37 -06:00
|
|
|
watcher_opt: Option<(Debouncer<RecommendedWatcher, FileIdMap>, HashSet<PathBuf>)>,
|
2025-03-22 19:28:36 -06:00
|
|
|
auto_scroll_speed: Option<i16>,
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl App {
|
2024-10-09 15:41:10 -06:00
|
|
|
fn button_view(&self) -> Element<Message> {
|
2024-10-10 11:15:32 -06:00
|
|
|
let cosmic_theme::Spacing {
|
2025-03-19 14:44:13 -06:00
|
|
|
space_xxxs,
|
2024-10-10 11:15:32 -06:00
|
|
|
space_xxs,
|
|
|
|
|
space_xs,
|
2025-03-19 14:44:13 -06:00
|
|
|
space_s,
|
|
|
|
|
space_l,
|
2024-10-10 11:15:32 -06:00
|
|
|
..
|
|
|
|
|
} = theme::active().cosmic().spacing;
|
2024-09-23 15:56:32 -06:00
|
|
|
|
2024-10-10 11:15:32 -06:00
|
|
|
let mut col = widget::column::with_capacity(2).spacing(space_xxs);
|
2024-10-09 15:41:10 -06:00
|
|
|
if let DialogKind::SaveFile { filename } = &self.flags.kind {
|
|
|
|
|
col = col.push(
|
|
|
|
|
widget::text_input("", filename)
|
|
|
|
|
.id(self.filename_id.clone())
|
|
|
|
|
.on_input(Message::Filename)
|
2025-03-15 11:59:03 -04:00
|
|
|
.on_submit(|_| Message::Save(false)),
|
2024-10-09 15:41:10 -06:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-23 15:56:32 -06:00
|
|
|
let mut row = widget::row::with_capacity(
|
|
|
|
|
if !self.filters.is_empty() { 1 } else { 0 } + self.choices.len() * 2 + 3,
|
|
|
|
|
)
|
2024-10-21 13:51:10 -06:00
|
|
|
.align_y(Alignment::Center)
|
2024-09-23 15:56:32 -06:00
|
|
|
.spacing(space_xxs);
|
|
|
|
|
if !self.filters.is_empty() {
|
|
|
|
|
row = row.push(widget::dropdown(
|
|
|
|
|
&self.filters,
|
|
|
|
|
self.filter_selected,
|
|
|
|
|
Message::Filter,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
for (choice_i, choice) in self.choices.iter().enumerate() {
|
|
|
|
|
match choice {
|
|
|
|
|
DialogChoice::CheckBox { label, value, .. } => {
|
2024-10-21 13:51:10 -06:00
|
|
|
row = row.push(widget::checkbox(label, *value).on_toggle(move |checked| {
|
2024-09-23 15:56:32 -06:00
|
|
|
Message::Choice(choice_i, if checked { 1 } else { 0 })
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
DialogChoice::ComboBox {
|
|
|
|
|
label,
|
|
|
|
|
options,
|
|
|
|
|
selected,
|
|
|
|
|
..
|
|
|
|
|
} => {
|
|
|
|
|
row = row.push(widget::text::heading(label));
|
|
|
|
|
row = row.push(widget::dropdown(options, *selected, move |option_i| {
|
|
|
|
|
Message::Choice(choice_i, option_i)
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-21 13:51:10 -06:00
|
|
|
row = row.push(widget::horizontal_space());
|
2024-09-23 15:56:32 -06:00
|
|
|
row = row.push(widget::button::standard(fl!("cancel")).on_press(Message::Cancel));
|
2025-03-19 14:44:13 -06:00
|
|
|
|
|
|
|
|
let mut has_selected = false;
|
|
|
|
|
if let Some(items) = self.tab.items_opt() {
|
|
|
|
|
for item in items.iter() {
|
|
|
|
|
if item.selected {
|
|
|
|
|
has_selected = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
row = row.push(
|
|
|
|
|
//TODO: easier way to create buttons with rich text
|
|
|
|
|
widget::button::custom(
|
|
|
|
|
widget::row::with_children(vec![Element::from(&self.accept_label)])
|
|
|
|
|
.padding([0, space_s])
|
|
|
|
|
.width(Length::Shrink)
|
|
|
|
|
.height(space_l)
|
|
|
|
|
.spacing(space_xxxs)
|
|
|
|
|
.align_y(Alignment::Center)
|
|
|
|
|
)
|
|
|
|
|
.padding(0)
|
|
|
|
|
.on_press_maybe(if self.flags.kind.save() {
|
|
|
|
|
Some(Message::Save(false))
|
2025-03-19 16:11:10 -06:00
|
|
|
} else if has_selected || self.flags.kind.is_dir() {
|
2025-03-19 14:44:13 -06:00
|
|
|
Some(Message::Open)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
})
|
|
|
|
|
.class(widget::button::ButtonClass::Suggested)
|
|
|
|
|
/*TODO: a11y feature: .label(&self.accept_label.text)*/
|
|
|
|
|
);
|
2024-09-23 15:56:32 -06:00
|
|
|
|
2024-10-09 15:41:10 -06:00
|
|
|
col = col.push(row);
|
|
|
|
|
|
2024-10-10 11:15:32 -06:00
|
|
|
widget::layer_container(col)
|
|
|
|
|
.layer(cosmic_theme::Layer::Primary)
|
2024-11-22 16:47:16 +01:00
|
|
|
.padding([8, space_xs])
|
2024-10-10 11:15:32 -06:00
|
|
|
.into()
|
2024-09-23 15:56:32 -06:00
|
|
|
}
|
|
|
|
|
|
2025-01-24 11:55:56 -07:00
|
|
|
fn preview<'a>(&'a self, kind: &'a PreviewKind) -> Element<'a, tab::Message> {
|
2025-02-24 00:47:32 -05:00
|
|
|
let military_time = self.tab.config.military_time;
|
2024-09-20 19:36:50 -06:00
|
|
|
let mut children = Vec::with_capacity(1);
|
|
|
|
|
match kind {
|
|
|
|
|
PreviewKind::Custom(PreviewItem(item)) => {
|
2025-02-24 00:47:32 -05:00
|
|
|
children.push(item.preview_view(None, IconSizes::default(), military_time));
|
2024-09-20 19:36:50 -06:00
|
|
|
}
|
|
|
|
|
PreviewKind::Location(location) => {
|
|
|
|
|
if let Some(items) = self.tab.items_opt() {
|
|
|
|
|
for item in items.iter() {
|
|
|
|
|
if item.location_opt.as_ref() == Some(location) {
|
2025-03-03 13:04:50 -07:00
|
|
|
children.push(item.preview_view(
|
|
|
|
|
None,
|
|
|
|
|
self.tab.config.icon_sizes,
|
|
|
|
|
military_time,
|
|
|
|
|
));
|
2024-09-20 19:36:50 -06:00
|
|
|
// Only show one property view to avoid issues like hangs when generating
|
|
|
|
|
// preview images on thousands of files
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
PreviewKind::Selected => {
|
|
|
|
|
if let Some(items) = self.tab.items_opt() {
|
|
|
|
|
for item in items.iter() {
|
|
|
|
|
if item.selected {
|
2025-03-03 13:04:50 -07:00
|
|
|
children.push(item.preview_view(
|
|
|
|
|
None,
|
|
|
|
|
self.tab.config.icon_sizes,
|
|
|
|
|
military_time,
|
|
|
|
|
));
|
2024-09-20 19:36:50 -06:00
|
|
|
// Only show one property view to avoid issues like hangs when generating
|
|
|
|
|
// preview images on thousands of files
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-10 13:53:01 -06:00
|
|
|
if children.is_empty() {
|
|
|
|
|
if let Some(item) = &self.tab.parent_item_opt {
|
2025-03-03 13:04:50 -07:00
|
|
|
children.push(item.preview_view(
|
|
|
|
|
None,
|
|
|
|
|
self.tab.config.icon_sizes,
|
|
|
|
|
military_time,
|
|
|
|
|
));
|
2024-10-10 13:53:01 -06:00
|
|
|
}
|
|
|
|
|
}
|
2024-09-20 19:36:50 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-11 23:53:25 +01:00
|
|
|
widget::column::with_children(children).into()
|
2024-09-20 19:36:50 -06:00
|
|
|
}
|
|
|
|
|
|
2024-10-21 13:51:10 -06:00
|
|
|
fn rescan_tab(&self) -> Task<Message> {
|
2024-02-15 15:03:01 -07:00
|
|
|
let location = self.tab.location.clone();
|
2024-02-18 02:44:54 -05:00
|
|
|
let icon_sizes = self.tab.config.icon_sizes;
|
2024-10-21 13:51:10 -06:00
|
|
|
Task::perform(
|
2024-02-01 15:55:52 -07:00
|
|
|
async move {
|
2024-10-10 13:53:01 -06:00
|
|
|
let location2 = location.clone();
|
|
|
|
|
match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
|
|
|
|
|
Ok((parent_item_opt, items)) => {
|
2025-03-15 11:59:03 -04:00
|
|
|
cosmic::action::app(Message::TabRescan(location, parent_item_opt, items))
|
2024-10-10 13:53:01 -06:00
|
|
|
}
|
2024-02-01 15:55:52 -07:00
|
|
|
Err(err) => {
|
|
|
|
|
log::warn!("failed to rescan: {}", err);
|
2025-03-15 11:59:03 -04:00
|
|
|
cosmic::action::none()
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|x| x,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-09 15:41:10 -06:00
|
|
|
fn search_get(&self) -> Option<&str> {
|
2024-09-11 13:56:35 -06:00
|
|
|
match &self.tab.location {
|
2024-10-09 15:41:10 -06:00
|
|
|
Location::Search(_, term, ..) => Some(term),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-21 13:51:10 -06:00
|
|
|
fn search_set(&mut self, term_opt: Option<String>) -> Task<Message> {
|
2024-10-09 15:41:10 -06:00
|
|
|
let location_opt = match term_opt {
|
|
|
|
|
Some(term) => match &self.tab.location {
|
|
|
|
|
Location::Path(path) | Location::Search(path, ..) => Some((
|
2024-10-09 21:02:12 -06:00
|
|
|
Location::Search(
|
|
|
|
|
path.to_path_buf(),
|
|
|
|
|
term,
|
|
|
|
|
self.tab.config.show_hidden,
|
|
|
|
|
Instant::now(),
|
|
|
|
|
),
|
2024-10-09 15:41:10 -06:00
|
|
|
true,
|
|
|
|
|
)),
|
|
|
|
|
_ => None,
|
|
|
|
|
},
|
|
|
|
|
None => match &self.tab.location {
|
|
|
|
|
Location::Search(path, ..) => Some((Location::Path(path.to_path_buf()), false)),
|
|
|
|
|
_ => None,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
if let Some((location, focus_search)) = location_opt {
|
|
|
|
|
self.tab.change_location(&location, None);
|
2024-10-21 13:51:10 -06:00
|
|
|
return Task::batch([
|
2024-10-09 15:41:10 -06:00
|
|
|
self.update_title(),
|
|
|
|
|
self.update_watcher(),
|
|
|
|
|
self.rescan_tab(),
|
|
|
|
|
if focus_search {
|
|
|
|
|
widget::text_input::focus(self.search_id.clone())
|
2024-09-11 13:56:35 -06:00
|
|
|
} else {
|
2024-10-21 13:51:10 -06:00
|
|
|
Task::none()
|
2024-10-09 15:41:10 -06:00
|
|
|
},
|
|
|
|
|
]);
|
2024-09-11 13:56:35 -06:00
|
|
|
}
|
2024-10-21 13:51:10 -06:00
|
|
|
Task::none()
|
2024-09-11 13:56:35 -06:00
|
|
|
}
|
|
|
|
|
|
2024-10-21 13:51:10 -06:00
|
|
|
fn update_config(&mut self) -> Task<Message> {
|
2024-09-11 09:08:20 -06:00
|
|
|
self.update_nav_model();
|
2025-04-14 08:59:32 -06:00
|
|
|
|
|
|
|
|
self.update(Message::TabMessage(tab::Message::Config(
|
|
|
|
|
self.flags.config.tab,
|
|
|
|
|
)))
|
2024-09-11 09:08:20 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn activate_nav_model_location(&mut self, location: &Location) {
|
|
|
|
|
let nav_bar_id = self.nav_model.iter().find(|&id| {
|
|
|
|
|
self.nav_model
|
|
|
|
|
.data::<Location>(id)
|
|
|
|
|
.map(|l| l == location)
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if let Some(id) = nav_bar_id {
|
|
|
|
|
self.nav_model.activate(id);
|
|
|
|
|
} else {
|
|
|
|
|
let active = self.nav_model.active();
|
|
|
|
|
segmented_button::Selectable::deactivate(&mut self.nav_model, active);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update_nav_model(&mut self) {
|
|
|
|
|
let mut nav_model = segmented_button::ModelBuilder::default();
|
|
|
|
|
|
|
|
|
|
nav_model = nav_model.insert(|b| {
|
|
|
|
|
b.text(fl!("recents"))
|
2024-09-12 14:09:36 +02:00
|
|
|
.icon(widget::icon::from_name("document-open-recent-symbolic"))
|
2024-09-11 09:08:20 -06:00
|
|
|
.data(Location::Recents)
|
|
|
|
|
});
|
|
|
|
|
|
2025-01-20 02:48:55 -05:00
|
|
|
for favorite in self.flags.config.favorites.iter() {
|
2024-09-11 09:08:20 -06:00
|
|
|
if let Some(path) = favorite.path_opt() {
|
|
|
|
|
let name = if matches!(favorite, Favorite::Home) {
|
|
|
|
|
fl!("home")
|
|
|
|
|
} else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
|
|
|
|
|
file_name.to_string()
|
|
|
|
|
} else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
nav_model = nav_model.insert(move |b| {
|
|
|
|
|
b.text(name.clone())
|
|
|
|
|
.icon(
|
|
|
|
|
widget::icon::icon(if path.is_dir() {
|
|
|
|
|
tab::folder_icon_symbolic(&path, 16)
|
|
|
|
|
} else {
|
|
|
|
|
widget::icon::from_name("text-x-generic-symbolic")
|
|
|
|
|
.size(16)
|
|
|
|
|
.handle()
|
|
|
|
|
})
|
|
|
|
|
.size(16),
|
|
|
|
|
)
|
|
|
|
|
.data(Location::Path(path.clone()))
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Collect all mounter items
|
|
|
|
|
let mut nav_items = Vec::new();
|
|
|
|
|
for (key, items) in self.mounter_items.iter() {
|
|
|
|
|
for item in items.iter() {
|
|
|
|
|
nav_items.push((*key, item));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Sort by name lexically
|
|
|
|
|
nav_items.sort_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name()));
|
|
|
|
|
// Add items to nav model
|
2024-09-11 12:53:25 -06:00
|
|
|
for (i, (key, item)) in nav_items.into_iter().enumerate() {
|
2024-09-11 09:08:20 -06:00
|
|
|
nav_model = nav_model.insert(|mut b| {
|
|
|
|
|
b = b.text(item.name()).data(MounterData(key, item.clone()));
|
|
|
|
|
if let Some(path) = item.path() {
|
|
|
|
|
b = b.data(Location::Path(path.clone()));
|
|
|
|
|
}
|
2024-10-04 16:28:30 -06:00
|
|
|
if let Some(icon) = item.icon(true) {
|
2024-09-11 09:08:20 -06:00
|
|
|
b = b.icon(widget::icon::icon(icon).size(16));
|
|
|
|
|
}
|
|
|
|
|
if item.is_mounted() {
|
|
|
|
|
b = b.closable();
|
|
|
|
|
}
|
2024-09-11 12:53:25 -06:00
|
|
|
if i == 0 {
|
|
|
|
|
b = b.divider_above();
|
|
|
|
|
}
|
2024-09-11 09:08:20 -06:00
|
|
|
b
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.nav_model = nav_model.build();
|
|
|
|
|
|
|
|
|
|
self.activate_nav_model_location(&self.tab.location.clone());
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-21 13:51:10 -06:00
|
|
|
fn update_title(&mut self) -> Task<Message> {
|
2024-07-03 09:25:23 -06:00
|
|
|
self.set_header_title(self.title.clone());
|
2024-10-21 13:51:10 -06:00
|
|
|
self.set_window_title(self.title.clone(), self.flags.window_id)
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-21 13:51:10 -06:00
|
|
|
fn update_watcher(&mut self) -> Task<Message> {
|
2024-02-01 15:55:52 -07:00
|
|
|
if let Some((mut watcher, old_paths)) = self.watcher_opt.take() {
|
|
|
|
|
let mut new_paths = HashSet::new();
|
2024-10-04 16:28:30 -06:00
|
|
|
if let Some(path) = &self.tab.location.path_opt() {
|
|
|
|
|
new_paths.insert(path.to_path_buf());
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Unwatch paths no longer used
|
|
|
|
|
for path in old_paths.iter() {
|
|
|
|
|
if !new_paths.contains(path) {
|
2024-03-20 11:54:37 -06:00
|
|
|
match watcher.watcher().unwatch(path) {
|
2024-02-01 15:55:52 -07:00
|
|
|
Ok(()) => {
|
|
|
|
|
log::debug!("unwatching {:?}", path);
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::debug!("failed to unwatch {:?}: {}", path, err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Watch new paths
|
|
|
|
|
for path in new_paths.iter() {
|
|
|
|
|
if !old_paths.contains(path) {
|
|
|
|
|
//TODO: should this be recursive?
|
2024-03-20 11:54:37 -06:00
|
|
|
match watcher
|
|
|
|
|
.watcher()
|
|
|
|
|
.watch(path, notify::RecursiveMode::NonRecursive)
|
|
|
|
|
{
|
2024-02-01 15:55:52 -07:00
|
|
|
Ok(()) => {
|
|
|
|
|
log::debug!("watching {:?}", path);
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::debug!("failed to watch {:?}: {}", path, err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.watcher_opt = Some((watcher, new_paths));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//TODO: should any of this run in a command?
|
2024-10-21 13:51:10 -06:00
|
|
|
Task::none()
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Implement [`Application`] to integrate with COSMIC.
|
|
|
|
|
impl Application for App {
|
|
|
|
|
/// Default async executor to use with the app.
|
|
|
|
|
type Executor = executor::Default;
|
|
|
|
|
|
|
|
|
|
/// Argument received
|
|
|
|
|
type Flags = Flags;
|
|
|
|
|
|
|
|
|
|
/// Message type specific to our [`App`].
|
|
|
|
|
type Message = Message;
|
|
|
|
|
|
|
|
|
|
/// The unique application ID to supply to the window manager.
|
2024-02-13 12:29:50 -07:00
|
|
|
const APP_ID: &'static str = "com.system76.CosmicFilesDialog";
|
2024-02-01 15:55:52 -07:00
|
|
|
|
|
|
|
|
fn core(&self) -> &Core {
|
|
|
|
|
&self.core
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn core_mut(&mut self) -> &mut Core {
|
|
|
|
|
&mut self.core
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Creates the application, and optionally emits command on initialize.
|
2024-10-21 13:51:10 -06:00
|
|
|
fn init(mut core: Core, flags: Self::Flags) -> (Self, Task<Message>) {
|
2024-09-20 11:41:15 -06:00
|
|
|
core.window.context_is_overlay = false;
|
2024-08-26 10:21:55 -06:00
|
|
|
core.window.show_close = false;
|
2024-02-01 15:55:52 -07:00
|
|
|
core.window.show_maximize = false;
|
|
|
|
|
core.window.show_minimize = false;
|
2024-10-15 10:46:26 -06:00
|
|
|
// Only show details context drawer by default in open dialog
|
|
|
|
|
core.window.show_context = !flags.kind.save();
|
2024-02-01 15:55:52 -07:00
|
|
|
|
2024-07-03 09:25:23 -06:00
|
|
|
let title = flags.kind.title();
|
|
|
|
|
let accept_label = flags.kind.accept_label();
|
|
|
|
|
|
2024-02-15 15:03:01 -07:00
|
|
|
let location = Location::Path(match &flags.path_opt {
|
2024-02-20 11:58:39 -07:00
|
|
|
Some(path) => path.to_path_buf(),
|
2024-02-15 15:03:01 -07:00
|
|
|
None => match env::current_dir() {
|
|
|
|
|
Ok(path) => path,
|
|
|
|
|
Err(_) => home_dir(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2024-09-11 09:22:36 -06:00
|
|
|
let tab_config = TabConfig {
|
|
|
|
|
view: tab::View::List,
|
|
|
|
|
folders_first: false,
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
let mut tab = Tab::new(location, tab_config);
|
2024-08-20 13:26:10 -06:00
|
|
|
tab.mode = tab::Mode::Dialog(flags.kind.clone());
|
2024-10-02 14:53:24 -06:00
|
|
|
tab.sort_name = tab::HeadingOptions::Modified;
|
|
|
|
|
tab.sort_direction = false;
|
2024-02-15 15:03:01 -07:00
|
|
|
|
2024-10-03 13:01:59 -06:00
|
|
|
let key_binds = key_binds(&tab.mode);
|
|
|
|
|
|
2024-02-01 15:55:52 -07:00
|
|
|
let mut app = App {
|
|
|
|
|
core,
|
2024-02-01 17:43:41 -07:00
|
|
|
flags,
|
2024-07-03 09:25:23 -06:00
|
|
|
title,
|
2025-03-19 14:44:13 -06:00
|
|
|
accept_label: DialogLabel::from(accept_label),
|
2024-07-03 09:25:23 -06:00
|
|
|
choices: Vec::new(),
|
2024-10-02 15:26:02 -06:00
|
|
|
context_page: ContextPage::Preview(None, PreviewKind::Selected),
|
2024-09-13 08:10:02 -06:00
|
|
|
dialog_pages: VecDeque::new(),
|
|
|
|
|
dialog_text_input: widget::Id::unique(),
|
2024-07-03 12:24:35 -06:00
|
|
|
filters: Vec::new(),
|
|
|
|
|
filter_selected: None,
|
2024-02-15 15:03:01 -07:00
|
|
|
filename_id: widget::Id::unique(),
|
2024-02-13 12:29:50 -07:00
|
|
|
modifiers: Modifiers::empty(),
|
2024-09-11 09:08:20 -06:00
|
|
|
mounter_items: HashMap::new(),
|
|
|
|
|
nav_model: segmented_button::ModelBuilder::default().build(),
|
2024-02-13 12:29:50 -07:00
|
|
|
result_opt: None,
|
2024-09-11 13:56:35 -06:00
|
|
|
search_id: widget::Id::unique(),
|
2024-02-15 15:03:01 -07:00
|
|
|
tab,
|
2024-10-03 13:01:59 -06:00
|
|
|
key_binds,
|
2024-02-01 15:55:52 -07:00
|
|
|
watcher_opt: None,
|
2025-03-22 19:28:36 -06:00
|
|
|
auto_scroll_speed: None,
|
2024-02-01 15:55:52 -07:00
|
|
|
};
|
|
|
|
|
|
2024-10-21 13:51:10 -06:00
|
|
|
let commands = Task::batch([
|
2024-09-11 09:08:20 -06:00
|
|
|
app.update_config(),
|
|
|
|
|
app.update_title(),
|
|
|
|
|
app.update_watcher(),
|
|
|
|
|
app.rescan_tab(),
|
|
|
|
|
]);
|
2024-02-01 15:55:52 -07:00
|
|
|
|
2024-02-15 15:03:01 -07:00
|
|
|
(app, commands)
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
|
2024-11-22 16:47:16 +01:00
|
|
|
fn context_drawer(&self) -> Option<context_drawer::ContextDrawer<Message>> {
|
2024-09-20 19:36:50 -06:00
|
|
|
if !self.core.window.show_context {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match &self.context_page {
|
2024-11-22 16:47:16 +01:00
|
|
|
ContextPage::Preview(_, kind) => {
|
|
|
|
|
let mut actions = Vec::with_capacity(3);
|
|
|
|
|
if let Some(items) = self.tab.items_opt() {
|
|
|
|
|
for item in items.iter() {
|
|
|
|
|
if item.selected {
|
|
|
|
|
actions.extend(
|
2024-11-25 03:24:24 +01:00
|
|
|
item.preview_header()
|
|
|
|
|
.into_iter()
|
2025-01-24 11:55:56 -07:00
|
|
|
.map(|element| element.map(Message::TabMessage)),
|
2024-11-22 16:47:16 +01:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
Some(
|
|
|
|
|
context_drawer::context_drawer(
|
2025-01-24 11:55:56 -07:00
|
|
|
self.preview(kind).map(Message::TabMessage),
|
2024-11-22 16:47:16 +01:00
|
|
|
Message::Preview,
|
|
|
|
|
)
|
|
|
|
|
.header_actions(actions),
|
|
|
|
|
)
|
|
|
|
|
}
|
2024-09-20 19:36:50 -06:00
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-27 09:52:39 -07:00
|
|
|
fn dialog(&self) -> Option<Element<Message>> {
|
2024-10-10 11:15:32 -06:00
|
|
|
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
|
|
|
|
|
2024-09-23 15:56:32 -06:00
|
|
|
//TODO: should gallery view just be a dialog?
|
|
|
|
|
if self.tab.gallery {
|
|
|
|
|
return Some(
|
|
|
|
|
widget::column::with_children(vec![
|
|
|
|
|
self.tab.gallery_view().map(Message::TabMessage),
|
|
|
|
|
// Draw button row as part of the overlay
|
2024-10-09 15:41:10 -06:00
|
|
|
widget::container(self.button_view())
|
2024-09-23 15:56:32 -06:00
|
|
|
.width(Length::Fill)
|
2024-10-10 11:15:32 -06:00
|
|
|
.padding(space_xxs)
|
2024-10-21 13:51:10 -06:00
|
|
|
.class(theme::Container::WindowBackground)
|
2024-09-23 15:56:32 -06:00
|
|
|
.into(),
|
|
|
|
|
])
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-13 08:10:02 -06:00
|
|
|
let dialog_page = match self.dialog_pages.front() {
|
|
|
|
|
Some(some) => some,
|
|
|
|
|
None => return None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let dialog = match dialog_page {
|
|
|
|
|
DialogPage::NewFolder { parent, name } => {
|
2024-11-11 09:14:03 -07:00
|
|
|
let mut dialog = widget::dialog().title(fl!("create-new-folder"));
|
2024-09-13 08:10:02 -06:00
|
|
|
|
|
|
|
|
let complete_maybe = if name.is_empty() {
|
|
|
|
|
None
|
|
|
|
|
} else if name == "." || name == ".." {
|
|
|
|
|
dialog = dialog.tertiary_action(widget::text::body(fl!(
|
|
|
|
|
"name-invalid",
|
|
|
|
|
filename = name.as_str()
|
|
|
|
|
)));
|
|
|
|
|
None
|
|
|
|
|
} else if name.contains('/') {
|
|
|
|
|
dialog = dialog.tertiary_action(widget::text::body(fl!("name-no-slashes")));
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
let path = parent.join(name);
|
|
|
|
|
if path.exists() {
|
|
|
|
|
if path.is_dir() {
|
|
|
|
|
dialog = dialog
|
|
|
|
|
.tertiary_action(widget::text::body(fl!("folder-already-exists")));
|
|
|
|
|
} else {
|
|
|
|
|
dialog = dialog
|
|
|
|
|
.tertiary_action(widget::text::body(fl!("file-already-exists")));
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
if name.starts_with('.') {
|
|
|
|
|
dialog = dialog.tertiary_action(widget::text::body(fl!("name-hidden")));
|
|
|
|
|
}
|
|
|
|
|
Some(Message::DialogComplete)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
dialog
|
|
|
|
|
.primary_action(
|
|
|
|
|
widget::button::suggested(fl!("save"))
|
|
|
|
|
.on_press_maybe(complete_maybe.clone()),
|
|
|
|
|
)
|
|
|
|
|
.secondary_action(
|
|
|
|
|
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
|
|
|
|
)
|
|
|
|
|
.control(
|
|
|
|
|
widget::column::with_children(vec![
|
|
|
|
|
widget::text::body(fl!("folder-name")).into(),
|
|
|
|
|
widget::text_input("", name.as_str())
|
|
|
|
|
.id(self.dialog_text_input.clone())
|
|
|
|
|
.on_input(move |name| {
|
|
|
|
|
Message::DialogUpdate(DialogPage::NewFolder {
|
|
|
|
|
parent: parent.clone(),
|
|
|
|
|
name,
|
|
|
|
|
})
|
|
|
|
|
})
|
2025-03-15 11:59:03 -04:00
|
|
|
.on_submit_maybe(
|
|
|
|
|
complete_maybe.clone().map(|maybe| move |_| maybe.clone()),
|
|
|
|
|
)
|
2024-09-13 08:10:02 -06:00
|
|
|
.into(),
|
|
|
|
|
])
|
|
|
|
|
.spacing(space_xxs),
|
|
|
|
|
)
|
2024-02-27 09:52:39 -07:00
|
|
|
}
|
2024-11-11 09:14:03 -07:00
|
|
|
DialogPage::Replace { filename } => widget::dialog()
|
|
|
|
|
.title(fl!("replace-title", filename = filename.as_str()))
|
|
|
|
|
.icon(widget::icon::from_name("dialog-question").size(64))
|
|
|
|
|
.body(fl!("replace-warning"))
|
|
|
|
|
.primary_action(
|
|
|
|
|
widget::button::suggested(fl!("replace")).on_press(Message::DialogComplete),
|
|
|
|
|
)
|
|
|
|
|
.secondary_action(
|
|
|
|
|
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
|
|
|
|
),
|
2024-09-13 08:10:02 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Some(dialog.into())
|
2024-02-27 09:52:39 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-10 11:15:32 -06:00
|
|
|
fn footer(&self) -> Option<Element<Message>> {
|
|
|
|
|
Some(self.button_view())
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-26 10:21:55 -06:00
|
|
|
fn header_end(&self) -> Vec<Element<Message>> {
|
2024-09-11 13:56:35 -06:00
|
|
|
let mut elements = Vec::with_capacity(3);
|
|
|
|
|
|
2024-10-09 15:41:10 -06:00
|
|
|
if let Some(term) = self.search_get() {
|
2024-10-10 11:46:46 -06:00
|
|
|
if self.core.is_condensed() {
|
|
|
|
|
elements.push(
|
|
|
|
|
//TODO: selected state is not appearing different
|
|
|
|
|
widget::button::icon(widget::icon::from_name("system-search-symbolic"))
|
|
|
|
|
.on_press(Message::SearchClear)
|
|
|
|
|
.padding(8)
|
|
|
|
|
.selected(true)
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
elements.push(
|
|
|
|
|
widget::text_input::search_input("", term)
|
|
|
|
|
.width(Length::Fixed(240.0))
|
|
|
|
|
.id(self.search_id.clone())
|
|
|
|
|
.on_clear(Message::SearchClear)
|
|
|
|
|
.on_input(Message::SearchInput)
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-09-11 13:56:35 -06:00
|
|
|
} else {
|
|
|
|
|
elements.push(
|
|
|
|
|
widget::button::icon(widget::icon::from_name("system-search-symbolic"))
|
|
|
|
|
.on_press(Message::SearchActivate)
|
2024-09-24 23:18:46 +02:00
|
|
|
.padding(8)
|
2024-09-11 13:56:35 -06:00
|
|
|
.into(),
|
2024-10-10 11:46:46 -06:00
|
|
|
);
|
2024-09-11 13:56:35 -06:00
|
|
|
}
|
|
|
|
|
|
2024-09-13 08:10:02 -06:00
|
|
|
if self.flags.kind.save() {
|
|
|
|
|
elements.push(
|
|
|
|
|
widget::button::icon(widget::icon::from_name("folder-new-symbolic"))
|
|
|
|
|
.on_press(Message::NewFolder)
|
2024-09-24 23:18:46 +02:00
|
|
|
.padding(8)
|
2024-09-13 08:10:02 -06:00
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-09-11 13:56:35 -06:00
|
|
|
|
2024-10-10 13:27:50 -06:00
|
|
|
let show_details = match self.context_page {
|
|
|
|
|
ContextPage::Preview(..) => self.core.window.show_context,
|
|
|
|
|
_ => false,
|
|
|
|
|
};
|
2025-01-20 02:48:55 -05:00
|
|
|
elements
|
|
|
|
|
.push(menu::dialog_menu(&self.tab, &self.key_binds, show_details).map(Message::from));
|
2024-09-11 13:56:35 -06:00
|
|
|
|
|
|
|
|
elements
|
2024-08-26 10:21:55 -06:00
|
|
|
}
|
|
|
|
|
|
2025-03-15 11:59:03 -04:00
|
|
|
fn nav_bar(&self) -> Option<Element<cosmic::Action<Self::Message>>> {
|
2024-09-11 09:08:20 -06:00
|
|
|
if !self.core().nav_bar_active() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let nav_model = self.nav_model()?;
|
|
|
|
|
|
|
|
|
|
let mut nav = cosmic::widget::nav_bar(nav_model, |entity| {
|
2025-03-15 11:59:03 -04:00
|
|
|
cosmic::action::cosmic(cosmic::app::Action::NavBar(entity))
|
2024-09-11 09:08:20 -06:00
|
|
|
})
|
2025-03-15 11:59:03 -04:00
|
|
|
//TODO .on_close(|entity| cosmic::cosmic::action::app(Message::NavBarClose(entity)))
|
2024-09-11 09:08:20 -06:00
|
|
|
.close_icon(
|
|
|
|
|
widget::icon::from_name("media-eject-symbolic")
|
|
|
|
|
.size(16)
|
|
|
|
|
.icon(),
|
|
|
|
|
)
|
|
|
|
|
.into_container();
|
|
|
|
|
|
|
|
|
|
if !self.core().is_condensed() {
|
|
|
|
|
nav = nav.max_width(280);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Some(Element::from(
|
|
|
|
|
// XXX both must be shrink to avoid flex layout from ignoring it
|
|
|
|
|
nav.width(Length::Shrink).height(Length::Shrink),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-01 15:55:52 -07:00
|
|
|
fn nav_model(&self) -> Option<&segmented_button::SingleSelectModel> {
|
|
|
|
|
Some(&self.nav_model)
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-14 11:15:54 -06:00
|
|
|
fn on_app_exit(&mut self) -> Option<Message> {
|
2024-02-13 12:29:50 -07:00
|
|
|
self.result_opt = Some(DialogResult::Cancel);
|
2024-05-14 11:15:54 -06:00
|
|
|
None
|
2024-02-13 12:29:50 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-21 13:51:10 -06:00
|
|
|
fn on_nav_select(&mut self, entity: segmented_button::Entity) -> Task<Message> {
|
2024-09-11 09:08:20 -06:00
|
|
|
self.nav_model.activate(entity);
|
|
|
|
|
if let Some(location) = self.nav_model.data::<Location>(entity) {
|
2024-02-15 15:03:01 -07:00
|
|
|
let message = Message::TabMessage(tab::Message::Location(location.clone()));
|
2024-02-01 15:55:52 -07:00
|
|
|
return self.update(message);
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-20 02:48:55 -05:00
|
|
|
if let Some(data) = self.nav_model.data::<MounterData>(entity) {
|
2024-10-09 15:41:10 -06:00
|
|
|
if let Some(mounter) = MOUNTERS.get(&data.0) {
|
2025-03-15 11:59:03 -04:00
|
|
|
return mounter
|
|
|
|
|
.mount(data.1.clone())
|
|
|
|
|
.map(|_| cosmic::action::none());
|
2024-09-11 09:08:20 -06:00
|
|
|
}
|
|
|
|
|
}
|
2024-10-21 13:51:10 -06:00
|
|
|
Task::none()
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-21 13:51:10 -06:00
|
|
|
fn on_escape(&mut self) -> Task<Message> {
|
2024-09-23 15:56:32 -06:00
|
|
|
if self.tab.gallery {
|
|
|
|
|
// Close gallery if open
|
|
|
|
|
self.tab.gallery = false;
|
2024-10-21 13:51:10 -06:00
|
|
|
return Task::none();
|
2024-09-23 15:56:32 -06:00
|
|
|
}
|
|
|
|
|
|
2024-10-09 15:41:10 -06:00
|
|
|
if self.search_get().is_some() {
|
2024-09-11 13:56:35 -06:00
|
|
|
// Close search if open
|
2024-10-09 15:41:10 -06:00
|
|
|
return self.search_set(None);
|
2024-09-11 13:56:35 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.tab.context_menu.is_some() {
|
|
|
|
|
self.tab.context_menu = None;
|
2024-10-21 13:51:10 -06:00
|
|
|
return Task::none();
|
2024-10-13 12:01:55 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.tab.edit_location.is_some() {
|
|
|
|
|
// Close location editing if enabled
|
|
|
|
|
self.tab.edit_location = None;
|
2024-10-21 13:51:10 -06:00
|
|
|
return Task::none();
|
2024-09-11 13:56:35 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let had_focused_button = self.tab.select_focus_id().is_some();
|
|
|
|
|
if self.tab.select_none() {
|
|
|
|
|
if had_focused_button {
|
|
|
|
|
// Unfocus if there was a focused button
|
|
|
|
|
return widget::button::focus(widget::Id::unique());
|
|
|
|
|
}
|
2024-10-21 13:51:10 -06:00
|
|
|
return Task::none();
|
2024-09-11 13:56:35 -06:00
|
|
|
}
|
|
|
|
|
|
2024-02-29 01:01:02 -05:00
|
|
|
self.update(Message::Cancel)
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-01 15:55:52 -07:00
|
|
|
/// Handle application events here.
|
2024-10-21 13:51:10 -06:00
|
|
|
fn update(&mut self, message: Message) -> Task<Message> {
|
2024-02-01 15:55:52 -07:00
|
|
|
match message {
|
2024-09-11 14:22:40 -06:00
|
|
|
Message::None => {}
|
2024-02-01 17:34:22 -07:00
|
|
|
Message::Cancel => {
|
2024-09-13 08:10:02 -06:00
|
|
|
self.result_opt = Some(DialogResult::Cancel);
|
2024-10-21 13:51:10 -06:00
|
|
|
return window::close(self.flags.window_id);
|
2024-02-01 17:34:22 -07:00
|
|
|
}
|
2024-07-03 09:25:23 -06:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-11 09:08:20 -06:00
|
|
|
Message::Config(config) => {
|
|
|
|
|
if config != self.flags.config {
|
|
|
|
|
log::info!("update config");
|
|
|
|
|
self.flags.config = config;
|
|
|
|
|
return self.update_config();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-22 19:28:36 -06:00
|
|
|
Message::CursorMoved(pos) => {
|
|
|
|
|
return self.update(Message::TabMessage(tab::Message::CursorMoved(pos)));
|
|
|
|
|
}
|
2024-09-13 08:10:02 -06:00
|
|
|
Message::DialogCancel => {
|
|
|
|
|
self.dialog_pages.pop_front();
|
|
|
|
|
}
|
|
|
|
|
Message::DialogComplete => {
|
|
|
|
|
if let Some(dialog_page) = self.dialog_pages.pop_front() {
|
|
|
|
|
match dialog_page {
|
|
|
|
|
DialogPage::NewFolder { parent, name } => {
|
|
|
|
|
let path = parent.join(name);
|
|
|
|
|
match fs::create_dir(&path) {
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
// cd to directory
|
|
|
|
|
let message = Message::TabMessage(tab::Message::Location(
|
|
|
|
|
Location::Path(path.clone()),
|
|
|
|
|
));
|
|
|
|
|
return self.update(message);
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::warn!("failed to create {:?}: {}", path, err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-13 15:13:37 -06:00
|
|
|
DialogPage::Replace { .. } => {
|
2024-09-13 08:10:02 -06:00
|
|
|
return self.update(Message::Save(true));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::DialogUpdate(dialog_page) => {
|
|
|
|
|
if !self.dialog_pages.is_empty() {
|
|
|
|
|
self.dialog_pages[0] = dialog_page;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-20 11:58:39 -07:00
|
|
|
Message::Filename(new_filename) => {
|
2024-02-15 15:03:01 -07:00
|
|
|
// Select based on filename
|
2024-02-29 15:21:59 -07:00
|
|
|
self.tab.select_name(&new_filename);
|
2024-02-29 14:34:41 -07:00
|
|
|
|
|
|
|
|
if let DialogKind::SaveFile { filename } = &mut self.flags.kind {
|
|
|
|
|
*filename = new_filename;
|
2024-02-15 15:03:01 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-07-03 12:24:35 -06:00
|
|
|
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();
|
|
|
|
|
}
|
2024-10-02 16:07:19 -06:00
|
|
|
Message::Key(modifiers, key) => {
|
|
|
|
|
for (key_bind, action) in self.key_binds.iter() {
|
|
|
|
|
if key_bind.matches(modifiers, &key) {
|
|
|
|
|
return self.update(Message::from(action.message()));
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-19 14:44:13 -06:00
|
|
|
if let Some(key_bind) = &self.accept_label.key_bind_opt {
|
|
|
|
|
if key_bind.matches(modifiers, &key) {
|
|
|
|
|
return self.update(if self.flags.kind.save() {
|
|
|
|
|
Message::Save(false)
|
|
|
|
|
} else {
|
|
|
|
|
Message::Open
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-02 16:07:19 -06:00
|
|
|
}
|
2025-03-26 00:20:50 +01:00
|
|
|
Message::ModifiersChanged(modifiers) => {
|
2024-02-01 15:55:52 -07:00
|
|
|
self.modifiers = modifiers;
|
|
|
|
|
}
|
2024-09-11 09:08:20 -06:00
|
|
|
Message::MounterItems(mounter_key, mounter_items) => {
|
|
|
|
|
// Check for unmounted folders
|
|
|
|
|
let mut unmounted = Vec::new();
|
|
|
|
|
if let Some(old_items) = self.mounter_items.get(&mounter_key) {
|
|
|
|
|
for old_item in old_items.iter() {
|
|
|
|
|
if let Some(old_path) = old_item.path() {
|
|
|
|
|
if old_item.is_mounted() {
|
|
|
|
|
let mut still_mounted = false;
|
|
|
|
|
for item in mounter_items.iter() {
|
|
|
|
|
if let Some(path) = item.path() {
|
2025-01-20 02:48:55 -05:00
|
|
|
if path == old_path && item.is_mounted() {
|
|
|
|
|
still_mounted = true;
|
|
|
|
|
break;
|
2024-09-11 09:08:20 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !still_mounted {
|
|
|
|
|
unmounted.push(Location::Path(old_path));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Go back to home in any tabs that were unmounted
|
|
|
|
|
let mut commands = Vec::new();
|
|
|
|
|
{
|
|
|
|
|
let home_location = Location::Path(home_dir());
|
|
|
|
|
if unmounted.contains(&self.tab.location) {
|
|
|
|
|
self.tab.change_location(&home_location, None);
|
|
|
|
|
commands.push(self.update_watcher());
|
|
|
|
|
commands.push(self.rescan_tab());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Insert new items
|
|
|
|
|
self.mounter_items.insert(mounter_key, mounter_items);
|
|
|
|
|
|
|
|
|
|
// Update nav bar
|
|
|
|
|
//TODO: this could change favorites IDs while they are in use
|
|
|
|
|
self.update_nav_model();
|
|
|
|
|
|
2024-10-21 13:51:10 -06:00
|
|
|
return Task::batch(commands);
|
2024-09-11 09:08:20 -06:00
|
|
|
}
|
2024-09-13 08:10:02 -06:00
|
|
|
Message::NewFolder => {
|
2024-10-04 16:28:30 -06:00
|
|
|
if let Some(path) = self.tab.location.path_opt() {
|
2024-09-13 08:10:02 -06:00
|
|
|
self.dialog_pages.push_back(DialogPage::NewFolder {
|
2024-10-04 16:28:30 -06:00
|
|
|
parent: path.to_path_buf(),
|
2024-09-13 08:10:02 -06:00
|
|
|
name: String::new(),
|
|
|
|
|
});
|
|
|
|
|
return widget::text_input::focus(self.dialog_text_input.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-03-20 11:54:37 -06:00
|
|
|
Message::NotifyEvents(events) => {
|
|
|
|
|
log::debug!("{:?}", events);
|
2024-02-01 15:55:52 -07:00
|
|
|
|
2024-10-04 16:28:30 -06:00
|
|
|
if let Some(path) = self.tab.location.path_opt() {
|
2024-02-15 15:03:01 -07:00
|
|
|
let mut contains_change = false;
|
2024-03-20 11:54:37 -06:00
|
|
|
for event in events.iter() {
|
|
|
|
|
for event_path in event.paths.iter() {
|
2025-01-20 02:48:55 -05:00
|
|
|
if event_path.starts_with(path) {
|
2024-03-20 16:27:00 -06:00
|
|
|
match event.kind {
|
|
|
|
|
notify::EventKind::Modify(
|
|
|
|
|
notify::event::ModifyKind::Metadata(_),
|
|
|
|
|
)
|
|
|
|
|
| notify::EventKind::Modify(notify::event::ModifyKind::Data(
|
|
|
|
|
_,
|
|
|
|
|
)) => {
|
|
|
|
|
// If metadata or data changed, find the matching item and reload it
|
|
|
|
|
//TODO: this could be further optimized by looking at what exactly changed
|
|
|
|
|
if let Some(items) = &mut self.tab.items_opt {
|
|
|
|
|
for item in items.iter_mut() {
|
2024-09-13 09:35:37 -06:00
|
|
|
if item.path_opt() == Some(event_path) {
|
2024-03-20 16:27:00 -06:00
|
|
|
//TODO: reload more, like mime types?
|
2025-01-20 02:48:55 -05:00
|
|
|
match fs::metadata(event_path) {
|
2024-03-20 16:27:00 -06:00
|
|
|
Ok(new_metadata) => {
|
2025-01-20 02:48:55 -05:00
|
|
|
if let ItemMetadata::Path {
|
|
|
|
|
metadata,
|
|
|
|
|
..
|
|
|
|
|
} = &mut item.metadata
|
|
|
|
|
{
|
|
|
|
|
*metadata = new_metadata;
|
2024-03-20 16:27:00 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::warn!("failed to reload metadata for {:?}: {}", path, err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//TODO item.thumbnail_opt =
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
// Any other events reload the whole tab
|
|
|
|
|
contains_change = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-03-20 11:54:37 -06:00
|
|
|
}
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-02-15 15:03:01 -07:00
|
|
|
if contains_change {
|
|
|
|
|
return self.rescan_tab();
|
|
|
|
|
}
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take()
|
|
|
|
|
{
|
2024-02-15 15:03:01 -07:00
|
|
|
Some(watcher) => {
|
2024-02-01 15:55:52 -07:00
|
|
|
self.watcher_opt = Some((watcher, HashSet::new()));
|
|
|
|
|
return self.update_watcher();
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
log::warn!("message did not contain notify watcher");
|
|
|
|
|
}
|
|
|
|
|
},
|
2024-02-01 17:34:22 -07:00
|
|
|
Message::Open => {
|
|
|
|
|
let mut paths = Vec::new();
|
2024-02-29 13:42:13 -07:00
|
|
|
if let Some(items) = self.tab.items_opt() {
|
|
|
|
|
for item in items.iter() {
|
2024-02-15 15:03:01 -07:00
|
|
|
if item.selected {
|
2024-10-04 16:28:30 -06:00
|
|
|
if let Some(path) = item.path_opt() {
|
2024-03-20 09:56:54 -06:00
|
|
|
paths.push(path.clone());
|
2024-09-03 00:09:49 +02:00
|
|
|
let _ = update_recently_used(
|
|
|
|
|
&path.clone(),
|
|
|
|
|
App::APP_ID.to_string(),
|
|
|
|
|
"cosmic-files".to_string(),
|
|
|
|
|
None,
|
|
|
|
|
);
|
2024-03-20 09:56:54 -06:00
|
|
|
}
|
2024-02-01 17:34:22 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-26 14:31:50 -07:00
|
|
|
|
|
|
|
|
// Ensure selection is allowed
|
|
|
|
|
//TODO: improve tab logic so this doesn't block the open button so often
|
|
|
|
|
for path in paths.iter() {
|
|
|
|
|
let path_is_dir = path.is_dir();
|
|
|
|
|
if path_is_dir != self.flags.kind.is_dir() {
|
|
|
|
|
if path_is_dir && paths.len() == 1 {
|
|
|
|
|
// If the only selected item is a directory and we are selecting files, cd to it
|
|
|
|
|
let message = Message::TabMessage(tab::Message::Location(
|
|
|
|
|
Location::Path(path.clone()),
|
|
|
|
|
));
|
|
|
|
|
return self.update(message);
|
|
|
|
|
} else {
|
|
|
|
|
// Otherwise, this is not a legal selection
|
2024-10-21 13:51:10 -06:00
|
|
|
return Task::none();
|
2024-02-26 14:31:50 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If there are proper matching items, return them
|
2024-02-15 15:03:01 -07:00
|
|
|
if !paths.is_empty() {
|
|
|
|
|
self.result_opt = Some(DialogResult::Open(paths));
|
2024-10-21 13:51:10 -06:00
|
|
|
return window::close(self.flags.window_id);
|
2024-02-26 14:31:50 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we are in directory mode, return the current directory
|
|
|
|
|
if self.flags.kind.is_dir() {
|
2025-01-20 02:48:55 -05:00
|
|
|
if let Location::Path(tab_path) = &self.tab.location {
|
|
|
|
|
self.result_opt = Some(DialogResult::Open(vec![tab_path.clone()]));
|
|
|
|
|
return window::close(self.flags.window_id);
|
2024-02-26 14:11:45 -07:00
|
|
|
}
|
2024-02-15 15:03:01 -07:00
|
|
|
}
|
2024-02-01 17:34:22 -07:00
|
|
|
}
|
2024-10-04 11:07:27 -06:00
|
|
|
Message::Preview => match self.context_page {
|
2024-10-09 15:41:10 -06:00
|
|
|
ContextPage::Preview(..) => {
|
2024-10-04 11:07:27 -06:00
|
|
|
self.core.window.show_context = !self.core.window.show_context;
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
self.context_page = ContextPage::Preview(None, PreviewKind::Selected);
|
|
|
|
|
self.core.window.show_context = true;
|
|
|
|
|
}
|
|
|
|
|
},
|
2024-02-26 15:15:49 -07:00
|
|
|
Message::Save(replace) => {
|
2024-02-20 11:58:39 -07:00
|
|
|
if let DialogKind::SaveFile { filename } = &self.flags.kind {
|
|
|
|
|
if !filename.is_empty() {
|
2024-10-04 16:28:30 -06:00
|
|
|
if let Some(tab_path) = self.tab.location.path_opt() {
|
2025-01-20 02:48:55 -05:00
|
|
|
let path = tab_path.join(filename);
|
2024-02-26 15:15:49 -07:00
|
|
|
if path.is_dir() {
|
|
|
|
|
// cd to directory
|
|
|
|
|
let message = Message::TabMessage(tab::Message::Location(
|
|
|
|
|
Location::Path(path.clone()),
|
|
|
|
|
));
|
|
|
|
|
return self.update(message);
|
|
|
|
|
} else if !replace && path.exists() {
|
2024-09-13 08:10:02 -06:00
|
|
|
self.dialog_pages.push_back(DialogPage::Replace {
|
|
|
|
|
filename: filename.clone(),
|
|
|
|
|
});
|
2024-02-26 15:15:49 -07:00
|
|
|
} else {
|
|
|
|
|
self.result_opt = Some(DialogResult::Open(vec![path]));
|
2024-10-21 13:51:10 -06:00
|
|
|
return window::close(self.flags.window_id);
|
2024-02-20 11:58:39 -07:00
|
|
|
}
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
2024-02-22 14:30:04 -07:00
|
|
|
}
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-03-22 19:28:36 -06:00
|
|
|
Message::ScrollTab(scroll_speed) => {
|
|
|
|
|
return self.update(Message::TabMessage(tab::Message::ScrollTab(
|
|
|
|
|
(scroll_speed as f32) / 10.0,
|
|
|
|
|
)));
|
|
|
|
|
}
|
2024-09-11 13:56:35 -06:00
|
|
|
Message::SearchActivate => {
|
2024-10-09 15:41:10 -06:00
|
|
|
return if self.search_get().is_none() {
|
|
|
|
|
self.search_set(Some(String::new()))
|
|
|
|
|
} else {
|
|
|
|
|
widget::text_input::focus(self.search_id.clone())
|
|
|
|
|
};
|
2024-09-11 13:56:35 -06:00
|
|
|
}
|
|
|
|
|
Message::SearchClear => {
|
2024-10-09 15:41:10 -06:00
|
|
|
return self.search_set(None);
|
2024-09-11 13:56:35 -06:00
|
|
|
}
|
|
|
|
|
Message::SearchInput(input) => {
|
2024-10-09 15:41:10 -06:00
|
|
|
return self.search_set(Some(input));
|
2024-09-11 13:56:35 -06:00
|
|
|
}
|
2024-02-15 15:03:01 -07:00
|
|
|
Message::TabMessage(tab_message) => {
|
|
|
|
|
let click_i_opt = match tab_message {
|
|
|
|
|
tab::Message::Click(click_i_opt) => click_i_opt,
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
|
2024-02-26 14:11:45 -07:00
|
|
|
let tab_commands = self.tab.update(tab_message, self.modifiers);
|
2024-02-15 15:03:01 -07:00
|
|
|
|
|
|
|
|
// Update filename box when anything is selected
|
2024-02-20 11:58:39 -07:00
|
|
|
if let DialogKind::SaveFile { filename } = &mut self.flags.kind {
|
2024-02-15 15:03:01 -07:00
|
|
|
if let Some(click_i) = click_i_opt {
|
2024-02-29 13:42:13 -07:00
|
|
|
if let Some(items) = self.tab.items_opt() {
|
2024-02-15 15:03:01 -07:00
|
|
|
if let Some(item) = items.get(click_i) {
|
2024-03-20 09:56:54 -06:00
|
|
|
if item.selected && !item.metadata.is_dir() {
|
2024-02-20 11:58:39 -07:00
|
|
|
*filename = item.name.clone();
|
2024-02-15 15:03:01 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-26 14:11:45 -07:00
|
|
|
let mut commands = Vec::new();
|
|
|
|
|
for tab_command in tab_commands {
|
|
|
|
|
match tab_command {
|
2024-09-20 19:36:50 -06:00
|
|
|
tab::Command::Action(action) => {
|
|
|
|
|
commands.push(self.update(Message::from(action.message())));
|
|
|
|
|
}
|
2024-11-19 20:17:58 -07:00
|
|
|
tab::Command::ChangeLocation(_tab_title, _tab_path, _selection_paths) => {
|
2024-10-21 13:51:10 -06:00
|
|
|
commands.push(Task::batch([self.update_watcher(), self.rescan_tab()]));
|
2024-02-26 14:11:45 -07:00
|
|
|
}
|
2024-08-20 13:26:10 -06:00
|
|
|
tab::Command::Iced(iced_command) => {
|
2025-03-15 11:59:03 -04:00
|
|
|
commands.push(iced_command.0.map(|tab_message| {
|
|
|
|
|
cosmic::action::app(Message::TabMessage(tab_message))
|
|
|
|
|
}));
|
2024-02-28 15:19:07 -07:00
|
|
|
}
|
2024-02-26 14:11:45 -07:00
|
|
|
tab::Command::OpenFile(_item_path) => {
|
2024-02-26 15:15:49 -07:00
|
|
|
if self.flags.kind.save() {
|
|
|
|
|
commands.push(self.update(Message::Save(false)));
|
|
|
|
|
} else {
|
|
|
|
|
commands.push(self.update(Message::Open));
|
|
|
|
|
}
|
2024-02-26 14:11:45 -07:00
|
|
|
}
|
2024-10-02 15:26:02 -06:00
|
|
|
tab::Command::Preview(kind) => {
|
|
|
|
|
self.context_page = ContextPage::Preview(None, kind);
|
|
|
|
|
self.set_show_context(true);
|
2024-09-20 19:36:50 -06:00
|
|
|
}
|
2024-09-23 15:56:32 -06:00
|
|
|
tab::Command::WindowDrag => {
|
2024-10-21 13:51:10 -06:00
|
|
|
commands.push(window::drag(self.flags.window_id));
|
2024-09-23 16:05:43 -06:00
|
|
|
}
|
|
|
|
|
tab::Command::WindowToggleMaximize => {
|
2024-10-21 13:51:10 -06:00
|
|
|
commands.push(window::toggle_maximize(self.flags.window_id));
|
2024-09-23 15:56:32 -06:00
|
|
|
}
|
2025-03-22 19:28:36 -06:00
|
|
|
tab::Command::AutoScroll(scroll_speed) => {
|
|
|
|
|
// converting an f32 to an i16 here by multiplying by 10 and casting to i16
|
|
|
|
|
// further resolution isn't necessary
|
|
|
|
|
if let Some(scroll_speed_float) = scroll_speed {
|
|
|
|
|
self.auto_scroll_speed = Some((scroll_speed_float * 10.0) as i16);
|
|
|
|
|
} else {
|
|
|
|
|
self.auto_scroll_speed = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-08-20 13:26:10 -06:00
|
|
|
unsupported => {
|
|
|
|
|
log::warn!("{unsupported:?} not supported in dialog mode");
|
2024-04-10 13:56:43 -04:00
|
|
|
}
|
2024-02-26 12:51:22 -07:00
|
|
|
}
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
2024-10-21 13:51:10 -06:00
|
|
|
return Task::batch(commands);
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
2024-10-10 13:53:01 -06:00
|
|
|
Message::TabRescan(location, parent_item_opt, mut items) => {
|
|
|
|
|
if location == self.tab.location {
|
|
|
|
|
// 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
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-07-03 12:24:35 -06:00
|
|
|
}
|
|
|
|
|
}
|
2024-10-10 13:53:01 -06:00
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-07-03 12:24:35 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-10 13:53:01 -06:00
|
|
|
items.retain(|item| {
|
|
|
|
|
if item.metadata.is_dir() {
|
|
|
|
|
// Directories are always shown
|
2024-07-03 12:24:35 -06:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-10 13:53:01 -06:00
|
|
|
// 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;
|
|
|
|
|
}
|
2024-07-03 12:24:35 -06:00
|
|
|
}
|
|
|
|
|
|
2024-10-10 13:53:01 -06:00
|
|
|
// No filters matched
|
|
|
|
|
false
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-07-03 12:24:35 -06:00
|
|
|
}
|
|
|
|
|
|
2024-10-10 13:53:01 -06:00
|
|
|
// Select based on filename
|
|
|
|
|
if let DialogKind::SaveFile { filename } = &self.flags.kind {
|
|
|
|
|
for item in items.iter_mut() {
|
|
|
|
|
item.selected = &item.name == filename;
|
|
|
|
|
}
|
2024-02-20 11:58:39 -07:00
|
|
|
}
|
2024-02-15 15:03:01 -07:00
|
|
|
|
2024-10-10 13:53:01 -06:00
|
|
|
self.tab.parent_item_opt = parent_item_opt;
|
|
|
|
|
self.tab.set_items(items);
|
2024-02-15 15:03:01 -07:00
|
|
|
|
2024-10-10 13:53:01 -06:00
|
|
|
// Reset focus on location change
|
|
|
|
|
if self.search_get().is_some() {
|
|
|
|
|
return widget::text_input::focus(self.search_id.clone());
|
|
|
|
|
} else {
|
|
|
|
|
return widget::text_input::focus(self.filename_id.clone());
|
|
|
|
|
}
|
2024-10-09 15:41:10 -06:00
|
|
|
}
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
2024-10-10 13:27:50 -06:00
|
|
|
Message::TabView(view) => {
|
|
|
|
|
self.tab.config.view = view;
|
|
|
|
|
}
|
2025-04-14 08:59:32 -06:00
|
|
|
Message::TimeConfigChange(time_config) => {
|
|
|
|
|
self.flags.config.tab.military_time = time_config.military_time;
|
|
|
|
|
return self.update_config();
|
|
|
|
|
}
|
2024-10-10 13:27:50 -06:00
|
|
|
Message::ToggleFoldersFirst => {
|
|
|
|
|
self.tab.config.folders_first = !self.tab.config.folders_first;
|
|
|
|
|
}
|
|
|
|
|
Message::ZoomDefault => match self.tab.config.view {
|
|
|
|
|
tab::View::List => self.tab.config.icon_sizes.list = 100.try_into().unwrap(),
|
|
|
|
|
tab::View::Grid => self.tab.config.icon_sizes.grid = 100.try_into().unwrap(),
|
|
|
|
|
},
|
|
|
|
|
Message::ZoomIn => {
|
|
|
|
|
let zoom_in = |size: &mut NonZeroU16, min: u16, max: u16| {
|
|
|
|
|
let mut step = min;
|
|
|
|
|
while step <= max {
|
|
|
|
|
if size.get() < step {
|
|
|
|
|
*size = step.try_into().unwrap();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
step += 25;
|
|
|
|
|
}
|
|
|
|
|
if size.get() > step {
|
|
|
|
|
*size = step.try_into().unwrap();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
match self.tab.config.view {
|
|
|
|
|
tab::View::List => zoom_in(&mut self.tab.config.icon_sizes.list, 50, 500),
|
|
|
|
|
tab::View::Grid => zoom_in(&mut self.tab.config.icon_sizes.grid, 50, 500),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::ZoomOut => {
|
|
|
|
|
let zoom_out = |size: &mut NonZeroU16, min: u16, max: u16| {
|
|
|
|
|
let mut step = max;
|
|
|
|
|
while step >= min {
|
|
|
|
|
if size.get() > step {
|
|
|
|
|
*size = step.try_into().unwrap();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
step -= 25;
|
|
|
|
|
}
|
|
|
|
|
if size.get() < step {
|
|
|
|
|
*size = step.try_into().unwrap();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
match self.tab.config.view {
|
|
|
|
|
tab::View::List => zoom_out(&mut self.tab.config.icon_sizes.list, 50, 500),
|
|
|
|
|
tab::View::Grid => zoom_out(&mut self.tab.config.icon_sizes.grid, 50, 500),
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-15 11:59:03 -04:00
|
|
|
Message::Surface(a) => {
|
|
|
|
|
return cosmic::task::message(cosmic::Action::Cosmic(
|
|
|
|
|
cosmic::app::Action::Surface(a),
|
|
|
|
|
));
|
|
|
|
|
}
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-21 13:51:10 -06:00
|
|
|
Task::none()
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Creates a view after each update.
|
2024-02-13 12:29:50 -07:00
|
|
|
fn view(&self) -> Element<Message> {
|
2024-10-10 11:46:46 -06:00
|
|
|
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
|
|
|
|
|
|
|
|
|
let mut col = widget::column::with_capacity(2);
|
|
|
|
|
|
|
|
|
|
if self.core.is_condensed() {
|
|
|
|
|
if let Some(term) = self.search_get() {
|
|
|
|
|
col = col.push(
|
|
|
|
|
widget::container(
|
|
|
|
|
widget::text_input::search_input("", term)
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
.id(self.search_id.clone())
|
|
|
|
|
.on_clear(Message::SearchClear)
|
|
|
|
|
.on_input(Message::SearchInput),
|
|
|
|
|
)
|
|
|
|
|
.padding(space_xxs),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-20 02:48:55 -05:00
|
|
|
col = col.push(self.tab.view(&self.key_binds).map(Message::TabMessage));
|
2024-10-10 11:46:46 -06:00
|
|
|
|
|
|
|
|
col.into()
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
|
2024-02-13 12:29:50 -07:00
|
|
|
fn subscription(&self) -> Subscription<Message> {
|
2024-02-01 15:55:52 -07:00
|
|
|
struct WatcherSubscription;
|
2025-04-14 08:59:32 -06:00
|
|
|
struct TimeSubscription;
|
2024-09-11 09:08:20 -06:00
|
|
|
let mut subscriptions = vec![
|
2024-10-22 08:21:51 -06:00
|
|
|
event::listen_with(|event, status, _window_id| match event {
|
|
|
|
|
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => match status {
|
|
|
|
|
event::Status::Ignored => Some(Message::Key(modifiers, key)),
|
|
|
|
|
event::Status::Captured => None,
|
|
|
|
|
},
|
|
|
|
|
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
|
2025-03-26 00:20:50 +01:00
|
|
|
Some(Message::ModifiersChanged(modifiers))
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
2025-03-22 19:28:36 -06:00
|
|
|
Event::Mouse(mouse::Event::CursorMoved { position: pos }) => {
|
|
|
|
|
Some(Message::CursorMoved(pos))
|
|
|
|
|
}
|
2024-10-22 08:21:51 -06:00
|
|
|
_ => None,
|
2024-02-01 15:55:52 -07:00
|
|
|
}),
|
2024-09-11 09:08:20 -06:00
|
|
|
Config::subscription().map(|update| {
|
|
|
|
|
if !update.errors.is_empty() {
|
|
|
|
|
log::info!(
|
|
|
|
|
"errors loading config {:?}: {:?}",
|
|
|
|
|
update.keys,
|
|
|
|
|
update.errors
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
Message::Config(update.config)
|
|
|
|
|
}),
|
2025-04-14 08:59:32 -06:00
|
|
|
cosmic_config::config_subscription::<_, TimeConfig>(
|
|
|
|
|
TypeId::of::<TimeSubscription>(),
|
|
|
|
|
TIME_CONFIG_ID.into(),
|
|
|
|
|
1,
|
|
|
|
|
)
|
|
|
|
|
.map(|update| {
|
|
|
|
|
if !update.errors.is_empty() {
|
|
|
|
|
log::info!(
|
|
|
|
|
"errors loading time config {:?}: {:?}",
|
|
|
|
|
update.keys,
|
|
|
|
|
update.errors
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
Message::TimeConfigChange(update.config)
|
|
|
|
|
}),
|
2024-10-21 13:51:10 -06:00
|
|
|
Subscription::run_with_id(
|
2024-02-01 15:55:52 -07:00
|
|
|
TypeId::of::<WatcherSubscription>(),
|
2024-10-21 13:51:10 -06:00
|
|
|
stream::channel(100, |mut output| async move {
|
2024-02-01 15:55:52 -07:00
|
|
|
let watcher_res = {
|
|
|
|
|
let mut output = output.clone();
|
2024-03-20 11:54:37 -06:00
|
|
|
new_debouncer(
|
2024-03-20 15:43:44 -06:00
|
|
|
time::Duration::from_millis(250),
|
|
|
|
|
Some(time::Duration::from_millis(250)),
|
2024-03-20 11:54:37 -06:00
|
|
|
move |events_res: notify_debouncer_full::DebounceEventResult| {
|
|
|
|
|
match events_res {
|
|
|
|
|
Ok(mut events) => {
|
|
|
|
|
events.retain(|event| {
|
|
|
|
|
match &event.kind {
|
|
|
|
|
notify::EventKind::Access(_) => {
|
|
|
|
|
// Data not mutated
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
notify::EventKind::Modify(
|
|
|
|
|
notify::event::ModifyKind::Metadata(e),
|
|
|
|
|
) if (*e != notify::event::MetadataKind::Any
|
|
|
|
|
&& *e
|
|
|
|
|
!= notify::event::MetadataKind::WriteTime) =>
|
|
|
|
|
{
|
|
|
|
|
// Data not mutated nor modify time changed
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
_ => true
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if !events.is_empty() {
|
|
|
|
|
match futures::executor::block_on(async {
|
|
|
|
|
output.send(Message::NotifyEvents(events)).await
|
|
|
|
|
}) {
|
|
|
|
|
Ok(()) => {}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::warn!(
|
|
|
|
|
"failed to send notify events: {:?}",
|
|
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-03-10 01:23:26 -05:00
|
|
|
}
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
2024-03-20 11:54:37 -06:00
|
|
|
Err(err) => {
|
|
|
|
|
log::warn!("failed to watch files: {:?}", err);
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match watcher_res {
|
|
|
|
|
Ok(watcher) => {
|
|
|
|
|
match output
|
|
|
|
|
.send(Message::NotifyWatcher(WatcherWrapper {
|
|
|
|
|
watcher_opt: Some(watcher),
|
|
|
|
|
}))
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(()) => {}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::warn!("failed to send notify watcher: {:?}", err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::warn!("failed to create file watcher: {:?}", err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-14 11:15:54 -06:00
|
|
|
std::future::pending().await
|
2024-10-21 13:51:10 -06:00
|
|
|
}),
|
2024-02-01 15:55:52 -07:00
|
|
|
),
|
2024-11-15 15:37:31 -07:00
|
|
|
self.tab
|
|
|
|
|
.subscription(
|
|
|
|
|
self.core.window.show_context
|
|
|
|
|
&& matches!(
|
|
|
|
|
self.context_page,
|
|
|
|
|
ContextPage::Preview(_, PreviewKind::Selected)
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.map(Message::TabMessage),
|
2024-09-11 09:08:20 -06:00
|
|
|
];
|
|
|
|
|
|
2025-03-22 19:28:36 -06:00
|
|
|
if let Some(scroll_speed) = self.auto_scroll_speed {
|
|
|
|
|
subscriptions.push(
|
|
|
|
|
iced::time::every(time::Duration::from_millis(10))
|
|
|
|
|
.with(scroll_speed)
|
|
|
|
|
.map(|(scroll_speed, _)| Message::ScrollTab(scroll_speed)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-09 15:41:10 -06:00
|
|
|
for (key, mounter) in MOUNTERS.iter() {
|
2025-01-05 13:50:08 -07:00
|
|
|
subscriptions.push(
|
|
|
|
|
mounter.subscription().with(*key).map(
|
|
|
|
|
|(key, mounter_message)| match mounter_message {
|
|
|
|
|
MounterMessage::Items(items) => Message::MounterItems(key, items),
|
|
|
|
|
_ => {
|
|
|
|
|
log::warn!("{:?} not supported in dialog mode", mounter_message);
|
|
|
|
|
Message::None
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
2024-09-11 09:08:20 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Subscription::batch(subscriptions)
|
2024-02-01 15:55:52 -07:00
|
|
|
}
|
|
|
|
|
}
|