diff --git a/apps/ely-pdf/src/annotate.rs b/apps/ely-pdf/src/annotate.rs index 99cbb90..c33dd93 100644 --- a/apps/ely-pdf/src/annotate.rs +++ b/apps/ely-pdf/src/annotate.rs @@ -254,11 +254,11 @@ impl PdfView { return; } let author = ely_ui::settings::current(cx).author.clone(); - self.settled_edit(what, cx, move |worker, doc| { + self.edit_at(what, move |worker, doc| { let name = ely_platform::user::named(&author)?; annots.iter_mut().for_each(|annot| annot.author = name.clone()); worker.annotate(doc, &annots) - }); + }, cx); } /// A press with a drawing or form tool. diff --git a/apps/ely-pdf/src/edits.rs b/apps/ely-pdf/src/edits.rs index 4e872df..93cdc58 100644 --- a/apps/ely-pdf/src/edits.rs +++ b/apps/ely-pdf/src/edits.rs @@ -19,6 +19,8 @@ pub struct Queued { doc: Doc, /// The edit may add, drop, move or reshape pages. pages: bool, + /// Aimed at a point: runs on the state it meets. + follow: bool, } /// The document as an edit left it. @@ -41,6 +43,23 @@ impl PdfView { self.queue_edit(what, None, false, op, cx); } + /// Queues a point-aimed edit; only moving pages refuse it. + pub fn edit_at( + &mut self, + what: &'static str, + op: impl FnOnce(&mut Worker, Doc) -> io::Result + Send + 'static, + cx: &mut Context, + ) { + if self.moving || self.queue.iter().any(|queued| queued.pages) { + let text = crate::viewer::Viewer::text(cx, "pdf.pages.changed", &[]); + cx.emit(crate::view::Failed(text.to_string())); + return cx.notify(); + } + log::info!("pdf: queues {what}"); + self.queue.push_back(Queued { what, op: Box::new(op), pick: None, doc: self.doc, pages: false, follow: true }); + self.next_edit(cx); + } + /// Queues an edit to the pages themselves. pub fn edit_pages( &mut self, @@ -62,7 +81,7 @@ impl PdfView { ) { log::info!("pdf: queues {what}"); // Aimed at what is shown now; later states refuse it. - self.queue.push_back(Queued { what, op: Box::new(op), pick, doc: self.doc, pages }); + self.queue.push_back(Queued { what, op: Box::new(op), pick, doc: self.doc, pages, follow: false }); self.next_edit(cx); } @@ -70,10 +89,12 @@ impl PdfView { if self.editing { return; } - let Some(Queued { what, op, pick, doc, pages }) = self.queue.pop_front() else { + let Some(Queued { what, op, pick, doc, pages, follow }) = self.queue.pop_front() else { return; }; - self.editing = true; + // Earlier edits have landed; a point still means the same. + let doc = if follow { self.doc } else { doc }; + (self.editing, self.moving) = (true, pages); self.off( move |worker| { let mark = op(worker, doc)?; @@ -83,7 +104,7 @@ impl PdfView { Ok(Fresh { doc, mark, pages, first, marks: worker.outline(doc)? }) }, move |view, fresh, cx| { - view.editing = false; + (view.editing, view.moving) = (false, false); match fresh { Ok(fresh) => { view.refresh(fresh, pages, cx); diff --git a/apps/ely-pdf/src/tests/notes.rs b/apps/ely-pdf/src/tests/notes.rs index bd369e8..57505b9 100644 --- a/apps/ely-pdf/src/tests/notes.rs +++ b/apps/ely-pdf/src/tests/notes.rs @@ -315,3 +315,48 @@ fn a_default_name_avoids_a_field_with_no_widget() { send(cx, &viewer, Change::Design(typed, Vec::new())); assert_eq!(notes(cx, &view).fields.len(), 1, "the field lands"); } + +#[test] +fn quick_strokes_queue_behind_each_other() { + let dir = tempfile::tempdir().expect("a temp dir"); + let mut cx = TestAppContext::single(); + let path = dir.path().join("one.pdf"); + let page: (f32, f32, &[&str]) = (612.0, 792.0, &["Hello"]); + std::fs::write(&path, pdf_with(&[page], &[], &Extras::default())).expect("writes"); + let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true); + let view = shown(cx, &viewer); + let note = |y: f32| ely_pdf_engine::NewAnnot { page: 0, shape: ely_pdf_engine::Shape::Note((100.0, y)), rgb: None, contents: "n".into(), author: String::new() }; + // The second lands while the first still runs. + cx.update(|_, app| view.update(app, |view, cx| { + view.annotate("a first note", vec![note(500.0)], cx); + view.annotate("a second note", vec![note(400.0)], cx); + })); + settle(cx, |app| view.read(app).settled()); + cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.side.update(cx, |side, cx| { + side.pane = crate::side::Pane::Notes; + cx.notify(); + }))); + let count = |cx: &mut gpui::VisualTestContext| { + settle(cx, |app| view.read(app).settled() && view.read(app).notes.contains_key(&0)); + cx.update(|_, app| view.read(app).notes[&0].annots.len()) + }; + assert_eq!(count(cx), 2, "both notes landed"); + let rotate = |view: &mut PdfView, cx: &mut gpui::Context| { + view.edit_pages("rotating", |worker, doc| worker.rotate_pages(doc, &[0], 90), cx) + }; + // A running rotation would move the point: refused. + cx.update(|_, app| view.update(app, |view, cx| { + rotate(view, cx); + view.annotate("a note while turning", vec![note(300.0)], cx); + })); + assert_eq!(count(cx), 2, "refused while pages move"); + // A rotation waiting behind another edit: refused too. + cx.update(|_, app| view.update(app, |view, cx| { + view.annotate("a third note", vec![note(250.0)], cx); + rotate(view, cx); + view.annotate("a note behind a turn", vec![note(200.0)], cx); + })); + assert_eq!(count(cx), 3, "only the note before the turn"); + cx.update(|_, app| view.update(app, |view, cx| view.annotate("a settled note", vec![note(150.0)], cx))); + assert_eq!(count(cx), 4, "taken once pages are still"); +} diff --git a/apps/ely-pdf/src/view.rs b/apps/ely-pdf/src/view.rs index 0938944..7a7cf97 100644 --- a/apps/ely-pdf/src/view.rs +++ b/apps/ely-pdf/src/view.rs @@ -65,6 +65,8 @@ pub struct PdfView { /// Edits waiting their turn, and whether one runs. pub queue: VecDeque, pub editing: bool, + /// The running edit may move pages. + pub moving: bool, /// History marks: now, and at the last load or save. pub mark: String, pub saved: String, @@ -186,6 +188,7 @@ impl PdfView { edited: 0, queue: VecDeque::new(), editing: false, + moving: false, saved: loaded.mark.clone(), mark: loaded.mark, thumbs: HashMap::new(), diff --git a/apps/ely-pdf/src/visible.rs b/apps/ely-pdf/src/visible.rs index e5c5c1f..404cf2d 100644 --- a/apps/ely-pdf/src/visible.rs +++ b/apps/ely-pdf/src/visible.rs @@ -92,15 +92,16 @@ impl PdfView { rgb: 0, bx: [at[0], at[1], self.sizes[page].0 - at[0]], }; - self.settled_edit("placing a typed signature", cx, move |worker, doc| match worker.put_text(doc, page, &writing, 0.0)? { + let write = move |worker: &mut ely_pdf_engine::Worker, doc| match worker.put_text(doc, page, &writing, 0.0)? { Wrote::Done(mark) => Ok(mark), Wrote::Missing(c) => Err(io::Error::new(io::ErrorKind::InvalidInput, format!("the font has no “{c}”"))), Wrote::Preview(..) => Err(io::Error::other("a preview where a write was asked")), - }); + }; + self.edit_at("placing a typed signature", write, cx); } Stamp::Picture { path, aspect } => { let at = placed(aspect, rect, click); - self.settled_edit("placing a signature picture", cx, move |worker, doc| worker.add_image(doc, page, &path, at)); + self.edit_at("placing a signature picture", move |worker, doc| worker.add_image(doc, page, &path, at), cx); } } } diff --git a/crates/ely-ui/src/i18n/pdf.rs b/crates/ely-ui/src/i18n/pdf.rs index 72f3951..557b175 100644 --- a/crates/ely-ui/src/i18n/pdf.rs +++ b/crates/ely-ui/src/i18n/pdf.rs @@ -73,6 +73,7 @@ pub(super) const EN: &[(&str, &str)] = &[ "pdf.object.changed", "The document changed meanwhile; nothing was done.", ), + ("pdf.pages.changed", "Pages are moving; nothing was done. Try again."), ("pdf.tool.edit", "Edit Text and Images"), ("pdf.tool.text", "Add Text"), ("pdf.tool.image", "Add Image…"), @@ -290,6 +291,7 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[ ("pdf.object.replace", "替换图片…"), ("pdf.object.delete", "删除"), ("pdf.object.changed", "文档已改动,未执行。"), + ("pdf.pages.changed", "页面正在变动,未执行,请再试。"), ("pdf.tool.edit", "编辑文字和图片"), ("pdf.tool.text", "添加文字"), ("pdf.tool.image", "添加图片…"), diff --git a/tasks/BUGS.md b/tasks/BUGS.md index 694a169..2e53471 100644 --- a/tasks/BUGS.md +++ b/tasks/BUGS.md @@ -37,7 +37,7 @@ - [x] P-02 每次编辑落地清掉全部图块与缩略图,页面空白、刚画的墨迹消失 — 图块随 R2.6;缩略图在编辑后留到重画;R1、R2 测试未证实际绘制;R3 PASS - [x] P-03 “编辑文字和图片”先命中图片对象再命中文字;一点抖动就移动了背景图 — 包住文字块的对象让位于文字,移动须过 3 个窗口像素;R1 漏掉同点的小图;R2 PASS - [x] P-04 `TEXT_BLOCKS` 对每个文字块重跑一遍整页内容过滤,编辑工具长时间无反应 — 整页一遍干跑判定全部文字块(1000 块 1.5 s→0.1 s);R1 转向文字后错位、共用原点只计一块;R2 PASS -- [ ] P-05 连续快画或快点被拒:“文档在此间已改动,未执行” +- [x] P-05 连续快画或快点被拒:“文档在此间已改动,未执行” — 按坐标的编辑排队、跑在出队时的状态上,只在页面变动时拒绝;R2 PASS - [ ] P-06 默认字体“原字体”在 Type 1/Type 3 字体的文件上失败,只给引擎原话 - [ ] P-07 含连字的文字块永远不可编辑 - [ ] P-08 页面读取失败后每帧重试、每帧弹提示