From b773a39efa4a65888fcce4a5d8dffcf4fa45700a Mon Sep 17 00:00:00 2001 From: F4LCON <107556180+itsF4LCON@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:49:34 +0200 Subject: [PATCH] Home screen: remove individual Recent entries (#2731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #2691. The hovered row of the Home screen's Recent list shows a × that takes the file off the list without opening it, and right-click offers Remove from Recent and Clear Recent Files. The change is stored in the preferences, so it survives a restart, and the file comes back when it is opened again. `file.removeRecent {"path"}` does the same for agents. Rows carry their file name as their accessible label. --- crates/ui-egui/src/canvas.rs | 94 +++++++++++++++++++++++++++-- crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/el.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/id.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/ko.tsv | 2 + crates/ui-egui/src/i18n/nl.tsv | 2 + crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + crates/ui-egui/src/lib.rs | 7 +++ crates/ui-egui/src/menus.rs | 18 +++++- 19 files changed, 145 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 223826e5ac..dcb61fdbe3 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2098,7 +2098,9 @@ const HOME_RECENT: usize = 6; /// Height of one Home-screen recent-file row. const HOME_RECENT_ROW: f32 = 30.0; -/// The Home screen's "Recent" list: file name, its folder on the right, click to open. +/// The Home screen's "Recent" list: file name, its folder on the right, click to open. The +/// hovered row shows a × that takes it off the list, and right-click offers Remove from Recent +/// and Clear Recent Files (#2691). fn home_recent(app: &mut PhotocraftApp, ui: &mut egui::Ui, recent: &[String]) { let t = crate::theme::Tokens::get(ui.ctx()); let width = 380.0; @@ -2109,27 +2111,60 @@ fn home_recent(app: &mut PhotocraftApp, ui: &mut egui::Ui, recent: &[String]) { }); ui.add_space(4.0); let mut open = None; + let mut remove = None; + let mut clear = false; for path in recent { let name = crate::file_open::display_name(path); let folder = std::path::Path::new(path).parent().map(|d| d.to_string_lossy().into_owned()).unwrap_or_default(); let (row, resp) = ui.allocate_exact_size(egui::vec2(width, HOME_RECENT_ROW), Sense::click()); - if resp.hovered() { + // The × sits on top of the row, so the row counts as hovered while the pointer is on it. + let hovered = ui.rect_contains_pointer(row); + if hovered { ui.painter().rect_filled(row, t.radius_sm, t.hover); } + let close = Rect::from_center_size(egui::pos2(row.right() - 16.0, row.center().y), egui::vec2(22.0, 22.0)); + let right = if hovered { close.left() - 4.0 } else { row.right() - 10.0 }; let icon = Rect::from_center_size(egui::pos2(row.left() + 16.0, row.center().y), egui::vec2(16.0, 16.0)); crate::icons::paint(ui, icon, "file", 14.0, t.text_dim); let x = row.left() + 32.0; let name_g = crate::tab_strip::elided(ui, &name, egui::FontId::proportional(12.5), t.text, 170.0); let name_w = name_g.size().x; ui.painter().galley(egui::pos2(x, row.center().y - name_g.size().y / 2.0), name_g, t.text); - let folder_max = (row.right() - 10.0 - (x + name_w + 14.0)).max(0.0); + let folder_max = (right - (x + name_w + 14.0)).max(0.0); if folder_max > 20.0 && !folder.is_empty() { let fg = crate::tab_strip::elided(ui, &folder, egui::FontId::proportional(11.5), t.text_faint, folder_max); - ui.painter().galley(egui::pos2(row.right() - 10.0 - fg.size().x, row.center().y - fg.size().y / 2.0), fg, t.text_faint); + ui.painter().galley(egui::pos2(right - fg.size().x, row.center().y - fg.size().y / 2.0), fg, t.text_faint); } - if resp.on_hover_text(path).on_hover_cursor(egui::CursorIcon::PointingHand).clicked() { + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &name)); + let resp = resp.on_hover_text(path).on_hover_cursor(egui::CursorIcon::PointingHand); + if hovered { + let x = ui.interact(close, resp.id.with("remove"), Sense::click()); + let label = crate::i18n::fmt(tl!("Remove {name} from Recent"), &[("name", &name)]); + x.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &label)); + let color = crate::icons::button_chrome(ui, close, false, x.hovered()); + crate::icons::paint(ui, close, "x", 12.0, color); + if x.on_hover_text(tl!("Remove from Recent")).clicked() { + remove = Some(path.clone()); + } + } + if resp.clicked() && remove.is_none() { open = Some(path.clone()); } + resp.context_menu(|ui| { + if ui.button(tl!("Remove from Recent")).clicked() { + remove = Some(path.clone()); + ui.close(); + } + if ui.button(tl!("Clear Recent Files")).clicked() { + clear = true; + ui.close(); + } + }); + } + if clear { + app.clear_recent(); + } else if let Some(path) = remove { + app.remove_recent(&path); } if let Some(path) = open && let Err(e) = app.open_path(&path) @@ -5038,6 +5073,55 @@ mod tests { assert!(hint.ends_with(&format!("paste a copied image with {}.", crate::notices::paste_hint(&wayland))), "{hint}"); } + /// The Home screen's × takes a file off Recent without opening it, and its right-click menu + /// removes one entry or clears the list; the preferences keep the change (#2691). + #[test] + fn home_recent_entries_can_be_removed() { + use egui_kittest::kittest::Queryable; + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + for name in ["c.tif", "b.psd", "a.png"] { + app.push_recent(&format!("/tmp/photocraft-2691/{name}")); + } + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(900.0, 760.0)).build_ui_state( + |ui, (app, ready): &mut (PhotocraftApp, bool)| { + if *ready { + egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| document_area(app, ui)); + } + }, + (app, false), + ); + PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::default()); + h.state_mut().1 = true; + h.run_steps(3); + let listed = |h: &egui_kittest::Harness<'_, (PhotocraftApp, bool)>| { + let app = &h.state().0; + assert_eq!(app.session.prefs().file_handling.recent_files, app.ui.recent_files, "stored in the preferences"); + app.ui.recent_files.iter().map(|p| crate::file_open::display_name(p)).collect::>() + }; + assert_eq!(listed(&h), ["a.png", "b.psd", "c.tif"]); + assert!(h.query_by_label("Remove b.psd from Recent").is_none(), "the × shows on the hovered row only"); + + h.get_by_label("b.psd").hover(); + h.run_steps(2); + h.get_by_label("Remove b.psd from Recent").click(); + h.run_steps(2); + assert_eq!(listed(&h), ["a.png", "c.tif"]); + assert!(h.state().0.session.documents().is_empty(), "the file was not opened"); + + h.get_by_label("c.tif").click_secondary(); + h.run_steps(2); + h.get_by_label("Remove from Recent").click(); + h.run_steps(2); + assert_eq!(listed(&h), ["a.png"]); + + h.get_by_label("a.png").click_secondary(); + h.run_steps(2); + h.get_by_label("Clear Recent Files").click(); + h.run_steps(2); + assert!(listed(&h).is_empty()); + assert!(h.state().0.session.documents().is_empty()); + } + #[test] fn pointer_moves_are_bounded_by_the_press_and_release() { use egui::{Event, PointerButton, pos2}; diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index a71a5d2318..a9c13c9724 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -47,6 +47,8 @@ Right Vpravo Open Recent Otevřít nedávné Clear Recent Files Vymazat nedávné soubory + Remove from Recent Odebrat z nedávných + Remove {name} from Recent Odebrat {name} z nedávných Close Zavřít Minimize Minimalizovat Maximize Maximalizovat diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 1a03bdb30d..cae545d06a 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -43,6 +43,8 @@ Right Rechts Open Recent Zuletzt geöffnete Dateien Clear Recent Files Zuletzt geöffnete Dateien leeren + Remove from Recent Aus zuletzt geöffneten Dateien entfernen + Remove {name} from Recent {name} aus zuletzt geöffneten Dateien entfernen Close Schließen Minimize Minimieren Maximize Maximieren diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 8e22df91b0..8894c40cef 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -25,6 +25,8 @@ Right Δεξιά Open Recent Πρόσφατα αρχεία Clear Recent Files Εκκαθάριση πρόσφατων αρχείων + Remove from Recent Αφαίρεση από τα πρόσφατα + Remove {name} from Recent Αφαίρεση του {name} από τα πρόσφατα Close Κλείσιμο Minimize Ελαχιστοποίηση Maximize Μεγιστοποίηση diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 2fab6c66c2..3e2835ad3e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -39,6 +39,8 @@ Right Derecha Open Recent Archivos recientes Clear Recent Files Vaciar archivos recientes + Remove from Recent Quitar de recientes + Remove {name} from Recent Quitar {name} de recientes Close Cerrar Minimize Minimizar Maximize Maximizar diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 30305d623f..0fbc0bb1a5 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -45,6 +45,8 @@ Right Droite Open Recent Ouvrir un fichier récent Clear Recent Files Effacer la liste des fichiers récents + Remove from Recent Retirer des fichiers récents + Remove {name} from Recent Retirer {name} des fichiers récents Close Fermer Minimize Réduire Maximize Agrandir diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 7137149893..3a320bfbfd 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -38,6 +38,8 @@ Right Kanan Open Recent Terakhir Dibuka Clear Recent Files Hapus Daftar Berkas Terakhir + Remove from Recent Hapus dari Daftar Terakhir + Remove {name} from Recent Hapus {name} dari Daftar Terakhir Close Tutup Minimize Minimalkan Maximize Maksimalkan diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 9379cc64d8..e76574f612 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -39,6 +39,8 @@ Right Destra Open Recent Apri recenti Clear Recent Files Cancella file recenti + Remove from Recent Rimuovi dai recenti + Remove {name} from Recent Rimuovi {name} dai recenti Close Chiudi Minimize Riduci a icona Maximize Ingrandisci diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index c288a32ea6..d2c194454f 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -43,6 +43,8 @@ Right 右 Open Recent 最近使ったファイルを開く Clear Recent Files 最近使ったファイルを消去 + Remove from Recent 最近使ったファイルから削除 + Remove {name} from Recent {name} を最近使ったファイルから削除 Close 閉じる Minimize 最小化 Maximize 最大化 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index f01f006338..c33e19c2a2 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -31,6 +31,8 @@ Right 오른쪽 Open Recent 최근 파일 열기 Clear Recent Files 최근 파일 목록 지우기 + Remove from Recent 최근 항목에서 제거 + Remove {name} from Recent 최근 항목에서 {name} 제거 Close 닫기 Minimize 최소화 Maximize 최대화 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index f23e96e09d..e9bc2f5ab7 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -35,6 +35,8 @@ Right Rechts Open Recent Recent geopend Clear Recent Files Recente bestanden wissen + Remove from Recent Verwijderen uit recente bestanden + Remove {name} from Recent {name} verwijderen uit recente bestanden Close Sluiten Minimize Minimaliseren Maximize Maximaliseren diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 1ca987136b..79a17c94c8 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -42,6 +42,8 @@ Right Prawa Open Recent Otwórz ostatnie Clear Recent Files Wyczyść listę ostatnich plików + Remove from Recent Usuń z ostatnich plików + Remove {name} from Recent Usuń {name} z ostatnich plików Close Zamknij Minimize Minimalizuj Maximize Maksymalizuj diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 8aec93ed3a..870397d490 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -42,6 +42,8 @@ Right Direita Open Recent Abrir recentes Clear Recent Files Limpar arquivos recentes + Remove from Recent Remover dos recentes + Remove {name} from Recent Remover {name} dos recentes Close Fechar Minimize Minimizar Maximize Maximizar diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index bda0ff3ddb..7e8c0432d3 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -49,6 +49,8 @@ Right Правый Open Recent Открыть недавние Clear Recent Files Очистить список недавних файлов + Remove from Recent Удалить из недавних + Remove {name} from Recent Удалить {name} из недавних Close Закрыть Minimize Свернуть Maximize Развернуть diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index fe9959d656..455228639e 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -40,6 +40,8 @@ Right Правий Open Recent Нещодавні файли Clear Recent Files Очистити список нещодавніх файлів + Remove from Recent Вилучити з нещодавніх + Remove {name} from Recent Вилучити {name} з нещодавніх Close Закрити Minimize Згорнути Maximize Розгорнути diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 86abd61f98..d1913bd90b 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -39,6 +39,8 @@ Right 右 Open Recent 打开最近文件 Clear Recent Files 清除最近文件 + Remove from Recent 从最近文件中移除 + Remove {name} from Recent 从最近文件中移除 {name} Close 关闭 Minimize 最小化 Maximize 最大化 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 6d53427dc5..8bb9d0c0be 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -47,6 +47,8 @@ Right 右 Open Recent 最近開啟的檔案 Clear Recent Files 清除最近檔案 + Remove from Recent 從最近檔案中移除 + Remove {name} from Recent 從最近檔案中移除 {name} Close 關閉 Minimize 最小化 Maximize 最大化 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 84bdf03bc8..760aaec8c3 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -955,6 +955,13 @@ impl PhotocraftApp { self.session.prefs.edit(|p| p.file_handling.recent_files.clear()); } + /// Take one file off the recent list (the Home screen's × or Remove from Recent, #2691). The + /// file stays on disk, and opening it again lists it again. + pub fn remove_recent(&mut self, path: &str) { + self.ui.recent_files.retain(|p| p != path); + self.session.prefs.edit(|p| p.file_handling.recent_files.retain(|r| r != path)); + } + /// How many recent files to remember ("Recent File List Contains", 0–100). pub fn recent_cap(&self) -> usize { self.session.prefs().file_handling.recent_file_count.min(100) as usize diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 8d584b5fcb..6475cf9772 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -285,6 +285,11 @@ pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id: app.clear_recent(); Ok(Value::Null) } + "file.removeRecent" => { + let path = params.get("path").and_then(Value::as_str).ok_or("file.removeRecent needs a \"path\"")?; + app.remove_recent(path); + Ok(Value::Null) + } id if id.starts_with("file.openRecent.") => { #[cfg(not(target_arch = "wasm32"))] { @@ -599,7 +604,7 @@ pub fn is_enabled(app: &PhotocraftApp, id: &str) -> bool { return e; } match id { - "file.open" | "file.exit" | "file.clearRecent" | "help.about" | "help.systemInfo" | "edit.search" => true, + "file.open" | "file.exit" | "file.clearRecent" | "file.removeRecent" | "help.about" | "help.systemInfo" | "edit.search" => true, i if i.starts_with("file.openRecent.") => true, i if crate::links::url_for(i).is_some() => true, i if i.starts_with("window.theme.") => true, @@ -1834,6 +1839,17 @@ mod open_recent_tests { assert!(!menu_items(&app).iter().any(|i| i.id.starts_with("file.openRecent."))); // A bad recent index errors gracefully (no panic). assert!(invoke(&mut app, &ctx, "file.openRecent.5", json!({})).is_err()); + + // Remove one entry (the Home screen's ×, #2691): the others keep their order. + for p in ["/tmp/c.tif", "/tmp/b.psd", "/tmp/a.png"] { + app.push_recent(p); + } + invoke(&mut app, &ctx, "file.removeRecent", json!({"path": "/tmp/b.psd"})).unwrap(); + assert_eq!(app.ui.recent_files, vec!["/tmp/a.png".to_string(), "/tmp/c.tif".to_string()]); + assert_eq!(app.session.prefs().file_handling.recent_files, app.ui.recent_files, "removed in the preferences too"); + invoke(&mut app, &ctx, "file.removeRecent", json!({"path": "/tmp/not-listed.png"})).unwrap(); + assert_eq!(app.ui.recent_files.len(), 2, "a path that isn't listed changes nothing"); + assert!(invoke(&mut app, &ctx, "file.removeRecent", json!({})).is_err(), "needs a path"); } #[cfg(not(target_arch = "wasm32"))]