add separate file for gui hooks

This commit is contained in:
Damien Elmes 2020-01-13 14:38:05 +10:00
parent d92e27ab50
commit 4bb3d7a958
8 changed files with 222 additions and 136 deletions

View file

@ -5,7 +5,7 @@ MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
RUNARGS :=
.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
$(shell mkdir -p .build ../dist)

129
pylib/anki/hooks_gen.py Normal file
View 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"))

View file

@ -11,109 +11,7 @@ To add a new hook:
"""
import os
import re
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"
from anki.hooks_gen import Hook, update_file
# Hook list
######################################################################
@ -121,26 +19,9 @@ def run_{self.full_name()}({self.cb_args}) -> {self.return_type}:
hooks = [
Hook(name="leech", cb_args="card: Card", legacy_hook="leech"),
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"))
######################################################################
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")
if __name__ == "__main__":
path = os.path.join(os.path.dirname(__file__), "..", "anki", "hooks.py")
update_file(path, hooks)

View file

@ -5,7 +5,7 @@ MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
.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
$(shell mkdir -p .build ../dist)
@ -35,7 +35,11 @@ TSDEPS := $(wildcard ts/src/*.ts)
(cd ts && npm i && npm run build)
@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
######################

47
qt/aqt/gui_hooks.py Normal file
View 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@@

View file

@ -30,6 +30,7 @@ from anki.hooks import addHook, runFilter, runHook
from anki.lang import _, ngettext
from anki.storage import Collection
from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields
from aqt import gui_hooks
from aqt.profiles import ProfileManager as ProfileManagerType
from aqt.qt import *
from aqt.qt import sip
@ -1158,9 +1159,10 @@ Difference to correct time: %s."""
addHook("remNotes", self.onRemNotes)
hooks.odue_invalid_hook.append(self.onOdueInvalid)
addHook("mpvWillPlay", self.onMpvWillPlay)
addHook("mpvIdleHook", self.onMpvIdle)
self._activeWindowOnPlay = None
gui_hooks.mpv_will_play_hook.append(self.on_mpv_will_play)
gui_hooks.mpv_idle_hook.append(self.on_mpv_idle)
self._activeWindowOnPlay: Optional[QWidget] = None
def onOdueInvalid(self):
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())
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):
return
self._activeWindowOnPlay = self.app.activeWindow() or self._activeWindowOnPlay
def onMpvIdle(self):
def on_mpv_idle(self) -> None:
w = self._activeWindowOnPlay
if not self.app.activeWindow() and w and not sip.isdeleted(w) and w.isVisible():
w.activateWindow()

View file

@ -11,10 +11,11 @@ import threading
import time
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.sound import allSounds
from anki.utils import isLin, isMac, isWin, tmpdir
from aqt import gui_hooks
from aqt.mpv import MPV, MPVBase
from aqt.qt import *
from aqt.utils import restoreGeom, saveGeom, showWarning
@ -156,8 +157,8 @@ class MpvManager(MPV):
def __init__(self) -> None:
super().__init__(window_id=None, debug=False)
def queueFile(self, file) -> None:
runHook("mpvWillPlay", file)
def queueFile(self, file: str) -> None:
gui_hooks.run_mpv_will_play_hook(file)
path = os.path.join(os.getcwd(), file)
self.command("loadfile", path, "append-play")
@ -172,7 +173,7 @@ class MpvManager(MPV):
self.command("seek", secs, "relative")
def on_idle(self) -> None:
runHook("mpvIdleHook")
gui_hooks.run_mpv_idle_hook()
def setMpvConfigBase(base) -> None:

22
qt/tools/genhooks.py Normal file
View 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)