Issue #430: clear the recent-files list, or remove one file from it (#557)

This commit is contained in:
pauliusiv123-rgb 2026-10-09 18:53:34 +03:00 committed by GitHub
parent 9acb29e418
commit 28804d55d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 168 additions and 18 deletions

View File

@ -19,7 +19,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "8444a3bd135d61feb6c5b559a9eb0babb50de2ebbd1d86b1e87ccb064e2d3f0b"
sha256 = "6bed3ae5e307b0a225c49bb6a4c688911869f4bbf5df527a3bf84d0997e6baaf"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "8771d80405b8fba9da4a909fdd97b760f6b25a822807ca83ff20dc5cec83a73c"
sha256 = "42c3498eac138f95ac700f71332742658ed59bec171460aa675b1cb67e97ca36"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "bf1ab8079af725264f9c4298b9737359ba831170dcba76a6bd39228239c87e83"
sha256 = "fb315c0c3dfbafd35d73e7dedd1260daa1da919b0b30db53d16725cbd0f0ae12"
[[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv"
@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "b23b03cf4ac163e7a63e49a0900a125507c05ad4fc7930468b859acf32b09ecd"
sha256 = "20043dd4e64d6a8405dbf3167e9f315f68d4894c3d76c0b2b68e9c7e2879c06f"
[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "1b3371f239ca89a658e2c9b80cc070be5cd941dd1ecd3e7a247696716f25721d"
sha256 = "4c576bf5573a127ba2565980623e92b104699e7e03ac68dc3eddf4e788c7e8a4"
[[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv"
@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "dbf725ffc9177a435e894f4405834e87c70b6fe31a34f1be533c60c356510b97"
sha256 = "66ea8fd85159745626643bfce304dbe7ae0f9ae45a006a5d3834c3cad181080b"
[[asset]]
path = "crates/ui-egui/src/i18n/de.tsv"
@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "3827d04146f65a02e9d5e69336a97efc46862a48b18a16acbbc023ee0c8b7e1b"
sha256 = "4ba9f2bfc1cca7e819dde79d09c3b128b19a289faf7caa32f6925791683e9538"
[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "f7a5ab0bcbdb8641b1bd4ae25db54ef187757dde3e4fe372e433a9e5c0fc4f3a"
sha256 = "2c399d16589f2d0c3659c87406cb45d27792e7286774ededaa6578a224e2fae9"
[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "259f795a3cdc032aa670693d2ebff010c5dd3bcf57dda11548d5bd46e78969b8"
sha256 = "1782bfb95566a44e9cc3e22a595ff158965c55b67936d994a8dae9b3e51975a2"
[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "e71f683e410618200c0abe66a362a720cb8ded5bfd155bf825f92644373c54b5"
sha256 = "eb0b3fa525198152d7cf514284460ff904bcb9af091142ddd1c09deb8461ed69"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "cdcdba828efea9d402cb9ce8be2b52ab1d58c8f7837d75d3b9c86f2a05e7fb2e"
sha256 = "2bfcb94d72df43a6146e45e971950433a13bb008fc18c68463c096089f715466"
[[asset]]
path = "assets/icons/accessibility.svg"

View File

@ -150,6 +150,8 @@ const HELP: Option<&str> = Some("Help");
pub const COMMANDS: &[CommandSpec] = &[
c("file.open", "Open…", FILE, Some(Shortcut::cmd("O")), Nothing, "folder-open"),
c("file.open_recent", "Open Recent", FILE, None, Nothing, "clock"),
// Shown at the foot of File ▸ Open Recent and on Home, not as its own File menu item.
c("file.clear_recent", "Clear Recent Files", None, None, Nothing, "trash-2"),
c("file.pin_folder", "Pin folder to Home…", FILE, None, Nothing, "folder-plus"),
c("create.blank", "New blank PDF", FILE, None, Nothing, "file-plus-2"),
c("measure.distance", "Measure distance", None, None, Annotate, "ruler"),

View File

@ -85,6 +85,8 @@ impl PdfCraftApp {
Some(p) => self.open_recent(&p),
None => self.notify_tr("No recent files"),
},
"file.clear_recent" if self.recent.is_empty() => self.notify_tr("No recent files"),
"file.clear_recent" => self.recent.clear(),
"file.pin_folder" => self.pin_folder_dialog(),
"page.combine" => self.open_combine_tab(),
"file.save" => {
@ -570,13 +572,15 @@ pub(crate) fn registry_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui, menu: &str
let label = commands::current_label(spec, &app.session, app.active_ids().map(|(_, id)| id));
let label = crate::i18n::menu_label(spec.id, &label);
// Open Recent is a submenu of the live recent list, not one action: disabled while the
// list is empty, otherwise each entry opens its file (or focuses the tab showing it).
// list is empty, otherwise each entry opens its file (or focuses the tab showing it), and
// Clear Recent Files at the foot empties the list (#430).
if spec.id == "file.open_recent" {
if app.recent.is_empty() {
ui.add_enabled(false, egui::Button::new(label));
continue;
}
let mut open: Option<String> = None;
let mut clear = false;
ui.menu_button(label, |ui| {
for r in &app.recent {
if ui.button(&r.name).on_hover_text(&r.path).clicked() {
@ -584,11 +588,20 @@ pub(crate) fn registry_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui, menu: &str
ui.close();
}
}
ui.separator();
if ui.button(tl!("Clear Recent Files")).clicked() {
clear = true;
ui.close();
}
});
if let Some(p) = open {
app.open_recent(&p);
ui.close();
}
if clear {
app.execute("file.clear_recent");
ui.close();
}
continue;
}
let shortcut = spec.shortcut.map(|s| s.label(mac)).unwrap_or_default();

View File

@ -104,17 +104,42 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
}
ui.add_space(26.0);
ui.label(egui::RichText::new(tl!("Recent")).font(theme::semibold(17.0)));
let mut clear = false;
ui.horizontal(|ui| {
ui.label(egui::RichText::new(tl!("Recent")).font(theme::semibold(17.0)));
if !app.recent.is_empty() {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let resp = widgets::ghost_button(ui, "trash-2", tl!("Clear"));
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl!("Clear Recent Files")));
clear = resp.on_hover_text(tl!("Clear Recent Files")).clicked();
});
}
});
ui.add_space(8.0);
if app.recent.is_empty() {
ui.label(egui::RichText::new(tl!("Files you open in PdfCraft appear here. Drop a PDF anywhere to open it.")).color(t.text_muted));
}
let mut open = None;
let mut remove = None;
for r in &app.recent {
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 46.0), Sense::click());
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &r.name));
if resp.hovered() {
// The row, not its response: the pointer stays "in" the row over its remove button.
let hovered = ui.rect_contains_pointer(rect);
if hovered {
ui.painter().rect_filled(rect, CornerRadius::same(8), t.hover);
// Remove just this file from the list (#430); the file itself is untouched.
let x_rect = Rect::from_center_size(rect.right_center() - vec2(26.0, 0.0), vec2(28.0, 28.0));
let x = ui.interact(x_rect, ui.id().with(("remove-recent", &r.path)), Sense::click());
let label = crate::i18n::fmt(tl!("Remove {name} from Recent"), &[("name", &r.name)]);
x.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &label));
if x.hovered() {
ui.painter().rect_filled(x_rect, CornerRadius::same(6), t.pressed);
}
icons::paint(ui, x_rect.shrink(6.0), "x", 16.0, t.icon);
if x.on_hover_text(tl!("Remove from Recent")).clicked() {
remove = Some(r.path.clone());
}
}
icons::paint(
ui,
@ -123,8 +148,9 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
22.0,
egui::Color32::from_rgb(0xE0, 0x3E, 0x3E),
);
// Room on the right for the remove button.
let detail = ui.painter().text(
rect.right_center() - vec2(12.0, 0.0),
rect.right_center() - vec2(48.0, 0.0),
Align2::RIGHT_CENTER,
format!("{} {} · {}", r.pages, tl!("pages"), human_size(r.size)),
theme::regular(12.0),
@ -141,6 +167,12 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
if let Some(p) = open {
app.open_recent(&p);
}
if let Some(p) = remove {
app.recent.retain(|r| r.path != p);
}
if clear {
app.execute("file.clear_recent");
}
ui.add_space(20.0);
widgets::section_title(ui, tl!("Privacy"));
ui.label(

View File

@ -2043,3 +2043,6 @@ signature pad Type Въвеждане
action wizard Start Старт
Margins (in) Полета (in)
Reopen the files that were open when PdfCraft last closed Отваряй отново файловете, които бяха отворени при последното затваряне на PdfCraft
Clear Recent Files Изчисти последно отваряните файлове
Remove from Recent Премахни от последно отваряните
Remove {name} from Recent Премахни {name} от последно отваряните

Can't render this file because it contains an unexpected character in line 8 and column 16.

View File

@ -29,6 +29,10 @@
OK OK
Open… Otevřít…
Open Recent Otevřít nedávné soubory
Clear Recent Files Vymazat nedávné soubory
Remove from Recent Odebrat z nedávných souborů
Remove {name} from Recent Odebrat {name} z nedávných souborů
Clear Vymazat
Pin folder to Home… Připnout složku na úvodní stránku…
No recent files Žádné nedávné soubory
New blank PDF Nový prázdný dokument PDF

Can't render this file because it contains an unexpected character in line 7 and column 81.

View File

@ -2045,3 +2045,6 @@ comment menu Edit Bearbeiten
signature pad Type Tippen
action wizard Start Starten
Reopen the files that were open when PdfCraft last closed Beim Start die Dateien wieder öffnen, die beim letzten Beenden von PdfCraft geöffnet waren
Clear Recent Files Liste „Zuletzt geöffnet“ leeren
Remove from Recent Aus „Zuletzt geöffnet“ entfernen
Remove {name} from Recent {name} aus „Zuletzt geöffnet“ entfernen

Can't render this file because it contains an unexpected character in line 6 and column 51.

View File

@ -22,6 +22,9 @@
Name on new comments Nombre en los comentarios nuevos
Open… Abrir…
Open Recent Abrir recientes
Clear Recent Files Borrar archivos recientes
Remove from Recent Quitar de recientes
Remove {name} from Recent Quitar {name} de recientes
Pin folder to Home… Anclar carpeta en Inicio…
Folders Carpetas
Pin a folder… Anclar una carpeta…

Can't render this file because it contains an unexpected character in line 6 and column 60.

View File

@ -2014,6 +2014,9 @@
Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Choisissez une image PNG ou JPEG (jusqu’à 4 Mio). La transparence est conservée.
Couldn't import the signature image: {e} Impossible d’importer l’image de la signature : {e}
Open Recent Ouvrir un fichier récent
Clear Recent Files Effacer les fichiers récents
Remove from Recent Retirer des fichiers récents
Remove {name} from Recent Retirer {name} des fichiers récents
Pin folder to Home… Épingler un dossier à l'Accueil…
Folders Dossiers
Pin a folder… Épingler un dossier…

Can't render this file because it contains an unexpected character in line 6 and column 39.

View File

@ -28,6 +28,9 @@
Name on new comments 新しい注釈の作成者名
Open… 開く…
Open Recent 最近のファイルを開く
Clear Recent Files 最近のファイルをクリア
Remove from Recent 最近のファイルから外す
Remove {name} from Recent {name} を最近のファイルから外す
Pin folder to Home… フォルダーをホームにピン留め…
Folders フォルダー
Pin a folder… フォルダーをピン留め…

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -21,6 +21,10 @@
Name on new comments Nome nos novos comentários
Open… Abrir…
Open Recent Abrir recentes
Clear Recent Files Limpar arquivos recentes
Remove from Recent Remover dos recentes
Remove {name} from Recent Remover {name} dos recentes
Clear Limpar
Pin folder to Home… Fixar pasta no Início…
No recent files Nenhum arquivo recente
New blank PDF Novo PDF em branco

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -21,6 +21,9 @@
Name on new comments Имя в новых комментариях
Open… Открыть…
Open Recent Открыть недавние
Clear Recent Files Очистить недавние файлы
Remove from Recent Убрать из недавних
Remove {name} from Recent Убрать {name} из недавних
Pin folder to Home… Закрепить папку на главной…
Folders Папки
Pin a folder… Закрепить папку…

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -2017,6 +2017,9 @@ action wizard Start ప్రారంభించు
Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. PNG లేదా JPEG చిత్రాన్ని ఎంచుకోండి (గరిష్ఠంగా 4 MiB). పారదర్శకత అలాగే ఉంటుంది.
Couldn't import the signature image: {e} సంతకం చిత్రాన్ని దిగుమతి చేయడం సాధ్యం కాలేదు: {e}
Open Recent ఇటీవలివి తెరువు
Clear Recent Files ఇటీవలి ఫైళ్లను క్లియర్ చేయి
Remove from Recent ఇటీవలి ఫైళ్ల నుండి తీసివేయి
Remove {name} from Recent {name}ను ఇటీవలి ఫైళ్ల నుండి తీసివేయి
Pin folder to Home… ఫోల్డర్‌ను హోమ్‌కు పిన్ చేయి…
No recent files ఇటీవలి ఫైళ్లు లేవు
Go to page… పేజీకి వెళ్లు…

Can't render this file because it contains an unexpected character in line 13 and column 16.

View File

@ -62,6 +62,9 @@
Name on new comments 新注释的作者姓名
Open… 打开…
Open Recent 打开最近的文件
Clear Recent Files 清除最近的文件
Remove from Recent 从最近的文件中移除
Remove {name} from Recent 从最近的文件中移除{name}
Pin folder to Home… 将文件夹固定到主页…
Folders 文件夹
Pin a folder… 固定文件夹…

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -21,6 +21,9 @@
Name on new comments 新註解的作者名稱
Open… 開啟…
Open Recent 開啟最近的檔案
Clear Recent Files 清除最近的檔案
Remove from Recent 從最近的檔案中移除
Remove {name} from Recent 從最近的檔案中移除{name}
Pin folder to Home… 將資料夾釘選到首頁…
Folders 資料夾
Pin a folder… 釘選資料夾…

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -243,6 +243,49 @@ fn the_open_recent_menu_is_disabled_while_the_list_is_empty() {
);
}
fn recent_file(name: &str) -> pdfcraft_ui_egui::RecentFile {
pdfcraft_ui_egui::RecentFile { name: name.into(), path: format!("/nowhere/{name}"), pages: 1, size: 0 }
}
#[test]
fn clear_recent_files_at_the_foot_of_open_recent_empties_the_list() {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
let mut app = PdfCraftApp::new();
app.open_bytes("doc.pdf", None, fixture(3)).unwrap();
app.recent.extend([recent_file("first.pdf"), recent_file("second.pdf")]);
app
});
h.run_steps(4);
h.get_by_label("Menu").click();
h.run_steps(2);
h.get_by_label("File ⏵").hover();
h.run_steps(3);
h.get_by_label("Open Recent ⏵").hover();
h.run_steps(3);
h.get_by_label("Clear Recent Files").click();
h.run_steps(3);
assert!(h.state().recent.is_empty(), "the list is empty");
h.get_by_label("Menu").click();
h.run_steps(2);
h.get_by_label("File ⏵").hover();
h.run_steps(3);
assert!(h.query_by(|n| n.label().as_deref() == Some("Open Recent") && n.is_disabled()).is_some(), "and Open Recent is disabled");
}
#[test]
fn clear_recent_files_runs_from_the_palette_and_is_saved() {
let mut app = PdfCraftApp::new();
app.recent.extend([recent_file("first.pdf"), recent_file("second.pdf")]);
assert!(app.execute("file.clear_recent"));
assert!(app.recent.is_empty());
let mut restarted = PdfCraftApp::new();
restarted.recent.push(recent_file("stale.pdf"));
restarted.restore(&app.persist());
assert!(restarted.recent.is_empty(), "the empty list is what's saved");
app.execute("file.clear_recent");
assert!(app.toast.as_ref().is_some_and(|(m, _)| m.contains("No recent files")), "nothing to clear is said, not silent");
}
#[test]
fn the_shortcuts_dialog_lists_the_real_bindings() {
let mut h = harness();

View File

@ -67,6 +67,31 @@ fn long_recent_names_and_paths_are_cut_before_the_page_count() {
}
}
#[test]
fn home_removes_one_recent_file_or_clears_them_all() {
// Tall enough that the Recent list is on screen without scrolling.
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1600.0)).build_eframe(|_cc| {
let mut app = PdfCraftApp::new();
for name in ["first.pdf", "second.pdf", "third.pdf"] {
app.recent.push(pdfcraft_ui_egui::RecentFile { name: name.into(), path: format!("/nowhere/{name}"), pages: 1, size: 0 });
}
app
});
h.run_steps(4);
assert!(h.query_by_label("Remove second.pdf from Recent").is_none(), "the remove button shows only over its row");
h.get_by_label("second.pdf").hover();
h.run_steps(2);
h.get_by_label("Remove second.pdf from Recent").click();
h.run_steps(2);
let names: Vec<String> = h.state().recent.iter().map(|r| r.name.clone()).collect();
assert_eq!(names, ["first.pdf", "third.pdf"], "only that file left the list");
h.get_by_label("Clear Recent Files").click();
h.run_steps(2);
assert!(h.state().recent.is_empty(), "Clear empties the list");
h.get_by_label_contains("Files you open in PdfCraft appear here");
assert!(h.query_by_label("Clear Recent Files").is_none(), "and goes away with nothing left to clear");
}
#[test]
fn every_catalog_tool_is_listed_after_view_more() {
let mut h = harness(|_| {});

View File

@ -656,10 +656,10 @@ area = "A"
tier = "P1"
milestone = "M3"
status = "shipped"
commands = ["file.open_recent"]
commands = ["file.open_recent", "file.clear_recent"]
tools = []
evidence = ["crates/ui-egui/tests/ui.rs::persistence_round_trips_and_tolerates_garbage", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_lists_files_and_opens_one", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_is_disabled_while_the_list_is_empty"]
notes = "never reads other apps' lists; File ▸ Open Recent lists them (#290), an already-open file focuses its tab instead of reopening it"
evidence = ["crates/ui-egui/tests/ui.rs::persistence_round_trips_and_tolerates_garbage", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_lists_files_and_opens_one", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_is_disabled_while_the_list_is_empty", "crates/ui-egui/tests/commands.rs::clear_recent_files_at_the_foot_of_open_recent_empties_the_list", "crates/ui-egui/tests/commands.rs::clear_recent_files_runs_from_the_palette_and_is_saved", "crates/ui-egui/tests/ui.rs::home_removes_one_recent_file_or_clears_them_all"]
notes = "never reads other apps' lists; File ▸ Open Recent lists them (#290), an already-open file focuses its tab instead of reopening it; Clear Recent Files at the foot of Open Recent, on Home and in the palette, and a remove button on each Home row (#430); missing: the row's remove button shows on pointer hover only (keyboard users clear the whole list)"
[[feature]]
id = "core.reopen-last-session"