Add back legacy code as separate screens

This commit is contained in:
Abdo 2025-07-21 22:30:25 +03:00
parent c91f943f29
commit 7dac2fc4ff
11 changed files with 2383 additions and 99 deletions

View file

@ -125,9 +125,11 @@ from aqt import stats, about, preferences, mediasync # isort:skip
class DialogManager: class DialogManager:
_dialogs: dict[str, list] = { _dialogs: dict[str, list] = {
"AddCards": [addcards.AddCards, None], "AddCards": [addcards.AddCards, None],
"NewAddCards": [addcards.NewAddCards, None],
"AddonsDialog": [addons.AddonsDialog, None], "AddonsDialog": [addons.AddonsDialog, None],
"Browser": [browser.Browser, None], "Browser": [browser.Browser, None],
"EditCurrent": [editcurrent.EditCurrent, None], "EditCurrent": [editcurrent.EditCurrent, None],
"NewEditCurrent": [editcurrent.NewEditCurrent, None],
"FilteredDeckConfigDialog": [filtered_deck.FilteredDeckConfigDialog, None], "FilteredDeckConfigDialog": [filtered_deck.FilteredDeckConfigDialog, None],
"DeckStats": [stats.DeckStats, None], "DeckStats": [stats.DeckStats, None],
"NewDeckStats": [stats.NewDeckStats, None], "NewDeckStats": [stats.NewDeckStats, None],

View file

@ -14,6 +14,7 @@ from anki.models import NotetypeId
from anki.notes import Note, NoteId from anki.notes import Note, NoteId
from anki.utils import html_to_text_line, is_mac from anki.utils import html_to_text_line, is_mac
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.addcards_legacy import *
from aqt.deckchooser import DeckChooser from aqt.deckchooser import DeckChooser
from aqt.notetypechooser import NotetypeChooser from aqt.notetypechooser import NotetypeChooser
from aqt.qt import * from aqt.qt import *
@ -30,7 +31,7 @@ from aqt.utils import (
) )
class AddCards(QMainWindow): class NewAddCards(QMainWindow):
def __init__(self, mw: AnkiQt) -> None: def __init__(self, mw: AnkiQt) -> None:
super().__init__(None, Qt.WindowType.Window) super().__init__(None, Qt.WindowType.Window)
self._close_event_has_cleaned_up = False self._close_event_has_cleaned_up = False
@ -79,7 +80,7 @@ class AddCards(QMainWindow):
self.setAndFocusNote(new_note) self.setAndFocusNote(new_note)
def setupEditor(self) -> None: def setupEditor(self) -> None:
self.editor = aqt.editor.Editor( self.editor = aqt.editor.NewEditor(
self.mw, self.mw,
self.form.fieldsArea, self.form.fieldsArea,
self, self,
@ -244,7 +245,7 @@ class AddCards(QMainWindow):
gui_hooks.operation_did_execute.remove(self.on_operation_did_execute) gui_hooks.operation_did_execute.remove(self.on_operation_did_execute)
self.mw.maybeReset() self.mw.maybeReset()
saveGeom(self, "add") saveGeom(self, "add")
aqt.dialogs.markClosed("AddCards") aqt.dialogs.markClosed("NewAddCards")
self._close_event_has_cleaned_up = True self._close_event_has_cleaned_up = True
self.mw.deferred_delete_and_garbage_collect(self) self.mw.deferred_delete_and_garbage_collect(self)
self.close() self.close()

414
qt/aqt/addcards_legacy.py Normal file
View file

@ -0,0 +1,414 @@
# 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
import aqt.editor
import aqt.forms
from anki._legacy import deprecated
from anki.collection import OpChanges, SearchNode
from anki.decks import DeckId
from anki.models import NotetypeId
from anki.notes import Note, NoteFieldsCheckResult, NoteId
from anki.utils import html_to_text_line, is_mac
from aqt import AnkiQt, gui_hooks
from aqt.deckchooser import DeckChooser
from aqt.notetypechooser import NotetypeChooser
from aqt.operations.note import add_note
from aqt.qt import *
from aqt.sound import av_player
from aqt.utils import (
HelpPage,
add_close_shortcut,
ask_user_dialog,
askUser,
downArrow,
openHelp,
restoreGeom,
saveGeom,
shortcut,
showWarning,
tooltip,
tr,
)
class AddCards(QMainWindow):
def __init__(self, mw: AnkiQt) -> None:
super().__init__(None, Qt.WindowType.Window)
self._close_event_has_cleaned_up = False
self.mw = mw
self.col = mw.col
form = aqt.forms.addcards.Ui_Dialog()
form.setupUi(self)
self.form = form
self.setWindowTitle(tr.actions_add())
self.setMinimumHeight(300)
self.setMinimumWidth(400)
self.setup_choosers()
self.setupEditor()
add_close_shortcut(self)
self._load_new_note()
self.setupButtons()
self.history: list[NoteId] = []
self._last_added_note: Note | None = None
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
restoreGeom(self, "add")
gui_hooks.add_cards_did_init(self)
if not is_mac:
self.setMenuBar(None)
self.show()
def set_deck(self, deck_id: DeckId) -> None:
self.deck_chooser.selected_deck_id = deck_id
def set_note_type(self, note_type_id: NotetypeId) -> None:
self.notetype_chooser.selected_notetype_id = note_type_id
def set_note(self, note: Note, deck_id: DeckId | None = None) -> None:
"""Set tags, field contents and notetype according to `note`. Deck is set
to `deck_id` or the deck last used with the notetype.
"""
self.notetype_chooser.selected_notetype_id = note.mid
if deck_id or (deck_id := self.col.default_deck_for_notetype(note.mid)):
self.deck_chooser.selected_deck_id = deck_id
new_note = self._new_note()
new_note.fields = note.fields[:]
new_note.tags = note.tags[:]
self.editor.orig_note_id = note.id
self.setAndFocusNote(new_note)
def setupEditor(self) -> None:
self.editor = aqt.editor.Editor(
self.mw,
self.form.fieldsArea,
self,
editor_mode=aqt.editor.EditorMode.ADD_CARDS,
)
def setup_choosers(self) -> None:
defaults = self.col.defaults_for_adding(
current_review_card=self.mw.reviewer.card
)
self.notetype_chooser = NotetypeChooser(
mw=self.mw,
widget=self.form.modelArea,
starting_notetype_id=NotetypeId(defaults.notetype_id),
on_button_activated=self.show_notetype_selector,
on_notetype_changed=self.on_notetype_change,
)
self.deck_chooser = DeckChooser(
self.mw,
self.form.deckArea,
starting_deck_id=DeckId(defaults.deck_id),
on_deck_changed=self.on_deck_changed,
)
def reopen(self, mw: AnkiQt) -> None:
if not self.editor.fieldsAreBlank():
return
defaults = self.col.defaults_for_adding(
current_review_card=self.mw.reviewer.card
)
self.set_note_type(NotetypeId(defaults.notetype_id))
self.set_deck(DeckId(defaults.deck_id))
def helpRequested(self) -> None:
openHelp(HelpPage.ADDING_CARD_AND_NOTE)
def setupButtons(self) -> None:
bb = self.form.buttonBox
ar = QDialogButtonBox.ButtonRole.ActionRole
# add
self.addButton = bb.addButton(tr.actions_add(), ar)
qconnect(self.addButton.clicked, self.add_current_note)
self.addButton.setShortcut(QKeySequence("Ctrl+Return"))
# qt5.14+ doesn't handle numpad enter on Windows
self.compat_add_shorcut = QShortcut(QKeySequence("Ctrl+Enter"), self)
qconnect(self.compat_add_shorcut.activated, self.addButton.click)
self.addButton.setToolTip(shortcut(tr.adding_add_shortcut_ctrlandenter()))
# close
self.closeButton = QPushButton(tr.actions_close())
self.closeButton.setAutoDefault(False)
bb.addButton(self.closeButton, QDialogButtonBox.ButtonRole.RejectRole)
qconnect(self.closeButton.clicked, self.close)
# help
self.helpButton = QPushButton(tr.actions_help(), clicked=self.helpRequested) # type: ignore
self.helpButton.setAutoDefault(False)
bb.addButton(self.helpButton, QDialogButtonBox.ButtonRole.HelpRole)
# history
b = bb.addButton(f"{tr.adding_history()} {downArrow()}", ar)
if is_mac:
sc = "Ctrl+Shift+H"
else:
sc = "Ctrl+H"
b.setShortcut(QKeySequence(sc))
b.setToolTip(tr.adding_shortcut(val=shortcut(sc)))
qconnect(b.clicked, self.onHistory)
b.setEnabled(False)
self.historyButton = b
def setAndFocusNote(self, note: Note) -> None:
self.editor.set_note(note, focusTo=0)
def show_notetype_selector(self) -> None:
self.editor.call_after_note_saved(self.notetype_chooser.choose_notetype)
def on_deck_changed(self, deck_id: int) -> None:
gui_hooks.add_cards_did_change_deck(deck_id)
def on_notetype_change(
self, notetype_id: NotetypeId, update_deck: bool = True
) -> None:
# need to adjust current deck?
if update_deck:
if deck_id := self.col.default_deck_for_notetype(notetype_id):
self.deck_chooser.selected_deck_id = deck_id
# only used for detecting changed sticky fields on close
self._last_added_note = None
# copy fields into new note with the new notetype
old_note = self.editor.note
new_note = self._new_note()
if old_note:
old_field_names = list(old_note.keys())
new_field_names = list(new_note.keys())
copied_field_names = set()
for f in new_note.note_type()["flds"]:
field_name = f["name"]
# copy identical non-empty fields
if field_name in old_field_names and old_note[field_name]:
new_note[field_name] = old_note[field_name]
copied_field_names.add(field_name)
new_idx = 0
for old_idx, old_field_value in enumerate(old_field_names):
# skip previously copied identical fields in new note
while (
new_idx < len(new_field_names)
and new_field_names[new_idx] in copied_field_names
):
new_idx += 1
if new_idx >= len(new_field_names):
break
# copy non-empty old fields
if (
old_field_value not in copied_field_names
and old_note.fields[old_idx]
):
new_note.fields[new_idx] = old_note.fields[old_idx]
new_idx += 1
new_note.tags = old_note.tags
# and update editor state
self.editor.note = new_note
self.editor.loadNote(
focusTo=min(self.editor.last_field_index or 0, len(new_note.fields) - 1)
)
gui_hooks.addcards_did_change_note_type(
self, old_note.note_type(), new_note.note_type()
)
def _load_new_note(self, sticky_fields_from: Note | None = None) -> None:
note = self._new_note()
if old_note := sticky_fields_from:
flds = note.note_type()["flds"]
# copy fields from old note
if old_note:
for n in range(min(len(note.fields), len(old_note.fields))):
if flds[n]["sticky"]:
note.fields[n] = old_note.fields[n]
# and tags
note.tags = old_note.tags
self.setAndFocusNote(note)
def on_operation_did_execute(
self, changes: OpChanges, handler: object | None
) -> None:
if (changes.notetype or changes.deck) and handler is not self.editor:
self.on_notetype_change(
NotetypeId(
self.col.defaults_for_adding(
current_review_card=self.mw.reviewer.card
).notetype_id
),
update_deck=False,
)
def _new_note(self) -> Note:
return self.col.new_note(
self.col.models.get(self.notetype_chooser.selected_notetype_id)
)
def addHistory(self, note: Note) -> None:
self.history.insert(0, note.id)
self.history = self.history[:15]
self.historyButton.setEnabled(True)
def onHistory(self) -> None:
m = QMenu(self)
for nid in self.history:
if self.col.find_notes(self.col.build_search_string(SearchNode(nid=nid))):
note = self.col.get_note(nid)
fields = note.fields
txt = html_to_text_line(", ".join(fields))
if len(txt) > 30:
txt = f"{txt[:30]}..."
line = tr.adding_edit(val=txt)
line = gui_hooks.addcards_will_add_history_entry(line, note)
line = line.replace("&", "&&")
# In qt action "&i" means "underline i, trigger this line when i is pressed".
# except for "&&" which is replaced by a single "&"
a = m.addAction(line)
qconnect(a.triggered, lambda b, nid=nid: self.editHistory(nid))
else:
a = m.addAction(tr.adding_note_deleted())
a.setEnabled(False)
gui_hooks.add_cards_will_show_history_menu(self, m)
m.exec(self.historyButton.mapToGlobal(QPoint(0, 0)))
def editHistory(self, nid: NoteId) -> None:
aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),))
def add_current_note(self) -> None:
if self.editor.current_notetype_is_image_occlusion():
self.editor.update_occlusions_field()
self.editor.call_after_note_saved(self._add_current_note)
self.editor.reset_image_occlusion()
else:
self.editor.call_after_note_saved(self._add_current_note)
def _add_current_note(self) -> None:
note = self.editor.note
if not self._note_can_be_added(note):
return
target_deck_id = self.deck_chooser.selected_deck_id
def on_success(changes: OpChanges) -> None:
# only used for detecting changed sticky fields on close
self._last_added_note = note
self.addHistory(note)
tooltip(tr.adding_added(), period=500)
av_player.stop_and_clear_queue()
self._load_new_note(sticky_fields_from=note)
gui_hooks.add_cards_did_add_note(note)
add_note(parent=self, note=note, target_deck_id=target_deck_id).success(
on_success
).run_in_background()
def _note_can_be_added(self, note: Note) -> bool:
result = note.fields_check()
# no problem, duplicate, and confirmed cloze cases
problem = None
if result == NoteFieldsCheckResult.EMPTY:
if self.editor.current_notetype_is_image_occlusion():
problem = tr.notetypes_no_occlusion_created2()
else:
problem = tr.adding_the_first_field_is_empty()
elif result == NoteFieldsCheckResult.MISSING_CLOZE:
if not askUser(tr.adding_you_have_a_cloze_deletion_note()):
return False
elif result == NoteFieldsCheckResult.NOTETYPE_NOT_CLOZE:
problem = tr.adding_cloze_outside_cloze_notetype()
elif result == NoteFieldsCheckResult.FIELD_NOT_CLOZE:
problem = tr.adding_cloze_outside_cloze_field()
# 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
optional_problems: list[str] = []
gui_hooks.add_cards_might_add_note(optional_problems, note)
if not all(askUser(op) for op in optional_problems):
return False
return True
def keyPressEvent(self, evt: QKeyEvent) -> None:
if evt.key() == Qt.Key.Key_Escape:
self.close()
else:
super().keyPressEvent(evt)
def closeEvent(self, evt: QCloseEvent) -> None:
if self._close_event_has_cleaned_up:
evt.accept()
return
self.ifCanClose(self._close)
evt.ignore()
def _close(self) -> None:
self.editor.cleanup()
self.notetype_chooser.cleanup()
self.deck_chooser.cleanup()
gui_hooks.operation_did_execute.remove(self.on_operation_did_execute)
self.mw.maybeReset()
saveGeom(self, "add")
aqt.dialogs.markClosed("AddCards")
self._close_event_has_cleaned_up = True
self.mw.deferred_delete_and_garbage_collect(self)
self.close()
def ifCanClose(self, onOk: Callable) -> None:
def callback(choice: int) -> None:
if choice == 0:
onOk()
def afterSave() -> None:
if self.editor.fieldsAreBlank(self._last_added_note):
return onOk()
ask_user_dialog(
tr.adding_discard_current_input(),
callback=callback,
buttons=[
QMessageBox.StandardButton.Discard,
(tr.adding_keep_editing(), QMessageBox.ButtonRole.RejectRole),
],
)
self.editor.call_after_note_saved(afterSave)
def closeWithCallback(self, cb: Callable[[], None]) -> None:
def doClose() -> None:
self._close()
cb()
self.ifCanClose(doClose)
# legacy aliases
@property
def deckChooser(self) -> DeckChooser:
if getattr(self, "form", None):
# show this warning only after Qt form has been initialized,
# or PyQt's introspection triggers it
print("deckChooser is deprecated; use deck_chooser instead")
return self.deck_chooser
addCards = add_current_note
_addCards = _add_current_note
onModelChange = on_notetype_change
@deprecated(info="obsolete")
def addNote(self, note: Note) -> None:
pass
@deprecated(info="does nothing; will go away")
def removeTempNote(self, note: Note) -> None:
pass

View file

@ -27,7 +27,6 @@ from anki.scheduler.base import ScheduleCardsAsNew
from anki.tags import MARKED_TAG from anki.tags import MARKED_TAG
from anki.utils import is_mac from anki.utils import is_mac
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.editor import Editor, EditorWebView
from aqt.errors import show_exception from aqt.errors import show_exception
from aqt.exporting import ExportDialog as LegacyExportDialog from aqt.exporting import ExportDialog as LegacyExportDialog
from aqt.import_export.exporting import ExportDialog from aqt.import_export.exporting import ExportDialog
@ -77,7 +76,7 @@ from aqt.utils import (
tr, tr,
) )
from ..addcards import AddCards from ..addcards import NewAddCards as AddCards
from ..changenotetype import change_notetype_dialog from ..changenotetype import change_notetype_dialog
from .card_info import BrowserCardInfo from .card_info import BrowserCardInfo
from .find_and_replace import FindAndReplaceDialog from .find_and_replace import FindAndReplaceDialog
@ -111,7 +110,7 @@ class MockModel:
class Browser(QMainWindow): class Browser(QMainWindow):
mw: AnkiQt mw: AnkiQt
col: Collection col: Collection
editor: Editor | None editor: aqt.editor.NewEditor | None
table: Table table: Table
def __init__( def __init__(
@ -267,7 +266,7 @@ class Browser(QMainWindow):
return None return None
def add_card(self, deck_id: DeckId): def add_card(self, deck_id: DeckId):
add_cards = cast(AddCards, aqt.dialogs.open("AddCards", self.mw)) add_cards = cast(AddCards, aqt.dialogs.open("NewAddCards", self.mw))
add_cards.set_deck(deck_id) add_cards.set_deck(deck_id)
if note_type_id := self.get_active_note_type_id(): if note_type_id := self.get_active_note_type_id():
@ -392,7 +391,7 @@ class Browser(QMainWindow):
add_ellipsis_to_action_label(f.action_forget) add_ellipsis_to_action_label(f.action_forget)
add_ellipsis_to_action_label(f.action_grade_now) add_ellipsis_to_action_label(f.action_grade_now)
def _editor_web_view(self) -> EditorWebView: def _editor_web_view(self) -> aqt.editor.NewEditorWebView:
assert self.editor is not None assert self.editor is not None
editor_web_view = self.editor.web editor_web_view = self.editor.web
assert editor_web_view is not None assert editor_web_view is not None
@ -592,12 +591,14 @@ class Browser(QMainWindow):
def setupEditor(self) -> None: def setupEditor(self) -> None:
QShortcut(QKeySequence("Ctrl+Shift+P"), self, self.onTogglePreview) QShortcut(QKeySequence("Ctrl+Shift+P"), self, self.onTogglePreview)
def add_preview_button(editor: Editor) -> None: def add_preview_button(
editor: aqt.editor.Editor | aqt.editor.NewEditor,
) -> None:
editor._links["preview"] = lambda _editor: self.onTogglePreview() editor._links["preview"] = lambda _editor: self.onTogglePreview()
gui_hooks.editor_did_init.remove(add_preview_button) gui_hooks.editor_did_init.remove(add_preview_button)
gui_hooks.editor_did_init.append(add_preview_button) gui_hooks.editor_did_init.append(add_preview_button)
self.editor = aqt.editor.Editor( self.editor = aqt.editor.NewEditor(
self.mw, self.mw,
self.form.fieldsArea, self.form.fieldsArea,
self, self,
@ -806,7 +807,7 @@ class Browser(QMainWindow):
assert current_card is not None assert current_card is not None
deck_id = current_card.current_deck_id() deck_id = current_card.current_deck_id()
aqt.dialogs.open("AddCards", self.mw).set_note(note, deck_id) aqt.dialogs.open("NewAddCards", self.mw).set_note(note, deck_id)
@no_arg_trigger @no_arg_trigger
@skip_if_selection_is_empty @skip_if_selection_is_empty
@ -1264,3 +1265,4 @@ class Browser(QMainWindow):
line_edit = self.form.searchEdit.lineEdit() line_edit = self.form.searchEdit.lineEdit()
assert line_edit is not None assert line_edit is not None
return line_edit return line_edit
return line_edit

View file

@ -7,11 +7,12 @@ from collections.abc import Callable
import aqt.editor import aqt.editor
from anki.collection import OpChanges from anki.collection import OpChanges
from aqt import gui_hooks from aqt import gui_hooks
from aqt.editcurrent_legacy import *
from aqt.qt import * from aqt.qt import *
from aqt.utils import add_close_shortcut, restoreGeom, saveGeom, tr from aqt.utils import add_close_shortcut, restoreGeom, saveGeom, tr
class EditCurrent(QMainWindow): class NewEditCurrent(QMainWindow):
def __init__(self, mw: aqt.AnkiQt) -> None: def __init__(self, mw: aqt.AnkiQt) -> None:
super().__init__(None, Qt.WindowType.Window) super().__init__(None, Qt.WindowType.Window)
self.mw = mw self.mw = mw
@ -22,7 +23,7 @@ class EditCurrent(QMainWindow):
self.setMinimumWidth(250) self.setMinimumWidth(250)
if not is_mac: if not is_mac:
self.setMenuBar(None) self.setMenuBar(None)
self.editor = aqt.editor.Editor( self.editor = aqt.editor.NewEditor(
self.mw, self.mw,
self.form.fieldsArea, self.form.fieldsArea,
self, self,
@ -46,7 +47,7 @@ class EditCurrent(QMainWindow):
gui_hooks.operation_did_execute.remove(self.on_operation_did_execute) gui_hooks.operation_did_execute.remove(self.on_operation_did_execute)
self.editor.cleanup() self.editor.cleanup()
saveGeom(self, "editcurrent") saveGeom(self, "editcurrent")
aqt.dialogs.markClosed("EditCurrent") aqt.dialogs.markClosed("NewEditCurrent")
def reopen(self, mw: aqt.AnkiQt) -> None: def reopen(self, mw: aqt.AnkiQt) -> None:
if card := self.mw.reviewer.card: if card := self.mw.reviewer.card:

View file

@ -0,0 +1,94 @@
# 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
import aqt.editor
from anki.collection import OpChanges
from anki.errors import NotFoundError
from aqt import gui_hooks
from aqt.qt import *
from aqt.utils import add_close_shortcut, restoreGeom, saveGeom, tr
class EditCurrent(QMainWindow):
def __init__(self, mw: aqt.AnkiQt) -> None:
super().__init__(None, Qt.WindowType.Window)
self.mw = mw
self.form = aqt.forms.editcurrent.Ui_Dialog()
self.form.setupUi(self)
self.setWindowTitle(tr.editing_edit_current())
self.setMinimumHeight(400)
self.setMinimumWidth(250)
if not is_mac:
self.setMenuBar(None)
self.editor = aqt.editor.Editor(
self.mw,
self.form.fieldsArea,
self,
editor_mode=aqt.editor.EditorMode.EDIT_CURRENT,
)
assert self.mw.reviewer.card is not None
self.editor.card = self.mw.reviewer.card
self.editor.set_note(self.mw.reviewer.card.note(), focusTo=0)
restoreGeom(self, "editcurrent")
self.buttonbox = QDialogButtonBox(Qt.Orientation.Horizontal)
self.form.verticalLayout.insertWidget(1, self.buttonbox)
self.buttonbox.addButton(QDialogButtonBox.StandardButton.Close)
qconnect(self.buttonbox.rejected, self.close)
close_button = self.buttonbox.button(QDialogButtonBox.StandardButton.Close)
assert close_button is not None
close_button.setShortcut(QKeySequence("Ctrl+Return"))
add_close_shortcut(self)
# qt5.14+ doesn't handle numpad enter on Windows
self.compat_add_shorcut = QShortcut(QKeySequence("Ctrl+Enter"), self)
qconnect(self.compat_add_shorcut.activated, close_button.click)
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
self.show()
def on_operation_did_execute(
self, changes: OpChanges, handler: object | None
) -> None:
if changes.note_text and handler is not self.editor:
# reload note
note = self.editor.note
try:
assert note is not None
note.load()
except NotFoundError:
# note's been deleted
self.cleanup()
self.close()
return
self.editor.set_note(note)
def cleanup(self) -> None:
gui_hooks.operation_did_execute.remove(self.on_operation_did_execute)
self.editor.cleanup()
saveGeom(self, "editcurrent")
aqt.dialogs.markClosed("EditCurrent")
def reopen(self, mw: aqt.AnkiQt) -> None:
if card := self.mw.reviewer.card:
self.editor.card = card
self.editor.set_note(card.note())
def closeEvent(self, evt: QCloseEvent | None) -> None:
self.editor.call_after_note_saved(self.cleanup)
def _saveAndClose(self) -> None:
self.cleanup()
self.mw.deferred_delete_and_garbage_collect(self)
self.close()
def closeWithCallback(self, onsuccess: Callable[[], None]) -> None:
def callback() -> None:
self._saveAndClose()
onsuccess()
self.editor.call_after_note_saved(callback)
onReset = on_operation_did_execute
onReset = on_operation_did_execute

View file

@ -10,7 +10,6 @@ import mimetypes
import os import os
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from random import randrange from random import randrange
from typing import Any from typing import Any
@ -21,58 +20,16 @@ from anki.models import NotetypeId
from anki.notes import Note, NoteId from anki.notes import Note, NoteId
from anki.utils import is_win from anki.utils import is_win
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.editor_legacy import *
from aqt.qt import * from aqt.qt import *
from aqt.sound import av_player from aqt.sound import av_player
from aqt.utils import shortcut, showWarning from aqt.utils import shortcut, showWarning
from aqt.webview import AnkiWebView, AnkiWebViewKind from aqt.webview import AnkiWebView, AnkiWebViewKind
pics = ("jpg", "jpeg", "png", "gif", "svg", "webp", "ico", "avif")
audio = (
"3gp",
"aac",
"avi",
"flac",
"flv",
"m4a",
"mkv",
"mov",
"mp3",
"mp4",
"mpeg",
"mpg",
"oga",
"ogg",
"ogv",
"ogx",
"opus",
"spx",
"swf",
"wav",
"webm",
)
class EditorMode(Enum):
ADD_CARDS = 0
EDIT_CURRENT = 1
BROWSER = 2
class EditorState(Enum):
"""
Current input state of the editing UI.
"""
INITIAL = -1
FIELDS = 0
IO_PICKER = 1
IO_MASKS = 2
IO_FIELDS = 3
def on_editor_ready(func: Callable) -> Callable: def on_editor_ready(func: Callable) -> Callable:
@functools.wraps(func) @functools.wraps(func)
def decorated(self: Editor, *args: Any, **kwargs: Any) -> None: def decorated(self: NewEditor, *args: Any, **kwargs: Any) -> None:
if self._ready: if self._ready:
func(self, *args, **kwargs) func(self, *args, **kwargs)
else: else:
@ -96,7 +53,7 @@ class NoteInfo:
self.mid = NotetypeId(int(self.mid)) self.mid = NotetypeId(int(self.mid))
class Editor: class NewEditor:
"""The screen that embeds an editing widget should listen for changes via """The screen that embeds an editing widget should listen for changes via
the `operation_did_execute` hook, and call set_note() when the editor needs the `operation_did_execute` hook, and call set_note() when the editor needs
redrawing. redrawing.
@ -152,7 +109,7 @@ class Editor:
self.outerLayout = l self.outerLayout = l
def add_webview(self) -> None: def add_webview(self) -> None:
self.web = EditorWebView(self.widget, self) self.web = NewEditorWebView(self.widget, self)
self.web.set_bridge_command(self.onBridgeCmd, self) self.web.set_bridge_command(self.onBridgeCmd, self)
self.web.hide_while_preserving_layout() self.web.hide_while_preserving_layout()
self.outerLayout.addWidget(self.web, 1) self.outerLayout.addWidget(self.web, 1)
@ -213,7 +170,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
self, self,
icon: str | None, icon: str | None,
cmd: str, cmd: str,
func: Callable[[Editor], None], func: Callable[[NewEditor], None],
tip: str = "", tip: str = "",
label: str = "", label: str = "",
id: str | None = None, id: str | None = None,
@ -224,7 +181,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
) -> str: ) -> str:
"""Assign func to bridge cmd, register shortcut, return button""" """Assign func to bridge cmd, register shortcut, return button"""
def wrapped_func(editor: Editor) -> None: def wrapped_func(editor: NewEditor) -> None:
self.call_after_note_saved(functools.partial(func, editor), keepFocus=True) self.call_after_note_saved(functools.partial(func, editor), keepFocus=True)
self._links[cmd] = wrapped_func self._links[cmd] = wrapped_func
@ -553,11 +510,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
def _init_links(self) -> None: def _init_links(self) -> None:
self._links: dict[str, Callable] = dict( self._links: dict[str, Callable] = dict(
fields=Editor.onFields, fields=NewEditor.onFields,
cards=Editor.onCardLayout, cards=NewEditor.onCardLayout,
paste=Editor.onPaste, paste=NewEditor.onPaste,
cut=Editor.onCut, cut=NewEditor.onCut,
copy=Editor.onCopy, copy=NewEditor.onCopy,
) )
def get_note_info(self, on_done: Callable[[NoteInfo], None]) -> None: def get_note_info(self, on_done: Callable[[NoteInfo], None]) -> None:
@ -571,8 +528,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
###################################################################### ######################################################################
class EditorWebView(AnkiWebView): class NewEditorWebView(AnkiWebView):
def __init__(self, parent: QWidget, editor: Editor) -> None: def __init__(self, parent: QWidget, editor: NewEditor) -> None:
AnkiWebView.__init__(self, kind=AnkiWebViewKind.EDITOR) AnkiWebView.__init__(self, kind=AnkiWebViewKind.EDITOR)
self.editor = editor self.editor = editor
self.setAcceptDrops(True) self.setAcceptDrops(True)
@ -592,3 +549,4 @@ class EditorWebView(AnkiWebView):
def onPaste(self) -> None: def onPaste(self) -> None:
self.triggerPageAction(QWebEnginePage.WebAction.Paste) self.triggerPageAction(QWebEnginePage.WebAction.Paste)
self.triggerPageAction(QWebEnginePage.WebAction.Paste)

1790
qt/aqt/editor_legacy.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -1280,14 +1280,20 @@ title="{}" {}>{}</button>""".format(
# Other menu operations # Other menu operations
########################################################################## ##########################################################################
def _open_new_or_legacy_dialog(self, name: str, *args: Any, **kwargs: Any) -> None:
want_old = KeyboardModifiersPressed().shift
if not want_old:
name = f"New{name}"
aqt.dialogs.open(name, self, *args, **kwargs)
def onAddCard(self) -> None: def onAddCard(self) -> None:
aqt.dialogs.open("AddCards", self) self._open_new_or_legacy_dialog("AddCards")
def onBrowse(self) -> None: def onBrowse(self) -> None:
aqt.dialogs.open("Browser", self, card=self.reviewer.card) aqt.dialogs.open("Browser", self, card=self.reviewer.card)
def onEditCurrent(self) -> None: def onEditCurrent(self) -> None:
aqt.dialogs.open("EditCurrent", self) self._open_new_or_legacy_dialog("EditCurrent")
def onOverview(self) -> None: def onOverview(self) -> None:
self.moveToState("overview") self.moveToState("overview")
@ -1296,11 +1302,7 @@ title="{}" {}>{}</button>""".format(
deck = self._selectedDeck() deck = self._selectedDeck()
if not deck: if not deck:
return return
want_old = KeyboardModifiersPressed().shift self._open_new_or_legacy_dialog("DeckStats", self)
if want_old:
aqt.dialogs.open("DeckStats", self)
else:
aqt.dialogs.open("NewDeckStats", self)
def onPrefs(self) -> None: def onPrefs(self) -> None:
aqt.dialogs.open("Preferences", self) aqt.dialogs.open("Preferences", self)

View file

@ -608,10 +608,10 @@ def editor_op_changes_request(endpoint: str) -> bytes:
response.ParseFromString(output) response.ParseFromString(output)
def handle_on_main() -> None: def handle_on_main() -> None:
from aqt.editor import Editor from aqt.editor import NewEditor
handler = aqt.mw.app.activeWindow() handler = aqt.mw.app.activeWindow()
if handler and isinstance(getattr(handler, "editor", None), Editor): if handler and isinstance(getattr(handler, "editor", None), NewEditor):
handler = handler.editor # type: ignore handler = handler.editor # type: ignore
on_op_finished(aqt.mw, response, handler) on_op_finished(aqt.mw, response, handler)
@ -808,10 +808,10 @@ def close_add_cards() -> bytes:
req.ParseFromString(request.data) req.ParseFromString(request.data)
def handle_on_main() -> None: def handle_on_main() -> None:
from aqt.addcards import AddCards from aqt.addcards import NewAddCards
window = aqt.mw.app.activeWindow() window = aqt.mw.app.activeWindow()
if isinstance(window, AddCards): if isinstance(window, NewAddCards):
window._close_if_user_wants_to_discard_changes(req.val) window._close_if_user_wants_to_discard_changes(req.val)
aqt.mw.taskman.run_on_main(lambda: QTimer.singleShot(0, handle_on_main)) aqt.mw.taskman.run_on_main(lambda: QTimer.singleShot(0, handle_on_main))
@ -820,10 +820,10 @@ def close_add_cards() -> bytes:
def close_edit_current() -> bytes: def close_edit_current() -> bytes:
def handle_on_main() -> None: def handle_on_main() -> None:
from aqt.editcurrent import EditCurrent from aqt.editcurrent import NewEditCurrent
window = aqt.mw.app.activeWindow() window = aqt.mw.app.activeWindow()
if isinstance(window, EditCurrent): if isinstance(window, NewEditCurrent):
window.close() window.close()
aqt.mw.taskman.run_on_main(lambda: QTimer.singleShot(0, handle_on_main)) aqt.mw.taskman.run_on_main(lambda: QTimer.singleShot(0, handle_on_main))
@ -1070,3 +1070,5 @@ def _extract_dynamic_get_request(path: str) -> DynamicRequest | None:
return legacy_page_data return legacy_page_data
else: else:
return None return None
return None
return None

View file

@ -1008,12 +1008,15 @@ hooks = [
################### ###################
Hook( Hook(
name="add_cards_will_show_history_menu", name="add_cards_will_show_history_menu",
args=["addcards: aqt.addcards.AddCards", "menu: QMenu"], args=[
"addcards: aqt.addcards.AddCards | aqt.addcards.NewAddCards",
"menu: QMenu",
],
legacy_hook="AddCards.onHistory", legacy_hook="AddCards.onHistory",
), ),
Hook( Hook(
name="add_cards_did_init", name="add_cards_did_init",
args=["addcards: aqt.addcards.AddCards"], args=["addcards: aqt.addcards.AddCards | aqt.addcards.NewAddCards"],
), ),
Hook( Hook(
name="add_cards_did_add_note", name="add_cards_did_add_note",
@ -1068,7 +1071,7 @@ hooks = [
Hook( Hook(
name="addcards_did_change_note_type", name="addcards_did_change_note_type",
args=[ args=[
"addcards: aqt.addcards.AddCards", "addcards: aqt.addcards.AddCards | aqt.addcards.NewAddCards",
"old: anki.models.NoteType", "old: anki.models.NoteType",
"new: anki.models.NoteType", "new: anki.models.NoteType",
], ],
@ -1087,20 +1090,26 @@ hooks = [
################### ###################
Hook( Hook(
name="editor_did_init_left_buttons", name="editor_did_init_left_buttons",
args=["buttons: list[str]", "editor: aqt.editor.Editor"], args=["buttons: list[str]", "editor: aqt.editor.Editor | aqt.editor.NewEditor"],
), ),
Hook( Hook(
name="editor_did_init_buttons", name="editor_did_init_buttons",
args=["buttons: list[str]", "editor: aqt.editor.Editor"], args=["buttons: list[str]", "editor: aqt.editor.Editor | aqt.editor.NewEditor"],
), ),
Hook( Hook(
name="editor_did_init_shortcuts", name="editor_did_init_shortcuts",
args=["shortcuts: list[tuple]", "editor: aqt.editor.Editor"], args=[
"shortcuts: list[tuple]",
"editor: aqt.editor.Editor | aqt.editor.NewEditor",
],
legacy_hook="setupEditorShortcuts", legacy_hook="setupEditorShortcuts",
), ),
Hook( Hook(
name="editor_will_show_context_menu", name="editor_will_show_context_menu",
args=["editor_webview: aqt.editor.EditorWebView", "menu: QMenu"], args=[
"editor_webview: aqt.editor.EditorWebView | aqt.editor.NewEditorWebView",
"menu: QMenu",
],
legacy_hook="EditorWebView.contextMenuEvent", legacy_hook="EditorWebView.contextMenuEvent",
), ),
Hook( Hook(
@ -1121,7 +1130,7 @@ hooks = [
), ),
Hook( Hook(
name="editor_did_load_note", name="editor_did_load_note",
args=["editor: aqt.editor.Editor"], args=["editor: aqt.editor.Editor | aqt.editor.NewEditor"],
legacy_hook="loadNote", legacy_hook="loadNote",
), ),
Hook( Hook(
@ -1131,7 +1140,7 @@ hooks = [
), ),
Hook( Hook(
name="editor_will_munge_html", name="editor_will_munge_html",
args=["txt: str", "editor: aqt.editor.Editor"], args=["txt: str", "editor: aqt.editor.Editor | aqt.editor.NewEditor"],
return_type="str", return_type="str",
doc="""Allows manipulating the text that will be saved by the editor""", doc="""Allows manipulating the text that will be saved by the editor""",
), ),
@ -1143,15 +1152,21 @@ hooks = [
), ),
Hook( Hook(
name="editor_web_view_did_init", name="editor_web_view_did_init",
args=["editor_web_view: aqt.editor.EditorWebView"], args=[
"editor_web_view: aqt.editor.EditorWebView | aqt.editor.NewEditorWebView"
],
), ),
Hook( Hook(
name="editor_did_init", name="editor_did_init",
args=["editor: aqt.editor.Editor"], args=["editor: aqt.editor.Editor | aqt.editor.NewEditor"],
), ),
Hook( Hook(
name="editor_will_load_note", name="editor_will_load_note",
args=["js: str", "note: anki.notes.Note", "editor: aqt.editor.Editor"], args=[
"js: str",
"note: anki.notes.Note",
"editor: aqt.editor.Editor | aqt.editor.NewEditor",
],
return_type="str", return_type="str",
doc="""Allows changing the javascript commands to load note before doc="""Allows changing the javascript commands to load note before
executing it and do change in the QT editor.""", executing it and do change in the QT editor.""",
@ -1159,7 +1174,7 @@ hooks = [
Hook( Hook(
name="editor_did_paste", name="editor_did_paste",
args=[ args=[
"editor: aqt.editor.Editor", "editor: aqt.editor.Editor | aqt.editor.NewEditor",
"html: str", "html: str",
"internal: bool", "internal: bool",
"extended: bool", "extended: bool",
@ -1170,7 +1185,7 @@ hooks = [
name="editor_will_process_mime", name="editor_will_process_mime",
args=[ args=[
"mime: QMimeData", "mime: QMimeData",
"editor_web_view: aqt.editor.EditorWebView", "editor_web_view: aqt.editor.EditorWebView | aqt.editor.NewEditorWebView",
"internal: bool", "internal: bool",
"extended: bool", "extended: bool",
"drop_event: bool", "drop_event: bool",
@ -1194,7 +1209,7 @@ hooks = [
Hook( Hook(
name="editor_state_did_change", name="editor_state_did_change",
args=[ args=[
"editor: aqt.editor.Editor", "editor: aqt.editor.Editor | aqt.editor.NewEditor",
"new_state: aqt.editor.EditorState", "new_state: aqt.editor.EditorState",
"old_state: aqt.editor.EditorState", "old_state: aqt.editor.EditorState",
], ],
@ -1203,7 +1218,10 @@ hooks = [
), ),
Hook( Hook(
name="editor_mask_editor_did_load_image", name="editor_mask_editor_did_load_image",
args=["editor: aqt.editor.Editor", "path_or_nid: str | anki.notes.NoteId"], args=[
"editor: aqt.editor.Editor | aqt.editor.NewEditor",
"path_or_nid: str | anki.notes.NoteId",
],
doc="""Called when the image occlusion mask editor has completed doc="""Called when the image occlusion mask editor has completed
loading an image. loading an image.