mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
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:
parent
2ff8c20686
commit
ea758f0092
10 changed files with 157 additions and 59 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
25
qt/aqt/operations/notetype.py
Normal file
25
qt/aqt/operations/notetype.py
Normal 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))
|
|
@ -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);
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue