anki/qt/aqt/fields.py

314 lines
10 KiB
Python
Raw Normal View History

2019-02-05 04:59:03 +01:00
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import os
from typing import Optional
import aqt
import aqt.forms
import aqt.operations
from anki.collection import OpChanges
2019-12-20 10:19:03 +01:00
from anki.consts import *
from anki.lang import without_unicode_isolation
from anki.models import NotetypeDict
from aqt import AnkiQt, gui_hooks
from aqt.operations.notetype import update_notetype_legacy
2019-12-20 10:19:03 +01:00
from aqt.qt import *
from aqt.schema_change_tracker import ChangeTracker
from aqt.utils import (
HelpPage,
askUser,
disable_help_button,
getOnlyText,
openHelp,
showWarning,
tooltip,
tr,
)
from aqt.webview import AnkiWebViewKind
2019-12-20 10:19:03 +01:00
class FieldDialog(QDialog):
def __init__(
2021-06-16 15:40:48 +02:00
self,
mw: AnkiQt,
nt: NotetypeDict,
parent: Optional[QWidget] = None,
2021-06-16 17:04:11 +02:00
open_at: int = 0,
) -> None:
QDialog.__init__(self, parent or mw)
mw.garbage_collect_on_dialog_finish(self)
self.mw = mw
self.col = self.mw.col
self.mm = self.mw.col.models
self.model = nt
self.mm._remove_from_cache(self.model["id"])
self.change_tracker = ChangeTracker(self.mw)
self.setWindowTitle(
without_unicode_isolation(tr.fields_fields_for(val=self.model["name"]))
)
if os.getenv("ANKI_EXPERIMENTAL_FIELDS_WEB"):
form = aqt.forms.fields_web.Ui_Dialog()
form.setupUi(self)
self.webview = form.webview
self.webview.set_kind(AnkiWebViewKind.FIELDS)
self.show()
self.refresh()
self.webview.set_bridge_command(self._on_bridge_cmd, self)
self.activateWindow()
return
self.form = aqt.forms.fields.Ui_Dialog()
self.form.setupUi(self)
self.webview = None
disable_help_button(self)
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help).setAutoDefault(
False
)
self.form.buttonBox.button(
QDialogButtonBox.StandardButton.Cancel
).setAutoDefault(False)
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Save).setAutoDefault(
False
)
self.currentIdx: Optional[int] = None
self.fillFields()
self.setupSignals()
self.form.fieldList.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
self.form.fieldList.dropEvent = self.onDrop # type: ignore[assignment]
2021-06-16 15:40:48 +02:00
self.form.fieldList.setCurrentRow(open_at)
self.exec()
def refresh(self) -> None:
self.webview.load_ts_page("fields")
def _on_bridge_cmd(self, cmd: str) -> bool:
return False
##########################################################################
def fillFields(self) -> None:
self.currentIdx = None
self.form.fieldList.clear()
2019-12-23 01:34:10 +01:00
for c, f in enumerate(self.model["flds"]):
self.form.fieldList.addItem(f"{c + 1}: {f['name']}")
def setupSignals(self) -> None:
f = self.form
qconnect(f.fieldList.currentRowChanged, self.onRowChange)
qconnect(f.fieldAdd.clicked, self.onAdd)
qconnect(f.fieldDelete.clicked, self.onDelete)
qconnect(f.fieldRename.clicked, self.onRename)
qconnect(f.fieldPosition.clicked, self.onPosition)
qconnect(f.sortField.clicked, self.onSortField)
qconnect(f.buttonBox.helpRequested, self.onHelp)
def onDrop(self, ev: QDropEvent) -> None:
fieldList = self.form.fieldList
indicatorPos = fieldList.dropIndicatorPosition()
if qtmajor == 5:
pos = ev.pos() # type: ignore
else:
pos = ev.position().toPoint()
dropPos = fieldList.indexAt(pos).row()
2020-05-26 11:27:38 +02:00
idx = self.currentIdx
if dropPos == idx:
return
if (
indicatorPos == QAbstractItemView.DropIndicatorPosition.OnViewport
): # to bottom.
2020-05-26 11:27:38 +02:00
movePos = fieldList.count() - 1
elif indicatorPos == QAbstractItemView.DropIndicatorPosition.AboveItem:
2020-05-26 11:27:38 +02:00
movePos = dropPos
elif indicatorPos == QAbstractItemView.DropIndicatorPosition.BelowItem:
2020-05-26 11:27:38 +02:00
movePos = dropPos + 1
# the item in idx is removed thus subtract 1.
if idx < dropPos:
movePos -= 1
self.moveField(movePos + 1) # convert to 1 based.
def onRowChange(self, idx: int) -> None:
if idx == -1:
return
self.saveField()
self.loadField(idx)
def _uniqueName(
self, prompt: str, ignoreOrd: Optional[int] = None, old: str = ""
) -> Optional[str]:
txt = getOnlyText(prompt, default=old).replace('"', "").strip()
if not txt:
return None
if txt[0] in "#^/":
2021-03-26 04:48:26 +01:00
showWarning(tr.fields_name_first_letter_not_valid())
return None
for letter in """:{"}""":
if letter in txt:
2021-03-26 04:48:26 +01:00
showWarning(tr.fields_name_invalid_letter())
return None
2019-12-23 01:34:10 +01:00
for f in self.model["flds"]:
if ignoreOrd is not None and f["ord"] == ignoreOrd:
continue
2019-12-23 01:34:10 +01:00
if f["name"] == txt:
2021-03-26 04:48:26 +01:00
showWarning(tr.fields_that_field_name_is_already_used())
return None
return txt
2021-02-01 14:28:21 +01:00
def onRename(self) -> None:
idx = self.currentIdx
2019-12-23 01:34:10 +01:00
f = self.model["flds"][idx]
2021-03-26 04:48:26 +01:00
name = self._uniqueName(tr.actions_new_name(), self.currentIdx, f["name"])
if not name:
return
2020-12-01 02:28:10 +01:00
old_name = f["name"]
self.change_tracker.mark_basic()
self.mm.rename_field(self.model, f, name)
2020-12-01 02:28:10 +01:00
gui_hooks.fields_did_rename_field(self, f, old_name)
self.saveField()
self.fillFields()
self.form.fieldList.setCurrentRow(idx)
def onAdd(self) -> None:
2021-03-26 04:48:26 +01:00
name = self._uniqueName(tr.fields_field_name())
if not name:
return
if not self.change_tracker.mark_schema():
return
self.saveField()
2021-06-27 05:49:58 +02:00
f = self.mm.new_field(name)
self.mm.add_field(self.model, f)
gui_hooks.fields_did_add_field(self, f)
self.fillFields()
2019-12-23 01:34:10 +01:00
self.form.fieldList.setCurrentRow(len(self.model["flds"]) - 1)
2021-02-01 14:28:21 +01:00
def onDelete(self) -> None:
2019-12-23 01:34:10 +01:00
if len(self.model["flds"]) < 2:
2021-03-26 04:48:26 +01:00
showWarning(tr.fields_notes_require_at_least_one_field())
2021-02-01 14:28:21 +01:00
return
2021-06-27 05:49:58 +02:00
count = self.mm.use_count(self.model)
c = tr.browsing_note_count(count=count)
if not askUser(tr.fields_delete_field_from(val=c)):
return
if not self.change_tracker.mark_schema():
return
2019-12-23 01:34:10 +01:00
f = self.model["flds"][self.form.fieldList.currentRow()]
self.mm.remove_field(self.model, f)
gui_hooks.fields_did_delete_field(self, f)
self.fillFields()
self.form.fieldList.setCurrentRow(0)
def onPosition(self, delta: int = -1) -> None:
idx = self.currentIdx
2019-12-23 01:34:10 +01:00
l = len(self.model["flds"])
txt = getOnlyText(tr.fields_new_position_1(val=l), default=str(idx + 1))
if not txt:
return
try:
pos = int(txt)
except ValueError:
return
if not 0 < pos <= l:
return
self.moveField(pos)
2021-02-01 14:28:21 +01:00
def onSortField(self) -> None:
if not self.change_tracker.mark_schema():
2021-02-01 14:28:21 +01:00
return
# don't allow user to disable; it makes no sense
self.form.sortField.setChecked(True)
self.mm.set_sort_index(self.model, self.form.fieldList.currentRow())
def moveField(self, pos: int) -> None:
if not self.change_tracker.mark_schema():
2021-02-01 14:28:21 +01:00
return
self.saveField()
2019-12-23 01:34:10 +01:00
f = self.model["flds"][self.currentIdx]
self.mm.reposition_field(self.model, f, pos - 1)
self.fillFields()
2019-12-23 01:34:10 +01:00
self.form.fieldList.setCurrentRow(pos - 1)
def loadField(self, idx: int) -> None:
self.currentIdx = idx
2019-12-23 01:34:10 +01:00
fld = self.model["flds"][idx]
f = self.form
2019-12-23 01:34:10 +01:00
f.fontFamily.setCurrentFont(QFont(fld["font"]))
f.fontSize.setValue(fld["size"])
f.sortField.setChecked(self.model["sortf"] == fld["ord"])
f.rtl.setChecked(fld["rtl"])
f.plainTextByDefault.setChecked(fld["plainText"])
f.collapseByDefault.setChecked(fld["collapsed"])
Add option to exclude fields from search (#2394) * Add option to exclude fields from unqualified searches * Use temp tables instead This is slightly faster according to my (very rough) tests. * Make query a bit more readable * exclude_from_search -> excludeFromSearch * Remove superfluous notetypes table from query * Rework to use field search logic Thanks to Rumo for the suggestion: https://github.com/ankitects/anki/pull/2394#issuecomment-1446702402 * Exclude fields from field searches too * Fix error on notetypes with no included fields * Add back the exclude_fields function This approach seems to perform better on average than the previously benchmarked ones. * Use pure-SQL approach to excluding fields * Change single field search to use new approach * Fix flawed any_excluded/sortf_excluded logic * Support field exclusion in the nc operator Also fix search text being wrapped in % in the any_excluded=true case. * Support field exclusion in the re and w operators * Label field exclusion as being slower * Unqualified search should be wrapped in % in all cases I was under the impression that it shouldn't be wrapped with the new field exclusion logic. * Remove unnecessary .collect() * Refactor some complex return types into structs * Do not exclude fields in field searches * Add a test and docstring for CollectRanges * Avoid destructuring in closures * Remove the exclude_fields function Minor wording tweaks by dae: * num_fields -> total_fields_in_note * fields -> field_ranges_to_search * fields -> fields_to_search * SingleField -> FieldQualified * mid -> ntid
2023-03-19 22:46:03 +01:00
f.excludeFromSearch.setChecked(fld["excludeFromSearch"])
f.fieldDescription.setText(fld.get("description", ""))
def saveField(self) -> None:
# not initialized yet?
if self.currentIdx is None:
return
idx = self.currentIdx
2019-12-23 01:34:10 +01:00
fld = self.model["flds"][idx]
f = self.form
font = f.fontFamily.currentFont().family()
if fld["font"] != font:
fld["font"] = font
self.change_tracker.mark_basic()
size = f.fontSize.value()
if fld["size"] != size:
fld["size"] = size
self.change_tracker.mark_basic()
rtl = f.rtl.isChecked()
if fld["rtl"] != rtl:
fld["rtl"] = rtl
self.change_tracker.mark_basic()
plain_text = f.plainTextByDefault.isChecked()
if fld["plainText"] != plain_text:
fld["plainText"] = plain_text
self.change_tracker.mark_basic()
collapsed = f.collapseByDefault.isChecked()
if fld["collapsed"] != collapsed:
fld["collapsed"] = collapsed
Add option to exclude fields from search (#2394) * Add option to exclude fields from unqualified searches * Use temp tables instead This is slightly faster according to my (very rough) tests. * Make query a bit more readable * exclude_from_search -> excludeFromSearch * Remove superfluous notetypes table from query * Rework to use field search logic Thanks to Rumo for the suggestion: https://github.com/ankitects/anki/pull/2394#issuecomment-1446702402 * Exclude fields from field searches too * Fix error on notetypes with no included fields * Add back the exclude_fields function This approach seems to perform better on average than the previously benchmarked ones. * Use pure-SQL approach to excluding fields * Change single field search to use new approach * Fix flawed any_excluded/sortf_excluded logic * Support field exclusion in the nc operator Also fix search text being wrapped in % in the any_excluded=true case. * Support field exclusion in the re and w operators * Label field exclusion as being slower * Unqualified search should be wrapped in % in all cases I was under the impression that it shouldn't be wrapped with the new field exclusion logic. * Remove unnecessary .collect() * Refactor some complex return types into structs * Do not exclude fields in field searches * Add a test and docstring for CollectRanges * Avoid destructuring in closures * Remove the exclude_fields function Minor wording tweaks by dae: * num_fields -> total_fields_in_note * fields -> field_ranges_to_search * fields -> fields_to_search * SingleField -> FieldQualified * mid -> ntid
2023-03-19 22:46:03 +01:00
self.change_tracker.mark_basic()
exclude_from_search = f.excludeFromSearch.isChecked()
if fld["excludeFromSearch"] != exclude_from_search:
fld["excludeFromSearch"] = exclude_from_search
self.change_tracker.mark_basic()
desc = f.fieldDescription.text()
if fld.get("description", "") != desc:
fld["description"] = desc
self.change_tracker.mark_basic()
def reject(self) -> None:
if self.webview:
self.webview.cleanup()
self.webview = None
if self.change_tracker.changed():
if not askUser("Discard changes?"):
return
QDialog.reject(self)
def accept(self) -> None:
self.saveField()
def on_done(changes: OpChanges) -> None:
2021-04-30 09:30:48 +02:00
tooltip(tr.card_templates_changes_saved(), parent=self.parentWidget())
QDialog.accept(self)
update_notetype_legacy(parent=self.mw, notetype=self.model).success(
on_done
).run_in_background()
2021-02-01 14:28:21 +01:00
def onHelp(self) -> None:
openHelp(HelpPage.CUSTOMIZING_FIELDS)