diff --git a/i18n/de/cosmic_files.ftl b/i18n/de/cosmic_files.ftl index 640904b..1d32d3b 100644 --- a/i18n/de/cosmic_files.ftl +++ b/i18n/de/cosmic_files.ftl @@ -79,6 +79,15 @@ save-file = Datei speichern open-with-title = Wie möchtest du „{$name}“ öffnen? browse-store = {$store} durchsuchen +## Dauerhaft Löschen Dialog +selected-items = {$items} gewählte Objekte +permanently-delete-question = {$target} dauerhaft löschen? +delete = Löschen +permanently-delete-warning = {$target} dauerhaft löschen, {$nb_items -> + [one] es kann + *[other] Sie können + } nicht wiederhergestellt werden. + # Umbenennen-Dialog rename-file = Datei umbenennen rename-folder = Ordner umbenennen @@ -212,6 +221,14 @@ restored = {$items} {$items -> *[other] Elemente wurden } aus dem {trash} wiederhergestellt unknown-folder = unbekannter Ordner +permanently-deleting = Lösche {$items} {$items -> + [one] Objekt + *[other] Objekte + } dauerhaft +permanently-deleted = {$items} {$items -> + [one] Objekt + *[other] Objekte + } dauerhaft gelöscht ## Öffnen mit menu-open-with = Öffnen mit @@ -229,6 +246,7 @@ calculating = Wird berechnet... ## Einstellungen settings = Einstellungen +settings-show-delete-permanently = Dauerhaft löschen ### Aussehen appearance = Aussehen @@ -245,6 +263,7 @@ new-file = Neue Datei new-folder = Neuer Ordner open-in-terminal = Im Terminal öffnen move-to-trash = In den Papierkorb verschieben +delete-permanently = Dauerhaft löschen... restore-from-trash = Aus dem Papierkorb wiederherstellen remove-from-sidebar = Von der Seitenleiste entfernen sort-by-name = Nach Name sortieren diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index f31de44..3841ecb 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -83,6 +83,15 @@ browse-store = Browse {$store} other-apps = Other applications related-apps = Related applications +## Permanently delete Dialog +selected-items = the {$items} selected items +permanently-delete-question = Permanently delete? +delete = Delete +permanently-delete-warning = Permanently delete {$target}, {$nb_items -> + [one] he + *[other] they + } can not be restored + ## Rename Dialog rename-file = Rename file rename-folder = Rename folder @@ -223,6 +232,14 @@ moved = Moved {$items} {$items -> [one] item *[other] items } from "{$from}" to "{$to}" +permanently-deleting = Permanently deleting "{$items}" "{$items -> + [one] item + *[other] items + }" +permanently-deleted = Permanently deleted "{$items}" "{$items -> + [one] item + *[other] items + }" renaming = Renaming "{$from}" to "{$to}" renamed = Renamed "{$from}" to "{$to}" restoring = Restoring {$items} {$items -> diff --git a/i18n/fr/cosmic_files.ftl b/i18n/fr/cosmic_files.ftl index 824a03a..94943bf 100644 --- a/i18n/fr/cosmic_files.ftl +++ b/i18n/fr/cosmic_files.ftl @@ -81,6 +81,15 @@ save-file = Enregistrer fichier open-with-title = Comment souhaitez-vous ouvrir "{$name}" ? browse-store = Parcourir {$store} +## Permanently delete Dialog +selected-items = les {$items} éléments sélectionnés +permanently-delete-question = Supprimer définitivement ? +delete = Supprimer +permanently-delete-warning = Supprimer définitivement {$target}, {$nb_items -> + [one] il ne pourras pas être restauré. + *[other] ils ne pourront pas être restaurés. +} + ## Rename Dialog rename-file = Renommer le fichier rename-folder = Renommer le dossier @@ -221,6 +230,14 @@ moved = {$items} {$items -> [one] élément déplacé *[other] éléments déplacés } depuis {$from} vers {$to} +permanently-deleting = Suppression définitive de "{$items}" "{$items -> + [one] item + *[other] items + }" +permanently-deleted = Supprimés définitivement "{$items}" "{$items -> + [one] item + *[other] items + }" renaming = Renommage de {$from} en {$to} renamed = {$from} renommé en {$to} restoring = Restauration de {$items} {$items -> diff --git a/src/app.rs b/src/app.rs index 4690e05..3b33e5e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -33,7 +33,7 @@ use cosmic::{ widget::{ self, dnd_destination::DragId, - horizontal_space, + horizontal_space, icon, menu::{action::MenuAction, key_bind::KeyBind}, segmented_button::{self, Entity}, vertical_space, @@ -138,6 +138,7 @@ pub enum Action { OpenTerminal, OpenWith, Paste, + PermanentlyDelete, Preview, Rename, RestoreFromTrash, @@ -205,6 +206,7 @@ impl Action { Action::OpenTerminal => Message::OpenTerminal(entity_opt), Action::OpenWith => Message::OpenWithDialog(entity_opt), Action::Paste => Message::Paste(entity_opt), + Action::PermanentlyDelete => Message::PermanentlyDelete(entity_opt), Action::Preview => Message::Preview(entity_opt), Action::Rename => Message::Rename(entity_opt), Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt), @@ -313,7 +315,7 @@ pub enum Message { Key(Modifiers, Key, Option), LaunchUrl(String), MaybeExit, - Modifiers(Modifiers), + ModifiersChanged(Modifiers), MounterItems(MounterKey, MounterItems), MountResult(MounterKey, MounterItem, Result), NavBarClose(Entity), @@ -346,6 +348,7 @@ pub enum Message { PendingError(u64, OperationError), PendingPause(u64, bool), PendingPauseAll(bool), + PermanentlyDelete(Option), Preview(Option), RescanTrash, Rename(Option), @@ -481,6 +484,9 @@ pub enum DialogPage { selected: usize, store_opt: Option, }, + PermanentlyDelete { + paths: Vec, + }, RenameItem { from: PathBuf, parent: PathBuf, @@ -2463,6 +2469,9 @@ impl Application for App { } } } + DialogPage::PermanentlyDelete { paths } => { + return self.operation(Operation::PermanentlyDelete { paths }); + } DialogPage::RenameItem { from, parent, name, .. } => { @@ -2620,8 +2629,13 @@ impl Application for App { log::warn!("failed to open {:?}: {}", url, err); } }, - Message::Modifiers(modifiers) => { + Message::ModifiersChanged(modifiers) => { self.modifiers = modifiers; + let entity = self.tab_model.active(); + return self.update(Message::TabMessage( + Some(entity), + tab::Message::ModifiersChanged(modifiers), + )); } Message::MounterItems(mounter_key, mounter_items) => { // Check for unmounted folders @@ -3143,6 +3157,13 @@ impl Application for App { } } } + Message::PermanentlyDelete(entity_opt) => { + let paths = self.selected_paths(entity_opt); + if !paths.is_empty() { + self.dialog_pages + .push_back(DialogPage::PermanentlyDelete { paths }); + } + } Message::Preview(entity_opt) => { match self.mode { Mode::App => { @@ -4661,6 +4682,35 @@ impl Application for App { dialog } + DialogPage::PermanentlyDelete { paths } => { + let target = if paths.len() == 1 { + format!( + "« {} »", + paths[0] + .file_name() + .map(std::ffi::OsStr::to_string_lossy) + .unwrap_or_else(|| paths[0].to_string_lossy()) + ) + } else { + fl!("selected-items", items = paths.len()) + }; + + widget::dialog() + .title(fl!("permanently-delete-question")) + .icon(icon::from_name("dialog-warning").size(32)) + .primary_action( + widget::button::destructive(fl!("delete")) + .on_press(Message::DialogComplete), + ) + .secondary_action( + widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), + ) + .control(widget::text(fl!( + "permanently-delete-warning", + nb_items = paths.len(), + target = target + ))) + } DialogPage::RenameItem { from, parent, @@ -4957,6 +5007,7 @@ impl Application for App { &self.core, self.tab_model.active_data::(), &self.config, + &self.modifiers, &self.key_binds, )] } @@ -5148,7 +5199,7 @@ impl Application for App { event::Status::Captured => None, }, Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { - Some(Message::Modifiers(modifiers)) + Some(Message::ModifiersChanged(modifiers)) } Event::Window(WindowEvent::Unfocused) => Some(Message::WindowUnfocus), #[cfg(all(feature = "desktop", feature = "wayland"))] diff --git a/src/dialog.rs b/src/dialog.rs index e226dea..442d4cc 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -382,7 +382,7 @@ enum Message { Filename(String), Filter(usize), Key(Modifiers, Key), - Modifiers(Modifiers), + ModifiersChanged(Modifiers), MounterItems(MounterKey, MounterItems), NewFolder, NotifyEvents(Vec), @@ -1290,8 +1290,11 @@ impl Application for App { } } } - Message::Modifiers(modifiers) => { + Message::ModifiersChanged(modifiers) => { self.modifiers = modifiers; + return self.update(Message::TabMessage(tab::Message::ModifiersChanged( + modifiers, + ))); } Message::MounterItems(mounter_key, mounter_items) => { // Check for unmounted folders @@ -1757,7 +1760,7 @@ impl Application for App { event::Status::Captured => None, }, Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { - Some(Message::Modifiers(modifiers)) + Some(Message::ModifiersChanged(modifiers)) } Event::Mouse(mouse::Event::CursorMoved { position: pos }) => { Some(Message::CursorMoved(pos)) diff --git a/src/key_bind.rs b/src/key_bind.rs index b139887..e0b528e 100644 --- a/src/key_bind.rs +++ b/src/key_bind.rs @@ -68,6 +68,7 @@ pub fn key_binds(mode: &tab::Mode) -> HashMap { bind!([Ctrl], Key::Character("c".into()), Copy); bind!([Ctrl], Key::Character("x".into()), Cut); bind!([], Key::Named(Named::Delete), Delete); + bind!([Shift], Key::Named(Named::Delete), PermanentlyDelete); bind!([Shift], Key::Named(Named::Enter), OpenInNewWindow); bind!([Ctrl], Key::Character("v".into()), Paste); bind!([], Key::Named(Named::F2), Rename); diff --git a/src/menu.rs b/src/menu.rs index 4182e83..af53dd1 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -2,7 +2,7 @@ use cosmic::{ app::Core, - iced::{Alignment, Background, Border, Length}, + iced::{keyboard::Modifiers, Alignment, Background, Border, Length}, theme, widget::{ self, button, column, container, divider, horizontal_space, @@ -55,6 +55,7 @@ fn menu_button_optional( pub fn context_menu<'a>( tab: &Tab, key_binds: &HashMap, + modifiers: &Modifiers, ) -> Element<'a, tab::Message> { let find_key = |action: &Action| -> String { for (key_bind, key_action) in key_binds.iter() { @@ -216,7 +217,13 @@ pub fn context_menu<'a>( children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into()); } children.push(divider::horizontal::light().into()); - children.push(menu_item(fl!("move-to-trash"), Action::Delete).into()); + if modifiers.shift() && !modifiers.control() { + children.push( + menu_item(fl!("delete-permanently"), Action::PermanentlyDelete).into(), + ); + } else { + children.push(menu_item(fl!("move-to-trash"), Action::Delete).into()); + } } else { //TODO: need better designs for menu with no selection //TODO: have things like properties but they apply to the folder? @@ -492,6 +499,7 @@ pub fn menu_bar<'a>( core: &Core, tab_opt: Option<&Tab>, config: &Config, + modifiers: &Modifiers, key_binds: &HashMap, ) -> Element<'a, Message> { let sort_options = tab_opt.map(|tab| tab.sort_options()); @@ -524,6 +532,12 @@ pub fn menu_bar<'a>( } }; + let (delete_item, delete_item_action) = if in_trash || modifiers.shift() { + (fl!("delete-permanently"), Action::Delete) + } else { + (fl!("move-to-trash"), Action::Delete) + }; + responsive_menu_bar() .item_height(ItemHeight::Dynamic(40)) .item_width(ItemWidth::Uniform(360)) @@ -562,14 +576,11 @@ pub fn menu_bar<'a>( ), menu::Item::Divider, menu_button_optional( - if in_trash { - fl!("delete-permanently") - } else { - fl!("move-to-trash") - }, - Action::Delete, - selected > 0, + fl!("restore-from-trash"), + Action::RestoreFromTrash, + selected > 0 && in_trash, ), + menu_button_optional(delete_item, delete_item_action, selected > 0), menu::Item::Divider, menu::Item::Button(fl!("close-tab"), None, Action::TabClose), menu::Item::Button(fl!("quit"), None, Action::WindowClose), diff --git a/src/operation/mod.rs b/src/operation/mod.rs index b91ae3d..e2f9352 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -496,6 +496,10 @@ pub enum Operation { NewFolder { path: PathBuf, }, + /// Permanently delete items, skipping the trash + PermanentlyDelete { + paths: Vec, + }, Rename { from: PathBuf, to: PathBuf, @@ -599,6 +603,7 @@ impl Operation { name = file_name(path), parent = parent_name(path) ), + Self::PermanentlyDelete { paths } => fl!("permanently-deleting", items = paths.len()), Self::Rename { from, to } => { fl!("renaming", from = file_name(from), to = file_name(to)) } @@ -657,6 +662,7 @@ impl Operation { name = file_name(path), parent = parent_name(path) ), + Self::PermanentlyDelete { paths } => fl!("permanently-deleted", items = paths.len()), Self::Rename { from, to } => fl!("renamed", from = file_name(from), to = file_name(to)), Self::Restore { items } => fl!("restored", items = items.len()), Self::SetExecutableAndLaunch { path } => { @@ -675,6 +681,7 @@ impl Operation { | Self::EmptyTrash | Self::Extract { .. } | Self::Move { .. } + | Self::PermanentlyDelete { .. } | Self::Restore { .. } => true, Self::NewFile { .. } | Self::NewFolder { .. } @@ -1075,6 +1082,32 @@ impl Operation { .await .map_err(wrap_compio_spawn_error)? .map_err(OperationError::from_str), + Self::PermanentlyDelete { paths } => { + let total = paths.len(); + for (idx, path) in paths.into_iter().enumerate() { + controller.check().await.map_err(OperationError::from_str)?; + + controller.set_progress((idx as f32) / (total as f32)); + + tokio::task::spawn_blocking(|| { + if path.is_symlink() || path.is_file() { + fs::remove_file(path) + } else if path.is_dir() { + fs::remove_dir_all(path) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "File to delete is not symlink, file or directory", + )) + } + }) + .await + .map_err(OperationError::from_str)? + .map_err(OperationError::from_str)?; + } + + Ok(OperationSelection::default()) + } Self::Rename { from, to } => compio::runtime::spawn(async move { controller.check().await.map_err(OperationError::from_str)?; compio::fs::rename(&from, &to) diff --git a/src/tab.rs b/src/tab.rs index 03224b3..a13430d 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1321,6 +1321,7 @@ pub enum Message { ItemUp, Location(Location), LocationUp, + ModifiersChanged(Modifiers), Open(Option), RightClick(Option), MiddleClick(usize), @@ -1974,6 +1975,7 @@ pub struct Tab { select_range: Option<(usize, usize)>, clicked: Option, selected_clicked: bool, + modifiers: Modifiers, last_right_click: Option, search_context: Option, global_cursor_position: Option, @@ -2070,6 +2072,7 @@ impl Tab { clicked: None, dnd_hovered: None, selected_clicked: false, + modifiers: Modifiers::default(), last_right_click: None, search_context: None, global_cursor_position: None, @@ -3110,6 +3113,9 @@ impl Tab { } } } + Message::ModifiersChanged(modifiers) => { + self.modifiers = modifiers; + } Message::Open(path_opt) => { match path_opt { Some(path) => { @@ -4935,10 +4941,12 @@ impl Tab { let mut popover = widget::popover(mouse_area); if let Some(point) = self.context_menu { + let context_menu = menu::context_menu(self, key_binds, &self.modifiers); popover = popover - .popup(menu::context_menu(self, key_binds)) + .popup(context_menu) .position(widget::popover::Position::Point(point)); } + let mut tab_column = widget::column::with_capacity(3); if let Some(location_view) = location_view_opt { tab_column = tab_column.push(location_view);