anki/qt/aqt/addcards.py
Henrik Giesel c2768e2188
Translate Editor entirely to Svelte (#1403)
* Translate editor to Svelte

Make editor fields grid rather than flexbox

Refactor ButtonToolbar margins

Remove remaining svelte.d.ts symlinks

Implement saveNow

Fix text surrounding

Remove HTML editor button

Clean up some empty files

Add visual for new field state badges

* Adds new IconConstrain.svelte to generalize the icon handling for
IconButton and Badge

Implement sticky functionality again

Enable Editable and Codable field state badges

Add shortcuts to FieldState badges

Add Shift+F9 shortcut back

Add inline padding back to editor fields, tag editor and toolbar

Make Editable and Codable only "visually hidden"

This way they are still updated in the background
Otherwise reshowing them will always start them up empty

Make empty editing area focusable

Start with moving fieldsKey and currentFieldKey to context.ts

Fix Codable being wrong size when opening for first time

Add back drag'n'drop

Make ButtonItem display: contents again

* This will break the gap between ButtonGroup items, however once we
  have a newer Chromium version we should use CSS gap property anyway

Fix most of typing issues

Use --label-color background color LabelContainer

Add back red color for dupes

Generalize the editor toolbar in the multiroot editor to widgets

Implement Notification.svelte for showing cloze hints

Add colorful icon to notification

Hook up Editable to EditingArea

Move EditingArea into EditorField

Include editorField in editor/context

Fix rebasing issues

Uniformly use SvelteComponentTyped

Take LabelContainer out of EditingArea

Use mirror-dom and node-store to export editable content

Fix editable update mechanism

Prepare passing the editing inputs as slots

Pass in editing inputs as slots

Use codable options again in codemirror

Delete editor/lib.ts

Remove CodableAdapter, Use more generic CodeMirror component

Fix clicking LabelContainer to focus

Use prettier

Rename Editable to ContentEditable

Fix writing Mathjax from Codable to Editable

Correctly adjust output HTML from editable

Refactor EditableStyles out of EditableContainer

Pass Image and Mathjax Handle via slots to Editable

Make Editable add its editingInputApi

Make Editable hideable

Fix font size not being set correctly

Refactor both fieldFocused and focusInCodable to focusInEditable

Fix focusIfField

Bring back $activeInput

Fix ClozeButton

Remove signifyCustomInput

Refactor MathjaxHandle

Refactor out some logic into store-subscribe

Fix Mathjax editor

Use focusTrap instead of focusing div

Delegate focus back to editingInput when refocusing focusTrap

Elegantly move focus between editing inputs when closing/opening

Make Codable tabbable

Automatically move caret to end on editable and codable

+ remove from editingInput api

Fix ButtonDropdown having two rows and missing button margins

Make svelte_check and eslint pass

Satisfy editor svelte_check

Save field updates to db again

Await editable styles before mounting content editable

Remove unused import from OldEditorAdapter

Add copyright header to OldEditorAdapter

Update button active state from contenteditable

* Use activateStickyShortcuts after waiting for noteEditorPromise

* Set fields via stores, make tags correctly set

* Add explaining comment to setFields

* Fix ClozeButton

* Send focus and blur events again

* Fix Codable not correctly updating on blur with invalid HTML

* Remove old code for special Enter behavior in tags

* Do not use logical properties for ButtonToolbar margins

* Remove getCurrentField

Instead use noteEditor->currentField or noteEditor->activeInput

* Remove Extensible type

* Use context-property for NoteEditor, EditorField and EditingArea

* Rename parameter in mirror-dom.allowResubscription

* Fix cutOrCopy

* Refactor context.ts into the individual components

* Move focusing of editingArea up to editorField

* Rename promiseResolve -> promiseWithResolver

* Rename Editable->RichTextInput and Codable->PlainTextInput

* Remove now unnecessary type assertion for `getNoteEditor` and `getEditingArea`

* Refocus field after adding, so subscription to editing area is refreshed
2021-10-18 22:01:15 +10:00

318 lines
12 KiB
Python

# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import Callable, Optional
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 htmlToTextLine, isMac
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,
addCloseShortcut,
askUser,
disable_help_button,
downArrow,
openHelp,
restoreGeom,
saveGeom,
shortcut,
showWarning,
tooltip,
tr,
)
class AddCards(QDialog):
def __init__(self, mw: AnkiQt) -> None:
QDialog.__init__(self, None, Qt.WindowType.Window)
mw.garbage_collect_on_dialog_finish(self)
self.mw = mw
self.col = mw.col
form = aqt.forms.addcards.Ui_Dialog()
form.setupUi(self)
self.form = form
self.setWindowTitle(tr.actions_add())
disable_help_button(self)
self.setMinimumHeight(300)
self.setMinimumWidth(400)
self.setup_choosers()
self.setupEditor()
self.setupButtons()
self._load_new_note()
self.history: list[NoteId] = []
self._last_added_note: Optional[Note] = None
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
restoreGeom(self, "add")
addCloseShortcut(self)
gui_hooks.add_cards_did_init(self)
self.show()
def setupEditor(self) -> None:
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self, True)
self.editor.web.eval("noteEditorPromise.then(() => activateStickyShortcuts());")
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 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)
# 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 isMac:
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) -> None:
# need to adjust current 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 = self.editor.note
new = self._new_note()
if old:
old_fields = list(old.keys())
new_fields = list(new.keys())
for n, f in enumerate(new.note_type()["flds"]):
field_name = f["name"]
# copy identical fields
if field_name in old_fields:
new[field_name] = old[field_name]
elif n < len(old.note_type()["flds"]):
# set non-identical fields by field index
old_field_name = old.note_type()["flds"][n]["name"]
if old_field_name not in new_fields:
new.fields[n] = old.fields[n]
new.tags = old.tags
# and update editor state
self.editor.note = new
self.editor.loadNote(
focusTo=min(self.editor.last_field_index or 0, len(new.fields) - 1)
)
gui_hooks.add_cards_did_change_note_type(old.note_type(), new.note_type())
def _load_new_note(self, sticky_fields_from: Optional[Note] = 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: Optional[object]
) -> 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
)
)
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 = htmlToTextLine(", ".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:
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:
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
return True
def reject(self) -> None:
self.ifCanClose(self._reject)
def _reject(self) -> None:
av_player.stop_and_clear_queue()
self.editor.cleanup()
self.notetype_chooser.cleanup()
gui_hooks.operation_did_execute.remove(self.on_operation_did_execute)
self.mw.maybeReset()
saveGeom(self, "add")
aqt.dialogs.markClosed("AddCards")
QDialog.reject(self)
def ifCanClose(self, onOk: Callable) -> None:
def afterSave() -> None:
ok = self.editor.fieldsAreBlank(self._last_added_note) or askUser(
tr.adding_close_and_lose_current_input(), defaultno=True
)
if ok:
onOk()
self.editor.call_after_note_saved(afterSave)
def closeWithCallback(self, cb: Callable[[], None]) -> None:
def doClose() -> None:
self._reject()
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