Simplify note adding and the deck/notetype choosers

The existing code was really difficult to reason about:

- The default notetype depended on the selected deck, and vice versa,
and this logic was buried in the deck and notetype choosing screens,
and models.py.
- Changes to the notetype were not passed back directly, but were fired
via a hook, which changed any screen in the app that had a notetype
selector.

It also wasn't great for performance, as the most recent deck and tags
were embedded in the notetype, which can be expensive to save and sync
for large notetypes.

To address these points:

- The current deck for a notetype, and notetype for a deck, are now
stored in separate config variables, instead of directly in the deck
or notetype. These are cheap to read and write, and we'll be able to
sync them individually in the future once config syncing is updated in
the future. I seem to recall some users not wanting the tag saving
behaviour, so I've dropped that for now, but if people end up missing
it, it would be simple to add as an extra auxiliary config variable.
- The logic for getting the starting deck and notetype has been moved
into the backend. It should be the same as the older Python code, with
one exception: when "change deck depending on notetype" is enabled in
the preferences, it will start with the current notetype ("curModel"),
instead of first trying to get a deck-specific notetype.
- ModelChooser has been duplicated into notetypechooser.py, and it
has been updated to solely be concerned with keeping track of a selected
notetype - it no longer alters global state.
This commit is contained in:
Damien Elmes 2021-03-08 23:23:24 +10:00
parent c4f6ec99f7
commit ce243c2cae
27 changed files with 678 additions and 183 deletions

View File

@ -7,7 +7,8 @@ ignored-classes=
FormatTimespanIn, FormatTimespanIn,
AnswerCardIn, AnswerCardIn,
UnburyCardsInCurrentDeckIn, UnburyCardsInCurrentDeckIn,
BuryOrSuspendCardsIn BuryOrSuspendCardsIn,
NoteIsDuplicateOrEmptyOut
[MESSAGES CONTROL] [MESSAGES CONTROL]
disable=C,R, disable=C,R,

View File

@ -28,7 +28,7 @@ from anki.decks import DeckManager
from anki.errors import AnkiError, DBError from anki.errors import AnkiError, DBError
from anki.lang import TR, FormatTimeSpan from anki.lang import TR, FormatTimeSpan
from anki.media import MediaManager, media_paths_from_col_path from anki.media import MediaManager, media_paths_from_col_path
from anki.models import ModelManager from anki.models import ModelManager, NoteType
from anki.notes import Note from anki.notes import Note
from anki.sched import Scheduler as V1Scheduler from anki.sched import Scheduler as V1Scheduler
from anki.scheduler import Scheduler as V2TestScheduler from anki.scheduler import Scheduler as V2TestScheduler
@ -55,6 +55,7 @@ GraphPreferences = _pb.GraphPreferences
BuiltinSort = _pb.SortOrder.Builtin BuiltinSort = _pb.SortOrder.Builtin
Preferences = _pb.Preferences Preferences = _pb.Preferences
UndoStatus = _pb.UndoStatus UndoStatus = _pb.UndoStatus
DefaultsForAdding = _pb.DeckAndNotetype
@dataclass @dataclass
@ -360,12 +361,8 @@ class Collection:
# Notes # Notes
########################################################################## ##########################################################################
def noteCount(self) -> Any: def new_note(self, notetype: NoteType) -> Note:
return self.db.scalar("select count() from notes") return Note(self, notetype)
def newNote(self, forDeck: bool = True) -> Note:
"Return a new note with the current model."
return Note(self, self.models.current(forDeck))
def add_note(self, note: Note, deck_id: int) -> None: def add_note(self, note: Note, deck_id: int) -> None:
note.id = self._backend.add_note(note=note._to_backend_note(), deck_id=deck_id) note.id = self._backend.add_note(note=note._to_backend_note(), deck_id=deck_id)
@ -385,8 +382,44 @@ class Collection:
def card_ids_of_note(self, note_id: int) -> Sequence[int]: def card_ids_of_note(self, note_id: int) -> Sequence[int]:
return self._backend.cards_of_note(note_id) return self._backend.cards_of_note(note_id)
def defaults_for_adding(
self, *, current_review_card: Optional[Card]
) -> DefaultsForAdding:
"""Get starting deck and notetype for add screen.
An option in the preferences controls whether this will be based on the current deck
or current notetype.
"""
if card := current_review_card:
home_deck = card.odid or card.did
else:
home_deck = 0
return self._backend.defaults_for_adding(
home_deck_of_current_review_card=home_deck,
)
def default_deck_for_notetype(self, notetype_id: int) -> Optional[int]:
"""If 'change deck depending on notetype' is enabled in the preferences,
return the last deck used with the provided notetype, if any.."""
if self.get_config_bool(Config.Bool.ADDING_DEFAULTS_TO_CURRENT_DECK):
return None
return (
self._backend.default_deck_for_notetype(
ntid=notetype_id,
)
or None
)
# legacy # legacy
def noteCount(self) -> int:
return self.db.scalar("select count() from notes")
def newNote(self, forDeck: bool = True) -> Note:
"Return a new note with the current model."
return Note(self, self.models.current(forDeck))
def addNote(self, note: Note) -> int: def addNote(self, note: Note) -> int:
self.add_note(note, note.model()["did"]) self.add_note(note, note.model()["did"])
return len(note.cards()) return len(note.cards())

View File

@ -374,7 +374,7 @@ class DeckManager:
return deck["name"] return deck["name"]
return self.col.tr(TR.DECKS_NO_DECK) return self.col.tr(TR.DECKS_NO_DECK)
def nameOrNone(self, did: int) -> Optional[str]: def name_if_exists(self, did: int) -> Optional[str]:
deck = self.get(did, default=False) deck = self.get(did, default=False)
if deck: if deck:
return deck["name"] return deck["name"]
@ -562,3 +562,4 @@ class DeckManager:
# legacy # legacy
newDyn = new_filtered newDyn = new_filtered
nameOrNone = name_if_exists

View File

@ -14,6 +14,8 @@ from anki.consts import MODEL_STD
from anki.models import NoteType, Template from anki.models import NoteType, Template
from anki.utils import joinFields from anki.utils import joinFields
DuplicateOrEmptyResult = _pb.NoteIsDuplicateOrEmptyOut.State
class Note: class Note:
# not currently exposed # not currently exposed
@ -186,8 +188,9 @@ class Note:
# Unique/duplicate check # Unique/duplicate check
################################################## ##################################################
def dupeOrEmpty(self) -> int: def duplicate_or_empty(self) -> DuplicateOrEmptyResult.V:
"1 if first is empty; 2 if first is a duplicate, 0 otherwise."
return self.col._backend.note_is_duplicate_or_empty( return self.col._backend.note_is_duplicate_or_empty(
self._to_backend_note() self._to_backend_note()
).state ).state
dupeOrEmpty = duplicate_or_empty

View File

@ -71,15 +71,15 @@ def test_noteAddDelete():
c0 = note.cards()[0] c0 = note.cards()[0]
assert "three" in c0.q() assert "three" in c0.q()
# it should not be a duplicate # it should not be a duplicate
assert not note.dupeOrEmpty() assert not note.duplicate_or_empty()
# now let's make a duplicate # now let's make a duplicate
note2 = col.newNote() note2 = col.newNote()
note2["Front"] = "one" note2["Front"] = "one"
note2["Back"] = "" note2["Back"] = ""
assert note2.dupeOrEmpty() assert note2.duplicate_or_empty()
# empty first field should not be permitted either # empty first field should not be permitted either
note2["Front"] = " " note2["Front"] = " "
assert note2.dupeOrEmpty() assert note2.duplicate_or_empty()
def test_fieldChecksum(): def test_fieldChecksum():

View File

@ -1,17 +1,18 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import Any, Callable, List, Optional
from typing import Callable, List, Optional
import aqt.deckchooser import aqt.deckchooser
import aqt.editor import aqt.editor
import aqt.forms import aqt.forms
import aqt.modelchooser
from anki.collection import SearchNode from anki.collection import SearchNode
from anki.consts import MODEL_CLOZE from anki.consts import MODEL_CLOZE
from anki.notes import Note from anki.notes import DuplicateOrEmptyResult, Note
from anki.utils import htmlToTextLine, isMac from anki.utils import htmlToTextLine, isMac
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.main import ResetReason from aqt.main import ResetReason
from aqt.notetypechooser import NoteTypeChooser
from aqt.qt import * from aqt.qt import *
from aqt.sound import av_player from aqt.sound import av_player
from aqt.utils import ( from aqt.utils import (
@ -42,15 +43,13 @@ class AddCards(QDialog):
disable_help_button(self) disable_help_button(self)
self.setMinimumHeight(300) self.setMinimumHeight(300)
self.setMinimumWidth(400) self.setMinimumWidth(400)
self.setupChoosers() self.setup_choosers()
self.setupEditor() self.setupEditor()
self.setupButtons() self.setupButtons()
self.onReset() self._load_new_note()
self.history: List[int] = [] self.history: List[int] = []
self.previousNote: Optional[Note] = None self._last_added_note: Optional[Note] = None
restoreGeom(self, "add") restoreGeom(self, "add")
gui_hooks.state_did_reset.append(self.onReset)
gui_hooks.current_note_type_did_change.append(self.onModelChange)
addCloseShortcut(self) addCloseShortcut(self)
gui_hooks.add_cards_did_init(self) gui_hooks.add_cards_did_init(self)
self.show() self.show()
@ -58,11 +57,20 @@ class AddCards(QDialog):
def setupEditor(self) -> None: def setupEditor(self) -> None:
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self, True) self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self, True)
def setupChoosers(self) -> None: def setup_choosers(self) -> None:
self.modelChooser = aqt.modelchooser.ModelChooser( defaults = self.mw.col.defaults_for_adding(
self.mw, self.form.modelArea, on_activated=self.show_notetype_selector current_review_card=self.mw.reviewer.card
)
self.notetype_chooser = NoteTypeChooser(
mw=self.mw,
widget=self.form.modelArea,
starting_notetype_id=defaults.notetype_id,
on_button_activated=self.show_notetype_selector,
on_notetype_changed=self.on_notetype_change,
)
self.deck_chooser = aqt.deckchooser.DeckChooser(
self.mw, self.form.deckArea, starting_deck_id=defaults.deck_id
) )
self.deckChooser = aqt.deckchooser.DeckChooser(self.mw, self.form.deckArea)
def helpRequested(self) -> None: def helpRequested(self) -> None:
openHelp(HelpPage.ADDING_CARD_AND_NOTE) openHelp(HelpPage.ADDING_CARD_AND_NOTE)
@ -72,7 +80,7 @@ class AddCards(QDialog):
ar = QDialogButtonBox.ActionRole ar = QDialogButtonBox.ActionRole
# add # add
self.addButton = bb.addButton(tr(TR.ACTIONS_ADD), ar) self.addButton = bb.addButton(tr(TR.ACTIONS_ADD), ar)
qconnect(self.addButton.clicked, self.addCards) qconnect(self.addButton.clicked, self.add_current_note)
self.addButton.setShortcut(QKeySequence("Ctrl+Return")) self.addButton.setShortcut(QKeySequence("Ctrl+Return"))
self.addButton.setToolTip(shortcut(tr(TR.ADDING_ADD_SHORTCUT_CTRLANDENTER))) self.addButton.setToolTip(shortcut(tr(TR.ADDING_ADD_SHORTCUT_CTRLANDENTER)))
# close # close
@ -99,42 +107,52 @@ class AddCards(QDialog):
self.editor.setNote(note, focusTo=0) self.editor.setNote(note, focusTo=0)
def show_notetype_selector(self) -> None: def show_notetype_selector(self) -> None:
self.editor.saveNow(self.modelChooser.onModelChange) self.editor.saveNow(self.notetype_chooser.choose_notetype)
def onModelChange(self, unused: Any = None) -> None: def on_notetype_change(self, notetype_id: int) -> None:
oldNote = self.editor.note # need to adjust current deck?
note = self.mw.col.newNote() if deck_id := self.mw.col.default_deck_for_notetype(notetype_id):
self.previousNote = None self.deck_chooser.selected_deck_id = deck_id
if oldNote:
oldFields = list(oldNote.keys()) # only used for detecting changed sticky fields on close
newFields = list(note.keys()) self._last_added_note = None
for n, f in enumerate(note.model()["flds"]):
fieldName = f["name"] # copy fields into new note with the new notetype
old = self.editor.note
new = self._new_note()
if old:
old_fields = list(old.keys())
new_fields = list(new.keys())
for n, f in enumerate(new.model()["flds"]):
field_name = f["name"]
# copy identical fields # copy identical fields
if fieldName in oldFields: if field_name in old_fields:
note[fieldName] = oldNote[fieldName] new[field_name] = old[field_name]
elif n < len(oldNote.model()["flds"]): elif n < len(old.model()["flds"]):
# set non-identical fields by field index # set non-identical fields by field index
oldFieldName = oldNote.model()["flds"][n]["name"] old_field_name = old.model()["flds"][n]["name"]
if oldFieldName not in newFields: if old_field_name not in new_fields:
note.fields[n] = oldNote.fields[n] new.fields[n] = old.fields[n]
self.editor.note = note
# When on model change is called, reset is necessarily called.
# Reset load note, so it is not required to load it here.
def onReset(self, model: None = None, keep: bool = False) -> None: # and update editor state
oldNote = self.editor.note self.editor.note = new
note = self.mw.col.newNote() self.editor.loadNote()
def _load_new_note(self, sticky_fields_from: Optional[Note] = None) -> None:
note = self._new_note()
if old_note := sticky_fields_from:
flds = note.model()["flds"] flds = note.model()["flds"]
# copy fields from old note # copy fields from old note
if oldNote: if old_note:
for n in range(min(len(note.fields), len(oldNote.fields))): for n in range(min(len(note.fields), len(old_note.fields))):
if not keep or flds[n]["sticky"]: if flds[n]["sticky"]:
note.fields[n] = oldNote.fields[n] note.fields[n] = old_note.fields[n]
self.setAndFocusNote(note) self.setAndFocusNote(note)
def removeTempNote(self, note: Note) -> None: def _new_note(self) -> Note:
print("removeTempNote() will go away") return self.mw.col.new_note(
self.mw.col.models.get(self.notetype_chooser.selected_notetype_id)
)
def addHistory(self, note: Note) -> None: def addHistory(self, note: Note) -> None:
self.history.insert(0, note.id) self.history.insert(0, note.id)
@ -163,42 +181,55 @@ class AddCards(QDialog):
def editHistory(self, nid: int) -> None: def editHistory(self, nid: int) -> None:
aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),)) aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),))
def addNote(self, note: Note) -> Optional[Note]: def add_current_note(self) -> None:
note.model()["did"] = self.deckChooser.selectedId() self.editor.saveNow(self._add_current_note)
ret = note.dupeOrEmpty()
problem = None
if ret == 1:
problem = tr(TR.ADDING_THE_FIRST_FIELD_IS_EMPTY)
problem = gui_hooks.add_cards_will_add_note(problem, note)
if problem is not None:
showWarning(problem, help=HelpPage.ADDING_CARD_AND_NOTE)
return None
if note.model()["type"] == MODEL_CLOZE:
if not note.cloze_numbers_in_fields():
if not askUser(tr(TR.ADDING_YOU_HAVE_A_CLOZE_DELETION_NOTE)):
return None
self.mw.col.add_note(note, self.deckChooser.selectedId())
self.addHistory(note)
self.previousNote = note
self.mw.requireReset(reason=ResetReason.AddCardsAddNote, context=self)
gui_hooks.add_cards_did_add_note(note)
return note
def addCards(self) -> None: def _add_current_note(self) -> None:
self.editor.saveNow(self._addCards) note = self.editor.note
def _addCards(self) -> None: if not self._note_can_be_added(note):
self.editor.saveAddModeVars()
if not self.addNote(self.editor.note):
return return
target_deck_id = self.deck_chooser.selected_deck_id
self.mw.col.add_note(note, target_deck_id)
# only used for detecting changed sticky fields on close
self._last_added_note = note
self.addHistory(note)
self.mw.requireReset(reason=ResetReason.AddCardsAddNote, context=self)
# workaround for PyQt focus bug # workaround for PyQt focus bug
self.editor.hideCompleters() self.editor.hideCompleters()
tooltip(tr(TR.ADDING_ADDED), period=500) tooltip(tr(TR.ADDING_ADDED), period=500)
av_player.stop_and_clear_queue() av_player.stop_and_clear_queue()
self.onReset(keep=True) self._load_new_note(sticky_fields_from=note)
self.mw.col.autosave() self.mw.col.autosave() # fixme:
gui_hooks.add_cards_did_add_note(note)
def _note_can_be_added(self, note: Note) -> bool:
result = note.duplicate_or_empty()
if result == DuplicateOrEmptyResult.EMPTY:
problem = tr(TR.ADDING_THE_FIRST_FIELD_IS_EMPTY)
else:
# duplicate entries are allowed these days
problem = None
# filter problem through add-ons
problem = gui_hooks.add_cards_will_add_note(problem, note)
if problem is not None:
showWarning(problem, help=HelpPage.ADDING_CARD_AND_NOTE)
return False
# missing cloze deletion?
if note.model()["type"] == MODEL_CLOZE:
if not note.cloze_numbers_in_fields():
if not askUser(tr(TR.ADDING_YOU_HAVE_A_CLOZE_DELETION_NOTE)):
return False
return True
def keyPressEvent(self, evt: QKeyEvent) -> None: def keyPressEvent(self, evt: QKeyEvent) -> None:
"Show answer on RET or register answer." "Show answer on RET or register answer."
@ -211,12 +242,9 @@ class AddCards(QDialog):
self.ifCanClose(self._reject) self.ifCanClose(self._reject)
def _reject(self) -> None: def _reject(self) -> None:
gui_hooks.state_did_reset.remove(self.onReset)
gui_hooks.current_note_type_did_change.remove(self.onModelChange)
av_player.stop_and_clear_queue() av_player.stop_and_clear_queue()
self.editor.cleanup() self.editor.cleanup()
self.modelChooser.cleanup() self.notetype_chooser.cleanup()
self.deckChooser.cleanup()
self.mw.maybeReset() self.mw.maybeReset()
saveGeom(self, "add") saveGeom(self, "add")
aqt.dialogs.markClosed("AddCards") aqt.dialogs.markClosed("AddCards")
@ -224,7 +252,7 @@ class AddCards(QDialog):
def ifCanClose(self, onOk: Callable) -> None: def ifCanClose(self, onOk: Callable) -> None:
def afterSave() -> None: def afterSave() -> None:
ok = self.editor.fieldsAreBlank(self.previousNote) or askUser( ok = self.editor.fieldsAreBlank(self._last_added_note) or askUser(
tr(TR.ADDING_CLOSE_AND_LOSE_CURRENT_INPUT), defaultno=True tr(TR.ADDING_CLOSE_AND_LOSE_CURRENT_INPUT), defaultno=True
) )
if ok: if ok:
@ -238,3 +266,15 @@ class AddCards(QDialog):
cb() cb()
self.ifCanClose(doClose) self.ifCanClose(doClose)
# legacy aliases
addCards = add_current_note
_addCards = _add_current_note
onModelChange = on_notetype_change
def addNote(self, note: Note) -> None:
print("addNote() is obsolete")
def removeTempNote(self, note: Note) -> None:
print("removeTempNote() will go away")

View File

@ -1,61 +1,79 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import Any
from aqt import AnkiQt, gui_hooks from typing import Optional
from aqt import AnkiQt
from aqt.qt import * from aqt.qt import *
from aqt.utils import TR, HelpPage, shortcut, tr from aqt.utils import TR, HelpPage, shortcut, tr
class DeckChooser(QHBoxLayout): class DeckChooser(QHBoxLayout):
def __init__( def __init__(
self, mw: AnkiQt, widget: QWidget, label: bool = True, start: Any = None self,
mw: AnkiQt,
widget: QWidget,
label: bool = True,
starting_deck_id: Optional[int] = None,
) -> None: ) -> None:
QHBoxLayout.__init__(self) QHBoxLayout.__init__(self)
self._widget = widget # type: ignore self._widget = widget # type: ignore
self.mw = mw self.mw = mw
self.label = label self._setup_ui(show_label=label)
self._selected_deck_id = 0
# default to current deck if starting id not provided
if starting_deck_id is None:
starting_deck_id = self.mw.col.get_config("curDeck", default=1) or 1
self.selected_deck_id = starting_deck_id
def _setup_ui(self, show_label: bool) -> None:
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
self.setSpacing(8) self.setSpacing(8)
self.setupDecks()
self._widget.setLayout(self)
gui_hooks.current_note_type_did_change.append(self.onModelChangeNew)
def setupDecks(self) -> None: # text label before button?
if self.label: if show_label:
self.deckLabel = QLabel(tr(TR.DECKS_DECK)) self.deckLabel = QLabel(tr(TR.DECKS_DECK))
self.addWidget(self.deckLabel) self.addWidget(self.deckLabel)
# decks box # decks box
self.deck = QPushButton(clicked=self.onDeckChange) # type: ignore self.deck = QPushButton()
qconnect(self.deck.clicked, self.choose_deck)
self.deck.setAutoDefault(False) self.deck.setAutoDefault(False)
self.deck.setToolTip(shortcut(tr(TR.QT_MISC_TARGET_DECK_CTRLANDD))) self.deck.setToolTip(shortcut(tr(TR.QT_MISC_TARGET_DECK_CTRLANDD)))
QShortcut(QKeySequence("Ctrl+D"), self._widget, activated=self.onDeckChange) # type: ignore qconnect(
self.addWidget(self.deck) QShortcut(QKeySequence("Ctrl+D"), self._widget).activated, self.choose_deck
# starting label
if self.mw.col.conf.get("addToCur", True):
col = self.mw.col
did = col.conf["curDeck"]
if col.decks.isDyn(did):
# if they're reviewing, try default to current card
c = self.mw.reviewer.card
if self.mw.state == "review" and c:
if not c.odid:
did = c.did
else:
did = c.odid
else:
did = 1
self.setDeckName(
self.mw.col.decks.nameOrNone(did) or tr(TR.QT_MISC_DEFAULT)
) )
else:
self.setDeckName(
self.mw.col.decks.nameOrNone(self.mw.col.models.current()["did"])
or tr(TR.QT_MISC_DEFAULT)
)
# layout
sizePolicy = QSizePolicy(QSizePolicy.Policy(7), QSizePolicy.Policy(0)) sizePolicy = QSizePolicy(QSizePolicy.Policy(7), QSizePolicy.Policy(0))
self.deck.setSizePolicy(sizePolicy) self.deck.setSizePolicy(sizePolicy)
self.addWidget(self.deck)
self._widget.setLayout(self)
def selected_deck_name(self) -> str:
return (
self.mw.col.decks.name_if_exists(self.selected_deck_id) or "missing default"
)
@property
def selected_deck_id(self) -> int:
self._ensure_selected_deck_valid()
return self._selected_deck_id
@selected_deck_id.setter
def selected_deck_id(self, id: int) -> None:
if id != self._selected_deck_id:
self._selected_deck_id = id
self._ensure_selected_deck_valid()
self._update_button_label()
def _ensure_selected_deck_valid(self) -> None:
if not self.mw.col.decks.get(self._selected_deck_id, default=False):
self.selected_deck_id = 1
def _update_button_label(self) -> None:
self.deck.setText(self.selected_deck_name().replace("&", "&&"))
def show(self) -> None: def show(self) -> None:
self._widget.show() # type: ignore self._widget.show() # type: ignore
@ -63,23 +81,10 @@ class DeckChooser(QHBoxLayout):
def hide(self) -> None: def hide(self) -> None:
self._widget.hide() # type: ignore self._widget.hide() # type: ignore
def cleanup(self) -> None: def choose_deck(self) -> None:
gui_hooks.current_note_type_did_change.remove(self.onModelChangeNew)
def onModelChangeNew(self, unused: Any = None) -> None:
self.onModelChange()
def onModelChange(self) -> None:
if not self.mw.col.conf.get("addToCur", True):
self.setDeckName(
self.mw.col.decks.nameOrNone(self.mw.col.models.current()["did"])
or tr(TR.QT_MISC_DEFAULT)
)
def onDeckChange(self) -> None:
from aqt.studydeck import StudyDeck from aqt.studydeck import StudyDeck
current = self.deckName() current = self.selected_deck_name()
ret = StudyDeck( ret = StudyDeck(
self.mw, self.mw,
current=current, current=current,
@ -91,20 +96,15 @@ class DeckChooser(QHBoxLayout):
geomKey="selectDeck", geomKey="selectDeck",
) )
if ret.name: if ret.name:
self.setDeckName(ret.name) self.selected_deck_id = self.mw.col.decks.byName(ret.name)["id"]
def setDeckName(self, name: str) -> None: # legacy
self.deck.setText(name.replace("&", "&&"))
self._deckName = name
def deckName(self) -> str: onDeckChange = choose_deck
return self._deckName deckName = selected_deck_name
def selectedId(self) -> int: def selectedId(self) -> int:
# save deck name return self.selected_deck_id
name = self.deckName()
if not name.strip(): def cleanup(self) -> None:
did = 1 pass
else:
did = self.mw.col.decks.id(name)
return did

View File

@ -563,7 +563,7 @@ class Editor:
def checkValid(self) -> None: def checkValid(self) -> None:
cols = [""] * len(self.note.fields) cols = [""] * len(self.note.fields)
err = self.note.dupeOrEmpty() err = self.note.duplicate_or_empty()
if err == 2: if err == 2:
cols[0] = "dupe" cols[0] = "dupe"
@ -680,19 +680,16 @@ class Editor:
self._save_current_note() self._save_current_note()
gui_hooks.editor_did_update_tags(self.note) gui_hooks.editor_did_update_tags(self.note)
def saveAddModeVars(self) -> None:
if self.addMode:
# save tags to model
m = self.note.model()
m["tags"] = self.note.tags
self.mw.col.models.save(m, updateReqs=False)
def hideCompleters(self) -> None: def hideCompleters(self) -> None:
self.tags.hideCompleter() self.tags.hideCompleter()
def onFocusTags(self) -> None: def onFocusTags(self) -> None:
self.tags.setFocus() self.tags.setFocus()
# legacy
def saveAddModeVars(self) -> None:
pass
# Format buttons # Format buttons
###################################################################### ######################################################################

View File

@ -191,7 +191,7 @@ class ImportDialog(QDialog):
self.mw.pm.profile["allowHTML"] = self.importer.allowHTML self.mw.pm.profile["allowHTML"] = self.importer.allowHTML
self.importer.tagModified = self.frm.tagModified.text() self.importer.tagModified = self.frm.tagModified.text()
self.mw.pm.profile["tagModified"] = self.importer.tagModified self.mw.pm.profile["tagModified"] = self.importer.tagModified
did = self.deck.selectedId() did = self.deck.selected_deck_id
self.importer.model["did"] = did self.importer.model["did"] = did
self.mw.col.models.save(self.importer.model, updateReqs=False) self.mw.col.models.save(self.importer.model, updateReqs=False)
self.mw.col.decks.select(did) self.mw.col.decks.select(did)

View File

@ -662,7 +662,7 @@ class AnkiQt(QMainWindow):
def _selectedDeck(self) -> Optional[Deck]: def _selectedDeck(self) -> Optional[Deck]:
did = self.col.decks.selected() did = self.col.decks.selected()
if not self.col.decks.nameOrNone(did): if not self.col.decks.name_if_exists(did):
showInfo(tr(TR.QT_MISC_PLEASE_SELECT_A_DECK)) showInfo(tr(TR.QT_MISC_PLEASE_SELECT_A_DECK))
return None return None
return self.col.decks.get(did) return self.col.decks.get(did)

View File

@ -8,6 +8,8 @@ from aqt.utils import TR, HelpPage, shortcut, tr
class ModelChooser(QHBoxLayout): class ModelChooser(QHBoxLayout):
"New code should prefer NoteTypeChooser."
def __init__( def __init__(
self, self,
mw: AnkiQt, mw: AnkiQt,

145
qt/aqt/notetypechooser.py Normal file
View File

@ -0,0 +1,145 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import List, Optional
from aqt import AnkiQt, gui_hooks
from aqt.qt import *
from aqt.utils import TR, HelpPage, shortcut, tr
class NoteTypeChooser(QHBoxLayout):
"""
Unlike the older modelchooser, this does not modify the "current model",
so changes made here do not affect other parts of the UI. To read the
currently selected notetype id, use .selected_notetype_id.
By default, a chooser will pop up when the button is pressed. You can
override this by providing `on_button_activated`. Call .choose_notetype()
to run the normal behaviour.
`on_notetype_changed` will be called with the new notetype ID if the user
selects a different notetype, or if the currently-selected notetype is
deleted.
"""
def __init__(
self,
*,
mw: AnkiQt,
widget: QWidget,
starting_notetype_id: int,
on_button_activated: Optional[Callable[[], None]] = None,
on_notetype_changed: Optional[Callable[[int], None]] = None,
show_prefix_label: bool = True,
) -> None:
QHBoxLayout.__init__(self)
self._widget = widget # type: ignore
self.mw = mw
if on_button_activated:
self.on_button_activated = on_button_activated
else:
self.on_button_activated = self.choose_notetype
self._setup_ui(show_label=show_prefix_label)
gui_hooks.state_did_reset.append(self.reset_state)
self._selected_notetype_id = 0
# triggers UI update; avoid firing changed hook on startup
self.on_notetype_changed = None
self.selected_notetype_id = starting_notetype_id
self.on_notetype_changed = on_notetype_changed
def _setup_ui(self, show_label: bool) -> None:
self.setContentsMargins(0, 0, 0, 0)
self.setSpacing(8)
if show_label:
self.label = QLabel(tr(TR.NOTETYPES_TYPE))
self.addWidget(self.label)
# button
self.button = QPushButton()
self.button.setToolTip(shortcut(tr(TR.QT_MISC_CHANGE_NOTE_TYPE_CTRLANDN)))
qconnect(
QShortcut(QKeySequence("Ctrl+N"), self._widget).activated,
self.on_button_activated,
)
self.button.setAutoDefault(False)
self.addWidget(self.button)
qconnect(self.button.clicked, self.on_button_activated)
sizePolicy = QSizePolicy(QSizePolicy.Policy(7), QSizePolicy.Policy(0))
self.button.setSizePolicy(sizePolicy)
self._widget.setLayout(self)
def cleanup(self) -> None:
gui_hooks.state_did_reset.remove(self.reset_state)
def reset_state(self) -> None:
self._ensure_selected_notetype_valid()
def show(self) -> None:
self._widget.show() # type: ignore
def hide(self) -> None:
self._widget.hide() # type: ignore
def onEdit(self) -> None:
import aqt.models
aqt.models.Models(self.mw, self._widget)
def choose_notetype(self) -> None:
from aqt.studydeck import StudyDeck
current = self.selected_notetype_name()
# edit button
edit = QPushButton(tr(TR.QT_MISC_MANAGE))
qconnect(edit.clicked, self.onEdit)
def nameFunc() -> List[str]:
return sorted(self.mw.col.models.allNames())
ret = StudyDeck(
self.mw,
names=nameFunc,
accept=tr(TR.ACTIONS_CHOOSE),
title=tr(TR.QT_MISC_CHOOSE_NOTE_TYPE),
help=HelpPage.NOTE_TYPE,
current=current,
parent=self._widget,
buttons=[edit],
cancel=True,
geomKey="selectModel",
)
if not ret.name:
return
notetype = self.mw.col.models.byName(ret.name)
if (id := notetype["id"]) != self._selected_notetype_id:
self.selected_notetype_id = id
@property
def selected_notetype_id(self) -> int:
# theoretically this should not be necessary, as we're listening to
# resets
self._ensure_selected_notetype_valid()
return self._selected_notetype_id
@selected_notetype_id.setter
def selected_notetype_id(self, id: int) -> None:
if id != self._selected_notetype_id:
self._selected_notetype_id = id
self._ensure_selected_notetype_valid()
self._update_button_label()
if func := self.on_notetype_changed:
func(self._selected_notetype_id)
def selected_notetype_name(self) -> str:
return self.mw.col.models.get(self.selected_notetype_id)["name"]
def _ensure_selected_notetype_valid(self) -> None:
if not self.mw.col.models.get(self._selected_notetype_id):
self.selected_notetype_id = self.mw.col.models.all_names_and_ids()[0].id
def _update_button_label(self) -> None:
self.button.setText(self.selected_notetype_name().replace("&", "&&"))

View File

@ -170,6 +170,8 @@ service BackendService {
rpc NewNote(NoteTypeID) returns (Note); rpc NewNote(NoteTypeID) returns (Note);
rpc AddNote(AddNoteIn) returns (NoteID); rpc AddNote(AddNoteIn) returns (NoteID);
rpc DefaultsForAdding(DefaultsForAddingIn) returns (DeckAndNotetype);
rpc DefaultDeckForNotetype(NoteTypeID) returns (DeckID);
rpc UpdateNote(UpdateNoteIn) returns (Empty); rpc UpdateNote(UpdateNoteIn) returns (Empty);
rpc GetNote(NoteID) returns (Note); rpc GetNote(NoteID) returns (Note);
rpc RemoveNotes(RemoveNotesIn) returns (Empty); rpc RemoveNotes(RemoveNotesIn) returns (Empty);
@ -403,7 +405,7 @@ message NoteTypeConfig {
Kind kind = 1; Kind kind = 1;
uint32 sort_field_idx = 2; uint32 sort_field_idx = 2;
string css = 3; string css = 3;
int64 target_deck_id = 4; int64 target_deck_id = 4; // moved into config var
string latex_pre = 5; string latex_pre = 5;
string latex_post = 6; string latex_post = 6;
bool latex_svg = 7; bool latex_svg = 7;
@ -1272,6 +1274,7 @@ message Config {
COLLAPSE_CARD_STATE = 7; COLLAPSE_CARD_STATE = 7;
COLLAPSE_FLAGS = 8; COLLAPSE_FLAGS = 8;
SCHED_2021 = 9; SCHED_2021 = 9;
ADDING_DEFAULTS_TO_CURRENT_DECK = 10;
} }
Key key = 1; Key key = 1;
} }
@ -1406,3 +1409,12 @@ message UndoStatus {
string undo = 1; string undo = 1;
string redo = 2; string redo = 2;
} }
message DefaultsForAddingIn {
int64 home_deck_of_current_review_card = 1;
}
message DeckAndNotetype {
int64 deck_id = 1;
int64 notetype_id = 2;
}

113
rslib/src/adding.rs Normal file
View File

@ -0,0 +1,113 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::sync::Arc;
use crate::prelude::*;
pub struct DeckAndNotetype {
pub deck_id: DeckID,
pub notetype_id: NoteTypeID,
}
impl Collection {
/// An option in the preferences screen governs the behaviour here.
///
/// - When 'default to the current deck' is enabled, we use the current deck
/// if it's normal, the provided reviewer card's deck as a fallback, and
/// Default as a final fallback. We then fetch the last used notetype stored
/// in the deck, falling back to the global notetype, or the first available one.
///
/// - Otherwise, each note type remembers the last deck cards were added to,
/// and we use that, defaulting to the current deck if missing, and
/// Default otherwise.
pub fn defaults_for_adding(
&mut self,
home_deck_of_reviewer_card: DeckID,
) -> Result<DeckAndNotetype> {
let deck_id;
let notetype_id;
if self.get_bool(BoolKey::AddingDefaultsToCurrentDeck) {
deck_id = self
.get_current_deck_for_adding(home_deck_of_reviewer_card)?
.id;
notetype_id = self.default_notetype_for_deck(deck_id)?.id;
} else {
notetype_id = self.get_current_notetype_for_adding()?.id;
deck_id = if let Some(deck_id) = self.default_deck_for_notetype(notetype_id)? {
deck_id
} else {
// default not set in notetype; fall back to current deck
self.get_current_deck_for_adding(home_deck_of_reviewer_card)?
.id
};
}
Ok(DeckAndNotetype {
deck_id,
notetype_id,
})
}
/// The currently selected deck, the home deck of the provided card, or the default deck.
fn get_current_deck_for_adding(
&mut self,
home_deck_of_reviewer_card: DeckID,
) -> Result<Arc<Deck>> {
// current deck, if not filtered
if let Some(current) = self.get_deck(self.get_current_deck_id())? {
if !current.is_filtered() {
return Ok(current);
}
}
// provided reviewer card's home deck
if let Some(home_deck) = self.get_deck(home_deck_of_reviewer_card)? {
return Ok(home_deck);
}
// default deck
self.get_deck(DeckID(1))?.ok_or(AnkiError::NotFound)
}
fn get_current_notetype_for_adding(&mut self) -> Result<Arc<NoteType>> {
// try global 'current' notetype
if let Some(ntid) = self.get_current_notetype_id() {
if let Some(nt) = self.get_notetype(ntid)? {
return Ok(nt);
}
}
// try first available notetype
if let Some((ntid, _)) = self.storage.get_all_notetype_names()?.first() {
Ok(self.get_notetype(*ntid)?.unwrap())
} else {
Err(AnkiError::NotFound)
}
}
fn default_notetype_for_deck(&mut self, deck: DeckID) -> Result<Arc<NoteType>> {
// try last notetype used by deck
if let Some(ntid) = self.get_last_notetype_for_deck(deck) {
if let Some(nt) = self.get_notetype(ntid)? {
return Ok(nt);
}
}
// fall back
self.get_current_notetype_for_adding()
}
/// Returns the last deck added to with this notetype, provided it is valid.
/// This is optional due to the inconsistent handling, where changes in notetype
/// may need to update the current deck, but not vice versa. If a previous deck is
/// not set, we want to keep the current selection, instead of resetting it.
pub(crate) fn default_deck_for_notetype(&mut self, ntid: NoteTypeID) -> Result<Option<DeckID>> {
if let Some(last_deck_id) = self.get_last_deck_added_to_for_notetype(ntid) {
if let Some(deck) = self.get_deck(last_deck_id)? {
if !deck.is_filtered() {
return Ok(Some(deck.id));
}
}
}
Ok(None)
}
}

View File

@ -0,0 +1,14 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::adding::DeckAndNotetype;
use crate::backend_proto::DeckAndNotetype as DeckAndNotetypeProto;
impl From<DeckAndNotetype> for DeckAndNotetypeProto {
fn from(s: DeckAndNotetype) -> Self {
DeckAndNotetypeProto {
deck_id: s.deck_id.0,
notetype_id: s.notetype_id.0,
}
}
}

View File

@ -21,6 +21,7 @@ impl From<BoolKeyProto> for BoolKey {
BoolKeyProto::CollapseCardState => BoolKey::CollapseCardState, BoolKeyProto::CollapseCardState => BoolKey::CollapseCardState,
BoolKeyProto::CollapseFlags => BoolKey::CollapseFlags, BoolKeyProto::CollapseFlags => BoolKey::CollapseFlags,
BoolKeyProto::Sched2021 => BoolKey::Sched2021, BoolKeyProto::Sched2021 => BoolKey::Sched2021,
BoolKeyProto::AddingDefaultsToCurrentDeck => BoolKey::AddingDefaultsToCurrentDeck,
} }
} }
} }

View File

@ -69,6 +69,12 @@ impl From<pb::DeckId> for DeckID {
} }
} }
impl From<DeckID> for pb::DeckId {
fn from(did: DeckID) -> Self {
pb::DeckId { did: did.0 }
}
}
impl From<pb::DeckConfigId> for DeckConfID { impl From<pb::DeckConfigId> for DeckConfID {
fn from(dcid: pb::DeckConfigId) -> Self { fn from(dcid: pb::DeckConfigId) -> Self {
DeckConfID(dcid.dcid) DeckConfID(dcid.dcid)

View File

@ -1,6 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mod adding;
mod card; mod card;
mod config; mod config;
mod dbproxy; mod dbproxy;
@ -1037,6 +1038,25 @@ impl BackendService for Backend {
}) })
} }
fn defaults_for_adding(
&self,
input: pb::DefaultsForAddingIn,
) -> BackendResult<pb::DeckAndNotetype> {
self.with_col(|col| {
let home_deck: DeckID = input.home_deck_of_current_review_card.into();
col.defaults_for_adding(home_deck).map(Into::into)
})
}
fn default_deck_for_notetype(&self, input: pb::NoteTypeId) -> BackendResult<pb::DeckId> {
self.with_col(|col| {
Ok(col
.default_deck_for_notetype(input.into())?
.unwrap_or(DeckID(0))
.into())
})
}
fn update_note(&self, input: pb::UpdateNoteIn) -> BackendResult<Empty> { fn update_note(&self, input: pb::UpdateNoteIn) -> BackendResult<Empty> {
self.with_col(|col| { self.with_col(|col| {
let op = if input.skip_undo_entry { let op = if input.skip_undo_entry {

45
rslib/src/config/deck.rs Normal file
View File

@ -0,0 +1,45 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::ConfigKey;
use crate::prelude::*;
use strum::IntoStaticStr;
/// Auxillary deck state, stored in the config table.
#[derive(Debug, Clone, Copy, IntoStaticStr)]
#[strum(serialize_all = "camelCase")]
enum DeckConfigKey {
LastNotetype,
}
impl DeckConfigKey {
fn for_deck(self, did: DeckID) -> String {
build_aux_deck_key(did, <&'static str>::from(self))
}
}
impl Collection {
pub(crate) fn get_current_deck_id(&self) -> DeckID {
self.get_config_optional(ConfigKey::CurrentDeckID)
.unwrap_or(DeckID(1))
}
pub(crate) fn clear_aux_config_for_deck(&self, ntid: DeckID) -> Result<()> {
self.remove_config_prefix(&build_aux_deck_key(ntid, ""))
}
pub(crate) fn get_last_notetype_for_deck(&self, id: DeckID) -> Option<NoteTypeID> {
let key = DeckConfigKey::LastNotetype.for_deck(id);
self.get_config_optional(key.as_str())
}
pub(crate) fn set_last_notetype_for_deck(&self, did: DeckID, ntid: NoteTypeID) -> Result<()> {
let key = DeckConfigKey::LastNotetype.for_deck(did);
self.set_config(key.as_str(), &ntid)
}
}
fn build_aux_deck_key(deck: DeckID, key: &str) -> String {
format!("_deck_{deck}_{key}", deck = deck, key = key)
}

View File

@ -2,14 +2,13 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mod bool; mod bool;
mod deck;
mod notetype;
pub(crate) mod schema11; pub(crate) mod schema11;
mod string; mod string;
pub use self::{bool::BoolKey, string::StringKey}; pub use self::{bool::BoolKey, string::StringKey};
use crate::{ use crate::prelude::*;
collection::Collection, decks::DeckID, err::Result, notetype::NoteTypeID,
timestamp::TimestampSecs,
};
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use serde_derive::Deserialize; use serde_derive::Deserialize;
use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_repr::{Deserialize_repr, Serialize_repr};
@ -92,13 +91,16 @@ impl Collection {
self.storage.remove_config(key.into()) self.storage.remove_config(key.into())
} }
pub(crate) fn get_browser_sort_kind(&self) -> SortKind { /// Remove all keys starting with provided prefix, which must end with '_'.
self.get_config_default(ConfigKey::BrowserSortKind) pub(crate) fn remove_config_prefix(&self, key: &str) -> Result<()> {
for (key, _val) in self.storage.get_config_prefix(key)? {
self.storage.remove_config(&key)?;
}
Ok(())
} }
pub(crate) fn get_current_deck_id(&self) -> DeckID { pub(crate) fn get_browser_sort_kind(&self) -> SortKind {
self.get_config_optional(ConfigKey::CurrentDeckID) self.get_config_default(ConfigKey::BrowserSortKind)
.unwrap_or(DeckID(1))
} }
pub(crate) fn get_creation_utc_offset(&self) -> Option<i32> { pub(crate) fn get_creation_utc_offset(&self) -> Option<i32> {
@ -130,15 +132,6 @@ impl Collection {
self.set_config(ConfigKey::Rollover, &hour) self.set_config(ConfigKey::Rollover, &hour)
} }
#[allow(dead_code)]
pub(crate) fn get_current_notetype_id(&self) -> Option<NoteTypeID> {
self.get_config_optional(ConfigKey::CurrentNoteTypeID)
}
pub(crate) fn set_current_notetype_id(&self, id: NoteTypeID) -> Result<()> {
self.set_config(ConfigKey::CurrentNoteTypeID, &id)
}
pub(crate) fn get_next_card_position(&self) -> u32 { pub(crate) fn get_next_card_position(&self) -> u32 {
self.get_config_default(ConfigKey::NextNewCardPosition) self.get_config_default(ConfigKey::NextNewCardPosition)
} }

View File

@ -0,0 +1,52 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::ConfigKey;
use crate::prelude::*;
use strum::IntoStaticStr;
/// Notetype config packed into a collection config key. This may change
/// frequently, and we want to avoid the potentially expensive notetype
/// write/sync.
#[derive(Debug, Clone, Copy, IntoStaticStr)]
#[strum(serialize_all = "camelCase")]
enum NoteTypeConfigKey {
#[strum(to_string = "lastDeck")]
LastDeckAddedTo,
}
impl NoteTypeConfigKey {
fn for_notetype(self, ntid: NoteTypeID) -> String {
build_aux_notetype_key(ntid, <&'static str>::from(self))
}
}
impl Collection {
#[allow(dead_code)]
pub(crate) fn get_current_notetype_id(&self) -> Option<NoteTypeID> {
self.get_config_optional(ConfigKey::CurrentNoteTypeID)
}
pub(crate) fn set_current_notetype_id(&self, ntid: NoteTypeID) -> Result<()> {
self.set_config(ConfigKey::CurrentNoteTypeID, &ntid)
}
pub(crate) fn clear_aux_config_for_notetype(&self, ntid: NoteTypeID) -> Result<()> {
self.remove_config_prefix(&build_aux_notetype_key(ntid, ""))
}
pub(crate) fn get_last_deck_added_to_for_notetype(&self, id: NoteTypeID) -> Option<DeckID> {
let key = NoteTypeConfigKey::LastDeckAddedTo.for_notetype(id);
self.get_config_optional(key.as_str())
}
pub(crate) fn set_last_deck_for_notetype(&self, id: NoteTypeID, did: DeckID) -> Result<()> {
let key = NoteTypeConfigKey::LastDeckAddedTo.for_notetype(id);
self.set_config(key.as_str(), &did)
}
}
fn build_aux_notetype_key(ntid: NoteTypeID, key: &str) -> String {
format!("_nt_{ntid}_{key}", ntid = ntid, key = key)
}

View File

@ -468,6 +468,7 @@ impl Collection {
DeckKind::Normal(_) => self.delete_all_cards_in_normal_deck(deck.id)?, DeckKind::Normal(_) => self.delete_all_cards_in_normal_deck(deck.id)?,
DeckKind::Filtered(_) => self.return_all_cards_in_filtered_deck(deck.id)?, DeckKind::Filtered(_) => self.return_all_cards_in_filtered_deck(deck.id)?,
} }
self.clear_aux_config_for_deck(deck.id)?;
if deck.id.0 == 1 { if deck.id.0 == 1 {
let mut deck = deck.to_owned(); let mut deck = deck.to_owned();
// fixme: separate key // fixme: separate key

View File

@ -3,6 +3,7 @@
#![deny(unused_must_use)] #![deny(unused_must_use)]
pub mod adding;
pub mod backend; pub mod backend;
mod backend_proto; mod backend_proto;
pub mod card; pub mod card;

View File

@ -321,7 +321,10 @@ impl Collection {
note.prepare_for_update(&ctx.notetype, normalize_text)?; note.prepare_for_update(&ctx.notetype, normalize_text)?;
note.set_modified(ctx.usn); note.set_modified(ctx.usn);
self.add_note_only_undoable(note)?; self.add_note_only_undoable(note)?;
self.generate_cards_for_new_note(ctx, note, did) self.generate_cards_for_new_note(ctx, note, did)?;
self.set_last_deck_for_notetype(note.notetype_id, did)?;
self.set_last_notetype_for_deck(did, note.notetype_id)?;
self.set_current_notetype_id(note.notetype_id)
} }
#[cfg(test)] #[cfg(test)]

View File

@ -499,6 +499,7 @@ impl Collection {
self.transact(None, |col| { self.transact(None, |col| {
col.storage.set_schema_modified()?; col.storage.set_schema_modified()?;
col.state.notetype_cache.remove(&ntid); col.state.notetype_cache.remove(&ntid);
col.clear_aux_config_for_notetype(ntid)?;
col.storage.remove_notetype(ntid)?; col.storage.remove_notetype(ntid)?;
let all = col.storage.get_all_notetype_names()?; let all = col.storage.get_all_notetype_names()?;
if all.is_empty() { if all.is_empty() {

View File

@ -10,7 +10,7 @@ pub use crate::{
err::{AnkiError, Result}, err::{AnkiError, Result},
i18n::{tr_args, tr_strs, TR}, i18n::{tr_args, tr_strs, TR},
notes::{Note, NoteID}, notes::{Note, NoteID},
notetype::NoteTypeID, notetype::{NoteType, NoteTypeID},
revlog::RevlogID, revlog::RevlogID,
timestamp::{TimestampMillis, TimestampSecs}, timestamp::{TimestampMillis, TimestampSecs},
types::Usn, types::Usn,

View File

@ -41,6 +41,17 @@ impl SqliteStorage {
.transpose() .transpose()
} }
/// Prefix is expected to end with '_'.
pub(crate) fn get_config_prefix(&self, prefix: &str) -> Result<Vec<(String, Vec<u8>)>> {
let mut end = prefix.to_string();
assert_eq!(end.pop(), Some('_'));
end.push(std::char::from_u32('_' as u32 + 1).unwrap());
self.db
.prepare("select key, val from config where key > ? and key < ?")?
.query_and_then(params![prefix, &end], |row| Ok((row.get(0)?, row.get(1)?)))?
.collect()
}
pub(crate) fn get_all_config(&self) -> Result<HashMap<String, Value>> { pub(crate) fn get_all_config(&self) -> Result<HashMap<String, Value>> {
self.db self.db
.prepare("select key, val from config")? .prepare("select key, val from config")?