diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index 2ac0f82022..25cb73beba 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -84,7 +84,9 @@ impl PdfCraftApp { if self.combine_showing() && matches!(spec.needs, commands::Needs::Undo | commands::Needs::Redo) { return self.combine_can_undo(spec.needs == commands::Needs::Undo); } - commands::is_enabled(spec, &self.session, self.active_ids().map(|(_, id)| id)) + // Redo also takes back what Undo took in a box open on the page, with no document step. + (commands::is_enabled(spec, &self.session, self.active_ids().map(|(_, id)| id)) + || spec.needs == commands::Needs::Redo && self.open_box_can_redo()) && (spec.id != "edit.copy" || self .active diff --git a/crates/ui-egui/src/editing.rs b/crates/ui-egui/src/editing.rs index 6040b5c76f..cba20e76bc 100644 --- a/crates/ui-egui/src/editing.rs +++ b/crates/ui-egui/src/editing.rs @@ -333,8 +333,19 @@ impl PdfCraftApp { } else { return false; }; - egui::TextEdit::load_state(ctx, id) - .is_some_and(|state| state.cursor.char_range().is_some_and(|range| state.undoer().has_redo(&(range, text.clone())))) + has_typing_to_redo(ctx, id, text) + } + + /// Whether the box open on the page has a Redo of its own, the step + /// [`Self::redo_in_page_box`] takes for Redo from the menu or toolbar: focus has moved to the + /// menu by then, so unlike [`Self::page_box_can_redo`] this doesn't ask for it. + pub(crate) fn open_box_can_redo(&self) -> bool { + let Some(ctx) = &self.ctx else { return false }; + let Some(view) = self.active.and_then(|i| self.views.get(i)) else { return false }; + if let Some(ed) = &view.line_editor { + return has_typing_to_redo(ctx, crate::edit_text_ui::input_id(), &ed.text); + } + view.content.draft.as_ref().is_some_and(|d| has_typing_to_redo(ctx, crate::content_ui::input_id(), &d.text)) } /// Typing in a box on the page (a paragraph being edited, added text) that the page doesn't @@ -1343,6 +1354,12 @@ fn bookmark_at<'a>(items: &'a [pdfcraft_render::OutlineItem], path: &[usize]) -> if rest.is_empty() { Some(item) } else { bookmark_at(&item.children, rest) } } +/// Whether the page text box `id`, showing `text`, has typing its own Redo would put back: the +/// test egui makes when it handles the key. +fn has_typing_to_redo(ctx: &egui::Context, id: egui::Id, text: &str) -> bool { + egui::TextEdit::load_state(ctx, id) + .is_some_and(|state| state.cursor.char_range().is_some_and(|range| state.undoer().has_redo(&(range, text.to_owned())))) +} #[cfg(test)] mod tests { use super::{SaveTarget, is_mail_temp_copy, save_dialog_folder, write_atomically, write_atomically_syncing, write_atomically_with}; diff --git a/crates/ui-egui/tests/it/content.rs b/crates/ui-egui/tests/it/content.rs index f8e93ecd28..592981acb7 100644 --- a/crates/ui-egui/tests/it/content.rs +++ b/crates/ui-egui/tests/it/content.rs @@ -1038,6 +1038,31 @@ fn redo_from_the_menu_with_retyping_in_the_box_keeps_the_documents_redo() { assert_eq!(s.session.get(s.views[0].id).unwrap().can_redo(), Some("Edit content"), "the document's Redo still waits"); } +/// Redo from the menu after Undo in an added text box, with nothing for the document to redo: the +/// menu item follows the box's own Redo too, and puts the typing back. +#[test] +fn redo_from_the_menu_is_enabled_for_the_added_text_boxs_own_typing() { + 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 })); + 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); + 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_eq!(h.state().session.get(h.state().views[0].id).unwrap().can_redo(), None, "the document has nothing to redo"); + assert!(h.state_mut().execute("edit.redo"), "Redo is enabled for the box's own step"); + h.run_steps(3); + assert_eq!(h.state().views[0].content.draft.as_ref().map(|d| d.text.as_str()), Some("Reviewed twice"), "Redo puts it back"); +} + /// 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. diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index 671d3d12b5..cae884622f 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -6478,6 +6478,29 @@ fn redo_from_the_menu_with_typing_in_the_box_keeps_the_documents_redo() { assert_eq!(s.session.get(s.views[0].id).unwrap().can_redo(), Some("Edit text"), "the document's Redo still waits"); } +/// Redo from the menu after Undo in the box took the typing back, with nothing for the document +/// to redo: the menu item follows the box's own Redo too, and puts the typing back. It was +/// disabled, as it followed only the document's Redo, and only the key could do it. +#[test] +fn redo_from_the_menu_is_enabled_for_the_boxs_own_typing() { + let mut h = harness(1, |_| {}); + assert!(h.state_mut().execute("edit.edit_text")); + h.run_steps(2); + 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(); + 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, "the typing is taken back"); + assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().can_redo(), None, "the document has nothing to redo"); + assert!(h.state_mut().execute("edit.redo"), "Redo is enabled for the box's own step"); + h.run_steps(3); + assert_eq!(h.state().views[0].line_editor.as_ref().expect("still open").text, typed, "Redo puts the typing back"); +} + /// 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 (the key or the menu) in the