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._backend.before_upload()
self.close(save=False, downgrade=True) self.close(save=False, downgrade=True)
# Object creation helpers # Object helpers
########################################################################## ##########################################################################
def get_card(self, id: CardId) -> Card: 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.""" """Get a new-style notetype object. This is not cached; avoid calling frequently."""
return self._backend.get_notetype(id) 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 getCard = get_card
getNote = get_note 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 # pylint: disable=unused-import
import anki._backend.backend_pb2 as _pb import anki._backend.backend_pb2 as _pb
from anki.collection import OpChanges, OpChangesWithId
from anki.consts import * from anki.consts import *
from anki.errors import NotFoundError from anki.errors import NotFoundError
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
@ -232,8 +233,18 @@ class ModelManager:
self._remove_from_cache(id) self._remove_from_cache(id)
self.col._backend.remove_notetype(id) self.col._backend.remove_notetype(id)
def add(self, m: NotetypeDict) -> None: def add(self, m: NotetypeDict) -> OpChangesWithId:
self.save(m) "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: def ensureNameUnique(self, m: NotetypeDict) -> None:
existing_id = self.id_for_name(m["name"]) existing_id = self.id_for_name(m["name"])
@ -241,7 +252,7 @@ class ModelManager:
m["name"] += "-" + checksum(str(time.time()))[:5] m["name"] += "-" + checksum(str(time.time()))[:5]
def update(self, m: NotetypeDict, preserve_usn: bool = True) -> None: 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._remove_from_cache(m["id"])
self.ensureNameUnique(m) self.ensureNameUnique(m)
m["id"] = self.col._backend.add_or_update_notetype( m["id"] = self.col._backend.add_or_update_notetype(
@ -250,6 +261,12 @@ class ModelManager:
self.setCurrent(m) self.setCurrent(m)
self._mutate_after_write(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: def _mutate_after_write(self, nt: NotetypeDict) -> None:
# existing code expects the note type to be mutated to reflect # existing code expects the note type to be mutated to reflect
# the changes made when adding, such as ordinal assignment :-( # the changes made when adding, such as ordinal assignment :-(
@ -273,14 +290,15 @@ class ModelManager:
# Copying # Copying
################################################## ##################################################
def copy(self, m: NotetypeDict) -> NotetypeDict: def copy(self, m: NotetypeDict, add: bool = True) -> NotetypeDict:
"Copy, save and return." "Copy, save and return."
m2 = copy.deepcopy(m) m2 = copy.deepcopy(m)
m2["name"] = without_unicode_isolation( m2["name"] = without_unicode_isolation(
self.col.tr.notetypes_copy(val=m2["name"]) self.col.tr.notetypes_copy(val=m2["name"])
) )
m2["id"] = 0 m2["id"] = 0
self.add(m2) if add:
self.add(m2)
return m2 return m2
# Fields # Fields

View file

@ -5,7 +5,7 @@ from __future__ import annotations
import copy import copy
import pprint 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 # pylint: disable=unused-import
import anki._backend.backend_pb2 as _pb import anki._backend.backend_pb2 as _pb
@ -30,12 +30,12 @@ class Note:
def __init__( def __init__(
self, self,
col: anki.collection.Collection, col: anki.collection.Collection,
model: Optional[NotetypeDict] = None, model: Optional[Union[NotetypeDict, NotetypeId]] = None,
id: Optional[NoteId] = None, id: Optional[NoteId] = None,
) -> None: ) -> None:
assert not (model and id) assert not (model and id)
notetype_id = model["id"] if isinstance(model, dict) else model
self.col = col.weakref() self.col = col.weakref()
# self.newlyAdded = False
if id: if id:
# existing note # existing note
@ -43,7 +43,7 @@ class Note:
self.load() self.load()
else: else:
# new note for provided notetype # 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: def load(self) -> None:
n = self.col._backend.get_note(self.id) 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 # pylint: disable=no-member
StockNotetypeKind = _pb.StockNotetype.Kind 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 # to this list to have it shown in the add/clone note type screen
models: List[Tuple] = [] models: List[Tuple] = []
def _add_stock_notetype( def _get_stock_notetype(
col: anki.collection.Collection, kind: StockNotetypeKind.V col: anki.collection.Collection, kind: StockNotetypeKind.V
) -> anki.models.NotetypeDict: ) -> anki.models.NotetypeDict:
m = from_json_bytes(col._backend.get_stock_notetype_legacy(kind)) return 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)
def get_stock_notetypes( def get_stock_notetypes(
@ -54,18 +30,21 @@ def get_stock_notetypes(
Tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]] Tuple[str, Callable[[anki.collection.Collection], anki.models.NotetypeDict]]
] = [] ] = []
# add standard # add standard
for (kind, func) in [ for kind in [
(StockNotetypeKind.BASIC, addBasicModel), StockNotetypeKind.BASIC,
(StockNotetypeKind.BASIC_TYPING, addBasicTypingModel), StockNotetypeKind.BASIC_TYPING,
(StockNotetypeKind.BASIC_AND_REVERSED, addForwardReverse), StockNotetypeKind.BASIC_AND_REVERSED,
( StockNotetypeKind.BASIC_OPTIONAL_REVERSED,
StockNotetypeKind.BASIC_OPTIONAL_REVERSED, StockNotetypeKind.CLOZE,
addForwardOptionalReverse,
),
(StockNotetypeKind.CLOZE, addClozeModel),
]: ]:
m = from_json_bytes(col._backend.get_stock_notetype_legacy(kind)) 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 # add extras from add-ons
for (name_or_func, func) in models: for (name_or_func, func) in models:
if not isinstance(name_or_func, str): if not isinstance(name_or_func, str):
@ -74,3 +53,40 @@ def get_stock_notetypes(
name = name_or_func name = name_or_func
out.append((name, func)) out.append((name, func))
return out 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.Cancel).setAutoDefault(False)
self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False) self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False)
self.currentIdx: Optional[int] = None self.currentIdx: Optional[int] = None
self.oldSortField = self.model["sortf"]
self.fillFields() self.fillFields()
self.setupSignals() self.setupSignals()
self.form.fieldList.setDragDropMode(QAbstractItemView.InternalMove) 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.models import NotetypeDict, NotetypeId, NotetypeNameIdUseCount
from anki.notes import Note from anki.notes import Note
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.operations.notetype import add_notetype_legacy
from aqt.qt import * from aqt.qt import *
from aqt.utils import ( from aqt.utils import (
HelpPage, HelpPage,
@ -49,7 +50,7 @@ class Models(QDialog):
self.form.buttonBox.helpRequested, self.form.buttonBox.helpRequested,
lambda: openHelp(HelpPage.ADDING_A_NOTE_TYPE), lambda: openHelp(HelpPage.ADDING_A_NOTE_TYPE),
) )
self.models: List[NotetypeNameIdUseCount] = [] self.models: Sequence[NotetypeNameIdUseCount] = []
self.setupModels() self.setupModels()
restoreGeom(self, "models") restoreGeom(self, "models")
self.exec_() self.exec_()
@ -100,6 +101,12 @@ class Models(QDialog):
self.mw.taskman.with_progress(self.col.models.all_use_counts, on_done, self) self.mw.taskman.with_progress(self.col.models.all_use_counts, on_done, self)
maybeHideClose(box) maybeHideClose(box)
def refresh_list(self) -> None:
self.mw.query_op(
self.col.models.all_use_counts,
success=self.updateModelsList,
)
def onRename(self) -> None: def onRename(self) -> None:
nt = self.current_notetype() nt = self.current_notetype()
txt = getText(tr.actions_new_name(), default=nt["name"]) 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) 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() row = self.form.modelsList.currentRow()
if row == -1: if row == -1:
row = 0 row = 0
@ -138,10 +145,19 @@ class Models(QDialog):
def onAdd(self) -> None: def onAdd(self) -> None:
m = AddModel(self.mw, self).get() m = AddModel(self.mw, self).get()
if m: if m:
txt = getText(tr.actions_name(), default=m["name"])[0].replace('"', "") # if legacy add-ons already added the notetype, skip adding
if txt: if m["id"]:
m["name"] = txt return
self.saveAndRefresh(m)
# 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: def onDelete(self) -> None:
if len(self.models) < 2: if len(self.models) < 2:
@ -258,11 +274,9 @@ class AddModel(QDialog):
def accept(self) -> None: def accept(self) -> None:
model = self.notetypes[self.dialog.models.currentRow()] model = self.notetypes[self.dialog.models.currentRow()]
if isinstance(model, dict): if isinstance(model, dict):
# add copy to deck # clone existing
self.model = self.mw.col.models.copy(model) self.model = self.mw.col.models.copy(model, add=False)
self.mw.col.models.setCurrent(self.model)
else: else:
# create
self.model = model(self.col) self.model = model(self.col)
QDialog.accept(self) 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 { service NotetypesService {
rpc AddNotetype(Notetype) returns (OpChangesWithId); rpc AddNotetype(Notetype) returns (OpChangesWithId);
rpc UpdateNotetype(Notetype) returns (OpChanges); rpc UpdateNotetype(Notetype) returns (OpChanges);
rpc AddNotetypeLegacy(Json) returns (OpChangesWithId);
rpc UpdateNotetypeLegacy(Json) returns (OpChanges);
rpc AddOrUpdateNotetype(AddOrUpdateNotetypeIn) returns (NotetypeId); rpc AddOrUpdateNotetype(AddOrUpdateNotetypeIn) returns (NotetypeId);
rpc GetStockNotetypeLegacy(StockNotetype) returns (Json); rpc GetStockNotetypeLegacy(StockNotetype) returns (Json);
rpc GetNotetype(NotetypeId) returns (Notetype); rpc GetNotetype(NotetypeId) returns (Notetype);

View file

@ -26,6 +26,24 @@ impl NotetypesService for Backend {
.map(Into::into) .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> { fn add_or_update_notetype(&self, input: pb::AddOrUpdateNotetypeIn) -> Result<pb::NotetypeId> {
self.with_col(|col| { self.with_col(|col| {
let legacy: NotetypeSchema11 = serde_json::from_slice(&input.json)?; 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<()> { pub(crate) fn add_notetype_inner(&mut self, notetype: &mut Notetype, usn: Usn) -> Result<()> {
notetype.prepare_for_update(None)?; notetype.prepare_for_update(None)?;
self.ensure_notetype_name_unique(notetype, usn)?; 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. /// - Caller must set notetype as modified if appropriate.