diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index fe080cd..9fb6e7c 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -46,6 +46,10 @@ resume = Resume ## Compress Dialog create-archive = Create archive +## Copy To Dialog +copy-to-title = Select copy destination +copy-to-button-label = Copy + ## Extract Dialog extract-password-required = Password required extract-to = Extract To... @@ -59,6 +63,10 @@ empty-trash-warning = Items in the Trash folder will be permanently deleted ## Mount Error Dialog mount-error = Unable to access drive +## Move To Dialog +move-to-title = Select move destination +move-to-button-label = Move + ## New File/Folder Dialog create-new-file = Create new file create-new-folder = Create new folder @@ -305,12 +313,14 @@ type-to-search-select = Selects the first matching file or folder # Context menu add-to-sidebar = Add to sidebar compress = Compress +copy-to = Copy to... delete-permanently = Delete permanently eject = Eject extract-here = Extract new-file = New file... new-folder = New folder... open-in-terminal = Open in terminal +move-to = Move to... move-to-trash = Move to trash restore-from-trash = Restore from trash remove-from-sidebar = Remove from sidebar diff --git a/src/app.rs b/src/app.rs index b1049db..3e6d9f0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -146,6 +146,7 @@ pub enum Action { AddToSidebar, Compress, Copy, + CopyTo, Cut, CosmicSettingsDesktop, CosmicSettingsDisplays, @@ -168,6 +169,7 @@ pub enum Action { ItemRight, ItemUp, LocationUp, + MoveTo, NewFile, NewFolder, Open, @@ -213,6 +215,7 @@ impl Action { Self::AddToSidebar => Message::AddToSidebar(entity_opt), Self::Compress => Message::Compress(entity_opt), Self::Copy => Message::Copy(entity_opt), + Self::CopyTo => Message::CopyTo(entity_opt), Self::Cut => Message::Cut(entity_opt), Self::CosmicSettingsDesktop => Message::CosmicSettings("desktop"), Self::CosmicSettingsDisplays => Message::CosmicSettings("displays"), @@ -237,6 +240,7 @@ impl Action { Self::ItemRight => Message::TabMessage(entity_opt, tab::Message::ItemRight), Self::ItemUp => Message::TabMessage(entity_opt, tab::Message::ItemUp), Self::LocationUp => Message::TabMessage(entity_opt, tab::Message::LocationUp), + Self::MoveTo => Message::MoveTo(entity_opt), Self::NewFile => Message::NewItem(entity_opt, false), Self::NewFolder => Message::NewItem(entity_opt, true), Self::Open => Message::TabMessage(entity_opt, tab::Message::Open(None)), @@ -335,6 +339,8 @@ pub enum Message { Compress(Option), Config(Config), Copy(Option), + CopyTo(Option), + CopyToResult(DialogResult), CosmicSettings(&'static str), Cut(Option), Delete(Option), @@ -359,6 +365,8 @@ pub enum Message { ModifiersChanged(window::Id, Modifiers), MounterItems(MounterKey, MounterItems), MountResult(MounterKey, MounterItem, Result), + MoveTo(Option), + MoveToResult(DialogResult), NavBarClose(Entity), NavBarContext(Entity), NavMenuAction(NavMenuAction), @@ -949,7 +957,13 @@ impl App { } } - fn extract_to(&mut self, paths: &[impl AsRef]) -> Task { + fn destination_selection_dialog( + &mut self, + paths: &[impl AsRef], + on_result: impl Fn(DialogResult) -> Message + 'static, + title: impl Into, + accept_label: impl AsRef, + ) -> Task { if let Some(destination) = paths .first() .and_then(|first| first.as_ref().parent()) @@ -960,10 +974,10 @@ impl App { .kind(DialogKind::OpenFolder) .path(destination), Message::FileDialogMessage, - Message::ExtractToResult, + on_result, ); - let set_title_task = dialog.set_title(fl!("extract-to-title")); - dialog.set_accept_label(fl!("extract-here")); + let set_title_task = dialog.set_title(title); + dialog.set_accept_label(accept_label); self.windows.insert( dialog.window_id(), Window::new(WindowKind::FileDialog(Some( @@ -977,6 +991,33 @@ impl App { } } + fn extract_to(&mut self, paths: &[impl AsRef]) -> Task { + self.destination_selection_dialog( + paths, + Message::ExtractToResult, + fl!("extract-to-title"), + fl!("extract-here"), + ) + } + + fn move_to(&mut self, paths: &[impl AsRef]) -> Task { + self.destination_selection_dialog( + paths, + Message::MoveToResult, + fl!("move-to-title"), + fl!("move-to-button-label"), + ) + } + + fn copy_to(&mut self, paths: &[impl AsRef]) -> Task { + self.destination_selection_dialog( + paths, + Message::CopyToResult, + fl!("copy-to-title"), + fl!("copy-to-button-label"), + ) + } + #[cfg(all(feature = "wayland", feature = "desktop-applet"))] fn handle_overlap(&mut self) { let mut overlaps: FxHashMap<_, _> = self @@ -2675,6 +2716,34 @@ impl Application for App { let contents = ClipboardCopy::new(ClipboardKind::Copy, paths); return clipboard::write_data(contents); } + Message::CopyTo(entity_opt) => { + let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect(); + return self.copy_to(&selected_paths); + } + Message::CopyToResult(result) => { + match result { + DialogResult::Cancel => {} + DialogResult::Open(selected_paths) => { + let mut file_paths = None; + if let Some(file_dialog) = &self.file_dialog_opt + && let Some(window) = self.windows.remove(&file_dialog.window_id()) + && let WindowKind::FileDialog(paths) = window.kind + { + file_paths = paths; + } + if let Some(file_paths) = file_paths + && !selected_paths.is_empty() + { + self.file_dialog_opt = None; + return self.operation(Operation::Copy { + paths: file_paths.to_vec(), + to: selected_paths[0].clone(), + }); + } + } + } + self.file_dialog_opt = None; + } Message::Cut(entity_opt) => { self.set_cut(entity_opt); let paths = self.selected_paths(entity_opt); @@ -3195,6 +3264,35 @@ impl Application for App { ); } }, + Message::MoveTo(entity_opt) => { + let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect(); + return self.move_to(&selected_paths); + } + Message::MoveToResult(result) => { + match result { + DialogResult::Cancel => {} + DialogResult::Open(selected_paths) => { + let mut file_paths = None; + if let Some(file_dialog) = &self.file_dialog_opt + && let Some(window) = self.windows.remove(&file_dialog.window_id()) + && let WindowKind::FileDialog(paths) = window.kind + { + file_paths = paths; + } + if let Some(file_paths) = file_paths + && !selected_paths.is_empty() + { + self.file_dialog_opt = None; + return self.operation(Operation::Move { + paths: file_paths.to_vec(), + to: selected_paths[0].clone(), + cross_device_copy: false, + }); + } + } + } + self.file_dialog_opt = None; + } Message::NetworkAuth(mounter_key, uri, auth, auth_tx) => { return self.push_dialog( DialogPage::NetworkAuth { diff --git a/src/menu.rs b/src/menu.rs index 6af7687..eb3929f 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -205,6 +205,10 @@ pub fn context_menu<'a>( children.push(menu_item(fl!("cut"), Action::Cut).into()); } children.push(menu_item(fl!("copy"), Action::Copy).into()); + if selected_mount_point == 0 { + children.push(menu_item(fl!("move-to"), Action::MoveTo).into()); + } + children.push(menu_item(fl!("copy-to"), Action::CopyTo).into()); children.push(divider::horizontal::light().into()); let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES;