mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 23:12:21 -04:00

Allows add-on authors to define their own label for a group of undoable operations. For example: def mark_and_bury( *, parent: QWidget, card_id: CardId, ) -> CollectionOp[OpChanges]: def op(col: Collection) -> OpChanges: target = col.add_custom_undo_entry("Mark and Bury") col.sched.bury_cards([card_id]) card = col.get_card(card_id) col.tags.bulk_add(note_ids=[card.nid], tags="marked") return col.merge_undo_entries(target) return CollectionOp(parent, op) The .add_custom_undo_entry() is for adding your own custom actions. When extending a standard Anki action, instead store `target = col.undo_status().last_step` after executing the standard operation. This started out as a bigger refactor that required a separate .commit_undoable() call to be run after each operation, instead of having each operation return changes directly. But that proved to be somewhat cumbersome in unit tests, and ran the risk of unexpected behaviour if the caller invoked an operation without remembering to finalize it.
123 lines
4.5 KiB
Rust
123 lines
4.5 KiB
Rust
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
use super::NoteTags;
|
|
use crate::{prelude::*, undo::UndoableChange};
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) enum UndoableNoteChange {
|
|
Added(Box<Note>),
|
|
Updated(Box<Note>),
|
|
Removed(Box<Note>),
|
|
GraveAdded(Box<(NoteId, Usn)>),
|
|
GraveRemoved(Box<(NoteId, Usn)>),
|
|
TagsUpdated(Box<NoteTags>),
|
|
}
|
|
|
|
impl Collection {
|
|
pub(crate) fn undo_note_change(&mut self, change: UndoableNoteChange) -> Result<()> {
|
|
match change {
|
|
UndoableNoteChange::Added(note) => self.remove_note_without_grave(*note),
|
|
UndoableNoteChange::Updated(note) => {
|
|
let current = self
|
|
.storage
|
|
.get_note(note.id)?
|
|
.ok_or_else(|| AnkiError::invalid_input("note disappeared"))?;
|
|
self.update_note_undoable(¬e, ¤t)
|
|
}
|
|
UndoableNoteChange::Removed(note) => self.restore_deleted_note(*note),
|
|
UndoableNoteChange::GraveAdded(e) => self.remove_note_grave(e.0, e.1),
|
|
UndoableNoteChange::GraveRemoved(e) => self.add_note_grave(e.0, e.1),
|
|
UndoableNoteChange::TagsUpdated(note_tags) => {
|
|
let current = self
|
|
.storage
|
|
.get_note_tags_by_id(note_tags.id)?
|
|
.ok_or_else(|| AnkiError::invalid_input("note disappeared"))?;
|
|
self.update_note_tags_undoable(¬e_tags, current)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Saves in the undo queue, and commits to DB.
|
|
/// No validation, card generation or normalization is done.
|
|
pub(super) fn update_note_undoable(&mut self, note: &Note, original: &Note) -> Result<()> {
|
|
self.save_undo(UndoableNoteChange::Updated(Box::new(original.clone())));
|
|
self.storage.update_note(note)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove a note. Cards must already have been deleted.
|
|
pub(crate) fn remove_note_only_undoable(&mut self, nid: NoteId, usn: Usn) -> Result<()> {
|
|
if let Some(note) = self.storage.get_note(nid)? {
|
|
self.save_undo(UndoableNoteChange::Removed(Box::new(note)));
|
|
self.storage.remove_note(nid)?;
|
|
self.add_note_grave(nid, usn)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// If note is edited multiple times in quick succession, avoid creating extra undo entries.
|
|
pub(crate) fn maybe_coalesce_note_undo_entry(&mut self, changes: &OpChanges) {
|
|
if changes.op != Op::UpdateNote {
|
|
return;
|
|
}
|
|
|
|
if let Some(previous_op) = self.previous_undo_op() {
|
|
if previous_op.kind != Op::UpdateNote {
|
|
return;
|
|
}
|
|
|
|
if let (
|
|
Some(UndoableChange::Note(UndoableNoteChange::Updated(previous))),
|
|
Some(UndoableChange::Note(UndoableNoteChange::Updated(current))),
|
|
) = (
|
|
previous_op.changes.last(),
|
|
self.current_undo_op().and_then(|op| op.changes.last()),
|
|
) {
|
|
if previous.id == current.id && previous_op.timestamp.elapsed_secs() < 60 {
|
|
self.pop_last_change();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Add a note, not adding any cards.
|
|
pub(super) fn add_note_only_undoable(&mut self, note: &mut Note) -> Result<(), AnkiError> {
|
|
self.storage.add_note(note)?;
|
|
self.save_undo(UndoableNoteChange::Added(Box::new(note.clone())));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn update_note_tags_undoable(
|
|
&mut self,
|
|
tags: &NoteTags,
|
|
original: NoteTags,
|
|
) -> Result<()> {
|
|
self.save_undo(UndoableNoteChange::TagsUpdated(Box::new(original)));
|
|
self.storage.update_note_tags(tags)
|
|
}
|
|
|
|
fn remove_note_without_grave(&mut self, note: Note) -> Result<()> {
|
|
self.storage.remove_note(note.id)?;
|
|
self.save_undo(UndoableNoteChange::Removed(Box::new(note)));
|
|
Ok(())
|
|
}
|
|
|
|
fn restore_deleted_note(&mut self, note: Note) -> Result<()> {
|
|
self.storage.add_or_update_note(¬e)?;
|
|
self.save_undo(UndoableNoteChange::Added(Box::new(note)));
|
|
Ok(())
|
|
}
|
|
|
|
fn add_note_grave(&mut self, nid: NoteId, usn: Usn) -> Result<()> {
|
|
self.save_undo(UndoableNoteChange::GraveAdded(Box::new((nid, usn))));
|
|
self.storage.add_note_grave(nid, usn)
|
|
}
|
|
|
|
fn remove_note_grave(&mut self, nid: NoteId, usn: Usn) -> Result<()> {
|
|
self.save_undo(UndoableNoteChange::GraveRemoved(Box::new((nid, usn))));
|
|
self.storage.remove_note_grave(nid)
|
|
}
|
|
}
|