mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 15:49:36 +00:00
fix(ui): Redo from the menu with typing in a page box stays in the box
Redo from the menu or toolbar closed the box first, which applied its typing: a new edit that dropped the document's waiting Redo, and the Redo itself then failed. The step was gone. Redo now goes to the box as the key does: what Undo took back there comes back, and typing the page doesn't have yet stays as it is, with the document's Redo waiting. A new text box the document refused was put back without the request to select the item once it exists, which stayed behind. It is put back with the box.
This commit is contained in:
1 parent
f1992f5aa1
commit
632ba10ed5
3 files changed
+148
-3
No files matched your search
@@ -222,11 +222,31 @@ impl PdfCraftApp {
|
||||
pub fn redo(&mut self) {
|
||||
if self.combine_showing() {
|
||||
self.combine_history_step(false);
|
||||
} else {
|
||||
} else if !self.redo_in_page_box() {
|
||||
self.history_step(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Redo from the menu or toolbar while a box on the page has typing of its own: the step is
|
||||
/// the box's, as the key's is. What Undo took back in the box comes back; typing the page
|
||||
/// doesn't have yet stays as it is, since applying it first is a new edit, which drops the
|
||||
/// document's Redo. True when the box took the step.
|
||||
fn redo_in_page_box(&mut self) -> bool {
|
||||
let Some(ctx) = self.ctx.clone() else { return false };
|
||||
let Some(view) = self.active.and_then(|i| self.views.get_mut(i)) else { return false };
|
||||
if let Some(ed) = view.line_editor.as_mut() {
|
||||
let Some(text) = redo_typing(&ctx, crate::edit_text_ui::input_id(), &ed.text) else { return ed.has_unsaved_text() };
|
||||
ed.text = text;
|
||||
return true;
|
||||
}
|
||||
if let Some(d) = view.content.draft.as_mut() {
|
||||
let Some(text) = redo_typing(&ctx, crate::content_ui::input_id(), &d.text) else { return d.has_unapplied_typing() };
|
||||
d.text = text;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn history_step(&mut self, undo: bool) {
|
||||
let Some((i, id)) = self.active_ids() else { return };
|
||||
if !self.close_text_editors(i, id) {
|
||||
@@ -274,12 +294,13 @@ impl PdfCraftApp {
|
||||
}
|
||||
let added = self.session.get(id).map(|d| d.added().to_vec()).unwrap_or_default();
|
||||
let Some(view) = self.views.get_mut(i) else { return true };
|
||||
let draft = view.content.draft.clone();
|
||||
// Finishing new text also asks for it to be selected once it exists: put that back too.
|
||||
let (draft, select_added) = (view.content.draft.clone(), view.content.select_added);
|
||||
if let Some(edit) = view.content.finish_draft(&added)
|
||||
&& !self.apply_edit(edit)
|
||||
{
|
||||
if let Some(view) = self.views.get_mut(i) {
|
||||
view.content.draft = draft;
|
||||
(view.content.draft, view.content.select_added) = (draft, select_added);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1217,6 +1238,18 @@ fn bookmark_at<'a>(items: &'a [pdfcraft_render::OutlineItem], path: &[usize]) ->
|
||||
if rest.is_empty() { Some(item) } else { bookmark_at(&item.children, rest) }
|
||||
}
|
||||
|
||||
/// The box's own Redo, as the key does it: the typing Undo took back, put back in the box's text,
|
||||
/// with the box's history and cursor moved along. None when the box has nothing to redo.
|
||||
fn redo_typing(ctx: &egui::Context, id: egui::Id, text: &str) -> Option<String> {
|
||||
let mut state = egui::TextEdit::load_state(ctx, id)?;
|
||||
let mut undoer = state.undoer();
|
||||
let (range, redone) = undoer.redo(&(state.cursor.char_range()?, text.to_owned()))?.clone();
|
||||
state.set_undoer(undoer);
|
||||
state.cursor.set_char_range(Some(range));
|
||||
egui::TextEdit::store_state(ctx, id, state);
|
||||
Some(redone)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{SaveTarget, is_mail_temp_copy, write_atomically, write_atomically_syncing, write_atomically_with};
|
||||
@@ -1493,6 +1526,45 @@ mod tests {
|
||||
out
|
||||
}
|
||||
|
||||
/// `one_page_pdf`, opening without a password, whose permissions refuse changes.
|
||||
fn no_changes_pdf() -> Vec<u8> {
|
||||
let mut doc = pdfcraft_cos::Document::open(std::sync::Arc::new(one_page_pdf())).unwrap();
|
||||
let encryption = pdfcraft_cos::NewEncryption {
|
||||
algorithm: pdfcraft_cos::Algorithm::Aes256,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1 ^ 8,
|
||||
encrypt_metadata: true,
|
||||
seed: [4; 32],
|
||||
};
|
||||
doc.set_encryption(&encryption).unwrap();
|
||||
pdfcraft_cos::write_full(&doc, &Default::default()).unwrap()
|
||||
}
|
||||
|
||||
/// Undo with a new text box open on a document that refuses changes: the box is put back as
|
||||
/// it was. Finishing it had asked for the new item to be selected, and that request stayed
|
||||
/// behind once the add was refused.
|
||||
#[test]
|
||||
fn a_refused_text_box_is_put_back_whole() {
|
||||
let mut app = crate::PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("a.pdf", None, no_changes_pdf()).unwrap();
|
||||
let style = crate::content_ui::default_style();
|
||||
app.views[0].content.draft = Some(crate::content_ui::TextDraft {
|
||||
page: 0,
|
||||
index: None,
|
||||
rect: [20.0, 20.0, 120.0, 40.0],
|
||||
text: "Typed".into(),
|
||||
style,
|
||||
focus: false,
|
||||
typing_from: None,
|
||||
});
|
||||
app.undo();
|
||||
let content = &app.views[0].content;
|
||||
assert_eq!(content.draft.as_ref().map(|d| d.text.as_str()), Some("Typed"), "the refused box stays open");
|
||||
assert_eq!(content.select_added, None, "nothing was added to select");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_asks_where_to_keep_an_email_attachment_and_leaves_the_temporary_copy_alone() {
|
||||
let dir = staging_dir("mail-temp");
|
||||
|
||||
@@ -892,6 +892,39 @@ fn redo_after_undoing_typing_in_added_text_puts_it_back() {
|
||||
assert_eq!(doc.can_redo(), Some("Edit content"), "the document's Redo still waits");
|
||||
}
|
||||
|
||||
/// Redo from the menu with retyping the page doesn't have yet in an added text box: the typing
|
||||
/// stays in the box and the document's Redo waits (closing the box first wrote the typing, which
|
||||
/// dropped that Redo). After Undo in the box, Redo from the menu puts the typing back there.
|
||||
#[test]
|
||||
fn redo_from_the_menu_with_retyping_in_the_box_keeps_the_documents_redo() {
|
||||
let mut h = harness();
|
||||
let rect = [40.0, 195.0, 240.0, 210.0];
|
||||
let text = pdfcraft_engine::AddedText { rect, text: "Reviewed".into(), ..Default::default() };
|
||||
assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::AddText { page: 0, text: text.clone() }));
|
||||
let bold = pdfcraft_engine::AddedText { bold: true, ..text };
|
||||
assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::UpdateContent { page: 0, index: 0, content: pdfcraft_engine::AddedContent::Text(bold) }));
|
||||
assert!(h.state_mut().execute("edit.undo"));
|
||||
assert!(h.state_mut().execute("edit.text"));
|
||||
h.run_steps(3);
|
||||
let point = at(&h, 45.0, 195.0);
|
||||
click(&mut h, point);
|
||||
assert_eq!(h.state().views[0].content.draft.as_ref().and_then(|d| d.index), Some(0), "the editor opened on the text");
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::End);
|
||||
h.event(egui::Event::Text(" twice".into()));
|
||||
h.run_steps(2);
|
||||
assert!(h.state_mut().execute("edit.redo"));
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().views[0].content.draft.as_ref().map(|d| d.text.as_str()), Some("Reviewed twice"), "the typing stays in the box");
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::Z);
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().views[0].content.draft.as_ref().map(|d| d.text.as_str()), Some("Reviewed"), "Undo takes it back in the box");
|
||||
assert!(h.state_mut().execute("edit.redo"));
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().views[0].content.draft.as_ref().map(|d| d.text.as_str()), Some("Reviewed twice"), "Redo from the menu puts it back");
|
||||
let s = h.state();
|
||||
assert_eq!(s.session.get(s.views[0].id).unwrap().can_redo(), Some("Edit content"), "the document's Redo still waits");
|
||||
}
|
||||
|
||||
/// Redo with an added text box open and unchanged reaches the document's waiting Redo, even when
|
||||
/// the box's text ends in a space: closing the unchanged box writes nothing, so it can't clear
|
||||
/// that Redo first.
|
||||
|
||||
@@ -6068,6 +6068,46 @@ fn redo_after_undoing_typing_puts_the_typing_back() {
|
||||
assert_eq!((doc.can_redo(), doc.text_blocks(0)[0].base_font.as_str()), (Some("Edit text"), "Helvetica"), "the document's Redo still waits");
|
||||
}
|
||||
|
||||
/// Redo from the menu or toolbar with typing in the box the page doesn't have yet. Closing the
|
||||
/// box first applied the typing, a new edit that dropped the document's waiting Redo, and the
|
||||
/// Redo itself then failed: the step was gone. The typing now stays in the box and the Redo
|
||||
/// waits; once Undo in the box takes the typing back, Redo from the menu puts it back there.
|
||||
#[test]
|
||||
fn redo_from_the_menu_with_typing_in_the_box_keeps_the_documents_redo() {
|
||||
let mut h = harness(1, |_| {});
|
||||
assert!(h.state_mut().execute("edit.edit_text"));
|
||||
h.run_steps(2);
|
||||
open_paragraph(&mut h);
|
||||
h.get_by_label("Bold").click();
|
||||
h.run_steps(4);
|
||||
// Undo the bold (closing the editor), so the document has it to redo.
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::Z);
|
||||
h.run_steps(4);
|
||||
open_paragraph(&mut h);
|
||||
let original = h.state().views[0].line_editor.as_ref().unwrap().text.clone();
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::End);
|
||||
h.event(egui::Event::Text(" more".into()));
|
||||
h.run_steps(2);
|
||||
let typed = h.state().views[0].line_editor.as_ref().unwrap().text.clone();
|
||||
assert!(h.state_mut().execute("edit.redo"));
|
||||
h.run_steps(3);
|
||||
{
|
||||
let s = h.state();
|
||||
let ed = s.views[0].line_editor.as_ref().expect("the box stays open");
|
||||
assert_eq!(ed.text, typed, "the typing stays in the box, unapplied");
|
||||
let doc = s.session.get(s.views[0].id).unwrap();
|
||||
assert_eq!((doc.can_redo(), doc.text_blocks(0)[0].base_font.as_str()), (Some("Edit text"), "Helvetica"), "the document's Redo still waits");
|
||||
}
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::Z);
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().views[0].line_editor.as_ref().unwrap().text, original, "Undo takes the typing back in the box");
|
||||
assert!(h.state_mut().execute("edit.redo"));
|
||||
h.run_steps(3);
|
||||
let s = h.state();
|
||||
assert_eq!(s.views[0].line_editor.as_ref().expect("still open").text, typed, "Redo from the menu puts the typing back");
|
||||
assert_eq!(s.session.get(s.views[0].id).unwrap().can_redo(), Some("Edit text"), "the document's Redo still waits");
|
||||
}
|
||||
|
||||
/// Two copies of a document, the same paragraph open in each. The paragraph boxes share one text
|
||||
/// box, and so one Undo history: a box opened on the same text as another tab's didn't clear it.
|
||||
/// Typing in one tab, taken back with Undo, then came back with Redo in the other tab.
|
||||
|
||||
Reference in new issue
Block a user