Merge remote-tracking branch 'danielelmes/master' into create_actions_for_windows_macos

# Conflicts:
#	.github/scripts/trailing-newlines.sh
#	.github/workflows/checks.yml
#	Makefile
#	pylib/Makefile
#	qt/Makefile
#	qt/tools/typecheck-setup.sh
#	rspy/Makefile
This commit is contained in:
evandrocoan 2020-03-24 22:49:49 -03:00
commit 721453f923
22 changed files with 299 additions and 116 deletions

View file

@ -2,7 +2,12 @@
set -eu -o pipefail ${SHELLFLAGS} set -eu -o pipefail ${SHELLFLAGS}
files=$(rg -l '[^\n]\z' -g '!*.{svg,scss,json,sql}' || true) # Checking version to force it fail the build if rg is not installed.
# Because `set -e` does not work inside the subshell $()
rg --version > /dev/null 2>&1
files=$(rg -l '[^\n]\z' -g '!*.{png,svg,scss,json,sql}' || true)
if [ "$files" != "" ]; then if [ "$files" != "" ]; then
echo "the following files are missing a newline on the last line:" echo "the following files are missing a newline on the last line:"
echo $files echo $files

View file

@ -29,6 +29,7 @@ Yoonchae Lee
Evandro Coan <github.com/evandrocoan> Evandro Coan <github.com/evandrocoan>
Alan Du <alanhdu@gmail.com> Alan Du <alanhdu@gmail.com>
Yuchen Lei <lyc@xuming.studio> Yuchen Lei <lyc@xuming.studio>
Henry Tang <hktang@ualberta.ca>
******************** ********************

View file

@ -8,10 +8,20 @@ endif
MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-builtin-rules
RUNARGS :=
MYPY_ARGS :=
FIND := $(if $(wildcard /bin/find),/bin/find,/usr/bin/find) FIND := $(if $(wildcard /bin/find),/bin/find,/usr/bin/find)
ifndef OS
OS := unknown
endif
# https://anki.tenderapp.com/discussions/beta-testing/1860-error-unused-type-ignore-comment
ifneq (${OS},Windows_NT)
MYPY_ARGS := --warn-unused-ignores
endif
.DELETE_ON_ERROR: .DELETE_ON_ERROR:
RUNARGS :=
.SUFFIXES: .SUFFIXES:
BLACKARGS := -t py36 anki tests setup.py tools/*.py --exclude='_pb2|buildinfo' BLACKARGS := -t py36 anki tests setup.py tools/*.py --exclude='_pb2|buildinfo'
ISORTARGS := anki tests setup.py ISORTARGS := anki tests setup.py
@ -39,7 +49,7 @@ PROTODEPS := $(wildcard ../proto/*.proto)
.build/hooks: tools/genhooks.py tools/hookslib.py .build/hooks: tools/genhooks.py tools/hookslib.py
python tools/genhooks.py python tools/genhooks.py
black anki/hooks.py python -m black anki/hooks.py
@touch $@ @touch $@
BUILD_STEPS := .build/run-deps .build/dev-deps .build/py-proto anki/buildinfo.py .build/hooks BUILD_STEPS := .build/run-deps .build/dev-deps .build/py-proto anki/buildinfo.py .build/hooks
@ -53,7 +63,7 @@ check: $(BUILD_STEPS) .build/mypy .build/test .build/fmt .build/imports .build/l
.PHONY: fix .PHONY: fix
fix: $(BUILD_STEPS) fix: $(BUILD_STEPS)
isort $(ISORTARGS) isort $(ISORTARGS)
black $(BLACKARGS) python -m black $(BLACKARGS)
.PHONY: clean .PHONY: clean
clean: clean:
@ -65,7 +75,7 @@ clean:
CHECKDEPS := $(shell ${FIND} anki tests -name '*.py' | grep -v buildinfo.py) CHECKDEPS := $(shell ${FIND} anki tests -name '*.py' | grep -v buildinfo.py)
.build/mypy: $(CHECKDEPS) .build/mypy: $(CHECKDEPS)
mypy anki python -m mypy ${MYPY_ARGS} anki
@touch $@ @touch $@
.build/test: $(CHECKDEPS) .build/test: $(CHECKDEPS)
@ -73,7 +83,8 @@ CHECKDEPS := $(shell ${FIND} anki tests -name '*.py' | grep -v buildinfo.py)
@touch $@ @touch $@
.build/lint: $(CHECKDEPS) .build/lint: $(CHECKDEPS)
pylint -j 0 --rcfile=.pylintrc -f colorized --extension-pkg-whitelist=ankirspy anki tests setup.py python -m pylint -j 0 --rcfile=.pylintrc -f colorized \
--extension-pkg-whitelist=ankirspy anki tests setup.py
@touch $@ @touch $@
.build/imports: $(CHECKDEPS) .build/imports: $(CHECKDEPS)
@ -81,7 +92,7 @@ CHECKDEPS := $(shell ${FIND} anki tests -name '*.py' | grep -v buildinfo.py)
@touch $@ @touch $@
.build/fmt: $(CHECKDEPS) .build/fmt: $(CHECKDEPS)
black --check $(BLACKARGS) python -m black --check $(BLACKARGS)
@touch $@ @touch $@
# Building # Building

View file

@ -48,7 +48,6 @@ class DBProxy:
for stmt in "insert", "update", "delete": for stmt in "insert", "update", "delete":
if s.startswith(stmt): if s.startswith(stmt):
self.mod = True self.mod = True
assert ":" not in sql
# fetch rows # fetch rows
return self._backend.db_query(sql, args, first_row_only) return self._backend.db_query(sql, args, first_row_only)
@ -84,7 +83,6 @@ class DBProxy:
def executemany(self, sql: str, args: Iterable[Sequence[ValueForDB]]) -> None: def executemany(self, sql: str, args: Iterable[Sequence[ValueForDB]]) -> None:
self.mod = True self.mod = True
assert ":" not in sql
if isinstance(args, list): if isinstance(args, list):
list_args = args list_args = args
else: else:

View file

@ -133,7 +133,11 @@ def lang_to_disk_lang(lang: str) -> str:
): ):
return lang.replace("_", "-") return lang.replace("_", "-")
# other languages have the region portion stripped # other languages have the region portion stripped
return re.match("(.*)_", lang).group(1) m = re.match("(.*)_", lang)
if m:
return m.group(1)
else:
return lang
# the currently set interface language # the currently set interface language

View file

@ -279,7 +279,7 @@ from notes where %s"""
lim = 250 lim = 250
while self.tablesLeft and lim: while self.tablesLeft and lim:
curTable = self.tablesLeft[0] curTable = self.tablesLeft[0]
if not self.chunkRows: if self.chunkRows is None:
self.chunkRows = self.getChunkRows(curTable) self.chunkRows = self.getChunkRows(curTable)
rows = self.chunkRows[:lim] rows = self.chunkRows[:lim]
self.chunkRows = self.chunkRows[lim:] self.chunkRows = self.chunkRows[lim:]

View file

@ -7,7 +7,6 @@ check_untyped_defs = true
disallow_untyped_decorators = True disallow_untyped_decorators = True
warn_redundant_casts = True warn_redundant_casts = True
warn_unused_configs = True warn_unused_configs = True
warn_unused_ignores = True
[mypy-win32file] [mypy-win32file]
ignore_missing_imports = True ignore_missing_imports = True

View file

@ -18,6 +18,15 @@ srcNotes = None
srcCards = None srcCards = None
def clear_tempfile(tf):
""" https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file """
try:
tf.close()
os.unlink(tf.name)
except:
pass
def test_anki2_mediadupes(): def test_anki2_mediadupes():
tmp = getEmptyCol() tmp = getEmptyCol()
# add a note that references a sound # add a note that references a sound
@ -208,13 +217,15 @@ def test_tsv_tag_modified():
n.addTag("four") n.addTag("four")
deck.addNote(n) deck.addNote(n)
with NamedTemporaryFile(mode="w") as tf: # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file
with NamedTemporaryFile(mode="w", delete=False) as tf:
tf.write("1\tb\tc\n") tf.write("1\tb\tc\n")
tf.flush() tf.flush()
i = TextImporter(deck, tf.name) i = TextImporter(deck, tf.name)
i.initMapping() i.initMapping()
i.tagModified = "boom" i.tagModified = "boom"
i.run() i.run()
clear_tempfile(tf)
n.load() n.load()
assert n["Front"] == "1" assert n["Front"] == "1"
@ -243,13 +254,15 @@ def test_tsv_tag_multiple_tags():
n.addTag("five") n.addTag("five")
deck.addNote(n) deck.addNote(n)
with NamedTemporaryFile(mode="w") as tf: # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file
with NamedTemporaryFile(mode="w", delete=False) as tf:
tf.write("1\tb\tc\n") tf.write("1\tb\tc\n")
tf.flush() tf.flush()
i = TextImporter(deck, tf.name) i = TextImporter(deck, tf.name)
i.initMapping() i.initMapping()
i.tagModified = "five six" i.tagModified = "five six"
i.run() i.run()
clear_tempfile(tf)
n.load() n.load()
assert n["Front"] == "1" assert n["Front"] == "1"
@ -273,13 +286,15 @@ def test_csv_tag_only_if_modified():
n["Left"] = "3" n["Left"] = "3"
deck.addNote(n) deck.addNote(n)
with NamedTemporaryFile(mode="w") as tf: # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file
with NamedTemporaryFile(mode="w", delete=False) as tf:
tf.write("1,2,3\n") tf.write("1,2,3\n")
tf.flush() tf.flush()
i = TextImporter(deck, tf.name) i = TextImporter(deck, tf.name)
i.initMapping() i.initMapping()
i.tagModified = "right" i.tagModified = "right"
i.run() i.run()
clear_tempfile(tf)
n.load() n.load()
assert n.tags == [] assert n.tags == []

View file

@ -1,4 +1,5 @@
# coding: utf-8 #!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import os import os
import tempfile import tempfile
@ -34,6 +35,6 @@ def test_graphs():
d = aopen(os.path.join(dir, "test.anki2")) d = aopen(os.path.join(dir, "test.anki2"))
g = d.stats() g = d.stats()
rep = g.report() rep = g.report()
with open(os.path.join(dir, "test.html"), "w") as f: with open(os.path.join(dir, "test.html"), "w", encoding="UTF-8") as f:
f.write(rep) f.write(rep)
return return

View file

@ -9,6 +9,19 @@ MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-builtin-rules
FIND := $(if $(wildcard /bin/find),/bin/find,/usr/bin/find) FIND := $(if $(wildcard /bin/find),/bin/find,/usr/bin/find)
MYPY_ARGS :=
PYLINT_ARGS :=
ifndef OS
OS := unknown
endif
# https://anki.tenderapp.com/discussions/beta-testing/1860-error-unused-type-ignore-comment
ifneq (${OS},Windows_NT)
MYPY_ARGS := --warn-unused-ignores
else
PYLINT_ARGS := --ignored-modules=win32file,pywintypes,socket,win32pipe
endif
.DELETE_ON_ERROR: .DELETE_ON_ERROR:
.SUFFIXES: .SUFFIXES:
@ -48,7 +61,7 @@ TSDEPS := $(wildcard ts/src/*.ts) $(wildcard ts/scss/*.scss)
.build/hooks: tools/genhooks_gui.py ../pylib/tools/hookslib.py .build/hooks: tools/genhooks_gui.py ../pylib/tools/hookslib.py
python tools/genhooks_gui.py python tools/genhooks_gui.py
black aqt/gui_hooks.py python -m black aqt/gui_hooks.py
@touch $@ @touch $@
BUILD_STEPS := .build/run-deps .build/dev-deps .build/js .build/ui aqt/buildinfo.py .build/hooks .build/i18n BUILD_STEPS := .build/run-deps .build/dev-deps .build/js .build/ui aqt/buildinfo.py .build/hooks .build/i18n
@ -62,7 +75,7 @@ check: $(BUILD_STEPS) .build/mypy .build/test .build/fmt .build/imports .build/l
.PHONY: fix .PHONY: fix
fix: $(BUILD_STEPS) fix: $(BUILD_STEPS)
isort $(ISORTARGS) isort $(ISORTARGS)
black $(BLACKARGS) python -m black $(BLACKARGS)
(cd ts && npm run pretty) (cd ts && npm run pretty)
.PHONY: clean .PHONY: clean
@ -86,7 +99,7 @@ PYLIB := ../pylib
CHECKDEPS := $(shell ${FIND} aqt tests -name '*.py' | grep -v buildinfo.py) CHECKDEPS := $(shell ${FIND} aqt tests -name '*.py' | grep -v buildinfo.py)
.build/mypy: $(CHECKDEPS) .build/qt-stubs .build/mypy: $(CHECKDEPS) .build/qt-stubs
mypy aqt python -m mypy ${MYPY_ARGS} aqt
@touch $@ @touch $@
.build/test: $(CHECKDEPS) .build/test: $(CHECKDEPS)
@ -94,7 +107,8 @@ CHECKDEPS := $(shell ${FIND} aqt tests -name '*.py' | grep -v buildinfo.py)
@touch $@ @touch $@
.build/lint: $(CHECKDEPS) .build/lint: $(CHECKDEPS)
pylint -j 0 --rcfile=.pylintrc -f colorized --extension-pkg-whitelist=PyQt5,ankirspy aqt tests setup.py python -m pylint -j 0 --rcfile=.pylintrc -f colorized ${PYLINT_ARGS} \
--extension-pkg-whitelist=PyQt5,ankirspy aqt tests setup.py
@touch $@ @touch $@
.build/imports: $(CHECKDEPS) .build/imports: $(CHECKDEPS)
@ -102,7 +116,7 @@ CHECKDEPS := $(shell ${FIND} aqt tests -name '*.py' | grep -v buildinfo.py)
@touch $@ @touch $@
.build/fmt: $(CHECKDEPS) .build/fmt: $(CHECKDEPS)
black --check $(BLACKARGS) python -m black --check $(BLACKARGS)
@touch $@ @touch $@
.build/qt-stubs: .build/qt-stubs:

View file

@ -865,7 +865,7 @@ QTableView {{ gridline-color: {grid} }}
def _onRowChanged(self, current, previous) -> None: def _onRowChanged(self, current, previous) -> None:
update = self.updateTitle() update = self.updateTitle()
show = self.model.cards and update == 1 show = self.model.cards and update == 1
self.form.splitter.widget(1).setVisible(not not show) self.form.splitter.widget(1).setVisible(bool(show))
idx = self.form.tableView.selectionModel().currentIndex() idx = self.form.tableView.selectionModel().currentIndex()
if idx.isValid(): if idx.isValid():
self.card = self.model.getCard(idx) self.card = self.model.getCard(idx)
@ -1660,12 +1660,12 @@ where id in %s"""
and self._previewState == "answer" and self._previewState == "answer"
and not self._previewBothSides and not self._previewBothSides
) )
self._previewPrev.setEnabled(not not (self.singleCard and canBack)) self._previewPrev.setEnabled(bool(self.singleCard and canBack))
canForward = ( canForward = (
self.currentRow() < self.model.rowCount(None) - 1 self.currentRow() < self.model.rowCount(None) - 1
or self._previewState == "question" or self._previewState == "question"
) )
self._previewNext.setEnabled(not not (self.singleCard and canForward)) self._previewNext.setEnabled(bool(self.singleCard and canForward))
def _closePreview(self): def _closePreview(self):
if self._previewWindow: if self._previewWindow:
@ -1925,7 +1925,7 @@ update cards set usn=?, mod=?, did=? where id in """
###################################################################### ######################################################################
def isSuspended(self): def isSuspended(self):
return not not (self.card and self.card.queue == QUEUE_TYPE_SUSPENDED) return bool(self.card and self.card.queue == QUEUE_TYPE_SUSPENDED)
def onSuspend(self): def onSuspend(self):
self.editor.saveNow(self._onSuspend) self.editor.saveNow(self._onSuspend)
@ -1986,7 +1986,7 @@ update cards set usn=?, mod=?, did=? where id in """
self.deleteTags(tags="marked", label=False) self.deleteTags(tags="marked", label=False)
def isMarked(self): def isMarked(self):
return not not (self.card and self.card.note().hasTag("Marked")) return bool(self.card and self.card.note().hasTag("Marked"))
# Repositioning # Repositioning
###################################################################### ######################################################################

View file

@ -63,9 +63,18 @@ _html = """
html { background: %s; } html { background: %s; }
#topbutsOuter { background: %s; } #topbutsOuter { background: %s; }
</style> </style>
<div id="topbutsOuter"><div id="topbuts" class="clearfix">%s</div></div> <div id="topbutsOuter">
<div id="fields"></div> <div id="topbuts" class="clearfix">
<div id="dupes" style="display:none;"><a href="#" onclick="pycmd('dupes');return false;">%s</a></div> %s
</div>
</div>
<div id="fields">
</div>
<div id="dupes" style="display:none;">
<a href="#" onclick="pycmd('dupes');return false;">
%s
</a>
</div>
""" """
# caller is responsible for resetting note on reset # caller is responsible for resetting note on reset
@ -83,6 +92,7 @@ class Editor:
self.setupWeb() self.setupWeb()
self.setupShortcuts() self.setupShortcuts()
self.setupTags() self.setupTags()
gui_hooks.editor_did_init(self)
# Initial setup # Initial setup
############################################################ ############################################################
@ -117,20 +127,35 @@ class Editor:
# The color selection buttons do not use an icon so the HTML must be specified manually # The color selection buttons do not use an icon so the HTML must be specified manually
tip = _("Set foreground colour (F7)") tip = _("Set foreground colour (F7)")
righttopbtns.append( righttopbtns.append(
"""<button tabindex=-1 class=linkb title="{}" """ <button tabindex=-1
type="button" onclick="pycmd('colour');return false;"> class=linkb
<div id=forecolor style="display:inline-block; background: #000;border-radius: 5px;" title="{}"
class=topbut></div></button>""".format( type="button"
onclick="pycmd('colour'); return false;"
>
<div id=forecolor
style="display:inline-block; background: #000;border-radius: 5px;"
class=topbut
>
</div>
</button>""".format(
tip tip
) )
) )
tip = _("Change colour (F8)") tip = _("Change colour (F8)")
righttopbtns.extend( righttopbtns.extend(
[ [
"""<button tabindex=-1 class=linkb title="{}" """<button tabindex=-1
type="button" onclick="pycmd('changeCol');return false;"> class=linkb
title="{}"
type="button"
onclick="pycmd('changeCol');return false;"
>
<div style="display:inline-block; border-radius: 5px;" <div style="display:inline-block; border-radius: 5px;"
class="topbut rainbow"></div></button>""".format( class="topbut rainbow"
>
</div>
</button>""".format(
tip tip
), ),
self._addButton( self._addButton(
@ -249,10 +274,16 @@ class Editor:
theclass = "linkb" theclass = "linkb"
if not disables: if not disables:
theclass += " perm" theclass += " perm"
return ( return """ <button tabindex=-1
'''<button tabindex=-1 {id} class="{theclass}" type="button" title="{tip}"''' {id}
""" onclick="pycmd('{cmd}');{togglesc}return false;">""" class="{theclass}"
"""{imgelm}{labelelm}</button>""".format( type="button"
title="{tip}"
onclick="pycmd('{cmd}');{togglesc}return false;"
>
{imgelm}
{labelelm}
</button>""".format(
imgelm=imgelm, imgelm=imgelm,
cmd=cmd, cmd=cmd,
tip=tip, tip=tip,
@ -261,7 +292,6 @@ class Editor:
togglesc=toggleScript, togglesc=toggleScript,
theclass=theclass, theclass=theclass,
) )
)
def setupShortcuts(self) -> None: def setupShortcuts(self) -> None:
# if a third element is provided, enable shortcut even when no field selected # if a third element is provided, enable shortcut even when no field selected
@ -426,6 +456,7 @@ class Editor:
json.dumps(focusTo), json.dumps(focusTo),
json.dumps(self.note.id), json.dumps(self.note.id),
) )
js = gui_hooks.editor_will_load_note(js, self.note, self)
self.web.evalWithCallback(js, oncallback) self.web.evalWithCallback(js, oncallback)
def fonts(self) -> List[Tuple[str, int, bool]]: def fonts(self) -> List[Tuple[str, int, bool]]:
@ -466,9 +497,10 @@ class Editor:
return True return True
m = self.note.model() m = self.note.model()
for c, f in enumerate(self.note.fields): for c, f in enumerate(self.note.fields):
f = f.replace("<br>", "").strip()
notChangedvalues = {"", "<br>"} notChangedvalues = {"", "<br>"}
if previousNote and m["flds"][c]["sticky"]: if previousNote and m["flds"][c]["sticky"]:
notChangedvalues.add(previousNote.fields[c]) notChangedvalues.add(previousNote.fields[c].replace("<br>", "").strip())
if f not in notChangedvalues: if f not in notChangedvalues:
return False return False
return True return True

View file

@ -1028,6 +1028,30 @@ class _EditorDidFocusFieldHook:
editor_did_focus_field = _EditorDidFocusFieldHook() editor_did_focus_field = _EditorDidFocusFieldHook()
class _EditorDidInitHook:
_hooks: List[Callable[["aqt.editor.Editor"], None]] = []
def append(self, cb: Callable[["aqt.editor.Editor"], None]) -> None:
"""(editor: aqt.editor.Editor)"""
self._hooks.append(cb)
def remove(self, cb: Callable[["aqt.editor.Editor"], None]) -> None:
if cb in self._hooks:
self._hooks.remove(cb)
def __call__(self, editor: aqt.editor.Editor) -> None:
for hook in self._hooks:
try:
hook(editor)
except:
# if the hook fails, remove it
self._hooks.remove(hook)
raise
editor_did_init = _EditorDidInitHook()
class _EditorDidInitButtonsHook: class _EditorDidInitButtonsHook:
_hooks: List[Callable[[List, "aqt.editor.Editor"], None]] = [] _hooks: List[Callable[[List, "aqt.editor.Editor"], None]] = []
@ -1183,6 +1207,40 @@ class _EditorWebViewDidInitHook:
editor_web_view_did_init = _EditorWebViewDidInitHook() editor_web_view_did_init = _EditorWebViewDidInitHook()
class _EditorWillLoadNoteFilter:
"""Allows changing the javascript commands to load note before
executing it and do change in the QT editor."""
_hooks: List[Callable[[str, "anki.notes.Note", "aqt.editor.Editor"], str]] = []
def append(
self, cb: Callable[[str, "anki.notes.Note", "aqt.editor.Editor"], str]
) -> None:
"""(js: str, note: anki.notes.Note, editor: aqt.editor.Editor)"""
self._hooks.append(cb)
def remove(
self, cb: Callable[[str, "anki.notes.Note", "aqt.editor.Editor"], str]
) -> None:
if cb in self._hooks:
self._hooks.remove(cb)
def __call__(
self, js: str, note: anki.notes.Note, editor: aqt.editor.Editor
) -> str:
for filter in self._hooks:
try:
js = filter(js, note, editor)
except:
# if the hook fails, remove it
self._hooks.remove(filter)
raise
return js
editor_will_load_note = _EditorWillLoadNoteFilter()
class _EditorWillShowContextMenuHook: class _EditorWillShowContextMenuHook:
_hooks: List[Callable[["aqt.editor.EditorWebView", QMenu], None]] = [] _hooks: List[Callable[["aqt.editor.EditorWebView", QMenu], None]] = []

View file

@ -1462,8 +1462,8 @@ will be lost. Continue?"""
# make sure ctypes is bundled # make sure ctypes is bundled
from ctypes import windll, wintypes # type: ignore from ctypes import windll, wintypes # type: ignore
_dummy = windll _dummy1 = windll
_dummy = wintypes _dummy2 = wintypes
def maybeHideAccelerators(self, tgt: Optional[Any] = None) -> None: def maybeHideAccelerators(self, tgt: Optional[Any] = None) -> None:
if not self.hideMenuAccels: if not self.hideMenuAccels:

View file

@ -6,7 +6,6 @@ show_error_codes = true
disallow_untyped_decorators = True disallow_untyped_decorators = True
warn_redundant_casts = True warn_redundant_casts = True
warn_unused_configs = True warn_unused_configs = True
warn_unused_ignores = True
[mypy-win32file] [mypy-win32file]
ignore_missing_imports = True ignore_missing_imports = True

View file

@ -13,4 +13,4 @@ case "$(uname -s)" in
;; ;;
esac esac
rsync -a "$qtTranslations"/qt* "$out" rsync -a "$qtTranslations/" "$out/"

View file

@ -497,6 +497,14 @@ def emptyNewCard():
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"],
), ),
Hook(name="editor_did_init", args=["editor: aqt.editor.Editor"],),
Hook(
name="editor_will_load_note",
args=["js: str", "note: anki.notes.Note", "editor: aqt.editor.Editor"],
return_type="str",
doc="""Allows changing the javascript commands to load note before
executing it and do change in the QT editor.""",
),
# Sound/video # Sound/video
################### ###################
Hook(name="av_player_will_play", args=["tag: anki.sound.AVTag"]), Hook(name="av_player_will_play", args=["tag: anki.sound.AVTag"]),

View file

@ -22,6 +22,9 @@ case "$(uname -s)" in
;; ;;
esac esac
cmd="rsync -a ${TOOLS}/stubs/PyQt5/ ${modDir}/" if [[ "w${OS}" == "wWindows_NT" ]];
then
$cmd > /dev/null 2>&1 || sudo $cmd rsync -a "${TOOLS}/stubs/PyQt5/" "${modDir}/"
else
rsync -a "${TOOLS}/stubs/PyQt5/" "${modDir}/" || sudo rsync -a "${TOOLS}/stubs/PyQt5/" "${modDir}/"
fi

View file

@ -333,20 +333,33 @@ function setFields(fields) {
if (!f) { if (!f) {
f = "<br>"; f = "<br>";
} }
txt += `<tr><td class=fname>${n}</td></tr><tr><td width=100%>`; txt += `
txt += `<div id=f${i} onkeydown='onKey(window.event);' oninput='onInput()' onmouseup='onKey(window.event);'`; <tr>
txt += <td class=fname id="name${i}">${n}</td>
" onfocus='onFocus(this);' onblur='onBlur();' class='field clearfix' "; </tr>
txt += "ondragover='onDragOver(this);' onpaste='onPaste(this);' "; <tr>
txt += "oncopy='onCutOrCopy(this);' oncut='onCutOrCopy(this);' "; <td width=100%>
txt += `contentEditable=true class=field>${f}</div>`; <div id=f${i}
txt += "</td></tr>"; onkeydown='onKey(window.event);'
oninput='onInput();'
onmouseup='onKey(window.event);'
onfocus='onFocus(this);'
onblur='onBlur();'
class='field clearfix'
ondragover='onDragOver(this);'
onpaste='onPaste(this);'
oncopy='onCutOrCopy(this);'
oncut='onCutOrCopy(this);'
contentEditable=true
class=field
>${f}</div>
</td>
</tr>`;
} }
$("#fields").html( $("#fields").html(`
"<table cellpadding=0 width=100% style='table-layout: fixed;'>" + <table cellpadding=0 width=100% style='table-layout: fixed;'>
txt + ${txt}
"</table>" </table>`);
);
maybeDisableButtons(); maybeDisableButtons();
} }

View file

@ -40,11 +40,16 @@ serde_repr = "0.1.5"
num_enum = "0.4.2" num_enum = "0.4.2"
unicase = "2.6.0" unicase = "2.6.0"
[target.'cfg(target_vendor="apple")'.dependencies] # pinned until rusqlite 0.22 comes out
rusqlite = { version = "0.21.0", features = ["trace", "functions", "collation"] } [target.'cfg(target_vendor="apple")'.dependencies.rusqlite]
git = "https://github.com/ankitects/rusqlite.git"
branch="nulsafe-text"
features = ["trace", "functions", "collation"]
[target.'cfg(not(target_vendor="apple"))'.dependencies] [target.'cfg(not(target_vendor="apple"))'.dependencies.rusqlite]
rusqlite = { version = "0.21.0", features = ["trace", "functions", "collation", "bundled"] } git = "https://github.com/ankitects/rusqlite.git"
branch="nulsafe-text"
features = ["trace", "functions", "collation", "bundled"]
[target.'cfg(linux)'.dependencies] [target.'cfg(linux)'.dependencies]
reqwest = { version = "0.10.1", features = ["json", "native-tls-vendored"] } reqwest = { version = "0.10.1", features = ["json", "native-tls-vendored"] }

View file

@ -108,6 +108,7 @@ fn main() -> std::io::Result<()> {
let path = entry.path(); let path = entry.path();
println!("cargo:rerun-if-changed=./ftl/{}", fname); println!("cargo:rerun-if-changed=./ftl/{}", fname);
buf += &fs::read_to_string(path)?; buf += &fs::read_to_string(path)?;
buf.push('\n');
} }
} }
let combined_ftl = Path::new("src/i18n/ftl/template.ftl"); let combined_ftl = Path::new("src/i18n/ftl/template.ftl");
@ -145,6 +146,7 @@ fn main() -> std::io::Result<()> {
let path = entry.path(); let path = entry.path();
println!("cargo:rerun-if-changed={:?}", entry.path()); println!("cargo:rerun-if-changed={:?}", entry.path());
buf += &fs::read_to_string(path)?; buf += &fs::read_to_string(path)?;
buf.push('\n');
} }
langs langs
.entry(lang_name) .entry(lang_name)

View file

@ -14,7 +14,7 @@ use crate::{
types::{ObjID, Usn}, types::{ObjID, Usn},
}; };
use regex::Regex; use regex::Regex;
use rusqlite::{params, Connection, NO_PARAMS}; use rusqlite::{functions::FunctionFlags, params, Connection, NO_PARAMS};
use std::cmp::Ordering; use std::cmp::Ordering;
use std::{ use std::{
borrow::Cow, borrow::Cow,
@ -71,27 +71,41 @@ fn open_or_create_collection_db(path: &Path) -> Result<Connection> {
/// to split provided fields and return field at zero-based index. /// to split provided fields and return field at zero-based index.
/// If out of range, returns empty string. /// If out of range, returns empty string.
fn add_field_index_function(db: &Connection) -> rusqlite::Result<()> { fn add_field_index_function(db: &Connection) -> rusqlite::Result<()> {
db.create_scalar_function("field_at_index", 2, true, |ctx| { db.create_scalar_function(
"field_at_index",
2,
FunctionFlags::SQLITE_DETERMINISTIC,
|ctx| {
let mut fields = ctx.get_raw(0).as_str()?.split('\x1f'); let mut fields = ctx.get_raw(0).as_str()?.split('\x1f');
let idx: u16 = ctx.get(1)?; let idx: u16 = ctx.get(1)?;
Ok(fields.nth(idx as usize).unwrap_or("").to_string()) Ok(fields.nth(idx as usize).unwrap_or("").to_string())
}) },
)
} }
fn add_without_combining_function(db: &Connection) -> rusqlite::Result<()> { fn add_without_combining_function(db: &Connection) -> rusqlite::Result<()> {
db.create_scalar_function("without_combining", 1, true, |ctx| { db.create_scalar_function(
"without_combining",
1,
FunctionFlags::SQLITE_DETERMINISTIC,
|ctx| {
let text = ctx.get_raw(0).as_str()?; let text = ctx.get_raw(0).as_str()?;
Ok(match without_combining(text) { Ok(match without_combining(text) {
Cow::Borrowed(_) => None, Cow::Borrowed(_) => None,
Cow::Owned(o) => Some(o), Cow::Owned(o) => Some(o),
}) })
}) },
)
} }
/// Adds sql function regexp(regex, string) -> is_match /// Adds sql function regexp(regex, string) -> is_match
/// Taken from the rusqlite docs /// Taken from the rusqlite docs
fn add_regexp_function(db: &Connection) -> rusqlite::Result<()> { fn add_regexp_function(db: &Connection) -> rusqlite::Result<()> {
db.create_scalar_function("regexp", 2, true, move |ctx| { db.create_scalar_function(
"regexp",
2,
FunctionFlags::SQLITE_DETERMINISTIC,
move |ctx| {
assert_eq!(ctx.len(), 2, "called with unexpected number of arguments"); assert_eq!(ctx.len(), 2, "called with unexpected number of arguments");
let saved_re: Option<&Regex> = ctx.get_aux(0)?; let saved_re: Option<&Regex> = ctx.get_aux(0)?;
@ -122,7 +136,8 @@ fn add_regexp_function(db: &Connection) -> rusqlite::Result<()> {
} }
Ok(is_match) Ok(is_match)
}) },
)
} }
/// Fetch schema version from database. /// Fetch schema version from database.