mirror of
https://github.com/ankitects/anki.git
synced 2026-01-07 02:53:54 -05:00
If the note type was renamed, the same note type is still selected. If a note type is added, it's selected. This allows both to see what was done (e.g. proof read the new name) and to follow adding the note type by editing its fields/card types.
311 lines
10 KiB
Python
311 lines
10 KiB
Python
# 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 collections.abc import Callable, Sequence
|
|
from concurrent.futures import Future
|
|
from operator import itemgetter
|
|
|
|
import aqt.clayout
|
|
from anki import stdmodels
|
|
from anki.collection import Collection, OpChangesWithId
|
|
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 import QueryOp
|
|
from aqt.operations.notetype import (
|
|
add_notetype_legacy,
|
|
remove_notetype,
|
|
update_notetype_legacy,
|
|
)
|
|
from aqt.qt import *
|
|
from aqt.schema_change_tracker import ChangeTracker
|
|
from aqt.utils import (
|
|
HelpPage,
|
|
askUser,
|
|
disable_help_button,
|
|
getText,
|
|
maybeHideClose,
|
|
openHelp,
|
|
restoreGeom,
|
|
saveGeom,
|
|
showInfo,
|
|
tr,
|
|
)
|
|
|
|
|
|
class Models(QDialog):
|
|
def __init__(
|
|
self,
|
|
mw: AnkiQt,
|
|
parent: QWidget | None = None,
|
|
fromMain: bool = False,
|
|
selected_notetype_id: NotetypeId | None = None,
|
|
):
|
|
self.mw = mw
|
|
parent = parent or mw
|
|
self.fromMain = fromMain
|
|
self.selected_notetype_id = selected_notetype_id
|
|
QDialog.__init__(self, parent or mw)
|
|
self.col = mw.col.weakref()
|
|
assert self.col
|
|
self.mm = self.col.models
|
|
self.form = aqt.forms.models.Ui_Dialog()
|
|
self.form.setupUi(self)
|
|
qconnect(
|
|
self.form.buttonBox.helpRequested,
|
|
lambda: openHelp(HelpPage.ADDING_A_NOTE_TYPE),
|
|
)
|
|
self.models: Sequence[NotetypeNameIdUseCount] = []
|
|
self.setupModels()
|
|
|
|
self.setWindowFlags(
|
|
self.windowFlags()
|
|
| Qt.WindowType.WindowMaximizeButtonHint
|
|
| Qt.WindowType.WindowMinimizeButtonHint
|
|
)
|
|
restoreGeom(self, "models")
|
|
|
|
self.show()
|
|
|
|
# Models
|
|
##########################################################################
|
|
|
|
def maybe_select_provided_notetype(
|
|
self, selected_notetype_id: NotetypeId | None = None, row: int = 0
|
|
) -> None:
|
|
"""Select the provided notetype ID, if any.
|
|
Otherwise the one at `self.selected_notetype_id`,
|
|
otherwise the `row`-th element."""
|
|
selected_notetype_id = selected_notetype_id or self.selected_notetype_id
|
|
if not selected_notetype_id:
|
|
self.form.modelsList.setCurrentRow(row)
|
|
return
|
|
for i, m in enumerate(self.models):
|
|
if m.id == selected_notetype_id:
|
|
self.form.modelsList.setCurrentRow(i)
|
|
break
|
|
|
|
def setupModels(self) -> None:
|
|
self.model = None
|
|
f = self.form
|
|
box = f.buttonBox
|
|
|
|
default_buttons = [
|
|
(tr.actions_add(), self.onAdd),
|
|
(tr.actions_rename(), self.onRename),
|
|
(tr.actions_delete(), self.onDelete),
|
|
]
|
|
|
|
if self.fromMain:
|
|
default_buttons.extend(
|
|
[
|
|
(tr.notetypes_fields(), self.onFields),
|
|
(tr.notetypes_cards(), self.onCards),
|
|
]
|
|
)
|
|
|
|
default_buttons.append((tr.notetypes_options(), self.onAdvanced))
|
|
|
|
for label, func in gui_hooks.models_did_init_buttons(default_buttons, self):
|
|
button = box.addButton(label, QDialogButtonBox.ButtonRole.ActionRole)
|
|
qconnect(button.clicked, func)
|
|
|
|
qconnect(f.modelsList.itemDoubleClicked, self.onRename)
|
|
|
|
def on_done(fut: Future) -> None:
|
|
self.updateModelsList(fut.result())
|
|
self.maybe_select_provided_notetype()
|
|
|
|
self.mw.taskman.with_progress(self.col.models.all_use_counts, on_done, self)
|
|
maybeHideClose(box)
|
|
|
|
def refresh_list(self, selected_notetype_id: NotetypeId | None = None) -> None:
|
|
QueryOp(
|
|
parent=self,
|
|
op=lambda col: col.models.all_use_counts(),
|
|
success=lambda notetypes: self.updateModelsList(
|
|
notetypes, selected_notetype_id
|
|
),
|
|
).run_in_background()
|
|
|
|
def onRename(self) -> None:
|
|
nt = self.current_notetype()
|
|
text, ok = getText(tr.actions_new_name(), default=nt["name"])
|
|
if ok and text.strip():
|
|
selected_notetype_id = nt["id"]
|
|
nt["name"] = text
|
|
|
|
update_notetype_legacy(parent=self, notetype=nt).success(
|
|
lambda _: self.refresh_list(selected_notetype_id)
|
|
).run_in_background()
|
|
|
|
def updateModelsList(
|
|
self,
|
|
notetypes: Sequence[NotetypeNameIdUseCount],
|
|
selected_notetype_id: NotetypeId | None = None,
|
|
) -> None:
|
|
row = self.form.modelsList.currentRow()
|
|
if row == -1:
|
|
row = 0
|
|
self.form.modelsList.clear()
|
|
|
|
self.models = notetypes
|
|
for m in self.models:
|
|
mUse = tr.browsing_note_count(count=m.use_count)
|
|
item = QListWidgetItem(f"{m.name} [{mUse}]")
|
|
self.form.modelsList.addItem(item)
|
|
self.maybe_select_provided_notetype(selected_notetype_id, row)
|
|
|
|
def current_notetype(self) -> NotetypeDict:
|
|
row = self.form.modelsList.currentRow()
|
|
return self.mm.get(NotetypeId(self.models[row].id))
|
|
|
|
def onAdd(self) -> None:
|
|
def on_success(notetype: NotetypeDict) -> None:
|
|
# if legacy add-ons already added the notetype, skip adding
|
|
nid = notetype["id"]
|
|
if nid:
|
|
self.refresh_list(nid)
|
|
return
|
|
|
|
# prompt for name
|
|
text, ok = getText(tr.actions_name(), default=notetype["name"], parent=self)
|
|
if not ok or not text.strip():
|
|
return
|
|
notetype["name"] = text
|
|
|
|
def refresh_list(op: OpChangesWithId) -> None:
|
|
self.refresh_list(NotetypeId(op.id))
|
|
|
|
add_notetype_legacy(parent=self, notetype=notetype).success(
|
|
refresh_list
|
|
).run_in_background()
|
|
|
|
AddModel(self.mw, on_success, self)
|
|
|
|
def onDelete(self) -> None:
|
|
if len(self.models) < 2:
|
|
showInfo(tr.notetypes_please_add_another_note_type_first(), parent=self)
|
|
return
|
|
idx = self.form.modelsList.currentRow()
|
|
if self.models[idx].use_count:
|
|
msg = tr.notetypes_delete_this_note_type_and_all()
|
|
else:
|
|
msg = tr.notetypes_delete_this_unused_note_type()
|
|
if not askUser(msg, parent=self):
|
|
return
|
|
|
|
tracker = ChangeTracker(self.mw)
|
|
if not tracker.mark_schema():
|
|
return
|
|
|
|
nt = self.current_notetype()
|
|
remove_notetype(parent=self, notetype_id=nt["id"]).success(
|
|
lambda _: self.refresh_list(None)
|
|
).run_in_background()
|
|
|
|
def onAdvanced(self) -> None:
|
|
nt = self.current_notetype()
|
|
d = QDialog(self)
|
|
disable_help_button(d)
|
|
frm = aqt.forms.modelopts.Ui_Dialog()
|
|
frm.setupUi(d)
|
|
frm.latexsvg.setChecked(nt.get("latexsvg", False))
|
|
frm.latexHeader.setText(nt["latexPre"])
|
|
frm.latexFooter.setText(nt["latexPost"])
|
|
d.setWindowTitle(
|
|
without_unicode_isolation(tr.actions_options_for(val=nt["name"]))
|
|
)
|
|
qconnect(frm.buttonBox.helpRequested, lambda: openHelp(HelpPage.LATEX))
|
|
restoreGeom(d, "modelopts")
|
|
gui_hooks.models_advanced_will_show(d)
|
|
d.exec()
|
|
saveGeom(d, "modelopts")
|
|
nt["latexsvg"] = frm.latexsvg.isChecked()
|
|
nt["latexPre"] = str(frm.latexHeader.toPlainText())
|
|
nt["latexPost"] = str(frm.latexFooter.toPlainText())
|
|
update_notetype_legacy(parent=self, notetype=nt).success(
|
|
lambda _: self.refresh_list(nt["id"])
|
|
).run_in_background()
|
|
|
|
def _tmpNote(self) -> Note:
|
|
nt = self.current_notetype()
|
|
return Note(self.col, nt)
|
|
|
|
def onFields(self) -> None:
|
|
from aqt.fields import FieldDialog
|
|
|
|
FieldDialog(self.mw, self.current_notetype(), parent=self)
|
|
|
|
def onCards(self) -> None:
|
|
from aqt.clayout import CardLayout
|
|
|
|
n = self._tmpNote()
|
|
CardLayout(self.mw, n, ord=0, parent=self, fill_empty=True)
|
|
|
|
# Cleanup
|
|
##########################################################################
|
|
|
|
def reject(self) -> None:
|
|
saveGeom(self, "models")
|
|
QDialog.reject(self)
|
|
|
|
|
|
class AddModel(QDialog):
|
|
model: NotetypeDict | None
|
|
|
|
def __init__(
|
|
self,
|
|
mw: AnkiQt,
|
|
on_success: Callable[[NotetypeDict], None],
|
|
parent: QWidget | None = None,
|
|
) -> None:
|
|
self.parent_ = parent or mw
|
|
self.mw = mw
|
|
self.col = mw.col
|
|
QDialog.__init__(self, self.parent_, Qt.WindowType.Window)
|
|
self.model = None
|
|
self.dialog = aqt.forms.addmodel.Ui_Dialog()
|
|
self.dialog.setupUi(self)
|
|
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
|
disable_help_button(self)
|
|
# standard models
|
|
self.notetypes: list[NotetypeDict | Callable[[Collection], NotetypeDict]] = []
|
|
for name, func in stdmodels.get_stock_notetypes(self.col):
|
|
item = QListWidgetItem(tr.notetypes_add(val=name))
|
|
self.dialog.models.addItem(item)
|
|
self.notetypes.append(func)
|
|
# add copies
|
|
for m in sorted(self.col.models.all(), key=itemgetter("name")):
|
|
item = QListWidgetItem(tr.notetypes_clone(val=m["name"]))
|
|
self.dialog.models.addItem(item)
|
|
self.notetypes.append(m)
|
|
self.dialog.models.setCurrentRow(0)
|
|
# the list widget will swallow the enter key
|
|
s = QShortcut(QKeySequence("Return"), self)
|
|
qconnect(s.activated, self.accept)
|
|
# help
|
|
qconnect(self.dialog.buttonBox.helpRequested, self.onHelp)
|
|
self.on_success = on_success
|
|
self.show()
|
|
|
|
def reject(self) -> None:
|
|
QDialog.reject(self)
|
|
|
|
def accept(self) -> None:
|
|
model = self.notetypes[self.dialog.models.currentRow()]
|
|
if isinstance(model, dict):
|
|
# clone existing
|
|
self.model = self.mw.col.models.copy(model, add=False)
|
|
else:
|
|
self.model = model(self.col)
|
|
QDialog.accept(self)
|
|
# On mac, we need to allow time for the existing modal to close or
|
|
# Qt gets confused.
|
|
self.mw.progress.single_shot(100, lambda: self.on_success(self.model), True)
|
|
|
|
def onHelp(self) -> None:
|
|
openHelp(HelpPage.ADDING_A_NOTE_TYPE)
|