mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-13 01:41:18 +00:00
Merge pull request #5559 from vangel76/feat/recent-reveal-5452
Reveal recent files in the system file manager from Home (#5452)
This commit is contained in:
8 files changed
+136
-18
No files matched your search
@@ -12,6 +12,7 @@
|
||||
|
||||
> **Last reviewed:** 2026-11-07 | **Last updated:** 2026-11-07 | **Change:** patch (M10: file-backed Windows PDF loading without whole-job buffering or per-chunk async waits; approved dependency chain retains #5006; physical output still unqualified; estimates unchanged)
|
||||
> **Last reviewed:** 2026-11-05 | **Last updated:** 2026-11-05 | **Change:** patch (M7 #2964: directly edit Thai source text inside supported Form XObjects; estimates unchanged)
|
||||
> **Last reviewed:** 2026-11-05 | **Last updated:** 2026-11-05 | **Change:** minor (M3, #5452: native Recent rows gain Reveal in Finder / File Explorer; estimates unchanged)
|
||||
> **Last reviewed:** 2026-11-03 | **Last updated:** 2026-11-03 | **Change:** patch (M5 #5850 review: indirect typewriter intent and composer edits preserve outer whitespace; movement regression commits by outside click; estimates unchanged)
|
||||
> **Last reviewed:** 2026-11-03 | **Last updated:** 2026-11-03 | **Change:** patch (M5 #5850: Enter adds Fill & Sign line breaks; outside clicks commit, Escape cancels, and saved multiline appearances retain their text and height; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-31 | **Last updated:** 2026-10-31 | **Change:** minor (M3/M4, #5330: Crop pages is directly available in the document quick toolbar; estimates unchanged)
|
||||
|
||||
@@ -177,6 +177,8 @@ impl PdfCraftApp {
|
||||
open_prepare_gate: None,
|
||||
pinned: Default::default(),
|
||||
recent_check: Default::default(),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
reveal_errors: Default::default(),
|
||||
pinned_tools: Vec::new(),
|
||||
pin_tool_search: String::new(),
|
||||
toast: None,
|
||||
@@ -608,6 +610,7 @@ impl PdfCraftApp {
|
||||
worker.set_context(ctx);
|
||||
}
|
||||
self.poll_pending_opens();
|
||||
self.show_reveal_errors();
|
||||
self.poll_pending_destinations();
|
||||
}
|
||||
self.check_restored_recent();
|
||||
|
||||
@@ -207,9 +207,29 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
let mut open = None;
|
||||
let mut remove = None;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut reveal = None;
|
||||
for r in app.recent.iter().take(app.recent_limit) {
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 46.0), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, &r.name));
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
// The Menu key or Shift+F10 on the focused row opens the same menu a secondary
|
||||
// click does, under the row, so Reveal works from the keyboard.
|
||||
let by_key =
|
||||
resp.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Menu) || (i.modifiers.shift && i.key_pressed(egui::Key::F10)));
|
||||
let mut popup = egui::Popup::context_menu(&resp);
|
||||
if by_key {
|
||||
popup = popup.open_memory(egui::SetOpenCommand::Bool(true)).anchor(rect).align(egui::RectAlign::BOTTOM_START);
|
||||
}
|
||||
popup.show(|ui| {
|
||||
let label = if cfg!(target_os = "macos") { tl!("Reveal in Finder") } else { tl!("Reveal in File Explorer") };
|
||||
if ui.button(label).clicked() {
|
||||
reveal = Some(r.path.clone());
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
let press = theme::Press::track(ui, &resp);
|
||||
// The row, not its response: the pointer stays "in" the row over its remove button.
|
||||
let hovered = ui.rect_contains_pointer(rect);
|
||||
@@ -260,6 +280,10 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
||||
if let Some(p) = open {
|
||||
app.open_recent(&p);
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(p) = reveal {
|
||||
app.reveal_path(std::path::Path::new(&p));
|
||||
}
|
||||
if let Some(p) = remove {
|
||||
app.recent.retain(|r| r.path != p);
|
||||
}
|
||||
@@ -315,6 +339,23 @@ mod tests {
|
||||
assert!(h.state().pinned_tools.is_empty());
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn a_focused_recent_row_opens_its_reveal_menu_from_the_keyboard() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
for chord in [(egui::Modifiers::NONE, egui::Key::Menu), (egui::Modifiers::SHIFT, egui::Key::F10)] {
|
||||
let mut h = review_home_harness();
|
||||
h.state_mut().recent = vec![crate::RecentFile { name: "notes.pdf".into(), path: "/nowhere/notes.pdf".into(), pages: 2, size: 2048 }];
|
||||
h.run_steps(3);
|
||||
assert!(!egui::Popup::is_any_open(&h.ctx));
|
||||
h.get_by_label("notes.pdf").focus();
|
||||
h.run_steps(2);
|
||||
h.key_press_modifiers(chord.0, chord.1);
|
||||
h.run_steps(3);
|
||||
assert!(egui::Popup::is_any_open(&h.ctx), "{chord:?} did not open the Reveal menu");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translated_card_description_leaves_room_for_the_action() {
|
||||
if !crate::i18n::needs_catalogs() {
|
||||
|
||||
@@ -518,6 +518,9 @@ pub struct PdfCraftApp {
|
||||
pub pinned: folders_ui::PinnedFolders,
|
||||
/// The recent files restored from settings, checked off the UI thread (#5682).
|
||||
recent_check: persist::recent_check::RecentCheck,
|
||||
/// Failures of the Reveal worker, taken each frame and shown as a notification.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
reveal_errors: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
|
||||
/// Tools pinned to Home, in user-defined order.
|
||||
pub pinned_tools: Vec<String>,
|
||||
/// Search query for the Pin a tool selection dialog.
|
||||
|
||||
@@ -15,6 +15,39 @@ pub(crate) enum Action {
|
||||
}
|
||||
|
||||
impl crate::PdfCraftApp {
|
||||
/// Reveal a local file without opening it or changing document/Recent state. The existence
|
||||
/// check and the file manager call can block (a disconnected network mount, a slow `dbus-send`
|
||||
/// or `explorer.exe`), so they run on a worker; a failure is shown by
|
||||
/// [`Self::show_reveal_errors`]. Tests run it inline ([`PdfCraftApp::run_inline`]).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn reveal_path(&mut self, path: &std::path::Path) {
|
||||
let path = path.to_path_buf();
|
||||
let errors = self.reveal_errors.clone();
|
||||
let ctx = self.ctx.clone();
|
||||
let work = move || {
|
||||
if let Err(e) = reveal(&path) {
|
||||
errors.lock().unwrap_or_else(std::sync::PoisonError::into_inner).push(e);
|
||||
if let Some(ctx) = &ctx {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
};
|
||||
if self.run_inline {
|
||||
work();
|
||||
} else if let Err(e) = std::thread::Builder::new().name("pdfcraft-reveal".into()).spawn(work) {
|
||||
self.notify_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the failures the reveal worker has reported (called each frame).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn show_reveal_errors(&mut self) {
|
||||
let errors = std::mem::take(&mut *self.reveal_errors.lock().unwrap_or_else(std::sync::PoisonError::into_inner));
|
||||
for e in errors {
|
||||
self.notify_error(e);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tab_action(&mut self, action: Action) {
|
||||
match action {
|
||||
Action::Close(tab) => {
|
||||
@@ -47,10 +80,8 @@ impl crate::PdfCraftApp {
|
||||
}
|
||||
Action::Reveal(id) => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(path) = self.session.get(id).and_then(|d| d.path.clone())
|
||||
&& let Err(e) = reveal(std::path::Path::new(&path))
|
||||
{
|
||||
self.notify_error(e);
|
||||
if let Some(path) = self.session.get(id).and_then(|d| d.path.clone()) {
|
||||
self.reveal_path(std::path::Path::new(&path));
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = id;
|
||||
@@ -60,6 +91,7 @@ impl crate::PdfCraftApp {
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Blocking: call it from a worker thread ([`crate::PdfCraftApp::reveal_path`]).
|
||||
fn reveal(path: &std::path::Path) -> Result<(), String> {
|
||||
// Absolute paths also prevent a filename starting with '-' becoming an option.
|
||||
let path = std::path::absolute(path).map_err(|e| e.to_string())?;
|
||||
@@ -69,16 +101,6 @@ fn reveal(path: &std::path::Path) -> Result<(), String> {
|
||||
let platform = std::env::consts::OS;
|
||||
let run =
|
||||
|program: &str, args: &[std::ffi::OsString]| reveal_process(program, args).output().map(|o| o.status.success()).map_err(|e| e.to_string());
|
||||
if matches!(platform, "linux" | "freebsd") {
|
||||
// `dbus-send --print-reply` waits up to 2 s for the file manager (then xdg-open may run),
|
||||
// so it runs off the UI thread; a failure there is only logged.
|
||||
let spawned = std::thread::Builder::new().name("pdfcraft-reveal".into()).spawn(move || {
|
||||
if let Err(e) = reveal_via(platform, &path, run) {
|
||||
log::warn!("reveal {}: {e}", path.display());
|
||||
}
|
||||
});
|
||||
return spawned.map(|_| ()).map_err(|e| e.to_string());
|
||||
}
|
||||
reveal_via(platform, &path, run)
|
||||
}
|
||||
|
||||
@@ -242,6 +264,16 @@ mod tests {
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_reveal_is_reported_back_to_the_ui_thread() {
|
||||
let mut app = crate::PdfCraftApp::new();
|
||||
app.run_inline = true;
|
||||
app.reveal_path(std::path::Path::new("/pdfcraft-no-such-dir/missing.pdf"));
|
||||
assert!(app.toast.is_none(), "nothing is shown until the UI thread takes the report");
|
||||
app.show_reveal_errors();
|
||||
assert_eq!(app.toast.as_ref().map(|t| t.0.as_str()), Some("The file no longer exists"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_file_manager_falls_back_safely_and_reports_failure() {
|
||||
let path = std::path::Path::new("/tmp/年度 & reports/file.pdf");
|
||||
|
||||
@@ -2133,3 +2133,38 @@ fn full_screen_neither_is_remembered_nor_hides_the_last_view() {
|
||||
view.leave_presentation();
|
||||
assert_eq!((view.fit, view.zoom, view.layout, view.continuous), (Fit::None, 0.5, PageLayout::TwoUp, false), "the remembered view");
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn home_recent_context_menu_reveals_the_target_without_opening_or_removing_it() {
|
||||
let missing = std::env::temp_dir().join(format!("pdfcraft-recent-missing-{}", std::process::id())).join("jährlicher report.pdf");
|
||||
assert!(!missing.exists());
|
||||
let expected = missing.to_string_lossy().into_owned();
|
||||
let target = expected.clone();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1600.0)).build_eframe(move |_| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
for (name, path) in [("first.pdf", ".".to_owned()), ("target.pdf", target)] {
|
||||
app.recent.push(pdfcraft_ui_egui::RecentFile { name: name.into(), path, pages: 1, size: 0 });
|
||||
}
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
let label = if cfg!(target_os = "macos") { "Reveal in Finder" } else { "Reveal in File Explorer" };
|
||||
h.get_by_label("target.pdf").click_secondary();
|
||||
h.run_steps(3);
|
||||
assert!(!h.get_by_label(label).accesskit_node().is_disabled());
|
||||
assert!(h.state().views.is_empty(), "right-click must not open the PDF");
|
||||
assert_eq!(h.state().recent.len(), 2);
|
||||
h.get_by_label(label).click();
|
||||
h.run_steps(3);
|
||||
assert!(h.query_by_label(label).is_none(), "choosing the action closes its menu");
|
||||
assert!(h.state().views.is_empty(), "reveal must not open the PDF");
|
||||
assert_eq!(h.state().active, None);
|
||||
assert_eq!(h.state().recent.len(), 2, "reveal must not remove missing recent files");
|
||||
assert_eq!(h.state().recent[1].path, expected);
|
||||
assert!(
|
||||
h.state().toast.as_ref().is_some_and(|(text, _)| text.contains("The file no longer exists")),
|
||||
"the selected path is checked and the existing reveal error is shown"
|
||||
);
|
||||
}
|
||||
+5
-2
@@ -23,6 +23,8 @@
|
||||
> **2026-11-04 (M6, PR #5699 review):** Bound worker capacity and synchronous deadlines, isolate expired/pending realms before edits, keep script prompts above other dialogs, and cover block ASI, control-position regex literals, event routing and complete undo state. Validation is reported in the PR; parity remains partial.
|
||||
> **2026-11-03 (M6, PR #5699 follow-up):** Accept independent top-level statements separated by automatic semicolon insertion while retaining continuation-chain limits; assert cyclic calculation results and preserve upstream multiline Fill & Sign coverage.
|
||||
|
||||
> **Last reviewed:** 2026-11-05 | **Last updated:** 2026-11-05 | **Change:** minor (M3, #5452: native Recent rows gain Reveal in Finder / File Explorer; estimates unchanged)
|
||||
|
||||
> **Last reviewed:** 2026-10-28 | **Last updated:** 2026-10-28 | **Change:** minor (M10, #4119 partial: Enhance scanned file cleans scanned pages in place: background removal, edge shadow removal, despeckle; estimates unchanged)
|
||||
> **Last reviewed:** 2026-11-03 | **Last updated:** 2026-11-03 | **Change:** patch (M5 #5850 review: indirect typewriter intent and composer edits preserve outer whitespace; movement regression commits by outside click; estimates unchanged)
|
||||
|
||||
@@ -43,7 +45,6 @@
|
||||
> **Last reviewed:** 2026-11-01 | **Last updated:** 2026-11-01 | **Change:** patch (M7, PR #2155 latest comments: explicit image-paste events, mixed-size field refresh, and documented mixed-script normalization with Undo; estimates unchanged)
|
||||
|
||||
> **Last reviewed:** 2026-11-01 | **Last updated:** 2026-11-01 | **Change:** patch (M7, PR #2155 review follow-up: preserve image crop/tag/stream ownership, resize in page orientation, harden clipboard and text formatting, and reload installed fonts; estimates unchanged)
|
||||
|
||||
> **Last reviewed:** 2026-10-31 | **Last updated:** 2026-10-31 | **Change:** patch (M2, PR #4172: GBK rasters compared with explicit Unicode and independently selected installed faces; wrong-glyph and wrong-family negative controls fail; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-31 | **Last updated:** 2026-10-31 | **Change:** patch (M2, PR #4172: integrate main fa5f9f8c; retain CJK viewer and upstream raster regressions, restore explicit display_config imports; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-30 | **Last updated:** 2026-10-30 | **Change:** minor (M6: scripts can read and change a push-button's normal caption with undo/redo and save/reopen coverage; estimates unchanged)
|
||||
@@ -252,7 +253,7 @@ total matches the parity estimate.
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. Page marks, added content and print output retain small page-unit transform coefficients when serialized. Forms without Resources use page Properties for ActualText and layer visibility; own dictionaries stop fallback. Oversized missing inline-image samples, including header and terminator overhead, no longer pin the content window (#3107 / PR #3438); normal large streamed images retain deterministic sample boundaries. Radial luminosity fades have synthetic coverage for the existing #2132 mask guard, including per-pixel references, antialiased clips, previews, scales and tiles; no additional production fix was needed. Dotted TOC leaders no longer inflate letter tracking or hide word spaces (#3086); phrase search and selection are covered. Short tracked ellipses retain their gap samples and remain searchable. Missing spacing glyphs in non-embedded Japanese CID substitutes render blank without changing PDF advances or Unicode (#3601). The desktop viewer can use installed CJK fonts, including Windows GBK Song/Hei/Kai/FangSong and subset-prefixed Founder names; Combine thumbnails and accessibility figure previews use the same display-only fallback, without changing export or print font policy (#234, continuation of #1567, review #4172). Print previews exclude installed faces and isolate their font cache; GBK regressions cover view/print switching for inline and pooled whole-page/tile rendering. Installed-font helpers and their regression contracts have function documentation. GBK regression rasters match explicit-Unicode references with independently selected installed faces; wrong-glyph and wrong-family negative controls fail. Integration with main fa5f9f8c retains viewer fallback across the split engine session modules, renderer tests and Combine thumbnail workers. |
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. The renderer keeps its one-point crop-size floor in physical points after UserUnit scaling, with regression coverage for crop origins, rotations and f32 reciprocal rounding. Existing unit policy and nearly-zero fallback are retained. |
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings |
|
||||
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). #3808: the Pages inspector offers a 50–150% thumbnail slider and buttons, also available through `ui.set`. Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. #3149: page-list changes discard render demand based on old geometry; actual raster dimensions are checked against the GPU limit before upload. Synthetic crash and continued-rendering regressions cover the replacement path. Save-prompt shortcuts yield to dialogs above them; stacked link-consent UI regression covers Escape and Return. Comment hover follows individual marked lines and original annotation paint order (#3541/#3620), independently of spatial inspection sorting; synthetic rotation, differing-upper-edge overlap and unchanged-document regressions. Optional page gaps join page edges without cropping PDF margins; current-document toggle, persisted default and UI control options are covered by viewer tests. Gap changes in Organize Pages preserve document scroll position, and both comparison panes follow menu and control choices. Repeated window resizes by an edge and moves by the tab strip on GNOME/Wayland work without a click in between. Optional --two-paged applies noncontinuous two-page layout to explicitly opened CLI files without changing defaults; explicit --layout wins. Crop pages is directly available in the quick toolbar (#5330). |
|
||||
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). #3808: the Pages inspector offers a 50–150% thumbnail slider and buttons, also available through `ui.set`. Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. #3149: page-list changes discard render demand based on old geometry; actual raster dimensions are checked against the GPU limit before upload. Synthetic crash and continued-rendering regressions cover the replacement path. Save-prompt shortcuts yield to dialogs above them; stacked link-consent UI regression covers Escape and Return. Comment hover follows individual marked lines and original annotation paint order (#3541/#3620), independently of spatial inspection sorting; synthetic rotation, differing-upper-edge overlap and unchanged-document regressions. Optional page gaps join page edges without cropping PDF margins; current-document toggle, persisted default and UI control options are covered by viewer tests. Gap changes in Organize Pages preserve document scroll position, and both comparison panes follow menu and control choices. Repeated window resizes by an edge and moves by the tab strip on GNOME/Wayland work without a click in between. Optional --two-paged applies noncontinuous two-page layout to explicitly opened CLI files without changing defaults; explicit --layout wins. Crop pages is directly available in the quick toolbar (#5330). Native Recent rows also offer Reveal in Finder / File Explorer (#5452). |
|
||||
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. Wrapped revision buttons stay above Binding in Document Properties (#2739); a synthetic UI test checks layout and saved revision content. |
|
||||
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Later native-open replies stay queued while a modal or picker owns the foreground; regressions cover recipient ownership, completion/cancellation and one-shot wakes. |
|
||||
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. #1980: exact quit modifiers preserve Ctrl+Alt+Q email input; Ctrl+Q/Cmd+Q asks before discarding unsaved documents. |
|
||||
@@ -341,6 +342,8 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st
|
||||
|
||||
## Revision history
|
||||
|
||||
**2026-10-31 · minor:** M3/#5452: native Home Recent context menus reuse tab reveal, retaining missing-file errors and leaving documents and history unchanged. Estimates unchanged.
|
||||
|
||||
**2026-11-09 · patch:** M4 #4594: reconcile upstream license notices, roadmap entries and UI test modules without changing the Document Properties draft guard or dependency policy. Estimates unchanged.
|
||||
|
||||
**2026-10-27 · minor:** M10 #5005: vector printing preserves annotation optional-content membership.
|
||||
|
||||
@@ -682,8 +682,8 @@ milestone = "M3"
|
||||
status = "shipped"
|
||||
commands = ["file.open_recent", "file.clear_recent"]
|
||||
tools = []
|
||||
evidence = ["crates/ui-egui/tests/it/ui.rs::persistence_round_trips_and_tolerates_garbage", "crates/ui-egui/tests/it/commands.rs::the_open_recent_menu_lists_files_and_opens_one", "crates/ui-egui/tests/it/commands.rs::the_open_recent_menu_is_disabled_while_the_list_is_empty", "crates/ui-egui/tests/it/commands.rs::clear_recent_files_at_the_foot_of_open_recent_empties_the_list", "crates/ui-egui/tests/it/commands.rs::clear_recent_files_runs_from_the_palette_and_is_saved", "crates/ui-egui/tests/it/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, from any open path (#1620); 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)"
|
||||
evidence = ["crates/ui-egui/tests/it/ui.rs::persistence_round_trips_and_tolerates_garbage", "crates/ui-egui/tests/it/commands.rs::the_open_recent_menu_lists_files_and_opens_one", "crates/ui-egui/tests/it/commands.rs::the_open_recent_menu_is_disabled_while_the_list_is_empty", "crates/ui-egui/tests/it/commands.rs::clear_recent_files_at_the_foot_of_open_recent_empties_the_list", "crates/ui-egui/tests/it/commands.rs::clear_recent_files_runs_from_the_palette_and_is_saved", "crates/ui-egui/tests/it/ui.rs::home_removes_one_recent_file_or_clears_them_all", "crates/ui-egui/tests/it/ui.rs::home_recent_context_menu_reveals_the_target_without_opening_or_removing_it"]
|
||||
notes = "never reads other apps' lists; File ▸ Open Recent lists them (#290), an already-open file focuses its tab instead of reopening it, from any open path (#1620); Clear Recent Files at the foot of Open Recent, on Home and in the palette, and a remove button on each Home row (#430); native Home rows offer Reveal in Finder / File Explorer through their context menu (#5452), reusing tab reveal without opening or removing the file (browser has no local file manager); missing: the row's remove button shows on pointer hover only (keyboard users clear the whole list)"
|
||||
|
||||
[[feature]]
|
||||
id = "core.reopen-last-session"
|
||||
|
||||
Reference in new issue
Block a user