From 36d24eae6e78b1c7d154eebdf86ae411a98fca4f Mon Sep 17 00:00:00 2001 From: Hasan Date: Mon, 2 Nov 2026 18:04:58 +0500 Subject: [PATCH] M14: Preferences can turn off the update check at start (#5737, #5460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop app asks for a newer release on its first frame, and nothing could stop it, though the README says PdfCraft works offline. A preference, on by default so nothing changes for anyone who leaves it, keeps the app off the network until Help ▸ Check for updates is chosen. It has its own settings key: check_updates_at_start was an opt-in, off unless chosen, so settings written then would turn this off unasked. --- ROADMAP.md | 2 + crates/ui-egui/src/app.rs | 1 + crates/ui-egui/src/app/control.rs | 3 + crates/ui-egui/src/app/prefs.rs | 6 ++ crates/ui-egui/src/dialogs/preferences.rs | 2 + crates/ui-egui/src/lib.rs | 3 + crates/ui-egui/src/platform/updates.rs | 6 +- crates/ui-egui/tests/it/updates.rs | 76 +++++++++++++++++++++++ docs/roadmap.md | 1 + parity/acrobat-features.toml | 4 +- 10 files changed, 100 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 5bfe1fc817..0a03866be3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -231,6 +231,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-11-02 (M10, #5743):** macOS print jobs name their title and owner (`com.apple.print.JobInfo.PMJobName`, `PMJobOwner`), as the system print dialog does, because a Kyocera filter crashes on a job without them. Checked by a test and through `doc_print` against a stand-in `lp`; not yet on a physical printer. +- **2026-11-02 (M14, #5737, #5460):** Preferences can turn off the update check at start. It stays on by default, and Help ▸ Check for updates still asks. + - **2026-11-01 (M7, #5657):** Header/footer, watermark and background additions and replacements stage the complete call before committing. Synthetic later-page failures preserve exact incremental bytes, object count, dirty state and allocation cursor; success controls retain other marks and shared/inherited resources. Estimates unchanged. - **2026-11-01 (M14, #5572):** Preferences ▸ Documents and view sets how many recent files to keep, from 0 to 50 with 12 by default. At 0 Home has no Recent section and File ▸ Open Recent is disabled. The list is cut when Preferences closes, so the number can pass through 0 on its way to another. diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index 6ac8ea8783..d2cec1a29f 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -128,6 +128,7 @@ impl PdfCraftApp { last_session: Default::default(), quit_session: None, reload_changed_files: true, + update_check_at_start: true, show_secured_notice: true, always_default_view: false, #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/ui-egui/src/app/control.rs b/crates/ui-egui/src/app/control.rs index 0309042bf1..403af6f4c6 100644 --- a/crates/ui-egui/src/app/control.rs +++ b/crates/ui-egui/src/app/control.rs @@ -47,6 +47,9 @@ impl PdfCraftApp { ("show_page_edge_shadows", _) => { self.show_page_edge_shadows = value.parse::().map_err(|_| "show_page_edge_shadows must be true or false")?; } + ("update_check_at_start", _) => { + self.update_check_at_start = value.parse::().map_err(|_| "update_check_at_start must be true or false")?; + } ("scroll_speed", _) => { self.scroll_speed = scroll_speed::ScrollSpeed::parse(value).ok_or("scroll_speed must be slow, normal, or fast")?; } diff --git a/crates/ui-egui/src/app/prefs.rs b/crates/ui-egui/src/app/prefs.rs index 08a20afb45..0df7d6f30b 100644 --- a/crates/ui-egui/src/app/prefs.rs +++ b/crates/ui-egui/src/app/prefs.rs @@ -147,6 +147,9 @@ impl PdfCraftApp { // Set outside json! above, which is at serde_json's macro recursion limit. v["hide_empty_highlight_hover"] = serde_json::Value::Bool(self.comment_prefs.hide_empty_highlight_hover); v["show_page_edge_shadows"] = self.show_page_edge_shadows.into(); + // Not `check_updates_at_start`: that key was an opt-in of its own, off unless chosen, and + // settings written then would turn this off unasked. + v["update_check_at_start"] = self.update_check_at_start.into(); v["hotbar"] = serde_json::to_value(&self.hotbar).unwrap_or_default(); v["comment_tool_pinned"] = serde_json::Value::Bool(self.comment_prefs.pinned); v["show_secured_notice"] = serde_json::Value::Bool(self.show_secured_notice); @@ -219,6 +222,9 @@ impl PdfCraftApp { if let Some(on) = v["reload_changed_files"].as_bool() { self.reload_changed_files = on; } + if let Some(on) = v["update_check_at_start"].as_bool() { + self.update_check_at_start = on; + } if let Some(on) = v["show_secured_notice"].as_bool() { self.show_secured_notice = on; } diff --git a/crates/ui-egui/src/dialogs/preferences.rs b/crates/ui-egui/src/dialogs/preferences.rs index 9e3182e416..ea985f3207 100644 --- a/crates/ui-egui/src/dialogs/preferences.rs +++ b/crates/ui-egui/src/dialogs/preferences.rs @@ -659,6 +659,8 @@ fn card_general(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, ctx: &mut ctx.checkbox(ui, &mut app.restore_last_view, tl!("Restore the last view (page, zoom and page display) when a document is opened again"), t); #[cfg(not(target_arch = "wasm32"))] ctx.checkbox(ui, &mut app.reload_changed_files, tl!("Reload documents when their files change on disk"), t); + #[cfg(not(target_arch = "wasm32"))] + ctx.checkbox(ui, &mut app.update_check_at_start, tl!("Check for updates when PdfCraft starts"), t); ctx.checkbox(ui, &mut app.show_secured_notice, tl!("Show a notice when a document is secured"), t); #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 643d5e90ff..cb6c99bbe5 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -370,6 +370,9 @@ pub struct PdfCraftApp { quit_session: Option, /// Preferences: reload a document when its file changes on disk (#431). pub reload_changed_files: bool, + /// Preferences: ask for a newer release when the desktop app starts (#5737). Off, the app + /// asks only from Help ▸ Check for updates. + pub update_check_at_start: bool, /// Preferences: tell the user, above the pages, that a document is secured (its security /// settings restrict changes). Off only hides that notice (#4035); kept in the settings. pub show_secured_notice: bool, diff --git a/crates/ui-egui/src/platform/updates.rs b/crates/ui-egui/src/platform/updates.rs index 6b7ea291fa..622006cd6e 100644 --- a/crates/ui-egui/src/platform/updates.rs +++ b/crates/ui-egui/src/platform/updates.rs @@ -21,7 +21,8 @@ pub(crate) struct Updates { updater: Updater, /// The Updates dialog is showing. pub(crate) open: bool, - /// Ask for a newer release at start; the desktop app turns it on, so tests never reach the network. + /// Ask for a newer release at start, unless Preferences says not to; the desktop app turns + /// it on, so tests never reach the network. #[cfg(not(target_arch = "wasm32"))] startup_check: bool, #[cfg(not(target_arch = "wasm32"))] @@ -73,10 +74,11 @@ impl PdfCraftApp { #[cfg(not(target_arch = "wasm32"))] { let Some(ctx) = self.ctx.clone() else { return }; + let at_start = self.update_check_at_start; let updates = &mut self.updates; if std::mem::take(&mut updates.check_requested) { updates.updater.check_now(&ctx); - } else if updates.startup_check { + } else if updates.startup_check && at_start { updates.updater.check_at_start(&ctx); } updates.updater.tick(&ctx); diff --git a/crates/ui-egui/tests/it/updates.rs b/crates/ui-egui/tests/it/updates.rs index 3ea55e434e..469a42f805 100644 --- a/crates/ui-egui/tests/it/updates.rs +++ b/crates/ui-egui/tests/it/updates.rs @@ -100,6 +100,82 @@ fn nothing_is_asked_until_the_user_checks() { h.get_by_label_contains("PdfCraft 99.0.0 is available"); } +/// The desktop app's start, with `settings` restored, and the count of what the source was asked. +fn started_with(settings: &'static str) -> (Harness<'static, PdfCraftApp>, Arc) { + let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let counted = calls.clone(); + let h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(move |_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.set_startup_update_check(true); + app.restore(settings); + let counted = counted.clone(); + app.set_update_source(Arc::new(move || { + counted.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + Ok(Some(release("v99.0.0"))) + })); + app + }); + (h, calls) +} + +/// Run frames for `time`, or until the source has been asked. The check at start shows nothing +/// while it runs, so `settle` can't wait for it. +fn run_until_asked(h: &mut Harness<'static, PdfCraftApp>, calls: &std::sync::atomic::AtomicUsize, time: std::time::Duration) { + let deadline = std::time::Instant::now() + time; + while calls.load(std::sync::atomic::Ordering::SeqCst) == 0 && std::time::Instant::now() < deadline { + h.run_steps(2); + std::thread::sleep(std::time::Duration::from_millis(5)); + } +} + +#[test] +fn the_preference_keeps_the_check_at_start_from_reaching_the_network() { + use std::sync::atomic::Ordering::SeqCst; + assert!(PdfCraftApp::new().update_check_at_start, "on by default"); + + // The same start with the preference left on does ask, so a count of none below means + // the preference held the check back and not that it hadn't run yet. + let (mut h, calls) = started_with("{}"); + run_until_asked(&mut h, &calls, std::time::Duration::from_secs(30)); + assert_eq!(calls.load(SeqCst), 1, "on, the app asks at start"); + + let (mut h, calls) = started_with(r#"{"update_check_at_start": false}"#); + run_until_asked(&mut h, &calls, std::time::Duration::from_millis(500)); + assert_eq!(calls.load(SeqCst), 0, "nothing is asked at start"); + + let mut restarted = PdfCraftApp::new(); + restarted.restore(&h.state().persist()); + assert!(!restarted.update_check_at_start, "the choice is kept"); + + // Help ▸ Check for updates still asks. + h.state_mut().execute("help.check_updates"); + settle(&mut h); + assert_eq!(calls.load(SeqCst), 1); + h.get_by_label_contains("PdfCraft 99.0.0 is available"); +} + +#[test] +fn preferences_turn_the_check_at_start_off() { + let (mut h, _calls) = started_with(r#"{"update_check_at_start": true}"#); + h.state_mut().execute("app.preferences"); + h.run_steps(3); + h.get_by_label("Check for updates when PdfCraft starts").click(); + h.run_steps(3); + assert!(!h.state().update_check_at_start); + // The control channel's option, as an agent sets it. + h.state_mut().set_option("update_check_at_start", "true").unwrap(); + assert!(h.state().update_check_at_start); + assert!(h.state_mut().set_option("update_check_at_start", "maybe").is_err()); +} + +#[test] +fn settings_from_the_old_opt_in_leave_the_check_at_start_on() { + let mut app = PdfCraftApp::new(); + app.restore(r#"{"check_updates_at_start": false}"#); + assert!(app.update_check_at_start, "a key of the removed option decides nothing"); +} + #[test] fn the_check_at_start_offers_the_prompt_and_later_dismisses_it() { let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_eframe(|_cc| { diff --git a/docs/roadmap.md b/docs/roadmap.md index f376f7cb00..1c99dddeb8 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -307,6 +307,7 @@ Estimates unchanged. | 2026-11-03 | patch | M5 #5850 review: indirect typewriter intent and composer edits preserve outer whitespace; movement regression commits by outside click; estimates unchanged. | | 2026-11-03 | 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. | | 2026-11-02 | patch | M10 #5743: macOS print jobs carry the job name and owner some vendors' filters need; not yet checked on a physical printer | +| 2026-11-02 | patch | M14 #5737, #5460: Preferences turns off the update check at start, on by default | | 2026-11-01 | patch | M7 #5657: header/footer, watermark and background operations commit all selected pages together; later-page errors preserve marks, content, shared/inherited resources and pending edits; estimates unchanged. | | 2026-10-31 | patch | #1577: reconcile optional page shadows with current viewer/organizer and settings modules; CI validation pending. | | 2026-10-31 | patch | M3 #1354: reconcile the launch override with split app/file modules and the structured open result; preserve headless help/version exits. CI validation pending. | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 8b7ac909a0..00b3dae2c2 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1778,8 +1778,8 @@ milestone = "M3" status = "partial" commands = ["help.check_updates"] tools = [] -evidence = ["crates/ui-egui/tests/it/updates.rs::a_newer_release_is_offered_for_download", "crates/ui-egui/tests/it/updates.rs::an_up_to_date_or_failed_check_says_so", "crates/ui-egui/tests/it/updates.rs::the_check_at_start_offers_the_prompt_and_later_dismisses_it", "crates/ui-egui/tests/it/updates.rs::nothing_is_asked_until_the_user_checks"] -notes = "asks GitHub Releases through craft-update at start (desktop app only) and on request, offers the release page, and updates and restarts in place where the install type allows it; an install waits while documents are unsaved" +evidence = ["crates/ui-egui/tests/it/updates.rs::a_newer_release_is_offered_for_download", "crates/ui-egui/tests/it/updates.rs::an_up_to_date_or_failed_check_says_so", "crates/ui-egui/tests/it/updates.rs::the_check_at_start_offers_the_prompt_and_later_dismisses_it", "crates/ui-egui/tests/it/updates.rs::nothing_is_asked_until_the_user_checks", "crates/ui-egui/tests/it/updates.rs::the_preference_keeps_the_check_at_start_from_reaching_the_network", "crates/ui-egui/tests/it/updates.rs::settings_from_the_old_opt_in_leave_the_check_at_start_on"] +notes = "asks GitHub Releases through craft-update at start (desktop app only) and on request, offers the release page, and updates and restarts in place where the install type allows it; an install waits while documents are unsaved; Preferences ▸ Check for updates when PdfCraft starts (update_check_at_start, on by default) turns the check at start off, leaving only the request from the Help menu" [[feature]] id = "view.single-key-accelerators"