update GUI to allow notetype addition undo

- backend now updates current notetype as part of addition
- frontend no longer implicitly adds, so we can assign a new name and
add in a single operation
This commit is contained in:
Damien Elmes 2021-04-30 15:16:44 +10:00
parent 2ff8c20686
commit ea758f0092
10 changed files with 157 additions and 59 deletions

View file

@ -310,7 +310,7 @@ class Collection:
self._backend.before_upload()
self.close(save=False, downgrade=True)
# Object creation helpers
# Object helpers
##########################################################################
def get_card(self, id: CardId) -> Card:
@ -347,6 +347,11 @@ class Collection:
"""Get a new-style notetype object. This is not cached; avoid calling frequently."""
return self._backend.get_notetype(id)
def update_notetype(self, notetype: Notetype) -> OpChanges:
"This may force a full sync; caller is responsible for notifying user."
self.models._remove_from_cache(NotetypeId(notetype.id))
return self._backend.update_notetype(notetype)
getCard = get_card
getNote = get_note

View file

@ -12,6 +12,7 @@ from typing import Any, Dict, List, NewType, Optional, Sequence, Tuple, Union
import anki # pylint: disable=unused-import
import anki._backend.backend_pb2 as _pb
from anki.collection import OpChanges, OpChangesWithId
from anki.consts import *
from anki.errors import NotFoundError
from anki.lang import without_unicode_isolation
@ -232,8 +233,18 @@ class ModelManager:
self._remove_from_cache(id)
self.col._backend.remove_notetype(id)
def add(self, m: NotetypeDict) -> None:
self.save(m)
def add(self, m: NotetypeDict) -> OpChangesWithId:
"Replaced with add_dict()"
self.ensureNameUnique(m)
out = self.col._backend.add_notetype_legacy(to_json_bytes(m))
m["id"] = out.id
self._mutate_after_write(m)
return out
def add_dict(self, m: NotetypeDict) -> OpChangesWithId:
"Notetype needs to be fetched from DB after adding."
self.ensureNameUnique(m)
return self.col._backend.add_notetype_legacy(to_json_bytes(m))
def ensureNameUnique(self, m: NotetypeDict) -> None:
existing_id = self.id_for_name(m["name"])
@ -241,7 +252,7 @@ class ModelManager:
m["name"] += "-" + checksum(str(time.time()))[:5]
def update(self, m: NotetypeDict, preserve_usn: bool = True) -> None:
"Add or update an existing model. Use .save() instead."
"Add or update an existing model. Use .update_dict() instead."
self._remove_from_cache(m["id"])
self.ensureNameUnique(m)
m["id"] = self.col._backend.add_or_update_notetype(
@ -250,6 +261,12 @@ class ModelManager:
self.setCurrent(m)
self._mutate_after_write(m)
def update_dict(self, m: NotetypeDict) -> OpChanges:
"Update a NotetypeDict. Caller will need to re-load notetype if new fields/cards added."
self._remove_from_cache(m["id"])
self.ensureNameUnique(m)
return self.col._backend.update_notetype_legacy(to_json_bytes(m))
def _mutate_after_write(self, nt: NotetypeDict) -> None:
# existing code expects the note type to be mutated to reflect
# the changes made when adding, such as ordinal assignment :-(
@ -273,14 +290,15 @@ class ModelManager:
# Copying
##################################################
def copy(self, m: NotetypeDict) -> NotetypeDict:
def copy(self, m: NotetypeDict, add: bool = True) -> NotetypeDict:
"Copy, save and return."
m2 = copy.deepcopy(m)
m2["name"] = without_unicode_isolation(
self.col.tr.notetypes_copy(val=m2["name"])
)
m2["id"] = 0
self.add(m2)
if add:
self.add(m2)
return m2
# Fields

View file

@ -5,7 +5,7 @@ from __future__ import annotations
import copy
import pprint
from typing import Any, List, NewType, Optional, Sequence, Tuple
from typing import Any, List, NewType, Optional, Sequence, Tuple, Union
import anki # pylint: disable=unused-import
import anki._backend.backend_pb2 as _pb
@ -30,12 +30,12 @@ class Note:
def __init__(
self,
col: anki.collection.Collection,
model: Optional[NotetypeDict] = None,
model: Optional[Union[NotetypeDict, NotetypeId]] = None,
id: Optional[NoteId] = None,
) -> None:
assert not (model and id)
notetype_id = model["id"] if isinstance(model, dict) else model
self.col = col.weakref()
# self.newlyAdded = False
if id:
# existing note
@ -43,7 +43,7 @@ class Note:
self.load()
else:
# new note for provided notetype
self._load_from_backend_note(self.col._backend.new_note(model["id"]))
self._load_from_backend_note(self.col._backend.new_note(notetype_id))
def load(self) -> None:
n = self.col._backend.get_note(self.id)

View file

@ -12,39 +12,15 @@ from anki.utils import from_json_bytes
# pylint: disable=no-member
StockNotetypeKind = _pb.StockNotetype.Kind
# add-on authors can add ("note type name", function_like_addBasicModel)
# add-on authors can add ("note type name", function)
# to this list to have it shown in the add/clone note type screen
models: List[Tuple] = []
def _add_stock_notetype(
def _get_stock_notetype(
col: anki.collection.Collection, kind: StockNotetypeKind.V
) -> anki.models.NotetypeDict:
m = from_json_bytes(col._backend.get_stock_notetype_legacy(kind))
col.models.add(m)
return m
def addBasicModel(col: anki.collection.Collection) -> anki.models.NotetypeDict:
return _add_stock_notetype(col, StockNotetypeKind.BASIC)
def addBasicTypingModel(col: anki.collection.Collection) -> anki.models.NotetypeDict:
return _add_stock_notetype(col, StockNotetypeKind.BASIC_TYPING)
def addForwardReverse(col: anki.collection.Collection) -> anki.models.NotetypeDict:
return _add_stock_notetype(col, StockNotetypeKind.BASIC_AND_REVERSED)
def addForwardOptionalReverse(
col: anki.collection.Collection,
) -> anki.models.NotetypeDict:
return _add_stock_notetype(col, StockNotetypeKind.BASIC_OPTIONAL_REVERSED)
def addClozeModel(col: anki.collection.Collection) -> anki.models.NotetypeDict:
return _add_stock_notetype(col, StockNotetypeKind.CLOZE)
return from_json_bytes(col._backend.get_stock_notetype_legacy(kind))
def get_stock_notetypes(
@ -54,18 +30,21 @@ def get_stock_notetypes(
Tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]
] = []
# add standard
for (kind, func) in [
(StockNotetypeKind.BASIC, addBasicModel),
(StockNotetypeKind.BASIC_TYPING, addBasicTypingModel),
(StockNotetypeKind.BASIC_AND_REVERSED, addForwardReverse),
(
StockNotetypeKind.BASIC_OPTIONAL_REVERSED,
addForwardOptionalReverse,
),
(StockNotetypeKind.CLOZE, addClozeModel),
for kind in [
StockNotetypeKind.BASIC,
StockNotetypeKind.BASIC_TYPING,
StockNotetypeKind.BASIC_AND_REVERSED,
StockNotetypeKind.BASIC_OPTIONAL_REVERSED,
StockNotetypeKind.CLOZE,
]:
m = from_json_bytes(col._backend.get_stock_notetype_legacy(kind))
out.append((m["name"], func))
def instance_getter(
col: anki.collection.Collection,
) -> anki.models.NotetypeDict:
return m # pylint:disable=cell-var-from-loop
out.append((m["name"], instance_getter))
# add extras from add-ons
for (name_or_func, func) in models:
if not isinstance(name_or_func, str):
@ -74,3 +53,40 @@ def get_stock_notetypes(
name = name_or_func
out.append((name, func))
return out
#
# Legacy functions that added the notetype before returning it
#
def addBasicModel(col: anki.collection.Collection) -> anki.models.NotetypeDict:
nt = _get_stock_notetype(col, StockNotetypeKind.BASIC)
col.models.add(nt)
return nt
def addBasicTypingModel(col: anki.collection.Collection) -> anki.models.NotetypeDict:
nt = _get_stock_notetype(col, StockNotetypeKind.BASIC_TYPING)
col.models.add(nt)
return nt
def addForwardReverse(col: anki.collection.Collection) -> anki.models.NotetypeDict:
nt = _get_stock_notetype(col, StockNotetypeKind.BASIC_AND_REVERSED)
col.models.add(nt)
return nt
def addForwardOptionalReverse(
col: anki.collection.Collection,
) -> anki.models.NotetypeDict:
nt = _get_stock_notetype(col, StockNotetypeKind.BASIC_OPTIONAL_REVERSED)
col.models.add(nt)
return nt
def addClozeModel(col: anki.collection.Collection) -> anki.models.NotetypeDict:
nt = _get_stock_notetype(col, StockNotetypeKind.CLOZE)
col.models.add(nt)
return nt

View file

@ -45,7 +45,6 @@ class FieldDialog(QDialog):
self.form.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False)
self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False)
self.currentIdx: Optional[int] = None
self.oldSortField = self.model["sortf"]
self.fillFields()
self.setupSignals()
self.form.fieldList.setDragDropMode(QAbstractItemView.InternalMove)

View file

@ -11,6 +11,7 @@ from anki.lang import without_unicode_isolation
from anki.models import NotetypeDict, NotetypeId, NotetypeNameIdUseCount
from anki.notes import Note
from aqt import AnkiQt, gui_hooks
from aqt.operations.notetype import add_notetype_legacy
from aqt.qt import *
from aqt.utils import (
HelpPage,
@ -49,7 +50,7 @@ class Models(QDialog):
self.form.buttonBox.helpRequested,
lambda: openHelp(HelpPage.ADDING_A_NOTE_TYPE),
)
self.models: List[NotetypeNameIdUseCount] = []
self.models: Sequence[NotetypeNameIdUseCount] = []
self.setupModels()
restoreGeom(self, "models")
self.exec_()
@ -100,6 +101,12 @@ class Models(QDialog):
self.mw.taskman.with_progress(self.col.models.all_use_counts, on_done, self)
maybeHideClose(box)
def refresh_list(self) -> None:
self.mw.query_op(
self.col.models.all_use_counts,
success=self.updateModelsList,
)
def onRename(self) -> None:
nt = self.current_notetype()
txt = getText(tr.actions_new_name(), default=nt["name"])
@ -118,7 +125,7 @@ class Models(QDialog):
self.mw.taskman.with_progress(save, on_done, self)
def updateModelsList(self, notetypes: List[NotetypeNameIdUseCount]) -> None:
def updateModelsList(self, notetypes: Sequence[NotetypeNameIdUseCount]) -> None:
row = self.form.modelsList.currentRow()
if row == -1:
row = 0
@ -138,10 +145,19 @@ class Models(QDialog):
def onAdd(self) -> None:
m = AddModel(self.mw, self).get()
if m:
txt = getText(tr.actions_name(), default=m["name"])[0].replace('"', "")
if txt:
m["name"] = txt
self.saveAndRefresh(m)
# if legacy add-ons already added the notetype, skip adding
if m["id"]:
return
# prompt for name
text, ok = getText(tr.actions_name(), default=m["name"])
if not ok or not text.strip():
return
m["name"] = text
add_notetype_legacy(parent=self, notetype=m).success(
lambda _: self.refresh_list()
).run_in_background()
def onDelete(self) -> None:
if len(self.models) < 2:
@ -258,11 +274,9 @@ class AddModel(QDialog):
def accept(self) -> None:
model = self.notetypes[self.dialog.models.currentRow()]
if isinstance(model, dict):
# add copy to deck
self.model = self.mw.col.models.copy(model)
self.mw.col.models.setCurrent(self.model)
# clone existing
self.model = self.mw.col.models.copy(model, add=False)
else:
# create
self.model = model(self.col)
QDialog.accept(self)

View file

@ -0,0 +1,25 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
from anki.collection import OpChanges, OpChangesWithId
from anki.models import NotetypeDict
from aqt import QWidget
from aqt.operations import CollectionOp
def add_notetype_legacy(
*,
parent: QWidget,
notetype: NotetypeDict,
) -> CollectionOp[OpChangesWithId]:
return CollectionOp(parent, lambda col: col.models.add_dict(notetype))
def update_notetype_legacy(
*,
parent: QWidget,
notetype: NotetypeDict,
) -> CollectionOp[OpChanges]:
return CollectionOp(parent, lambda col: col.models.update_dict(notetype))

View file

@ -202,6 +202,8 @@ service ConfigService {
service NotetypesService {
rpc AddNotetype(Notetype) returns (OpChangesWithId);
rpc UpdateNotetype(Notetype) returns (OpChanges);
rpc AddNotetypeLegacy(Json) returns (OpChangesWithId);
rpc UpdateNotetypeLegacy(Json) returns (OpChanges);
rpc AddOrUpdateNotetype(AddOrUpdateNotetypeIn) returns (NotetypeId);
rpc GetStockNotetypeLegacy(StockNotetype) returns (Json);
rpc GetNotetype(NotetypeId) returns (Notetype);

View file

@ -26,6 +26,24 @@ impl NotetypesService for Backend {
.map(Into::into)
}
fn add_notetype_legacy(&self, input: pb::Json) -> Result<pb::OpChangesWithId> {
let legacy: NotetypeSchema11 = serde_json::from_slice(&input.json)?;
let mut notetype: Notetype = legacy.into();
self.with_col(|col| {
Ok(col
.add_notetype(&mut notetype)?
.map(|_| notetype.id.0)
.into())
})
}
fn update_notetype_legacy(&self, input: pb::Json) -> Result<pb::OpChanges> {
let legacy: NotetypeSchema11 = serde_json::from_slice(&input.json)?;
let mut notetype: Notetype = legacy.into();
self.with_col(|col| col.update_notetype(&mut notetype))
.map(Into::into)
}
fn add_or_update_notetype(&self, input: pb::AddOrUpdateNotetypeIn) -> Result<pb::NotetypeId> {
self.with_col(|col| {
let legacy: NotetypeSchema11 = serde_json::from_slice(&input.json)?;

View file

@ -470,7 +470,8 @@ impl Collection {
pub(crate) fn add_notetype_inner(&mut self, notetype: &mut Notetype, usn: Usn) -> Result<()> {
notetype.prepare_for_update(None)?;
self.ensure_notetype_name_unique(notetype, usn)?;
self.add_notetype_undoable(notetype)
self.add_notetype_undoable(notetype)?;
self.set_current_notetype_id(notetype.id)
}
/// - Caller must set notetype as modified if appropriate.