Move desktop dialogs to their own windows

This commit is contained in:
Jeremy Soller 2025-03-12 08:37:06 -06:00
parent 5dad1f0d26
commit 73e1d7ce52
2 changed files with 204 additions and 81 deletions

View file

@ -85,7 +85,7 @@ optional = true
git = "https://github.com/pop-os/libcosmic.git" git = "https://github.com/pop-os/libcosmic.git"
default-features = false default-features = false
#TODO: a11y feature crashes #TODO: a11y feature crashes
features = ["multi-window", "tokio", "winit", "surface-message"] features = ["autosize","multi-window", "tokio", "winit", "surface-message"]
[features] [features]
default = [ default = [

View file

@ -311,6 +311,7 @@ pub enum Message {
Delete(Option<Entity>), Delete(Option<Entity>),
DesktopConfig(DesktopConfig), DesktopConfig(DesktopConfig),
DesktopViewOptions, DesktopViewOptions,
DesktopDialogs(bool),
DialogCancel, DialogCancel,
DialogComplete, DialogComplete,
DragId(window::Id), DragId(window::Id),
@ -406,6 +407,7 @@ pub enum Message {
UndoTrashStart(Vec<TrashItem>), UndoTrashStart(Vec<TrashItem>),
WindowClose, WindowClose,
WindowCloseRequested(window::Id), WindowCloseRequested(window::Id),
WindowMaximize(window::Id, bool),
WindowNew, WindowNew,
WindowUnfocus, WindowUnfocus,
ZoomDefault(Option<Entity>), ZoomDefault(Option<Entity>),
@ -530,6 +532,63 @@ pub enum DialogPage {
}, },
} }
pub struct DialogPages {
pages: VecDeque<DialogPage>,
}
impl DialogPages {
pub fn new() -> Self {
Self {
pages: VecDeque::new(),
}
}
pub fn front(&self) -> Option<&DialogPage> {
self.pages.front()
}
pub fn front_mut(&mut self) -> Option<&mut DialogPage> {
self.pages.front_mut()
}
pub fn push_back(&mut self, page: DialogPage) -> Task<Message> {
let task = if self.pages.is_empty() {
Task::done(cosmic::Action::App(Message::DesktopDialogs(true)))
} else {
Task::none()
};
self.pages.push_back(page);
task
}
pub fn push_front(&mut self, page: DialogPage) -> Task<Message> {
let task = if self.pages.is_empty() {
Task::done(cosmic::Action::App(Message::DesktopDialogs(true)))
} else {
Task::none()
};
self.pages.push_front(page);
task
}
#[must_use]
pub fn pop_front(&mut self) -> Option<(DialogPage, Task<Message>)> {
let page = self.pages.pop_front()?;
let task = if self.pages.is_empty() {
Task::done(cosmic::Action::App(Message::DesktopDialogs(false)))
} else {
Task::none()
};
Some((page, task))
}
pub fn update_front(&mut self, page: DialogPage) {
if !self.pages.is_empty() {
self.pages[0] = page;
}
}
}
pub struct FavoriteIndex(usize); pub struct FavoriteIndex(usize);
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@ -545,6 +604,7 @@ pub struct MounterData(MounterKey, MounterItem);
pub enum WindowKind { pub enum WindowKind {
Desktop(Entity), Desktop(Entity),
DesktopViewOptions, DesktopViewOptions,
Dialogs(widget::Id),
Preview(Option<Entity>, PreviewKind), Preview(Option<Entity>, PreviewKind),
FileDialog(Option<Vec<PathBuf>>), FileDialog(Option<Vec<PathBuf>>),
} }
@ -571,7 +631,7 @@ impl PartialEq for WatcherWrapper {
} }
} }
/// The [`App`] stores application-specific state. // The [`App`] stores application-specific state.
pub struct App { pub struct App {
core: Core, core: Core,
nav_bar_context_id: segmented_button::Entity, nav_bar_context_id: segmented_button::Entity,
@ -585,7 +645,7 @@ pub struct App {
app_themes: Vec<String>, app_themes: Vec<String>,
compio_tx: mpsc::Sender<Pin<Box<dyn Future<Output = ()> + Send>>>, compio_tx: mpsc::Sender<Pin<Box<dyn Future<Output = ()> + Send>>>,
context_page: ContextPage, context_page: ContextPage,
dialog_pages: VecDeque<DialogPage>, dialog_pages: DialogPages,
dialog_text_input: widget::Id, dialog_text_input: widget::Id,
key_binds: HashMap<KeyBind, Action>, key_binds: HashMap<KeyBind, Action>,
margin: HashMap<window::Id, (f32, f32, f32, f32)>, margin: HashMap<window::Id, (f32, f32, f32, f32)>,
@ -739,30 +799,31 @@ impl App {
let len = commands.len(); let len = commands.len();
for (i, mut command) in commands.into_iter().enumerate() { for (i, mut command) in commands.into_iter().enumerate() {
if let Err(err) = spawn_detached(&mut command) { match spawn_detached(&mut command) {
// More than one command: The app doesn't support lists of paths so each command Ok(()) => {
// is associated with one instance for path in paths {
// let _ = recently_used_xbel::update_recently_used(
// One command: Attempted to launch one app with multiple paths &path.into(),
let path = if len > 1 { App::APP_ID.to_string(),
format!("{:?}", paths.get(i)) "cosmic-files".to_string(),
} else { None,
format!("{paths:?}") );
}; }
log::warn!("failed to open {:?} with {:?}: {}", path, app.id, err); }
Err(err) => {
// More than one command: The app doesn't support lists of paths so each command
// is associated with one instance
//
// One command: Attempted to launch one app with multiple paths
let path = if len > 1 {
format!("{:?}", paths.get(i))
} else {
format!("{paths:?}")
};
log::warn!("failed to open {:?} with {:?}: {}", path, app.id, err);
}
} }
} }
for path in paths {
let _ = recently_used_xbel::update_recently_used(
&path.into(),
App::APP_ID.to_string(),
"cosmic-files".to_string(),
None,
);
}
return true;
} }
// No app matched for mimes and paths // No app matched for mimes and paths
@ -2032,7 +2093,7 @@ impl Application for App {
app_themes, app_themes,
compio_tx, compio_tx,
context_page: ContextPage::Preview(None, PreviewKind::Selected), context_page: ContextPage::Preview(None, PreviewKind::Selected),
dialog_pages: VecDeque::new(), dialog_pages: DialogPages::new(),
dialog_text_input: widget::Id::unique(), dialog_text_input: widget::Id::unique(),
key_binds, key_binds,
margin: HashMap::new(), margin: HashMap::new(),
@ -2324,8 +2385,8 @@ impl Application for App {
let entity = self.tab_model.active(); let entity = self.tab_model.active();
// Close dialog if open // Close dialog if open
if self.dialog_pages.pop_front().is_some() { if let Some((_page, task)) = self.dialog_pages.pop_front() {
return Task::none(); return task;
} }
// Close gallery mode if open // Close gallery mode if open
@ -2463,14 +2524,16 @@ impl Application for App {
let to = destination.0.to_path_buf(); let to = destination.0.to_path_buf();
let name = destination.1.to_str().unwrap_or_default().to_string(); let name = destination.1.to_str().unwrap_or_default().to_string();
let archive_type = ArchiveType::default(); let archive_type = ArchiveType::default();
self.dialog_pages.push_back(DialogPage::Compress { return Task::batch([
paths, self.dialog_pages.push_back(DialogPage::Compress {
to, paths,
name, to,
archive_type, name,
password: None, archive_type,
}); password: None,
return widget::text_input::focus(self.dialog_text_input.clone()); }),
widget::text_input::focus(self.dialog_text_input.clone()),
]);
} }
} }
} }
@ -2575,11 +2638,52 @@ impl Application for App {
self.windows.insert(id, WindowKind::DesktopViewOptions); self.windows.insert(id, WindowKind::DesktopViewOptions);
return command.map(|_id| cosmic::action::none()); return command.map(|_id| cosmic::action::none());
} }
Message::DesktopDialogs(show) => {
if matches!(self.mode, Mode::Desktop) {
if show {
//TODO: would it be better to make this a layer surface?
let mut settings = window::Settings {
decorations: false,
level: window::Level::AlwaysOnTop,
max_size: Some(Size::new(1280.0, 640.0)),
min_size: Some(Size::new(320.0, 180.0)),
position: window::Position::Centered,
resizable: false,
size: Size::new(640.0, 320.0),
transparent: true,
..Default::default()
};
#[cfg(target_os = "linux")]
{
// Use the dialog ID to make it float
settings.platform_specific.application_id =
"com.system76.CosmicFilesDialog".to_string();
}
let (id, command) = window::open(settings);
self.windows
.insert(id, WindowKind::Dialogs(widget::Id::unique()));
return command.map(|_id| cosmic::Action::None);
} else {
let mut tasks = Vec::new();
for (id, kind) in self.windows.iter() {
if matches!(kind, WindowKind::Dialogs(_)) {
tasks.push(window::close(*id));
}
}
return Task::batch(tasks);
}
}
}
Message::DialogCancel => { Message::DialogCancel => {
self.dialog_pages.pop_front(); if let Some((_page, task)) = self.dialog_pages.pop_front() {
return task;
}
} }
Message::DialogComplete => { Message::DialogComplete => {
if let Some(dialog_page) = self.dialog_pages.pop_front() { if let Some((dialog_page, task)) = self.dialog_pages.pop_front() {
let mut tasks = vec![task];
match dialog_page { match dialog_page {
DialogPage::Compress { DialogPage::Compress {
paths, paths,
@ -2631,13 +2735,13 @@ impl Application for App {
auth, auth,
auth_tx, auth_tx,
} => { } => {
return Task::perform( tasks.push(Task::perform(
async move { async move {
auth_tx.send(auth).await.unwrap(); auth_tx.send(auth).await.unwrap();
cosmic::action::none() cosmic::action::none()
}, },
|x| x, |x| x,
); ));
} }
DialogPage::NetworkError { DialogPage::NetworkError {
mounter_key: _, mounter_key: _,
@ -2645,10 +2749,8 @@ impl Application for App {
error: _, error: _,
} => { } => {
//TODO: re-use mounter_key? //TODO: re-use mounter_key?
return Task::batch([ tasks.push(self.update(Message::NetworkDriveInput(uri)));
self.update(Message::NetworkDriveInput(uri)), tasks.push(self.update(Message::NetworkDriveSubmit));
self.update(Message::NetworkDriveSubmit),
]);
} }
DialogPage::NewItem { parent, name, dir } => { DialogPage::NewItem { parent, name, dir } => {
let path = parent.join(name); let path = parent.join(name);
@ -2723,15 +2825,14 @@ impl Application for App {
} }
} }
} }
return Task::batch(tasks);
} }
} }
Message::DialogPush(dialog_page) => { Message::DialogPush(dialog_page) => {
self.dialog_pages.push_back(dialog_page); return self.dialog_pages.push_back(dialog_page);
} }
Message::DialogUpdate(dialog_page) => { Message::DialogUpdate(dialog_page) => {
if !self.dialog_pages.is_empty() { self.dialog_pages.update_front(dialog_page);
self.dialog_pages[0] = dialog_page;
}
} }
Message::DialogUpdateComplete(dialog_page) => { Message::DialogUpdateComplete(dialog_page) => {
return Task::batch([ return Task::batch([
@ -2944,7 +3045,7 @@ impl Application for App {
} }
Err(error) => { Err(error) => {
log::warn!("failed to connect to {:?}: {}", item, error); log::warn!("failed to connect to {:?}: {}", item, error);
self.dialog_pages.push_back(DialogPage::MountError { return self.dialog_pages.push_back(DialogPage::MountError {
mounter_key, mounter_key,
item, item,
error, error,
@ -2952,13 +3053,15 @@ impl Application for App {
} }
}, },
Message::NetworkAuth(mounter_key, uri, auth, auth_tx) => { Message::NetworkAuth(mounter_key, uri, auth, auth_tx) => {
self.dialog_pages.push_back(DialogPage::NetworkAuth { return Task::batch([
mounter_key, self.dialog_pages.push_back(DialogPage::NetworkAuth {
uri, mounter_key,
auth, uri,
auth_tx, auth,
}); auth_tx,
return widget::text_input::focus(self.dialog_text_input.clone()); }),
widget::text_input::focus(self.dialog_text_input.clone()),
]);
} }
Message::NetworkDriveInput(input) => { Message::NetworkDriveInput(input) => {
self.network_drive_input = input; self.network_drive_input = input;
@ -2993,7 +3096,7 @@ impl Application for App {
} }
Err(error) => { Err(error) => {
log::warn!("failed to connect to {:?}: {}", uri, error); log::warn!("failed to connect to {:?}: {}", uri, error);
self.dialog_pages.push_back(DialogPage::NetworkError { return self.dialog_pages.push_back(DialogPage::NetworkError {
mounter_key, mounter_key,
uri, uri,
error, error,
@ -3005,12 +3108,14 @@ impl Application for App {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) { if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
if let Some(path) = &tab.location.path_opt() { if let Some(path) = &tab.location.path_opt() {
self.dialog_pages.push_back(DialogPage::NewItem { return Task::batch([
parent: path.to_path_buf(), self.dialog_pages.push_back(DialogPage::NewItem {
name: String::new(), parent: path.to_path_buf(),
dir, name: String::new(),
}); dir,
return widget::text_input::focus(self.dialog_text_input.clone()); }),
widget::text_input::focus(self.dialog_text_input.clone()),
]);
} }
} }
} }
@ -3171,11 +3276,14 @@ impl Application for App {
)) ))
} }
Message::OpenWithBrowse => match self.dialog_pages.pop_front() { Message::OpenWithBrowse => match self.dialog_pages.pop_front() {
Some(DialogPage::OpenWith { Some((
mime, DialogPage::OpenWith {
store_opt: Some(app), mime,
.. store_opt: Some(app),
}) => { ..
},
task,
)) => {
let url = format!("mime:///{mime}"); let url = format!("mime:///{mime}");
// TODO: Support multiple URLs // TODO: Support multiple URLs
if let Some(mut command) = if let Some(mut command) =
@ -3191,9 +3299,11 @@ impl Application for App {
app.id app.id
); );
} }
return task;
} }
Some(dialog_page) => { Some((dialog_page, task)) => {
self.dialog_pages.push_front(dialog_page); log::warn!("tried to open with browse from the wrong dialog");
return Task::batch([task, self.dialog_pages.push_front(dialog_page)]);
} }
None => {} None => {}
}, },
@ -3346,16 +3456,17 @@ impl Application for App {
self.progress_operations.clear(); self.progress_operations.clear();
} }
Message::PendingError(id, err) => { Message::PendingError(id, err) => {
let mut tasks = Vec::new();
if let Some((op, controller)) = self.pending_operations.remove(&id) { if let Some((op, controller)) = self.pending_operations.remove(&id) {
// Only show dialog if not cancelled // Only show dialog if not cancelled
if !controller.is_cancelled() { if !controller.is_cancelled() {
self.dialog_pages.push_back(match err.kind { tasks.push(self.dialog_pages.push_back(match err.kind {
OperationErrorType::Generic(_) => DialogPage::FailedOperation(id), OperationErrorType::Generic(_) => DialogPage::FailedOperation(id),
OperationErrorType::PasswordRequired => DialogPage::ExtractPassword { OperationErrorType::PasswordRequired => DialogPage::ExtractPassword {
id, id,
password: String::from(""), password: String::from(""),
}, },
}); }));
} }
// Remove from progress // Remove from progress
self.progress_operations.remove(&id); self.progress_operations.remove(&id);
@ -3371,7 +3482,8 @@ impl Application for App {
self.progress_operations.clear(); self.progress_operations.clear();
} }
// Manually rescan any trash tabs after any operation is completed // Manually rescan any trash tabs after any operation is completed
return self.rescan_trash(); tasks.push(self.rescan_trash());
return Task::batch(tasks);
} }
Message::PendingPause(id, pause) => { Message::PendingPause(id, pause) => {
if let Some((_, controller)) = self.pending_operations.get(&id) { if let Some((_, controller)) = self.pending_operations.get(&id) {
@ -3473,6 +3585,7 @@ impl Application for App {
} }
if !selected.is_empty() { if !selected.is_empty() {
//TODO: batch rename //TODO: batch rename
let mut tasks = Vec::new();
for path in selected { for path in selected {
let parent = match path.parent() { let parent = match path.parent() {
Some(some) => some.to_path_buf(), Some(some) => some.to_path_buf(),
@ -3483,20 +3596,21 @@ impl Application for App {
None => continue, None => continue,
}; };
let dir = path.is_dir(); let dir = path.is_dir();
self.dialog_pages.push_back(DialogPage::RenameItem { tasks.push(self.dialog_pages.push_back(DialogPage::RenameItem {
from: path, from: path,
parent, parent,
name, name,
dir, dir,
}); }));
} }
return widget::text_input::focus(self.dialog_text_input.clone()); tasks.push(widget::text_input::focus(self.dialog_text_input.clone()));
return Task::batch(tasks);
} }
} }
} }
} }
Message::ReplaceResult(replace_result) => { Message::ReplaceResult(replace_result) => {
if let Some(dialog_page) = self.dialog_pages.pop_front() { if let Some((dialog_page, task)) = self.dialog_pages.pop_front() {
match dialog_page { match dialog_page {
DialogPage::Replace { tx, .. } => { DialogPage::Replace { tx, .. } => {
return Task::perform( return Task::perform(
@ -3509,7 +3623,7 @@ impl Application for App {
} }
other => { other => {
log::warn!("tried to send replace result to the wrong dialog"); log::warn!("tried to send replace result to the wrong dialog");
self.dialog_pages.push_front(other); return Task::batch([task, self.dialog_pages.push_front(other)]);
} }
} }
} }
@ -3715,7 +3829,7 @@ impl Application for App {
commands.push(self.update(Message::PasteContents(to, from))); commands.push(self.update(Message::PasteContents(to, from)));
} }
tab::Command::EmptyTrash => { tab::Command::EmptyTrash => {
self.dialog_pages.push_back(DialogPage::EmptyTrash); return self.dialog_pages.push_back(DialogPage::EmptyTrash);
} }
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
tab::Command::ExecEntryAction(entry, action) => { tab::Command::ExecEntryAction(entry, action) => {
@ -3949,6 +4063,9 @@ impl Application for App {
Message::WindowCloseRequested(id) => { Message::WindowCloseRequested(id) => {
self.remove_window(&id); self.remove_window(&id);
} }
Message::WindowMaximize(id, maximized) => {
return window::maximize(id, maximized);
}
Message::WindowNew => match env::current_exe() { Message::WindowNew => match env::current_exe() {
Ok(exe) => match process::Command::new(&exe).spawn() { Ok(exe) => match process::Command::new(&exe).spawn() {
Ok(_child) => {} Ok(_child) => {}
@ -4153,7 +4270,7 @@ impl Application for App {
.and_then(|x| x.path_opt()) .and_then(|x| x.path_opt())
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
{ {
self.open_file(&[path]); return self.open_file(&[path]).into();
} }
} }
NavMenuAction::OpenWith(entity) => { NavMenuAction::OpenWith(entity) => {
@ -4282,7 +4399,7 @@ impl Application for App {
} }
NavMenuAction::EmptyTrash => { NavMenuAction::EmptyTrash => {
self.dialog_pages.push_front(DialogPage::EmptyTrash); return self.dialog_pages.push_front(DialogPage::EmptyTrash);
} }
}, },
Message::Recents => { Message::Recents => {
@ -5477,9 +5594,11 @@ impl Application for App {
}; };
let mut popover = widget::popover(tab_view); let mut popover = widget::popover(tab_view);
/*
if let Some(dialog) = self.dialog() { if let Some(dialog) = self.dialog() {
popover = popover.popup(dialog); popover = popover.popup(dialog);
} }
*/
tab_column = tab_column.push(popover); tab_column = tab_column.push(popover);
// The toaster is added on top of an empty element to ensure that it does not override context menus // The toaster is added on top of an empty element to ensure that it does not override context menus
@ -5507,6 +5626,10 @@ impl Application for App {
}; };
} }
Some(WindowKind::DesktopViewOptions) => self.desktop_view_options(), Some(WindowKind::DesktopViewOptions) => self.desktop_view_options(),
Some(WindowKind::Dialogs(id)) => match self.dialog() {
Some(element) => return widget::autosize::autosize(element, id.clone()).into(),
None => widget::horizontal_space().into(),
},
Some(WindowKind::Preview(entity_opt, kind)) => self Some(WindowKind::Preview(entity_opt, kind)) => self
.preview(entity_opt, kind, false) .preview(entity_opt, kind, false)
.map(|x| Message::TabMessage(*entity_opt, x)), .map(|x| Message::TabMessage(*entity_opt, x)),