diff --git a/Cargo.lock b/Cargo.lock index e8e9d6b..1ce819a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1425,7 +1425,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1446,7 +1446,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "quote", "syn 2.0.106", @@ -1609,7 +1609,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "almost", "cosmic-config", @@ -3160,7 +3160,7 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "dnd", "iced_accessibility", @@ -3178,7 +3178,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "accesskit", "accesskit_winit", @@ -3187,7 +3187,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "bitflags 2.9.4", "bytes", @@ -3211,7 +3211,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "futures", "iced_core", @@ -3237,7 +3237,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "bitflags 2.9.4", "bytemuck", @@ -3259,7 +3259,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -3271,7 +3271,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "bytes", "cosmic-client-toolkit", @@ -3286,7 +3286,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "bytemuck", "cosmic-text", @@ -3302,7 +3302,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "as-raw-xcb-connection", "bitflags 2.9.4", @@ -3333,7 +3333,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -3352,7 +3352,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -4437,7 +4437,7 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#f44d82a7e83af15270a9ca3beb832f4799699337" +source = "git+https://github.com/pop-os/libcosmic.git#529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5" dependencies = [ "apply", "ashpd 0.12.0", diff --git a/i18n/cs/cosmic_files.ftl b/i18n/cs/cosmic_files.ftl index 3819d63..d8c41b7 100644 --- a/i18n/cs/cosmic_files.ftl +++ b/i18n/cs/cosmic_files.ftl @@ -258,7 +258,7 @@ network-drive-schemes = WebDAV,dav:// nebo davs:// network-drive-error = Nelze přistoupit k síťovému disku remember-password = Zapamatovat heslo -try-again = Zkuste to znovu +try-again = Zkusit znovu cancelled = Zrušené edit-history = Historie úprav history = Historie @@ -359,7 +359,7 @@ items = Položky: { $items } item-size = Velikost: { $size } item-created = Vytvořeno: { $created } item-modified = Změněno: { $modified } -item-accessed = Přístup: { $accessed } +item-accessed = Poslední přístup: { $accessed } calculating = Vypočítávání... single-click = Otevřít jedním kliknutím type-to-search = Vyhledávání psaním diff --git a/i18n/et/cosmic_files.ftl b/i18n/et/cosmic_files.ftl index d439655..c4a8785 100644 --- a/i18n/et/cosmic_files.ftl +++ b/i18n/et/cosmic_files.ftl @@ -163,3 +163,54 @@ read-only = Ainult loetav read-execute = Loetav ja käivitatav read-write = Loetav ja kirjutatav read-write-execute = Loetav, kirjutatav ja käivitatav +favorite-path-error-description = + „{ $path }“ asukoha avamine ei õnnestu. + Teda kas pole olemas või sul pole õigusi tema avamiseks. + + Kas sooviksid ta külgribalt eemaldada? +keep = Säilita +network-drive-description = + Serveri aadressides peab olema protokolli eesliide ja aadress ise. + Näited: ssh://192.168.0.1, ftp://[2001:db8::1] +network-drive-schemes = + Kasutatavad protokollid,eesliide + AppleTalk,afp:// + File Transfer Protocol,ftp:// või ftps:// + Network File System,nfs:// + Server Message Block,smb:// + SSH File Transfer Protocol,sftp:// või ssh:// + WebDAV,dav:// või davs:// +network-drive-error = Puudub ligipääs võrgus asuvale andmekandjale +type-to-search = Otsimiseks kirjuta +type-to-search-recursive = Otsing sellest kaustast ja alamkaustadest +type-to-search-enter-path = Sisestab kausta või faili asukoha +add-to-sidebar = Lisa külgribale +remove-from-sidebar = Eemalda külgribalt +copy_noun = Kopeeri +creating = Loon: „{ $name }“ asukohas „{ $parent }“ +created = „{ $name }“ on loodud asukohta „{ $parent }“ +compress = Paki kokku +delete-permanently = Kustuta jäädavalt +eject = Väljasta +extract-here = Paki lahti +new-file = Uus fail... +new-folder = Uus kaust... +open-in-terminal = Ava terminalis +move-to-trash = Viska prügikasti +restore-from-trash = Taasta prügikastist +sort-by-name = Järjesta nime alusel +sort-by-modified = Järjesta muutmise alusel +sort-by-size = Järjesta suuruse alusel +sort-by-trashed = Järjesta kustutamise aja alusel +remove-from-recents = Eemalda hiljutiste failide loendist +change-wallpaper = Muuda taustapilti... +desktop-appearance = Töölaua välimus... +display-settings = Ekraani seadistused... +menu-about = Rakenduse teave: COSMICu failid... +sort = Järjesta +sort-a-z = A-Z +sort-z-a = Z-A +sort-newest-first = Esmalt uuemad +sort-oldest-first = Esmalt vanemad +sort-smallest-to-largest = Väiksemast suuremani +sort-largest-to-smallest = Suuremast väiksemani diff --git a/i18n/fa/cosmic_files.ftl b/i18n/fa/cosmic_files.ftl index 058f715..972e3c9 100644 --- a/i18n/fa/cosmic_files.ftl +++ b/i18n/fa/cosmic_files.ftl @@ -84,7 +84,7 @@ save-file = ذخیره فایل ## Open With Dialog open-with-title = چگونه می‌خواهید "{ $name }" را باز کنید؟ -browse-store = تصفح { $store } +browse-store = مرور { $store } other-apps = برنامه‌های دیگر related-apps = برنامه‌های مرتبط diff --git a/i18n/is/cosmic_files.ftl b/i18n/is/cosmic_files.ftl index 127b436..300f1fb 100644 --- a/i18n/is/cosmic_files.ftl +++ b/i18n/is/cosmic_files.ftl @@ -19,7 +19,7 @@ paste = Líma select-all = Velja allt password = Lykilorð skip = Sleppa -cosmic-files = COSMIC skráastjóri +cosmic-files = COSMIC Skráastjóri empty-folder = Tóm mappa empty-folder-hidden = Tóm mappa (inniheldur falin atriði) no-results = Engar niðurstöður fundust diff --git a/i18n/it/cosmic_files.ftl b/i18n/it/cosmic_files.ftl index fe1aeff..d767990 100644 --- a/i18n/it/cosmic_files.ftl +++ b/i18n/it/cosmic_files.ftl @@ -364,7 +364,7 @@ new-window = Nuova finestra reload-folder = Aggiorna cartella rename = Rinomina... close-tab = Chiudi scheda -quit = Chiudi +quit = Esci ## Edit diff --git a/i18n/pl/cosmic_files.ftl b/i18n/pl/cosmic_files.ftl index 82396fb..bd0a6ad 100644 --- a/i18n/pl/cosmic_files.ftl +++ b/i18n/pl/cosmic_files.ftl @@ -104,7 +104,7 @@ rename-folder = Zmień nazwę katalogu # Replace Dialog replace = Zastąp replace-title = „{ $filename }” już istnieje w tym miejscu. -replace-warning = Czy chcesz by został on zastąpiony? To nadpisze jego zawartość. +replace-warning = Czy chcesz by został on zastąpiony przez wybrany element? To nadpisze jego zawartość. replace-warning-operation = Czy chcesz by został on zastąpiony? To nadpisze jego zawartość. original-file = Oryginalny plik replace-with = Zastąpiony przez @@ -230,8 +230,8 @@ compressed = *[other] elementów } z „{ $from }” do „{ $to }” copy_noun = Kopiuj -creating = Tworzy { $name } w { $parent } -created = Stworzono { $name } w { $parent } +creating = Tworzy „{ $name }” w „{ $parent }” +created = Stworzono „{ $name }” w „{ $parent }” copying = Kopiowanie { $items } { $items -> [one] elementu @@ -269,8 +269,8 @@ extracted = } z „{ $from }” do „{ $to }” setting-executable-and-launching = Ustawianie „{ $name }” jako wykonywalnego i uruchamianie set-executable-and-launched = Ustaw „{ $name }” jako wykonywalny i uruchom -setting-permissions = Ustawianie uprawnień dla "{ $name }" na { $mode } -set-permissions = Ustaw uprawnienia dla "{ $name }" na { $mode } +setting-permissions = Ustawianie uprawnień dla „{ $name }” na { $mode } +set-permissions = Ustaw uprawnienia dla „{ $name }” na { $mode } moving = Przenoszenie { $items } { $items -> [one] elementu @@ -304,8 +304,8 @@ removed-from-recents = [few] elementy *[other] elementów } z Poprzednich -renaming = Zmieniana nazwa { $from } na { $to } -renamed = Zmieniono nazwę { $from } na { $to } +renaming = Zmieniana nazwa z „{ $from }” na „{ $to }” +renamed = Zmieniono nazwę z „{ $from }” na „{ $to }” restoring = Przywracanie { $items } { $items -> [one] elementu @@ -400,7 +400,7 @@ select-all = Zaznacz wszystko ## View -zoom-in = Przybliż +zoom-in = Zbliż default-size = Domyślny rozmiar zoom-out = Oddal view = Widok diff --git a/i18n/sr-Cyrl/cosmic_files.ftl b/i18n/sr-Cyrl/cosmic_files.ftl index 9b2c63b..6a35f11 100644 --- a/i18n/sr-Cyrl/cosmic_files.ftl +++ b/i18n/sr-Cyrl/cosmic_files.ftl @@ -51,3 +51,25 @@ view = Приказ grid-view = Прикажи мрежу list-view = Прикажи списак menu-settings = Подешавања... +cosmic-files = COSMIC Фајлови +open-file = Отвори фајл +cancel = Прекини +repository = Репозиторијум +support = Подршка +no-results = Није пронађен ниједан резултат +home = Кућа +open-folder = Отвори директоријум +password = Шифра +networks = Мреже +notification-in-progress = Операције над фајловима су у току. +skip = Прескочи +recents = Скорије +undo = Поништи промену +today = Данас +desktop-view-options = Опције изгледа радне површине... +show-on-desktop = Покажи на радној површини +desktop-folder-content = Садржај директоријума радне површине +mounted-drives = Приључена складишта података +trash-folder-icon = Иконица корпе са отпаткама +icon-size-and-spacing = Величина иконице и размак +icon-size = Величина иконице diff --git a/i18n/sr-Latn/cosmic_files.ftl b/i18n/sr-Latn/cosmic_files.ftl index 7020f22..411bc55 100644 --- a/i18n/sr-Latn/cosmic_files.ftl +++ b/i18n/sr-Latn/cosmic_files.ftl @@ -51,3 +51,9 @@ view = Prikaz grid-view = Prikaži mrežu list-view = Prikaži spisak menu-settings = Podešavanja... +repository = Repozitorijum +support = Podrška +cancel = Poništi +zoom-in = Uvećaj +default-size = Podrazumevana veličina +zoom-out = Umanji diff --git a/i18n/uk/cosmic_files.ftl b/i18n/uk/cosmic_files.ftl index b57da04..db4d7bd 100644 --- a/i18n/uk/cosmic_files.ftl +++ b/i18n/uk/cosmic_files.ftl @@ -1,11 +1,11 @@ cosmic-files = Файли COSMIC -empty-folder = Тека порожня -empty-folder-hidden = Тека порожня (містить приховані елементи) +empty-folder = Порожня тека +empty-folder-hidden = Порожня тека (містить приховані елементи) filesystem = Файлова система home = Домівка trash = Смітник -recents = недавній -undo = Скасувати +recents = Нещодавні +undo = Відмінити # List view name = Назва modified = Змінено @@ -184,14 +184,14 @@ dismiss = Сховати повідомлення remove = Видалити cancelled = Скасовані no-results = Нічого не знайдено -networks = Мережа +networks = Мережі notification-in-progress = Виконуються операції з файлами. today = Сьогодні -desktop-view-options = Параметри піктограм стільниці... +desktop-view-options = Параметри вигляду стільниці... show-on-desktop = Показувати на стільниці desktop-folder-content = Вміст теки Стільниця mounted-drives = Змонтовані диски -trash-folder-icon = Піктограму теки Смітник +trash-folder-icon = Піктограма теки Смітник icon-size-and-spacing = Розмір піктограм та відстань між ними icon-size = Розмір піктограм grid-spacing = Відстань між піктограмами @@ -226,7 +226,7 @@ browse-store = Пошукати в { $store } other-apps = Інші застосунки related-apps = Пов'язані застосунки permanently-delete-question = Видалити назавжди -delete = Видалити +delete = Вилучити permanently-delete-warning = Ви впевнені, що бажаєте назавжди видалити { $target }? Цю дію відмінити неможливо. set-executable-and-launch = Дозволити виконання та запустити set-executable-and-launch-description = Ви впевнені, що бажаєте дозволити виконання файлу "{ $name }" та запустити його? diff --git a/src/app.rs b/src/app.rs index c07f3e5..398fcc0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,7 @@ #[cfg(all(feature = "wayland", feature = "desktop-applet"))] use cosmic::iced::{ - Limits, + Limits, Point, event::wayland::{Event as WaylandEvent, OutputEvent, OverlapNotifyEvent}, platform_specific::runtime::wayland::layer_surface::{ IcedMargin, IcedOutput, SctkLayerSurfaceSettings, @@ -20,7 +20,7 @@ use cosmic::{ cosmic_config::{self, ConfigSet}, cosmic_theme, executor, iced::{ - self, Alignment, Event, Length, Point, Rectangle, Size, Subscription, + self, Alignment, Event, Length, Rectangle, Size, Subscription, clipboard::dnd::DndAction, core::SmolStr, event, @@ -31,6 +31,7 @@ use cosmic::{ window::{self, Event as WindowEvent, Id as WindowId}, }, iced_runtime::clipboard, + iced_widget::button::focus, style, surface, theme, widget::{ self, @@ -59,7 +60,7 @@ use std::{ path::{Path, PathBuf}, pin::Pin, process, - sync::{Arc, Mutex}, + sync::{Arc, LazyLock, Mutex}, time::{self, Duration, Instant}, }; use tokio::sync::mpsc; @@ -93,6 +94,27 @@ use crate::{ }; use crate::{config::State, dialog::DialogSettings}; +static PERMANENT_DELETE_BUTTON_ID: LazyLock = + LazyLock::new(|| widget::Id::new("permanent-delete-button")); + +static CONFIRM_OPEN_WITH_BUTTON_ID: LazyLock = + LazyLock::new(|| widget::Id::new("confirm-open-with-button")); + +static EMPTY_TRASH_BUTTON_ID: LazyLock = + LazyLock::new(|| widget::Id::new("empty-trash-button")); + +static SET_EXECUTABLE_AND_LAUNCH_CONFIRM_BUTTON_ID: LazyLock = + LazyLock::new(|| widget::Id::new("set-executable-and-launch-confirm-button")); + +static FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID: LazyLock = + LazyLock::new(|| widget::Id::new("favorite-path-error-remove-button")); + +static MOUNT_ERROR_TRY_AGAIN_BUTTON_ID: LazyLock = + LazyLock::new(|| widget::Id::new("mount-error-try-again-button")); + +pub(crate) static REPLACE_BUTTON_ID: LazyLock = + LazyLock::new(|| widget::Id::new("replace-button")); + #[derive(Clone, Debug)] pub enum Mode { App, @@ -317,7 +339,7 @@ pub enum Message { DialogComplete, Eject, FileDialogMessage(DialogMessage), - DialogPush(DialogPage), + DialogPush(DialogPage, Option), DialogUpdate(DialogPage), DialogUpdateComplete(DialogPage), ExtractHere(Option), @@ -665,6 +687,7 @@ pub struct App { network_drive_input: String, #[cfg(feature = "notify")] notification_opt: Option>>, + #[cfg(all(feature = "wayland", feature = "desktop-applet"))] overlap: FxHashMap, pending_operation_id: u64, pending_operations: BTreeMap, @@ -695,6 +718,15 @@ pub struct App { } impl App { + fn push_dialog(&mut self, page: DialogPage, focus_id: Option) -> Task { + let t = self.dialog_pages.push_back(page); + if let Some(focus_id) = focus_id { + Task::batch(vec![t, focus(focus_id)]) + } else { + t + } + } + fn open_file(&mut self, paths: &[impl AsRef]) -> Task { let mut tasks = Vec::new(); @@ -741,10 +773,11 @@ impl App { Err(err) => match err.kind() { io::ErrorKind::PermissionDenied => { // If permission is denied, try marking as executable, then running - tasks.push(self.dialog_pages.push_back( + tasks.push(self.push_dialog( DialogPage::SetExecutableAndLaunch { path: path.to_path_buf(), }, + Some(SET_EXECUTABLE_AND_LAUNCH_CONFIRM_BUTTON_ID.clone()), )); } _ => { @@ -1092,9 +1125,12 @@ impl App { let mut tasks = Vec::new(); if !dialog_paths.is_empty() { - tasks.push(self.dialog_pages.push_back(DialogPage::PermanentlyDelete { - paths: dialog_paths, - })); + tasks.push(self.update(Message::DialogPush( + DialogPage::PermanentlyDelete { + paths: dialog_paths, + }, + Some(PERMANENT_DELETE_BUTTON_ID.clone()), + ))); } if !trash_paths.is_empty() { tasks.push(self.operation(Operation::Delete { paths: trash_paths })); @@ -2124,7 +2160,7 @@ impl Application for App { compio_tx, context_page: ContextPage::Preview(None, PreviewKind::Selected), dialog_pages: DialogPages::new(), - dialog_text_input: widget::Id::unique(), + dialog_text_input: widget::Id::new("Dialog Text Input"), key_binds, margin: FxHashMap::default(), mime_app_cache: MimeAppCache::new(), @@ -2135,14 +2171,15 @@ impl Application for App { network_drive_input: String::new(), #[cfg(feature = "notify")] notification_opt: None, + #[cfg(all(feature = "wayland", feature = "desktop-applet"))] overlap: FxHashMap::default(), pending_operation_id: 0, pending_operations: BTreeMap::new(), progress_operations: BTreeSet::new(), complete_operations: BTreeMap::new(), failed_operations: BTreeMap::new(), - scrollable_id: widget::Id::unique(), - search_id: widget::Id::unique(), + scrollable_id: widget::Id::new("File Scrollable"), + search_id: widget::Id::new("File Search"), size: None, #[cfg(all(feature = "wayland", feature = "desktop-applet"))] surface_ids: FxHashMap::default(), @@ -2345,27 +2382,36 @@ impl Application for App { } log::warn!("failed to open favorite, path does not exist: {:?}", path); - return self.dialog_pages.push_back(DialogPage::FavoritePathError { - path: path.clone(), - entity, - }); + return self.push_dialog( + DialogPage::FavoritePathError { + path: path.clone(), + entity, + }, + Some(FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID.clone()), + ); } Location::Path(path) | Location::Network(_, _, Some(path)) => { match path.try_exists() { Ok(true) => true, Ok(false) => { log::warn!("failed to open favorite, path does not exist: {:?}", path); - return self.dialog_pages.push_back(DialogPage::FavoritePathError { - path: path.clone(), - entity, - }); + return self.push_dialog( + DialogPage::FavoritePathError { + path: path.clone(), + entity, + }, + Some(FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID.clone()), + ); } Err(err) => { log::warn!("failed to open favorite for path: {:?}, {}", path, err); - return self.dialog_pages.push_back(DialogPage::FavoritePathError { - path: path.clone(), - entity, - }); + return self.push_dialog( + DialogPage::FavoritePathError { + path: path.clone(), + entity, + }, + Some(FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID.clone()), + ); } } } @@ -2551,16 +2597,16 @@ impl Application for App { let to = destination.0.to_path_buf(); let name = destination.1.to_str().unwrap_or_default().to_string(); let archive_type = ArchiveType::default(); - return Task::batch([ - self.dialog_pages.push_back(DialogPage::Compress { + return self.push_dialog( + DialogPage::Compress { paths, to, name, archive_type, password: None, - }), - widget::text_input::focus(self.dialog_text_input.clone()), - ]); + }, + Some(self.dialog_text_input.clone()), + ); } } } @@ -2855,8 +2901,8 @@ impl Application for App { return Task::batch(tasks); } } - Message::DialogPush(dialog_page) => { - return self.dialog_pages.push_back(dialog_page); + Message::DialogPush(dialog_page, focused_id) => { + return self.push_dialog(dialog_page, focused_id); } Message::DialogUpdate(dialog_page) => { self.dialog_pages.update_front(dialog_page); @@ -2915,7 +2961,11 @@ impl Application for App { } } Message::Key(window_id, modifiers, key, text) => { - if self.core.main_window_id() == Some(window_id) { + #[cfg(all(feature = "wayland", feature = "desktop-applet"))] + let in_surface_ids = self.surface_ids.values().any(|id| *id == window_id); + #[cfg(not(all(feature = "wayland", feature = "desktop-applet")))] + let in_surface_ids = false; + if self.core.main_window_id() == Some(window_id) || in_surface_ids { let entity = self.tab_model.active(); for (key_bind, action) in self.key_binds.iter() { if key_bind.matches(modifiers, &key) { @@ -3061,23 +3111,26 @@ impl Application for App { } Err(error) => { log::warn!("failed to connect to {:?}: {}", item, error); - return self.dialog_pages.push_back(DialogPage::MountError { - mounter_key, - item, - error, - }); + return self.push_dialog( + DialogPage::MountError { + mounter_key, + item, + error, + }, + Some(MOUNT_ERROR_TRY_AGAIN_BUTTON_ID.clone()), + ); } }, Message::NetworkAuth(mounter_key, uri, auth, auth_tx) => { - return Task::batch([ - self.dialog_pages.push_back(DialogPage::NetworkAuth { + return self.push_dialog( + DialogPage::NetworkAuth { mounter_key, uri, auth, auth_tx, - }), - widget::text_input::focus(self.dialog_text_input.clone()), - ]); + }, + Some(self.dialog_text_input.clone()), + ); } Message::NetworkDriveInput(input) => { self.network_drive_input = input; @@ -3338,17 +3391,20 @@ impl Application for App { let Some(path) = item.path_opt() else { continue; }; - return self.update(Message::DialogPush(DialogPage::OpenWith { - path: path.to_path_buf(), - mime: item.mime.clone(), - selected: 0, - store_opt: "x-scheme-handler/mime" - .parse::() - .ok() - .and_then(|mime| { - self.mime_app_cache.get(&mime).first().cloned() - }), - })); + return self.push_dialog( + DialogPage::OpenWith { + path: path.to_path_buf(), + mime: item.mime.clone(), + selected: 0, + store_opt: "x-scheme-handler/mime" + .parse::() + .ok() + .and_then(|mime| { + self.mime_app_cache.get(&mime).first().cloned() + }), + }, + Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()), + ); } } } @@ -3488,6 +3544,8 @@ impl Application for App { }, })); } + tasks.push(widget::text_input::focus(self.dialog_text_input.clone())); + // Remove from progress self.progress_operations.remove(&id); self.failed_operations @@ -3526,9 +3584,10 @@ impl Application for App { Message::PermanentlyDelete(entity_opt) => { let paths = self.selected_paths(entity_opt); if !paths.is_empty() { - return self - .dialog_pages - .push_back(DialogPage::PermanentlyDelete { paths }); + return self.push_dialog( + DialogPage::PermanentlyDelete { paths }, + Some(PERMANENT_DELETE_BUTTON_ID.clone()), + ); } } Message::Preview(entity_opt) => { @@ -3944,7 +4003,10 @@ impl Application for App { commands.push(self.update(Message::PasteContents(to, from))); } tab::Command::EmptyTrash => { - return self.dialog_pages.push_back(DialogPage::EmptyTrash); + return self.push_dialog( + DialogPage::EmptyTrash, + Some(EMPTY_TRASH_BUTTON_ID.clone()), + ); } #[cfg(feature = "desktop")] tab::Command::ExecEntryAction(entry, action) => { @@ -4400,17 +4462,20 @@ impl Application for App { { match tab::item_from_path(&path, IconSizes::default()) { Ok(item) => { - return self.update(Message::DialogPush(DialogPage::OpenWith { - path: path.to_path_buf(), - mime: item.mime.clone(), - selected: 0, - store_opt: "x-scheme-handler/mime" - .parse::() - .ok() - .and_then(|mime| { - self.mime_app_cache.get(&mime).first().cloned() - }), - })); + return self.push_dialog( + DialogPage::OpenWith { + path: path.to_path_buf(), + mime: item.mime.clone(), + selected: 0, + store_opt: "x-scheme-handler/mime" + .parse::() + .ok() + .and_then(|mime| { + self.mime_app_cache.get(&mime).first().cloned() + }), + }, + None, + ); } Err(err) => { log::warn!("failed to get item for path {:?}: {}", path, err); @@ -4517,7 +4582,8 @@ impl Application for App { } NavMenuAction::EmptyTrash => { - return self.dialog_pages.push_front(DialogPage::EmptyTrash); + return self + .push_dialog(DialogPage::EmptyTrash, Some(EMPTY_TRASH_BUTTON_ID.clone())); } }, Message::Recents => { @@ -4572,7 +4638,7 @@ impl Application for App { id: surface_id, layer: Layer::Bottom, keyboard_interactivity: KeyboardInteractivity::OnDemand, - pointer_interactivity: true, + input_zone: None, anchor: Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT, output: IcedOutput::Output(output), namespace: "cosmic-files-applet".into(), @@ -4902,7 +4968,9 @@ impl Application for App { .title(fl!("empty-trash")) .body(fl!("empty-trash-warning")) .primary_action( - widget::button::suggested(fl!("empty-trash")).on_press(Message::DialogComplete), + widget::button::suggested(fl!("empty-trash")) + .on_press(Message::DialogComplete) + .id(EMPTY_TRASH_BUTTON_ID.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), @@ -4921,23 +4989,24 @@ impl Application for App { widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), ) } - DialogPage::ExtractPassword { id, password } => { - widget::dialog() - .title(fl!("extract-password-required")) - .icon(icon::from_name("dialog-error").size(64)) - .control(widget::text_input("", password).password().on_input( - move |password| { + DialogPage::ExtractPassword { id, password } => widget::dialog() + .title(fl!("extract-password-required")) + .icon(icon::from_name("dialog-error").size(64)) + .control( + widget::text_input("", password) + .password() + .on_input(move |password| { Message::DialogUpdate(DialogPage::ExtractPassword { id: *id, password }) - }, - )) - .primary_action( - widget::button::suggested(fl!("extract-here")) - .on_press(Message::DialogComplete), - ) - .secondary_action( - widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), - ) - } + }) + .id(self.dialog_text_input.clone()), + ) + .primary_action( + widget::button::suggested(fl!("extract-here")) + .on_press(Message::DialogComplete), + ) + .secondary_action( + widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), + ), DialogPage::MountError { mounter_key: _, item: _, @@ -4947,7 +5016,9 @@ impl Application for App { .body(error) .icon(icon::from_name("dialog-error").size(64)) .primary_action( - widget::button::standard(fl!("try-again")).on_press(Message::DialogComplete), + widget::button::standard(fl!("try-again")) + .on_press(Message::DialogComplete) + .id(MOUNT_ERROR_TRY_AGAIN_BUTTON_ID.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), @@ -5233,7 +5304,9 @@ impl Application for App { let mut dialog = widget::dialog() .title(fl!("open-with-title", name = name)) .primary_action( - widget::button::suggested(fl!("open")).on_press(Message::DialogComplete), + widget::button::suggested(fl!("open")) + .on_press(Message::DialogComplete) + .id(CONFIRM_OPEN_WITH_BUTTON_ID.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), @@ -5279,7 +5352,8 @@ impl Application for App { .title(fl!("permanently-delete-question")) .primary_action( widget::button::destructive(fl!("delete")) - .on_press(Message::DialogComplete), + .on_press(Message::DialogComplete) + .id(PERMANENT_DELETE_BUTTON_ID.clone()), ) .secondary_action( widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), @@ -5385,9 +5459,13 @@ impl Application for App { from.replace_view(fl!("replace-with"), military_time) .map(|x| Message::TabMessage(None, x)), ) - .primary_action(widget::button::suggested(fl!("replace")).on_press( - Message::ReplaceResult(ReplaceResult::Replace(*apply_to_all)), - )); + .primary_action( + widget::button::suggested(fl!("replace")) + .on_press(Message::ReplaceResult(ReplaceResult::Replace( + *apply_to_all, + ))) + .id(REPLACE_BUTTON_ID.clone()), + ); if *multiple { dialog .control( @@ -5434,7 +5512,8 @@ impl Application for App { .primary_action( widget::button::text(fl!("set-and-launch")) .class(theme::Button::Suggested) - .on_press(Message::DialogComplete), + .on_press(Message::DialogComplete) + .id(SET_EXECUTABLE_AND_LAUNCH_CONFIRM_BUTTON_ID.clone()), ) .secondary_action( widget::button::text(fl!("cancel")) @@ -5454,7 +5533,9 @@ impl Application for App { )) .icon(icon::from_name("dialog-error").size(64)) .primary_action( - widget::button::destructive(fl!("remove")).on_press(Message::DialogComplete), + widget::button::destructive(fl!("remove")) + .on_press(Message::DialogComplete) + .id(FAVORITE_PATH_ERROR_REMOVE_BUTTON_ID.clone()), ) .secondary_action( widget::button::standard(fl!("keep")).on_press(Message::DialogCancel), @@ -5816,7 +5897,7 @@ impl Application for App { Some(Message::OutputEvent(output_event, output)) } #[cfg(feature = "desktop")] - WaylandEvent::OverlapNotify(event) => { + WaylandEvent::OverlapNotify(event, _, _) => { Some(Message::Overlap(window_id, event)) } _ => None, diff --git a/src/dialog.rs b/src/dialog.rs index 27ad491..c9ee8e1 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -37,7 +37,9 @@ use std::{ }; use crate::{ - app::{Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind}, + app::{ + Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID, + }, config::{Config, DialogConfig, Favorite, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch}, fl, home_dir, key_bind::key_binds, @@ -977,15 +979,15 @@ impl Application for App { context_menu_window: None, context_page: ContextPage::Preview(None, PreviewKind::Selected), dialog_pages: VecDeque::new(), - dialog_text_input: widget::Id::unique(), + dialog_text_input: widget::Id::new("Dialog Text Input"), filters: Vec::new(), filter_selected: None, - filename_id: widget::Id::unique(), + filename_id: widget::Id::new("Dialog Filename"), modifiers: Modifiers::empty(), mounter_items: FxHashMap::default(), nav_model: segmented_button::ModelBuilder::default().build(), result_opt: None, - search_id: widget::Id::unique(), + search_id: widget::Id::new("Dialog File Search"), tab, key_binds, watcher_opt: None, @@ -1598,6 +1600,7 @@ impl Application for App { self.dialog_pages.push_back(DialogPage::Replace { filename: filename.clone(), }); + return widget::button::focus(REPLACE_BUTTON_ID.clone()); } else { self.result_opt = Some(DialogResult::Open(vec![path])); return window::close(self.flags.window_id); diff --git a/src/operation/mod.rs b/src/operation/mod.rs index 7d7e525..2eb9014 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -1,5 +1,5 @@ use crate::{ - app::{ArchiveType, DialogPage, Message}, + app::{ArchiveType, DialogPage, Message, REPLACE_BUTTON_ID}, config::IconSizes, fl, spawn_detached::spawn_detached, @@ -53,13 +53,16 @@ async fn handle_replace( let _ = msg_tx .lock() .await - .send(Message::DialogPush(DialogPage::Replace { - from: item_from, - to: item_to, - multiple, - apply_to_all: false, - tx, - })) + .send(Message::DialogPush( + DialogPage::Replace { + from: item_from, + to: item_to, + multiple, + apply_to_all: false, + tx, + }, + Some(REPLACE_BUTTON_ID.clone()), + )) .await; rx.recv().await.unwrap_or(ReplaceResult::Cancel) } @@ -1198,7 +1201,7 @@ mod tests { let handle_messages = async move { while let Some(msg) = rx.next().await { match msg { - Message::DialogPush(DialogPage::Replace { tx, .. }) => { + Message::DialogPush(DialogPage::Replace { tx, .. }, _id_to_focus) => { debug!("[{id}] Replace request"); tx.send(ReplaceResult::Cancel) .await diff --git a/src/tab.rs b/src/tab.rs index b9f779f..e518a7e 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1579,7 +1579,6 @@ pub enum Message { SetSort(HeadingOptions, bool), TabComplete(PathBuf, Vec<(String, PathBuf)>), Thumbnail(PathBuf, ItemThumbnail), - View(View), ToggleSort(HeadingOptions), Drop(Option<(Location, ClipboardPaste)>), DndHover(Location), @@ -3000,73 +2999,60 @@ impl Tab { .select_range .map_or(Some((click_i, click_i)), |r| Some((r.0, click_i))); if let Some(range) = self.select_range { - let min = range.0.min(range.1); - let max = range.0.max(range.1); - let (sort_name, sort_direction, _) = self.sort_options(); - //TODO: this assumes the default sort order! - if sort_name == HeadingOptions::Name && sort_direction { - // A default/unsorted tab's view is consistent with how the - // Items are laid out internally (items_opt), so Items can be - // linearly selected - if let Some(ref mut items) = self.items_opt { - for item in items.iter_mut().skip(min).take(max - min + 1) { + let range_min = range.0.min(range.1); + let range_max = range.0.max(range.1); + // A sorted tab's items can't be linearly selected + // Let's say we have: + // index | file + // 0 | file0 + // 1 | file1 + // 2 | file2 + // This is both the default sort and internal ordering + // When sorted it may be displayed as: + // 1 | file1 + // 0 | file0 + // 2 | file2 + // However, the internal ordering is still the same thus + // linearly selecting items doesn't work. Shift selecting + // file0 and file2 would select indices 0 to 2 when it should + // select indices 0 AND 2 from items_opt + let indices: Vec<_> = self + .column_sort() + .map(|sorted| sorted.into_iter().map(|(i, _)| i).collect()) + .unwrap_or_else(|| { + let len = self + .items_opt + .as_deref() + .map(|items| items.len()) + .unwrap_or_default(); + (0..len).collect() + }); + + // Find the true indices for the min and max element w.r.t. + // a sorted tab. + let min = indices + .iter() + .position(|&offset| offset == range_min) + .unwrap_or_default(); + // We can't skip `min_real` elements here because the index of + // `max` may actually be before `min` in a sorted tab + let max = indices + .iter() + .position(|&offset| offset == range_max) + .unwrap_or(indices.len()); + let min_real = min.min(max); + let max_real = max.max(min); + + if let Some(ref mut items) = self.items_opt { + for index in indices + .into_iter() + .skip(min_real) + .take(max_real - min_real + 1) + { + if let Some(item) = items.get_mut(index) { item.selected = true; } } - } else { - // A sorted tab's items can't be linearly selected - // Let's say we have: - // index | file - // 0 | file0 - // 1 | file1 - // 2 | file2 - // This is both the default sort and internal ordering - // When sorted it may be displayed as: - // 1 | file1 - // 0 | file0 - // 2 | file2 - // However, the internal ordering is still the same thus - // linearly selecting items doesn't work. Shift selecting - // file0 and file2 would select indices 0 to 2 when it should - // select indices 0 AND 2 from items_opt - let indices: Vec<_> = self - .column_sort() - .map(|sorted| sorted.into_iter().map(|(i, _)| i).collect()) - .unwrap_or_else(|| { - let len = self - .items_opt - .as_deref() - .map(|items| items.len()) - .unwrap_or_default(); - (0..len).collect() - }); - - // Find the true indices for the min and max element w.r.t. - // a sorted tab. - let min = indices - .iter() - .position(|&offset| offset == min) - .unwrap_or_default(); - // We can't skip `min_real` elements here because the index of - // `max` may actually be before `min` in a sorted tab - let max = indices - .iter() - .position(|&offset| offset == max) - .unwrap_or(indices.len()); - let min_real = min.min(max); - let max_real = max.max(min); - - if let Some(ref mut items) = self.items_opt { - for index in indices - .into_iter() - .skip(min_real) - .take(max_real - min_real + 1) - { - if let Some(item) = items.get_mut(index) { - item.selected = true; - } - } - } } } self.clicked = click_i_opt; @@ -3130,6 +3116,12 @@ impl Tab { )); } } + // Unhighlight all items when config changes + if let Some(ref mut items) = self.items_opt { + for item in items.iter_mut() { + item.highlighted = false; + } + } } Message::ContextAction(action) => { // Close context menu @@ -3816,9 +3808,6 @@ impl Tab { } } } - Message::View(view) => { - self.config.view = view; - } Message::ToggleSort(heading_option) => { if !matches!(self.location, Location::Search(..)) { let heading_sort = if self.sort_name == heading_option {