Skip to main content

common/
undo_redo.rs

1// Generated by Qleany v1.7.3 from undo_redo.tera
2use crate::event::{Event, EventHub, Origin, UndoRedoEvent};
3use crate::types::EntityId;
4use anyhow::{Result, anyhow};
5use std::any::Any;
6use std::collections::HashMap;
7use std::fmt;
8use std::sync::Arc;
9
10/// Trait for commands that can be undone and redone.
11///
12/// Implementors can optionally support command merging by overriding the
13/// `can_merge` and `merge` methods. This allows the UndoRedoManager to combine
14/// multiple commands of the same type into a single command, which is useful for
15/// operations like continuous typing or dragging.
16pub trait UndoRedoCommand: Send {
17    /// Undoes the command, reverting its effects
18    fn undo(&mut self) -> Result<()>;
19
20    /// Redoes the command, reapplying its effects
21    fn redo(&mut self) -> Result<()>;
22
23    /// Returns true if this command can be merged with the other command.
24    ///
25    /// By default, commands cannot be merged. Override this method to enable
26    /// merging for specific command types.
27    ///
28    /// # Example
29    /// ```test
30    /// fn can_merge(&self, other: &dyn UndoRedoCommand) -> bool {
31    ///     // Check if the other command is of the same type
32    ///     if let Some(_) = other.as_any().downcast_ref::<Self>() {
33    ///         return true;
34    ///     }
35    ///     false
36    /// }
37    /// ```
38    fn can_merge(&self, _other: &dyn UndoRedoCommand) -> bool {
39        false
40    }
41
42    /// Merges this command with the other command.
43    /// Returns true if the merge was successful.
44    ///
45    /// This method is called only if `can_merge` returns true.
46    ///
47    /// # Example
48    /// ```test
49    /// use common::undo_redo::UndoRedoCommand;
50    ///
51    /// fn merge(&mut self, other: &dyn UndoRedoCommand) -> bool {
52    ///     if let Some(other_cmd) = other.as_any().downcast_ref::<Self>() {
53    ///         // Merge the commands
54    ///         self.value += other_cmd.value;
55    ///         return true;
56    ///     }
57    ///     false
58    /// }
59    /// ```
60    fn merge(&mut self, _other: &dyn UndoRedoCommand) -> bool {
61        false
62    }
63
64    /// Returns the type ID of this command for type checking.
65    ///
66    /// This is used for downcasting in the `can_merge` and `merge` methods.
67    ///
68    /// # Example
69    /// ```test
70    /// fn as_any(&self) -> &dyn Any {
71    ///     self
72    /// }
73    /// ```
74    fn as_any(&self) -> &dyn Any;
75}
76
77/// A composite command that groups multiple commands as one.
78///
79/// This allows treating a sequence of commands as a single unit for undo/redo operations.
80/// When a composite command is undone or redone, all its contained commands are undone
81/// or redone in the appropriate order.
82///
83/// # Example
84/// ```test
85/// use common::undo_redo::CompositeCommand;
86/// let mut composite = CompositeCommand::new();
87/// composite.add_command(Box::new(Command1::new()));
88/// composite.add_command(Box::new(Command2::new()));
89/// // Now composite can be treated as a single command
90/// ```
91pub struct CompositeCommand {
92    commands: Vec<Box<dyn UndoRedoCommand>>,
93    pub stack_id: u64,
94}
95
96impl fmt::Debug for CompositeCommand {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        f.debug_struct("CompositeCommand")
99            .field("commands_len", &self.commands.len())
100            .field("stack_id", &self.stack_id)
101            .finish()
102    }
103}
104
105impl CompositeCommand {
106    /// Creates a new empty composite command.
107    pub fn new(stack_id: Option<u64>) -> Self {
108        CompositeCommand {
109            commands: Vec::new(),
110            stack_id: stack_id.unwrap_or(0),
111        }
112    }
113
114    /// Adds a command to this composite.
115    ///
116    /// Commands are executed, undone, and redone in the order they are added.
117    pub fn add_command(&mut self, command: Box<dyn UndoRedoCommand>) {
118        self.commands.push(command);
119    }
120
121    /// Returns true if this composite contains no commands.
122    pub fn is_empty(&self) -> bool {
123        self.commands.is_empty()
124    }
125}
126
127impl UndoRedoCommand for CompositeCommand {
128    fn undo(&mut self) -> Result<()> {
129        // Undo commands in reverse order
130        for command in self.commands.iter_mut().rev() {
131            command.undo()?;
132        }
133        Ok(())
134    }
135
136    fn redo(&mut self) -> Result<()> {
137        // Redo commands in original order
138        for command in self.commands.iter_mut() {
139            command.redo()?;
140        }
141        Ok(())
142    }
143
144    fn as_any(&self) -> &dyn Any {
145        self
146    }
147}
148/// Trait for commands that can be executed asynchronously with progress tracking and cancellation.
149///
150/// This trait extends the basic UndoRedoCommand trait with asynchronous capabilities.
151/// Implementors must also implement the UndoRedoCommand trait to ensure compatibility
152/// with the existing undo/redo system.
153pub trait AsyncUndoRedoCommand: UndoRedoCommand {
154    /// Starts the undo operation asynchronously and returns immediately.
155    /// Returns Ok(()) if the operation was successfully started.
156    fn start_undo(&mut self) -> Result<()>;
157
158    /// Starts the redo operation asynchronously and returns immediately.
159    /// Returns Ok(()) if the operation was successfully started.
160    fn start_redo(&mut self) -> Result<()>;
161
162    /// Checks the progress of the current operation.
163    /// Returns a value between 0.0 (not started) and 1.0 (completed).
164    fn check_progress(&self) -> f32;
165
166    /// Attempts to cancel the in-progress operation.
167    /// Returns Ok(()) if cancellation was successful or if no operation is in progress.
168    fn cancel(&mut self) -> Result<()>;
169
170    /// Checks if the current operation is complete.
171    /// Returns true if the operation has finished successfully.
172    fn is_complete(&self) -> bool;
173}
174
175#[derive(Default)]
176struct StackData {
177    undo_stack: Vec<Box<dyn UndoRedoCommand>>,
178    redo_stack: Vec<Box<dyn UndoRedoCommand>>,
179}
180
181impl fmt::Debug for StackData {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        f.debug_struct("StackData")
184            .field("undo_len", &self.undo_stack.len())
185            .field("redo_len", &self.redo_stack.len())
186            .finish()
187    }
188}
189
190/// Manager for undo and redo operations.
191///
192/// The UndoRedoManager maintains multiple stacks of commands:
193/// - Each stack has an undo stack for commands that can be undone
194/// - Each stack has a redo stack for commands that have been undone and can be redone
195///
196/// It also supports:
197/// - Grouping multiple commands as a single unit using begin_composite/end_composite
198/// - Merging commands of the same type when appropriate
199/// - Switching between different stacks
200#[derive(Debug)]
201pub struct UndoRedoManager {
202    stacks: HashMap<u64, StackData>,
203    next_stack_id: u64,
204    in_progress_composite: Option<CompositeCommand>,
205    composite_nesting_level: usize,
206    composite_stack_id: Option<u64>,
207    event_hub: Option<Arc<EventHub>>,
208}
209
210impl Default for UndoRedoManager {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216impl UndoRedoManager {
217    /// Creates a new empty UndoRedoManager with one default stack (ID 0).
218    pub fn new() -> Self {
219        let mut stacks = HashMap::new();
220        stacks.insert(0, StackData::default());
221        UndoRedoManager {
222            stacks,
223            next_stack_id: 1,
224            in_progress_composite: None,
225            composite_nesting_level: 0,
226            composite_stack_id: None,
227            event_hub: None,
228        }
229    }
230
231    /// Inject the event hub to allow sending undo/redo related events
232    pub fn set_event_hub(&mut self, event_hub: &Arc<EventHub>) {
233        self.event_hub = Some(Arc::clone(event_hub));
234    }
235
236    /// Undoes the most recent command on the specified stack.
237    /// If `stack_id` is None, the global stack (ID 0) is used.
238    ///
239    /// The undone command is moved to the redo stack.
240    /// Returns Ok(()) if successful or if there are no commands to undo.
241    pub fn undo(&mut self, stack_id: Option<u64>) -> Result<()> {
242        let target_stack_id = stack_id.unwrap_or(0);
243        let stack = self
244            .stacks
245            .get_mut(&target_stack_id)
246            .ok_or_else(|| anyhow!("Stack with ID {} not found", target_stack_id))?;
247
248        if let Some(mut command) = stack.undo_stack.pop() {
249            if let Err(e) = command.undo() {
250                log::error!("Undo failed, re-pushing command to undo stack: {e}");
251                stack.undo_stack.push(command);
252                return Err(e);
253            }
254            stack.redo_stack.push(command);
255            if let Some(event_hub) = &self.event_hub {
256                event_hub.send_event(Event {
257                    origin: Origin::UndoRedo(UndoRedoEvent::Undone),
258                    ids: Vec::<EntityId>::new(),
259                    data: None,
260                });
261            }
262        }
263        Ok(())
264    }
265
266    /// Redoes the most recently undone command on the specified stack.
267    /// If `stack_id` is None, the global stack (ID 0) is used.
268    ///
269    /// The redone command is moved back to the undo stack.
270    /// Returns Ok(()) if successful or if there are no commands to redo.
271    pub fn redo(&mut self, stack_id: Option<u64>) -> Result<()> {
272        let target_stack_id = stack_id.unwrap_or(0);
273        let stack = self
274            .stacks
275            .get_mut(&target_stack_id)
276            .ok_or_else(|| anyhow!("Stack with ID {} not found", target_stack_id))?;
277
278        if let Some(mut command) = stack.redo_stack.pop() {
279            if let Err(e) = command.redo() {
280                log::error!("Redo failed, re-pushing command to redo stack: {e}");
281                stack.redo_stack.push(command);
282                return Err(e);
283            }
284            stack.undo_stack.push(command);
285            if let Some(event_hub) = &self.event_hub {
286                event_hub.send_event(Event {
287                    origin: Origin::UndoRedo(UndoRedoEvent::Redone),
288                    ids: Vec::<EntityId>::new(),
289                    data: None,
290                });
291            }
292        }
293        Ok(())
294    }
295
296    /// Begins a composite command group.
297    ///
298    /// All commands added between begin_composite and end_composite will be treated as a single command.
299    /// This is useful for operations that logically represent a single action but require multiple
300    /// commands to implement.
301    ///
302    /// # Example
303    /// ```test
304    /// let mut manager = UndoRedoManager::new();
305    /// manager.begin_composite();
306    /// manager.add_command(Box::new(Command1::new()));
307    /// manager.add_command(Box::new(Command2::new()));
308    /// manager.end_composite();
309    /// // Now undo() will undo both commands as a single unit
310    /// ```
311    pub fn begin_composite(&mut self, stack_id: Option<u64>) -> Result<()> {
312        if self.composite_stack_id.is_some() && self.composite_stack_id != stack_id {
313            return Err(anyhow!(
314                "Cannot begin a composite on a different stack while another composite is in progress"
315            ));
316        }
317
318        // Set the target stack ID for this composite
319        self.composite_stack_id = stack_id;
320
321        // Increment the nesting level
322        self.composite_nesting_level += 1;
323
324        // If there's no composite in progress, create one
325        if self.in_progress_composite.is_none() {
326            self.in_progress_composite = Some(CompositeCommand::new(stack_id));
327        }
328
329        // not sure if we want to send events for composites
330        if let Some(event_hub) = &self.event_hub {
331            event_hub.send_event(Event {
332                origin: Origin::UndoRedo(UndoRedoEvent::BeginComposite),
333                ids: Vec::<EntityId>::new(),
334                data: None,
335            });
336        }
337        Ok(())
338    }
339
340    /// Ends the current composite command group and adds it to the specified undo stack.
341    ///
342    /// If no commands were added to the composite, nothing is added to the undo stack.
343    /// If this is a nested composite, only the outermost composite is added to the undo stack.
344    pub fn end_composite(&mut self) {
345        // Decrement the nesting level
346        if self.composite_nesting_level > 0 {
347            self.composite_nesting_level -= 1;
348        }
349
350        // Only end the composite if we're at the outermost level
351        if self.composite_nesting_level == 0 {
352            if let Some(composite) = self.in_progress_composite.take()
353                && !composite.is_empty()
354            {
355                let target_stack_id = self.composite_stack_id.unwrap_or(0);
356                let stack = self
357                    .stacks
358                    .get_mut(&target_stack_id)
359                    .expect("Stack must exist");
360                stack.undo_stack.push(Box::new(composite));
361                stack.redo_stack.clear();
362            }
363            // not sure if we want to send events for composites
364            if let Some(event_hub) = &self.event_hub {
365                event_hub.send_event(Event {
366                    origin: Origin::UndoRedo(UndoRedoEvent::EndComposite),
367                    ids: Vec::<EntityId>::new(),
368                    data: None,
369                });
370            }
371        }
372    }
373
374    pub fn cancel_composite(&mut self) {
375        // Decrement the nesting level
376        if self.composite_nesting_level > 0 {
377            self.composite_nesting_level -= 1;
378        }
379
380        // Undo any sub-commands that were already executed in this composite
381        if let Some(ref mut composite) = self.in_progress_composite {
382            let _ = composite.undo();
383        }
384
385        self.in_progress_composite = None;
386        self.composite_stack_id = None;
387
388        // not sure if we want to send events for composites
389        if let Some(event_hub) = &self.event_hub {
390            event_hub.send_event(Event {
391                origin: Origin::UndoRedo(UndoRedoEvent::CancelComposite),
392                ids: Vec::<EntityId>::new(),
393                data: None,
394            });
395        }
396    }
397
398    /// Adds a command to the global undo stack (ID 0).
399    pub fn add_command(&mut self, command: Box<dyn UndoRedoCommand>) {
400        let _ = self.add_command_to_stack(command, None);
401    }
402
403    /// Adds a command to the specified undo stack.
404    /// If `stack_id` is None, the global stack (ID 0) is used.
405    ///
406    /// This method handles several cases:
407    /// 1. If a composite command is in progress, the command is added to the composite
408    /// 2. If the command can be merged with the last command on the specified undo stack, they are merged
409    /// 3. Otherwise, the command is added to the specified undo stack as a new entry
410    ///
411    /// In all cases, the redo stack of the stack is cleared when a new command is added.
412    pub fn add_command_to_stack(
413        &mut self,
414        command: Box<dyn UndoRedoCommand>,
415        stack_id: Option<u64>,
416    ) -> Result<()> {
417        // If we have a composite in progress, add the command to it
418        if let Some(composite) = &mut self.in_progress_composite {
419            // ensure that the stack_id is the same as the composite's stack
420            if composite.stack_id != stack_id.unwrap_or(0) {
421                return Err(anyhow!(
422                    "Cannot add command to composite with different stack ID"
423                ));
424            }
425            composite.add_command(command);
426            return Ok(());
427        }
428
429        let target_stack_id = stack_id.unwrap_or(0);
430        let stack = self
431            .stacks
432            .get_mut(&target_stack_id)
433            .ok_or_else(|| anyhow!("Stack with ID {} does not exist", target_stack_id))?;
434
435        // Try to merge with the last command if possible
436        if let Some(last_command) = stack.undo_stack.last_mut()
437            && last_command.can_merge(&*command)
438            && last_command.merge(&*command)
439        {
440            // Successfully merged, no need to add the new command
441            stack.redo_stack.clear();
442            return Ok(());
443        }
444
445        // If we couldn't merge, just add the command normally
446        stack.undo_stack.push(command);
447        stack.redo_stack.clear();
448        Ok(())
449    }
450
451    /// Returns true if there are commands that can be undone on the specified stack.
452    /// If `stack_id` is None, the global stack (ID 0) is used.
453    pub fn can_undo(&self, stack_id: Option<u64>) -> bool {
454        let target_stack_id = stack_id.unwrap_or(0);
455        self.stacks
456            .get(&target_stack_id)
457            .map(|s| !s.undo_stack.is_empty())
458            .unwrap_or(false)
459    }
460
461    /// Returns true if there are commands that can be redone on the specified stack.
462    /// If `stack_id` is None, the global stack (ID 0) is used.
463    pub fn can_redo(&self, stack_id: Option<u64>) -> bool {
464        let target_stack_id = stack_id.unwrap_or(0);
465        self.stacks
466            .get(&target_stack_id)
467            .map(|s| !s.redo_stack.is_empty())
468            .unwrap_or(false)
469    }
470
471    /// Clears the undo and redo history for a specific stack.
472    ///
473    /// This method removes all commands from both the undo and redo stacks of the specified stack.
474    pub fn clear_stack(&mut self, stack_id: u64) {
475        if let Some(stack) = self.stacks.get_mut(&stack_id) {
476            stack.undo_stack.clear();
477            stack.redo_stack.clear();
478        }
479    }
480
481    /// Clears all undo and redo history from all stacks.
482    pub fn clear_all_stacks(&mut self) {
483        for stack in self.stacks.values_mut() {
484            stack.undo_stack.clear();
485            stack.redo_stack.clear();
486        }
487        self.in_progress_composite = None;
488        self.composite_nesting_level = 0;
489    }
490
491    /// Creates a new undo/redo stack and returns its ID.
492    pub fn create_new_stack(&mut self) -> u64 {
493        let id = self.next_stack_id;
494        self.stacks.insert(id, StackData::default());
495        self.next_stack_id += 1;
496        id
497    }
498
499    /// Deletes an undo/redo stack by its ID.
500    ///
501    /// The default stack (ID 0) cannot be deleted.
502    pub fn delete_stack(&mut self, stack_id: u64) -> Result<()> {
503        if stack_id == 0 {
504            return Err(anyhow!("Cannot delete the default stack"));
505        }
506        if self.stacks.remove(&stack_id).is_some() {
507            Ok(())
508        } else {
509            Err(anyhow!("Stack with ID {} does not exist", stack_id))
510        }
511    }
512
513    /// Gets the size of the undo stack for a specific stack.
514    pub fn get_stack_size(&self, stack_id: u64) -> usize {
515        self.stacks
516            .get(&stack_id)
517            .map(|s| s.undo_stack.len())
518            .unwrap_or(0)
519    }
520}