mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
add separate file for gui hooks
This commit is contained in:
parent
d92e27ab50
commit
4bb3d7a958
8 changed files with 222 additions and 136 deletions
|
@ -5,7 +5,7 @@ MAKEFLAGS += --warn-undefined-variables
|
||||||
MAKEFLAGS += --no-builtin-rules
|
MAKEFLAGS += --no-builtin-rules
|
||||||
RUNARGS :=
|
RUNARGS :=
|
||||||
.SUFFIXES:
|
.SUFFIXES:
|
||||||
BLACKARGS := -t py36 anki tests setup.py --exclude='backend_pb2|buildinfo'
|
BLACKARGS := -t py36 anki tests setup.py tools/*.py --exclude='backend_pb2|buildinfo'
|
||||||
ISORTARGS := anki tests setup.py
|
ISORTARGS := anki tests setup.py
|
||||||
|
|
||||||
$(shell mkdir -p .build ../dist)
|
$(shell mkdir -p .build ../dist)
|
||||||
|
|
129
pylib/anki/hooks_gen.py
Normal file
129
pylib/anki/hooks_gen.py
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
"""
|
||||||
|
Code for generating parts of hooks.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from operator import attrgetter
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Hook:
|
||||||
|
# the name of the hook. _filter or _hook is appending automatically.
|
||||||
|
name: str
|
||||||
|
# string of the typed arguments passed to the callback, eg
|
||||||
|
# "kind: str, val: int"
|
||||||
|
cb_args: str = ""
|
||||||
|
# string of the return type. if set, hook is a filter.
|
||||||
|
return_type: Optional[str] = None
|
||||||
|
# if add-ons may be relying on the legacy hook name, add it here
|
||||||
|
legacy_hook: Optional[str] = None
|
||||||
|
|
||||||
|
def callable(self) -> str:
|
||||||
|
"Convert args into a Callable."
|
||||||
|
types = []
|
||||||
|
for arg in self.cb_args.split(","):
|
||||||
|
if not arg:
|
||||||
|
continue
|
||||||
|
(name, type) = arg.split(":")
|
||||||
|
types.append(type.strip())
|
||||||
|
types_str = ", ".join(types)
|
||||||
|
return f"Callable[[{types_str}], {self.return_type or 'None'}]"
|
||||||
|
|
||||||
|
def arg_names(self) -> List[str]:
|
||||||
|
names = []
|
||||||
|
for arg in self.cb_args.split(","):
|
||||||
|
if not arg:
|
||||||
|
continue
|
||||||
|
(name, type) = arg.split(":")
|
||||||
|
names.append(name.strip())
|
||||||
|
return names
|
||||||
|
|
||||||
|
def full_name(self) -> str:
|
||||||
|
return f"{self.name}_{self.kind()}"
|
||||||
|
|
||||||
|
def kind(self) -> str:
|
||||||
|
if self.return_type is not None:
|
||||||
|
return "filter"
|
||||||
|
else:
|
||||||
|
return "hook"
|
||||||
|
|
||||||
|
def list_code(self) -> str:
|
||||||
|
return f"""\
|
||||||
|
{self.full_name()}: List[{self.callable()}] = []
|
||||||
|
"""
|
||||||
|
|
||||||
|
def fire_code(self) -> str:
|
||||||
|
if self.return_type is not None:
|
||||||
|
# filter
|
||||||
|
return self.filter_fire_code()
|
||||||
|
else:
|
||||||
|
# hook
|
||||||
|
return self.hook_fire_code()
|
||||||
|
|
||||||
|
def hook_fire_code(self) -> str:
|
||||||
|
arg_names = self.arg_names()
|
||||||
|
out = f"""\
|
||||||
|
def run_{self.full_name()}({self.cb_args}) -> None:
|
||||||
|
for hook in {self.full_name()}:
|
||||||
|
try:
|
||||||
|
hook({", ".join(arg_names)})
|
||||||
|
except:
|
||||||
|
# if the hook fails, remove it
|
||||||
|
{self.full_name()}.remove(hook)
|
||||||
|
raise
|
||||||
|
"""
|
||||||
|
if self.legacy_hook:
|
||||||
|
args = ", ".join([f'"{self.legacy_hook}"'] + arg_names)
|
||||||
|
out += f"""\
|
||||||
|
# legacy support
|
||||||
|
runHook({args})
|
||||||
|
"""
|
||||||
|
return out + "\n\n"
|
||||||
|
|
||||||
|
def filter_fire_code(self) -> str:
|
||||||
|
arg_names = self.arg_names()
|
||||||
|
out = f"""\
|
||||||
|
def run_{self.full_name()}({self.cb_args}) -> {self.return_type}:
|
||||||
|
for filter in {self.full_name()}:
|
||||||
|
try:
|
||||||
|
{arg_names[0]} = filter({", ".join(arg_names)})
|
||||||
|
except:
|
||||||
|
# if the hook fails, remove it
|
||||||
|
{self.full_name()}.remove(filter)
|
||||||
|
raise
|
||||||
|
"""
|
||||||
|
if self.legacy_hook:
|
||||||
|
args = ", ".join([f'"{self.legacy_hook}"'] + arg_names)
|
||||||
|
out += f"""\
|
||||||
|
# legacy support
|
||||||
|
runFilter({args})
|
||||||
|
"""
|
||||||
|
|
||||||
|
out += f"""\
|
||||||
|
return {arg_names[0]}
|
||||||
|
"""
|
||||||
|
return out + "\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
def update_file(path: str, hooks: List[Hook]):
|
||||||
|
hooks.sort(key=attrgetter("name"))
|
||||||
|
code = ""
|
||||||
|
for hook in hooks:
|
||||||
|
code += hook.list_code()
|
||||||
|
code += "\n\n"
|
||||||
|
for hook in hooks:
|
||||||
|
code += hook.fire_code()
|
||||||
|
|
||||||
|
orig = open(path).read()
|
||||||
|
new = re.sub(
|
||||||
|
"(?s)# @@AUTOGEN@@.*?# @@AUTOGEN@@\n",
|
||||||
|
f"# @@AUTOGEN@@\n\n{code}# @@AUTOGEN@@\n",
|
||||||
|
orig,
|
||||||
|
)
|
||||||
|
|
||||||
|
open(path, "wb").write(new.encode("utf8"))
|
|
@ -11,109 +11,7 @@ To add a new hook:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
from anki.hooks_gen import Hook, update_file
|
||||||
from dataclasses import dataclass
|
|
||||||
from operator import attrgetter
|
|
||||||
from typing import Optional, List
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Hook:
|
|
||||||
# the name of the hook. _filter or _hook is appending automatically.
|
|
||||||
name: str
|
|
||||||
# string of the typed arguments passed to the callback, eg
|
|
||||||
# "kind: str, val: int"
|
|
||||||
cb_args: str = ""
|
|
||||||
# string of the return type. if set, hook is a filter.
|
|
||||||
return_type: Optional[str] = None
|
|
||||||
# if add-ons may be relying on the legacy hook name, add it here
|
|
||||||
legacy_hook: Optional[str] = None
|
|
||||||
|
|
||||||
def callable(self) -> str:
|
|
||||||
"Convert args into a Callable."
|
|
||||||
types = []
|
|
||||||
for arg in self.cb_args.split(","):
|
|
||||||
if not arg:
|
|
||||||
continue
|
|
||||||
(name, type) = arg.split(":")
|
|
||||||
types.append(type.strip())
|
|
||||||
types_str = ", ".join(types)
|
|
||||||
return f"Callable[[{types_str}], {self.return_type or 'None'}]"
|
|
||||||
|
|
||||||
def arg_names(self) -> List[str]:
|
|
||||||
names = []
|
|
||||||
for arg in self.cb_args.split(","):
|
|
||||||
if not arg:
|
|
||||||
continue
|
|
||||||
(name, type) = arg.split(":")
|
|
||||||
names.append(name.strip())
|
|
||||||
return names
|
|
||||||
|
|
||||||
def full_name(self) -> str:
|
|
||||||
return f"{self.name}_{self.kind()}"
|
|
||||||
|
|
||||||
def kind(self) -> str:
|
|
||||||
if self.return_type is not None:
|
|
||||||
return "filter"
|
|
||||||
else:
|
|
||||||
return "hook"
|
|
||||||
|
|
||||||
def list_code(self) -> str:
|
|
||||||
return f"""\
|
|
||||||
{self.full_name()}: List[{self.callable()}] = []
|
|
||||||
"""
|
|
||||||
|
|
||||||
def fire_code(self) -> str:
|
|
||||||
if self.return_type is not None:
|
|
||||||
# filter
|
|
||||||
return self.filter_fire_code()
|
|
||||||
else:
|
|
||||||
# hook
|
|
||||||
return self.hook_fire_code()
|
|
||||||
|
|
||||||
def hook_fire_code(self) -> str:
|
|
||||||
arg_names = self.arg_names()
|
|
||||||
out = f"""\
|
|
||||||
def run_{self.full_name()}({self.cb_args}) -> None:
|
|
||||||
for hook in {self.full_name()}:
|
|
||||||
try:
|
|
||||||
hook({", ".join(arg_names)})
|
|
||||||
except:
|
|
||||||
# if the hook fails, remove it
|
|
||||||
{self.full_name()}.remove(hook)
|
|
||||||
raise
|
|
||||||
"""
|
|
||||||
if self.legacy_hook:
|
|
||||||
args = ", ".join([f'"{self.legacy_hook}"'] + arg_names)
|
|
||||||
out += f"""\
|
|
||||||
# legacy support
|
|
||||||
runHook({args})
|
|
||||||
"""
|
|
||||||
return out + "\n\n"
|
|
||||||
|
|
||||||
def filter_fire_code(self) -> str:
|
|
||||||
arg_names = self.arg_names()
|
|
||||||
out = f"""\
|
|
||||||
def run_{self.full_name()}({self.cb_args}) -> {self.return_type}:
|
|
||||||
for filter in {self.full_name()}:
|
|
||||||
try:
|
|
||||||
{arg_names[0]} = filter({", ".join(arg_names)})
|
|
||||||
except:
|
|
||||||
# if the hook fails, remove it
|
|
||||||
{self.full_name()}.remove(filter)
|
|
||||||
raise
|
|
||||||
"""
|
|
||||||
if self.legacy_hook:
|
|
||||||
args = ", ".join([f'"{self.legacy_hook}"'] + arg_names)
|
|
||||||
out += f"""\
|
|
||||||
# legacy support
|
|
||||||
runFilter({args})
|
|
||||||
"""
|
|
||||||
|
|
||||||
out += f"""\
|
|
||||||
return {arg_names[0]}
|
|
||||||
"""
|
|
||||||
return out + "\n\n"
|
|
||||||
|
|
||||||
# Hook list
|
# Hook list
|
||||||
######################################################################
|
######################################################################
|
||||||
|
@ -121,26 +19,9 @@ def run_{self.full_name()}({self.cb_args}) -> {self.return_type}:
|
||||||
hooks = [
|
hooks = [
|
||||||
Hook(name="leech", cb_args="card: Card", legacy_hook="leech"),
|
Hook(name="leech", cb_args="card: Card", legacy_hook="leech"),
|
||||||
Hook(name="odue_invalid"),
|
Hook(name="odue_invalid"),
|
||||||
Hook(name="mod_schema", cb_args="proceed: bool", return_type="bool")
|
Hook(name="mod_schema", cb_args="proceed: bool", return_type="bool"),
|
||||||
]
|
]
|
||||||
|
|
||||||
hooks.sort(key=attrgetter("name"))
|
if __name__ == "__main__":
|
||||||
|
path = os.path.join(os.path.dirname(__file__), "..", "anki", "hooks.py")
|
||||||
######################################################################
|
update_file(path, hooks)
|
||||||
|
|
||||||
tools_dir = os.path.dirname(__file__)
|
|
||||||
hooks_py = os.path.join(tools_dir, "..", "anki", "hooks.py")
|
|
||||||
|
|
||||||
code = ""
|
|
||||||
for hook in hooks:
|
|
||||||
code += hook.list_code()
|
|
||||||
code += "\n\n"
|
|
||||||
for hook in hooks:
|
|
||||||
code += hook.fire_code()
|
|
||||||
|
|
||||||
orig = open(hooks_py).read()
|
|
||||||
new = re.sub("(?s)# @@AUTOGEN@@.*?# @@AUTOGEN@@\n", f"# @@AUTOGEN@@\n\n{code}# @@AUTOGEN@@\n", orig)
|
|
||||||
|
|
||||||
open(hooks_py, "wb").write(new.encode("utf8"))
|
|
||||||
|
|
||||||
print("Updated hooks.py")
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ MAKEFLAGS += --warn-undefined-variables
|
||||||
MAKEFLAGS += --no-builtin-rules
|
MAKEFLAGS += --no-builtin-rules
|
||||||
.SUFFIXES:
|
.SUFFIXES:
|
||||||
|
|
||||||
BLACKARGS := -t py36 aqt tests setup.py --exclude='aqt/forms|buildinfo'
|
BLACKARGS := -t py36 aqt tests setup.py tools/*.py --exclude='aqt/forms|buildinfo'
|
||||||
ISORTARGS := aqt tests setup.py
|
ISORTARGS := aqt tests setup.py
|
||||||
|
|
||||||
$(shell mkdir -p .build ../dist)
|
$(shell mkdir -p .build ../dist)
|
||||||
|
@ -35,7 +35,11 @@ TSDEPS := $(wildcard ts/src/*.ts)
|
||||||
(cd ts && npm i && npm run build)
|
(cd ts && npm i && npm run build)
|
||||||
@touch $@
|
@touch $@
|
||||||
|
|
||||||
BUILD_STEPS := .build/run-deps .build/dev-deps .build/js .build/ui .build/i18n aqt/buildinfo.py
|
.build/hooks: tools/genhooks.py
|
||||||
|
python tools/genhooks.py
|
||||||
|
@touch $@
|
||||||
|
|
||||||
|
BUILD_STEPS := .build/run-deps .build/dev-deps .build/js .build/ui .build/hooks .build/i18n aqt/buildinfo.py
|
||||||
|
|
||||||
# Checking
|
# Checking
|
||||||
######################
|
######################
|
||||||
|
|
47
qt/aqt/gui_hooks.py
Normal file
47
qt/aqt/gui_hooks.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
"""
|
||||||
|
See pylib/anki/hooks.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Callable, Dict, List # pylint: disable=unused-import
|
||||||
|
|
||||||
|
from anki.hooks import runFilter, runHook # pylint: disable=unused-import
|
||||||
|
|
||||||
|
# New hook/filter handling
|
||||||
|
##############################################################################
|
||||||
|
# The code in this section is automatically generated - any edits you make
|
||||||
|
# will be lost. To add new hooks, see ../tools/genhooks.py
|
||||||
|
#
|
||||||
|
# @@AUTOGEN@@
|
||||||
|
|
||||||
|
mpv_idle_hook: List[Callable[[], None]] = []
|
||||||
|
mpv_will_play_hook: List[Callable[[str], None]] = []
|
||||||
|
|
||||||
|
|
||||||
|
def run_mpv_idle_hook() -> None:
|
||||||
|
for hook in mpv_idle_hook:
|
||||||
|
try:
|
||||||
|
hook()
|
||||||
|
except:
|
||||||
|
# if the hook fails, remove it
|
||||||
|
mpv_idle_hook.remove(hook)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def run_mpv_will_play_hook(file: str) -> None:
|
||||||
|
for hook in mpv_will_play_hook:
|
||||||
|
try:
|
||||||
|
hook(file)
|
||||||
|
except:
|
||||||
|
# if the hook fails, remove it
|
||||||
|
mpv_will_play_hook.remove(hook)
|
||||||
|
raise
|
||||||
|
# legacy support
|
||||||
|
runHook("mpvWillPlay", file)
|
||||||
|
|
||||||
|
|
||||||
|
# @@AUTOGEN@@
|
|
@ -30,6 +30,7 @@ from anki.hooks import addHook, runFilter, runHook
|
||||||
from anki.lang import _, ngettext
|
from anki.lang import _, ngettext
|
||||||
from anki.storage import Collection
|
from anki.storage import Collection
|
||||||
from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields
|
from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields
|
||||||
|
from aqt import gui_hooks
|
||||||
from aqt.profiles import ProfileManager as ProfileManagerType
|
from aqt.profiles import ProfileManager as ProfileManagerType
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.qt import sip
|
from aqt.qt import sip
|
||||||
|
@ -1158,9 +1159,10 @@ Difference to correct time: %s."""
|
||||||
addHook("remNotes", self.onRemNotes)
|
addHook("remNotes", self.onRemNotes)
|
||||||
hooks.odue_invalid_hook.append(self.onOdueInvalid)
|
hooks.odue_invalid_hook.append(self.onOdueInvalid)
|
||||||
|
|
||||||
addHook("mpvWillPlay", self.onMpvWillPlay)
|
gui_hooks.mpv_will_play_hook.append(self.on_mpv_will_play)
|
||||||
addHook("mpvIdleHook", self.onMpvIdle)
|
gui_hooks.mpv_idle_hook.append(self.on_mpv_idle)
|
||||||
self._activeWindowOnPlay = None
|
|
||||||
|
self._activeWindowOnPlay: Optional[QWidget] = None
|
||||||
|
|
||||||
def onOdueInvalid(self):
|
def onOdueInvalid(self):
|
||||||
showWarning(
|
showWarning(
|
||||||
|
@ -1175,13 +1177,13 @@ and if the problem comes up again, please ask on the support site."""
|
||||||
head, ext = os.path.splitext(file.lower())
|
head, ext = os.path.splitext(file.lower())
|
||||||
return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi")
|
return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi")
|
||||||
|
|
||||||
def onMpvWillPlay(self, file):
|
def on_mpv_will_play(self, file: str) -> None:
|
||||||
if not self._isVideo(file):
|
if not self._isVideo(file):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._activeWindowOnPlay = self.app.activeWindow() or self._activeWindowOnPlay
|
self._activeWindowOnPlay = self.app.activeWindow() or self._activeWindowOnPlay
|
||||||
|
|
||||||
def onMpvIdle(self):
|
def on_mpv_idle(self) -> None:
|
||||||
w = self._activeWindowOnPlay
|
w = self._activeWindowOnPlay
|
||||||
if not self.app.activeWindow() and w and not sip.isdeleted(w) and w.isVisible():
|
if not self.app.activeWindow() and w and not sip.isdeleted(w) and w.isVisible():
|
||||||
w.activateWindow()
|
w.activateWindow()
|
||||||
|
|
|
@ -11,10 +11,11 @@ import threading
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from anki.hooks import addHook, runHook
|
from anki.hooks import addHook
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
from anki.sound import allSounds
|
from anki.sound import allSounds
|
||||||
from anki.utils import isLin, isMac, isWin, tmpdir
|
from anki.utils import isLin, isMac, isWin, tmpdir
|
||||||
|
from aqt import gui_hooks
|
||||||
from aqt.mpv import MPV, MPVBase
|
from aqt.mpv import MPV, MPVBase
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import restoreGeom, saveGeom, showWarning
|
from aqt.utils import restoreGeom, saveGeom, showWarning
|
||||||
|
@ -156,8 +157,8 @@ class MpvManager(MPV):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(window_id=None, debug=False)
|
super().__init__(window_id=None, debug=False)
|
||||||
|
|
||||||
def queueFile(self, file) -> None:
|
def queueFile(self, file: str) -> None:
|
||||||
runHook("mpvWillPlay", file)
|
gui_hooks.run_mpv_will_play_hook(file)
|
||||||
|
|
||||||
path = os.path.join(os.getcwd(), file)
|
path = os.path.join(os.getcwd(), file)
|
||||||
self.command("loadfile", path, "append-play")
|
self.command("loadfile", path, "append-play")
|
||||||
|
@ -172,7 +173,7 @@ class MpvManager(MPV):
|
||||||
self.command("seek", secs, "relative")
|
self.command("seek", secs, "relative")
|
||||||
|
|
||||||
def on_idle(self) -> None:
|
def on_idle(self) -> None:
|
||||||
runHook("mpvIdleHook")
|
gui_hooks.run_mpv_idle_hook()
|
||||||
|
|
||||||
|
|
||||||
def setMpvConfigBase(base) -> None:
|
def setMpvConfigBase(base) -> None:
|
||||||
|
|
22
qt/tools/genhooks.py
Normal file
22
qt/tools/genhooks.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
"""
|
||||||
|
See pylib/tools/genhooks.py for more info.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from anki.hooks_gen import Hook, update_file
|
||||||
|
|
||||||
|
# Hook list
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
hooks = [
|
||||||
|
Hook(name="mpv_idle"),
|
||||||
|
Hook(name="mpv_will_play", cb_args="file: str", legacy_hook="mpvWillPlay"),
|
||||||
|
]
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
path = os.path.join(os.path.dirname(__file__), "..", "aqt", "gui_hooks.py")
|
||||||
|
update_file(path, hooks)
|
Loading…
Reference in a new issue