mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 23:12:21 -04:00
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:
commit
721453f923
22 changed files with 299 additions and 116 deletions
7
.github/scripts/trailing-newlines.sh
vendored
7
.github/scripts/trailing-newlines.sh
vendored
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
********************
|
********************
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 == []
|
||||||
|
|
|
@ -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
|
||||||
|
|
24
qt/Makefile
24
qt/Makefile
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]] = []
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -13,4 +13,4 @@ case "$(uname -s)" in
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
rsync -a "$qtTranslations"/qt* "$out"
|
rsync -a "$qtTranslations/" "$out/"
|
||||||
|
|
|
@ -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"]),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue