mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
Updates: Restart to Update with unsaved changes opens the save prompt (#6726)
This commit is contained in:
1 parent
9ccd6964ee
commit
709f5fb80f
1 file changed
+185
-12
@@ -30,6 +30,9 @@ pub(crate) struct Updates {
|
||||
/// How many times `on_exit` called [`Updates::exiting`].
|
||||
#[cfg(test)]
|
||||
exits: u32,
|
||||
/// Stands in for the close that a finished installation queues in the `Updater`.
|
||||
#[cfg(test)]
|
||||
close_queued: bool,
|
||||
}
|
||||
|
||||
impl Default for Updates {
|
||||
@@ -44,6 +47,8 @@ impl Default for Updates {
|
||||
check_requested: false,
|
||||
#[cfg(test)]
|
||||
exits: 0,
|
||||
#[cfg(test)]
|
||||
close_queued: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +63,34 @@ impl Updates {
|
||||
self.exits += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the startup prompt and returns what it asks for. `can_start` is false while unsaved
|
||||
/// work holds back a ready update.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn show_prompt(&mut self, ctx: &egui::Context, can_start: bool) -> Action {
|
||||
#[cfg(test)]
|
||||
if can_start && std::mem::take(&mut self.close_queued) {
|
||||
return Action::Close;
|
||||
}
|
||||
self.updater.show_if(ctx, can_start)
|
||||
}
|
||||
|
||||
/// Asks the next frame that may close to close the app, so that a ready update installs.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn queue_close(&mut self) {
|
||||
self.updater.restart_to_update();
|
||||
#[cfg(test)]
|
||||
{
|
||||
self.close_queued = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the close that a finished installation queued, so a save after Cancel does not close
|
||||
/// the app. `show_prompt` draws an open startup prompt, so this is for when none is open.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn take_queued_close(&mut self, ctx: &egui::Context) {
|
||||
let _ = self.show_prompt(ctx, true);
|
||||
}
|
||||
}
|
||||
|
||||
impl PdfCraftApp {
|
||||
@@ -110,7 +143,7 @@ pub(crate) fn dialog(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
{
|
||||
// The restart helper is scheduled before the app closes, so no update starts over unsaved work.
|
||||
let clean = app.first_dirty().is_none();
|
||||
match app.updates.updater.show_if(ctx, clean) {
|
||||
match app.updates.show_prompt(ctx, clean) {
|
||||
Action::None => {}
|
||||
Action::OpenUrl(url) => app.open_url(&url),
|
||||
Action::Close => {
|
||||
@@ -123,27 +156,167 @@ pub(crate) fn dialog(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
if app.updates.open {
|
||||
let dialog = CheckResultDialog::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION"));
|
||||
match dialog.show(ctx, app.updates.updater.status()) {
|
||||
CheckResultResponse::None => {}
|
||||
CheckResultResponse::Close => {
|
||||
app.updates.open = false;
|
||||
app.updates.updater.dismiss_status();
|
||||
}
|
||||
CheckResultResponse::CheckAgain => app.updates.check_requested = true,
|
||||
CheckResultResponse::RestartToUpdate => {
|
||||
app.updates.updater.restart_to_update();
|
||||
}
|
||||
}
|
||||
let response = dialog.show(ctx, app.updates.updater.status());
|
||||
answer_result(app, ctx, response);
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = (app, ctx);
|
||||
}
|
||||
|
||||
/// Carries out the answer to the Updates dialog.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn answer_result(app: &mut PdfCraftApp, ctx: &egui::Context, response: CheckResultResponse) {
|
||||
match response {
|
||||
CheckResultResponse::None => {}
|
||||
CheckResultResponse::Close => {
|
||||
app.updates.open = false;
|
||||
app.updates.updater.dismiss_status();
|
||||
}
|
||||
CheckResultResponse::CheckAgain => app.updates.check_requested = true,
|
||||
CheckResultResponse::RestartToUpdate => restart_to_update(app, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
/// Restart to Update. With unsaved changes the Updates dialog closes and the quit path asks about
|
||||
/// them; Cancel there leaves the update ready. An open startup prompt keeps its own Restart to
|
||||
/// Update and save-first note, so the app stays open.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn restart_to_update(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
if app.first_dirty().is_none() {
|
||||
app.updates.queue_close();
|
||||
return;
|
||||
}
|
||||
app.updates.open = false;
|
||||
app.updates.updater.dismiss_status();
|
||||
if app.updates.updater.prompt_open() {
|
||||
return;
|
||||
}
|
||||
app.updates.take_queued_close(ctx);
|
||||
app.request_quit(ctx);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::CloseRequest;
|
||||
use crate::editing::SaveTarget;
|
||||
use eframe::App as _;
|
||||
use pdfcraft_engine::{Edit, FieldValue};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// An app with one open document holding a form, and with `dirty` an unsaved change to it.
|
||||
fn app(dirty: bool) -> PdfCraftApp {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.open_bytes("source.pdf", None, include_bytes!("../../tests/data/form.pdf").to_vec()).unwrap();
|
||||
if dirty {
|
||||
assert!(app.apply_edit(Edit::SetFieldValue { name: "name".into(), value: FieldValue::Text("Changed".into()) }));
|
||||
assert!(app.first_dirty().is_some());
|
||||
}
|
||||
app.updates.open = true;
|
||||
app
|
||||
}
|
||||
|
||||
/// An app with an unsaved change to a document in a new temporary folder, which Save writes
|
||||
/// without asking where. Returns the document's path too.
|
||||
fn app_with_file(test: &str) -> (PdfCraftApp, PathBuf) {
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-ui-updates-{test}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("source.pdf");
|
||||
let bytes = include_bytes!("../../tests/data/form.pdf").to_vec();
|
||||
std::fs::write(&path, &bytes).unwrap();
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.open_bytes("source.pdf", Some(path.to_string_lossy().into_owned()), bytes).unwrap();
|
||||
assert!(app.apply_edit(Edit::SetFieldValue { name: "name".into(), value: FieldValue::Text("Changed".into()) }));
|
||||
app.updates.open = true;
|
||||
(app, path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restart_to_update_with_unsaved_changes_opens_the_save_prompt() {
|
||||
let mut app = app(true);
|
||||
let ctx = egui::Context::default();
|
||||
answer_result(&mut app, &ctx, CheckResultResponse::RestartToUpdate);
|
||||
assert!(!app.updates.open, "the modal Updates dialog closes");
|
||||
assert_eq!(app.close_request, Some(CloseRequest::Quit), "the save prompt opens");
|
||||
assert!(!app.allow_quit);
|
||||
app.resolve_close(&ctx, None);
|
||||
assert!(app.close_request.is_none(), "Cancel closes the prompt");
|
||||
assert!(!app.allow_quit, "Cancel keeps the app open");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restart_to_update_with_unsaved_changes_dismisses_the_check_result() {
|
||||
let mut app = app(true);
|
||||
app.set_update_source(Arc::new(|| Ok(None)));
|
||||
let ctx = egui::Context::default();
|
||||
app.updates.updater.check_now(&ctx);
|
||||
assert!(!matches!(app.updates.updater.status(), Status::Idle));
|
||||
answer_result(&mut app, &ctx, CheckResultResponse::RestartToUpdate);
|
||||
assert!(matches!(app.updates.updater.status(), Status::Idle));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saving_at_the_restart_prompt_writes_the_file_and_quits() {
|
||||
let (mut app, path) = app_with_file("save");
|
||||
let ctx = egui::Context::default();
|
||||
answer_result(&mut app, &ctx, CheckResultResponse::RestartToUpdate);
|
||||
app.resolve_close(&ctx, Some(true));
|
||||
assert!(app.allow_quit, "the app quits once the document is saved");
|
||||
|
||||
let mut reopened = PdfCraftApp::new();
|
||||
reopened.open_bytes("source.pdf", Some(path.to_string_lossy().into_owned()), std::fs::read(&path).unwrap()).unwrap();
|
||||
let doc = reopened.session.get(reopened.views[0].id).unwrap();
|
||||
let name = doc.form.iter().find(|f| f.name == "name").and_then(|f| f.value.first().cloned());
|
||||
assert_eq!(name.as_deref(), Some("Changed"), "the change is in the file");
|
||||
let _ = std::fs::remove_dir_all(path.parent().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saving_after_canceling_the_restart_keeps_the_app_open() {
|
||||
let (mut app, path) = app_with_file("cancel");
|
||||
app.updates.close_queued = true;
|
||||
let ctx = egui::Context::default();
|
||||
answer_result(&mut app, &ctx, CheckResultResponse::RestartToUpdate);
|
||||
app.resolve_close(&ctx, None);
|
||||
assert!(app.save_active(SaveTarget::InPlace));
|
||||
assert!(app.first_dirty().is_none());
|
||||
dialog(&mut app, &ctx);
|
||||
assert!(!app.allow_quit, "the save does not close the app");
|
||||
assert!(app.close_request.is_none());
|
||||
let _ = std::fs::remove_dir_all(path.parent().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restart_to_update_with_nothing_unsaved_leaves_the_close_to_the_updater() {
|
||||
let mut app = app(false);
|
||||
let ctx = egui::Context::default();
|
||||
answer_result(&mut app, &ctx, CheckResultResponse::RestartToUpdate);
|
||||
assert!(app.updates.open, "the dialog stays until the updater closes the app");
|
||||
assert!(app.close_request.is_none());
|
||||
assert!(!app.allow_quit);
|
||||
dialog(&mut app, &ctx);
|
||||
assert!(app.allow_quit, "the next frame closes the app");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restart_to_update_with_the_startup_prompt_open_leaves_the_app_open() {
|
||||
let mut app = app(true);
|
||||
app.updates.updater.offer(craft_update::Release {
|
||||
tag: "v99.0.0".into(),
|
||||
version: craft_update::parse_version("99.0.0"),
|
||||
name: "PdfCraft 99.0.0".into(),
|
||||
html_url: "https://github.com/storytold/pdfcraft/releases/tag/v99.0.0".into(),
|
||||
..Default::default()
|
||||
});
|
||||
assert!(app.updates.updater.prompt_open());
|
||||
answer_result(&mut app, &egui::Context::default(), CheckResultResponse::RestartToUpdate);
|
||||
assert!(!app.updates.open, "the result dialog closes");
|
||||
assert!(app.updates.updater.prompt_open(), "the prompt stays");
|
||||
assert!(app.close_request.is_none(), "the prompt carries the restart");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_exit_calls_the_update_exit_hook_once() {
|
||||
|
||||
Reference in new issue
Block a user