mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-10 10:10:34 -06:00
Home screen: remove individual Recent entries (#2731)
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.
This commit is contained in:
@@ -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::<Vec<_>>()
|
||||
};
|
||||
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};
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -25,6 +25,8 @@
|
||||
Right Δεξιά
|
||||
Open Recent Πρόσφατα αρχεία
|
||||
Clear Recent Files Εκκαθάριση πρόσφατων αρχείων
|
||||
Remove from Recent Αφαίρεση από τα πρόσφατα
|
||||
Remove {name} from Recent Αφαίρεση του {name} από τα πρόσφατα
|
||||
Close Κλείσιμο
|
||||
Minimize Ελαχιστοποίηση
|
||||
Maximize Μεγιστοποίηση
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -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
|
||||
|
||||
|
@@ -43,6 +43,8 @@
|
||||
Right 右
|
||||
Open Recent 最近使ったファイルを開く
|
||||
Clear Recent Files 最近使ったファイルを消去
|
||||
Remove from Recent 最近使ったファイルから削除
|
||||
Remove {name} from Recent {name} を最近使ったファイルから削除
|
||||
Close 閉じる
|
||||
Minimize 最小化
|
||||
Maximize 最大化
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -31,6 +31,8 @@
|
||||
Right 오른쪽
|
||||
Open Recent 최근 파일 열기
|
||||
Clear Recent Files 최근 파일 목록 지우기
|
||||
Remove from Recent 최근 항목에서 제거
|
||||
Remove {name} from Recent 최근 항목에서 {name} 제거
|
||||
Close 닫기
|
||||
Minimize 최소화
|
||||
Maximize 최대화
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -49,6 +49,8 @@
|
||||
Right Правый
|
||||
Open Recent Открыть недавние
|
||||
Clear Recent Files Очистить список недавних файлов
|
||||
Remove from Recent Удалить из недавних
|
||||
Remove {name} from Recent Удалить {name} из недавних
|
||||
Close Закрыть
|
||||
Minimize Свернуть
|
||||
Maximize Развернуть
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -40,6 +40,8 @@
|
||||
Right Правий
|
||||
Open Recent Нещодавні файли
|
||||
Clear Recent Files Очистити список нещодавніх файлів
|
||||
Remove from Recent Вилучити з нещодавніх
|
||||
Remove {name} from Recent Вилучити {name} з нещодавніх
|
||||
Close Закрити
|
||||
Minimize Згорнути
|
||||
Maximize Розгорнути
|
||||
|
||||
|
@@ -39,6 +39,8 @@
|
||||
Right 右
|
||||
Open Recent 打开最近文件
|
||||
Clear Recent Files 清除最近文件
|
||||
Remove from Recent 从最近文件中移除
|
||||
Remove {name} from Recent 从最近文件中移除 {name}
|
||||
Close 关闭
|
||||
Minimize 最小化
|
||||
Maximize 最大化
|
||||
|
||||
|
@@ -47,6 +47,8 @@
|
||||
Right 右
|
||||
Open Recent 最近開啟的檔案
|
||||
Clear Recent Files 清除最近檔案
|
||||
Remove from Recent 從最近檔案中移除
|
||||
Remove {name} from Recent 從最近檔案中移除 {name}
|
||||
Close 關閉
|
||||
Minimize 最小化
|
||||
Maximize 最大化
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
Reference in New Issue
Block a user