Files
anki/qt
Timothy (Tim) Lee e8d368ca23 fix: image occlusion masks lost when adding notes (#5155)
## Linked issue (required)

Closes #4754

## Summary / motivation (required)

Image occlusion masks could silently go missing from a note at add time.
Two separate problems caused this, both timing dependent, which is why
the bug was rare and hard to reproduce.

**Problem 1: the editor reset races the add (desktop).** Clicking Add on
an image occlusion note saved the note, queued the actual add, and reset
the mask editor all at once. The reset clears the canvas, and clearing
the canvas writes an empty value into the Occlusions field of the same
note object the add was about to read. Depending on which message
arrived first: the add worked normally (the common case), the note was
added with no masks and no warning, or the add was wrongly rejected with
"no occlusion created" while the masks were plainly visible. Fix: reset
the mask editor only after the add succeeds, matching the ordering the
non-legacy TS editor page already uses. This also means a rejected add
no longer wipes the masks you drew.

**Problem 2: one field's save cancels another's.** All fields committed
through a single shared save timer, and scheduling a save cancelled the
timer's previous pending action. Mask edits commit the Occlusions field
through that timer with a 600 ms debounce, so typing into Header or Back
Extra within 600 ms of the last mask edit threw away the pending
occlusions write. It was never retried, because later saves saw the
store already held the same value and skipped notifying. Every mask
drawn since the last committed write was then missing from the added
note. Fix: give each field its own timer. This also surfaced a second
bug in `ChangeTimer` itself: an action scheduled while
`fireImmediately()` was awaiting a previous action was silently
discarded when that await finished; fixed by taking the action before
running it and letting a concurrent flush wait for the run in flight.

## Steps to reproduce (required, use N/A if not applicable)

Problem 2 (reliably reproducible):
1. Add an Image Occlusion note, draw a mask, wait for it to auto-commit
(~1s).
2. Draw a second mask.
3. Immediately (within ~600 ms) switch to the fields view and type a
character into Header.
4. Wait a second, click Add.
5. Before this fix: the added note is missing the second mask. No
warning is shown.

Problem 1 (event-loop timing dependent, not reliably reproducible by
hand):
1. Add an Image Occlusion note, paste an image, draw one or more masks.
2. Click Add.
3. Before this fix: rarely, the note is added with an empty Occlusions
field and no warning, or Add is rejected with "no occlusion created"
while masks are visible on screen.

## How to test (required)

### Checklist (minimum)

- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.

### Details

- `ts/tests/e2e/io-mask-save-race.spec.ts` reproduces problem 2 with
real mouse drawing and real keystrokes at the real 600 ms timing. Fails
on current `main` (payload contains 1 cloze instead of 2, consistently
across repeated runs) and passes with this change.
- `ts/lib/editable/change-timer.test.ts` covers the discarded-action
case in `ChangeTimer` and fails without the fix.
- Full Playwright e2e suite (`just test-e2e`) passes repeatedly across
fresh profiles.
- `just fmt`, `just lint`, and the vitest suite are clean.
- Problem 1 has no reliable automated test: the outcome depends on how
the Qt event loop interleaves two webChannel messages and a background
task. I verified the mechanism with a scripted headless Anki that drives
the real `AddCards` flow and delivers the racing write inside the
vulnerable window, and confirmed by inspection that after this change
the empty write can no longer be issued while an add is in flight.

## Before / after behavior (optional)

Before: masks could be silently dropped from a newly added note (problem
2, common), or the note could be added completely empty of masks /
spuriously rejected (problem 1, rare). After: neither race is possible;
a rejected add also no longer clears the masks you've drawn, so you can
fix the note and retry instead of redrawing.

## Risk / compatibility / migration (optional)

Low risk. Both fixes are ordering/scoping changes with no behavior
change outside the affected race windows. No schema, API, or migration
changes.

## UI evidence (required for visual changes; otherwise N/A)

N/A (no visual/UI changes).

## Scope

- [x] This PR is focused on one change (no unrelated edits).

This PR contains two fixes rather than one. I considered splitting them,
but both are root causes of the same reported bug (#4754) discovered
during the same investigation, they are already isolated into two
separate commits for review, and neither is a complete fix for the issue
on its own. Happy to split into two PRs if you'd prefer.

---------

Co-authored-by: llama <gh@siid.sh>
2026-07-21 12:32:50 +08:00
..
2026-07-11 20:56:00 +03:00

aqt — Anki Qt GUI

aqt is the Qt-based desktop interface for Anki, the spaced repetition flashcard program.

It provides all the visual components of the Anki desktop app: the deck browser, card editor, reviewer, browser, add-on manager, and more. Under the hood it uses PyQt6 and communicates with Anki's core logic via the anki package.

Running

Once installed, Anki can be launched from the command line:

anki

Add-on development

If you are building an Anki add-on, this is the package that exposes the GUI hooks and Qt widgets you need. See the Add-on Guide for full documentation.

Source code

https://github.com/ankitects/anki