Add-on porting notes for Anki 2.1.45

Some porting notes for add-ons wanting to support Anki 2.1.45:

(this has been made a wiki post - please add anything you feel would be useful here, and if you don’t have permission, please reply below)

Type Hints

A reminder that most of Anki’s codebase now has type hints, and you can easily install Anki packages (including betas) with ‘pip install’. Instead of just clicking around in your add-on to see what if anything has broken, investing some time getting mypy working is recommended, as it can help alert you to cases where you’re calling a function that doesn’t exist, or has a different type signature. See the beta docs for how to install a beta build into your local Python, and the add-on guide for how you can use it to get code completion and type checking with mypy.

Debug messages

Some deprecated functions will print a message to standard out, which will not be visible if you’re only using the GUI. While developing add-ons, it’s recommended to run Anki from a console.

  • On Windows, use anki-console.exe
  • On Mac, run /Applications/Anki.app/Contents/MacOS/AnkiMac from Terminal.app
  • On Linux, run ‘anki’ from a terminal

Changes by area

Undo/Redo

This release completely overhauls the undo system. Instead of delayed saving via mw.checkpoint() and a reviewer-specific undo queue, most operations in the Rust backend record their changes into an undo log, which allows multiple undo steps, and redoing previously undone operations.

Undoable operations can be identified by their type signature - they will return a list of changes made by the operation, eg in collection.py:

    def set_deck(self, card_ids: Sequence[CardId], deck_id: int) -> OpChangesWithCount:
        return self._backend.set_deck(card_ids=card_ids, deck_id=deck_id)

More about this in the next section.

For compatibility with existing code, the old checkpointing system, and the undo log in the v1 and v2 scheduler have been retained. To ensure the collection does not get into an inconsistent state however, any of the following operations will clear the new undo queue:

  • a call to mw.checkpoint()
  • a call to a pylib routine that does not support undo (eg fixIntegrity())
  • directly writing to the database with col.db (read-only queries are fine)
  • reviewing a card with the v1 and v2 scheduler (v3 uses the new system)

And likewise, any operation that supports the new undo code will clear any existing checkpoint, and the v1/v2 undo queue.

To prevent your add-on from clearing the user’s undo history, you’ll need to:

  • remove calls to mw.checkpoint()
  • replace direct DB writes with helpers in pylib. For example, instead of using SQL like “update notes where …”, you can fetch the required note ids with col.find_notes(), and update the notes via col.update_note().

For compatibility with v1/v2 add-ons, card.flush() and note.flush() do not use the new undo code. col.update_card() and col.update_note() have been provided as replacements that do support undo.

Updates to the collection config (col.set_config(), and the legacy col.conf) are invisible to the undo system by default. They will not clear nor modify either the legacy or new undo systems. You can optionally mark a config change as undoable with an arg to set_config().

col.add_custom_undo_entry() will allow you to add an empty undo entry with a custom name. You can then perform one or more undoable operations, and use col.merge_undo_entries() to merge them into your custom name. This allows add-ons to provide custom undo actions like they previously did with .checkpoint().

Currently the undo queue is limited to 30, so if you call more than 30 undoable operations without merging, you’ll be unable to merge them all. In 2.1.46, you will be able to use col.update_cards() and .update_notes() to update multiple cards or notes in a single step.

UI change tracking

Because the new undo code is aware of which kinds of objects have been changed, it makes it easier for the UI to update itself as actions are performed. Instead of manually calling mw.reset() or manually redrawing the UI after making an alteration, each window can now subscribe to change events, and update itself appropriately.

For example, the Add screen watches for changes to a deck or notetype, and takes action when its editor component has not declared it has already handled them:

    def on_operation_did_execute(
        self, changes: OpChanges, handler: Optional[object]
    ) -> None:
        if (changes.notetype or changes.deck) and handler is not self.editor:
            self.on_notetype_change(...)

pylib is not hooked up directly to this change notification system - it just returns OpChanges* objects for most calls. Instead of calling the routines on pylib directly, you should instead call them via a CollectionOp helper. The helper takes care of processing the request in a background thread so that it does not block the UI, and it passes the change list to the on_operation_did_execute hook so that the rest of the UI is updated. It also takes care of catching errors, showing a progress bar, and updating the undo/redo menu items.

Many pylib routines have already been wrapped for convenience - you can find them in aqt/operations/. For example, the set_deck() call mentioned in the previous section can
be found in aqt/operations/card.py:

def set_card_deck(
    *, parent: QWidget, card_ids: Sequence[CardId], deck_id: DeckId
) -> CollectionOp[OpChangesWithCount]:
    return CollectionOp(parent, lambda col: col.set_deck(card_ids, deck_id)).success(
        lambda out: tooltip(tr.browsing_cards_updated(count=out.count), parent=parent)
    )

It is invoked by the Browse screen like so:

set_card_deck(parent=self, card_ids=cids, deck_id=did).run_in_background()

run_in_background() can optionally be passed an instance so that windows can ignore changes they initiated themselves. For example, the review screen redraws the current flag directly:

        set_card_flag(parent=self.mw, card_ids=[self.card.id], flag=flag).success(
            redraw_flag
        ).run_in_background(initiator=self)

and in its change routine, it ignores changes it’s already handled itself:

    def op_executed(
        self, changes: OpChanges, handler: Optional[object], focused: bool
    ) -> bool:
        if handler is not self:
            if changes.study_queues:
                self._refresh_needed = RefreshNeeded.QUEUES
       ...

Once you have updated your code to use the new operations, you can remove any calls to mw.reset() you may have had previously.

If you want to combine multiple operations into a single custom undo step, you’ll need to write your own CollectionOp that performs the operations. The last line should return col.merge_undo_entries(…), which will return all changes made during the operation.

Scheduler

  • Some add-ons will not work with the new v3 scheduler. Add-ons that modify cards directly outside of the normal review process should continue to work, but add-ons will no longer be able to modify or augment the behaviour of the card gathering and answering routines when the v3 scheduler is enabled.
  • The v1 and v2 schedulers has been moved into new files, but can still be imported from their old import locations as well. Aside from some cleanup, the functionality of the v1 and v2 schedulers is unchanged.

Editor

  • The editor toolbar has been rewritten in Svelte. There is some compatibility code so that existing add-ons should continue to work.
  • The old hooks for adding toolbar buttons are still available, but are more limited in functionality
  • The toolbar provides a new way to extend and modify it
    • This requires writing Svelte and an extra compilation step for the add-on.
    • For a minimal example add-on, that adds several buttons, see here.

Browser

  • browser.py has been split into multiple files, with some legacy aliases so that some existing imports should continue to work
  • The Change Notetype dialog has been reimplemented. If your add-on was using the old one, you’ll need to copy and paste the code in from a previous Anki release. The legacy model.change() call in pylib has been retained

Table

The table handling has been largely rewritten in order to support the new “notes” mode and can now be found in its own module aqt.browser.table. Gathering the cell data has been moved into the backend and is now done on a row-by-row basis. There aren’t any caches for cards and notes anymore.
Several hooks exist to facilitate the addition of custom columns:

  1. Use gui_hooks.browser_did_fetch_columns to add header information. The new column will be automatically added to the header context menu.
Example
def on_browser_did_fetch_columns(columns):
   columns["myColumnKey"] = aqt.browser.Column(
       key="myColumnKey",
       cards_mode_label="My Card Column",
       notes_mode_label="My Note Column",
       sorting=aqt.browser.Columns.SORTING_NORMAL,
       uses_cell_font=False,
       alignment=aqt.browser.Columns.ALIGNMENT_CENTER,
   )
  1. If you specified your column as sortable, you should use gui_hooks.browser_will_search to add a SQL statement (or pass a native column which Anki knows how to sort by).
Example
def on_browser_will_search(context):
   # order is usually a Column, but could also be str or bool
   if getattr(context.order, "key", None) == "myColumnKey":
       order = "select ... from ... where cid = c.id asc"
  1. For every row, the backend will fill in the column data for all active native columns. Use gui_hooks.browser_did_fetch_row to add your own data.
Example
def on_browser_did_fetch_row(item, is_notes_mode, row, active_columns):
   for index, key in enumerate(active_columns):
       if key == "myColumnKey":
           # item may be a card or note id, State will handle it appropriately
           card = table._state.get_card(item)
           note = table._state.get_note(item)
           if is_notes_mode:
               row.cells[index].text = my_data_in_notes_mode(card, note)
           else:
               row.cells[index].text = my_data_in_cards_mode(card, note)
           # Cell and CellRow also store information about text direction, colour
           # and font that you can customise.

Deck Options

  • The deck options screen has been rewritten, and will be shown to all users on v2/v3.
  • The legacy deck options screen can currently be accessed with a shift+click.
  • The new screen provides an API for extending it. Some earlier examples were:

The code has since changed; @hengiesel could you provide an updated example?

6 Likes

I’ve added an example of how to add a custom column, which I think summarises the new API quite well. If anyone thinks there’s something else about the new browser/table that should be explained here, let me know.

3 Likes

I’m not sure if I understand how to port an existing add-on to use the new CollectionOp system. Let’s say I have a card, and I want to change its due. Before I would write something like this:

browser.model.beginReset()
mw.checkpoint("Change due")

card.due = int(time.time())
card.flush()

browser.model.endReset()
mw.reset()

But how do I translate it to the new system without losing the ability to undo the change?

Additionally, what if I have multiple cards that I need to change, but add only one undo entry?

Something like

def my_blocking_action(col: Collection):
    pos = col.add_custom_undo_entry("change due")
    card1 = col.get_card(CardId(123))
    card1.due = 456
    col.update_card(card1)
    card2 = col.get_card(CardId(100))
    card2.due = 400
    col.update_card(card2)
    return col.merge_undo_entries(pos)

def my_op(*, parent: QWidget) -> CollectionOp[OpChanges]:
    return CollectionOp(parent, lambda col: my_blocking_action(col))

my_op(parent=mw).run_in_background()

For more than 30 cards:

1 Like

Thank you! This helped a lot.

No problem. 2.1.46 will add bulk update_cards() and update_notes() routines, so the two separate update_card() calls can be replaced with a single update_cards().

the Svelve changes for editor open up great possibilites for addon developers. How can I make use of the Svelte components that Anki provides (like ButtonGroupItem) ? Also in general, is the UI moving towards Svelte instead of PyQT ?

I’m hitting an issue with the editor example code here: JS error /_addons/new_format_pack/web/editor.js:219 Uncaught (in promise) TypeError: Cannot read property '$$' of undefined · Issue #2 · hgiesel/anki_new_format_pack · GitHub

I’m new to Svelte, but not new to web dev. Currently studying up on it.

Is there a bigger write up on this new adoption of Svelte ? I’d love to educate myself on the goals of adopting this framework within Anki.

Svelte was first introduced with the new graphs screen, because Anki needed something lightweight to replace jQuery. We are still feeling out the add-on integration story, as the current approach appears to have problems.

2 posts were split to a new topic: Updating card flags