Tom*_*Tom 12 javascript python pyqt anki
Anki使卡片可以使用JavaScript.例如,卡片可以包含以下内容:
<script>
//JavaScript code here
</script>
Run Code Online (Sandbox Code Playgroud)
并且当显示卡时将执行JavaScript代码.
为了通过启用此类脚本与Anki后端进行交互(例如,为了更改注释字段的值,添加标签,影响调度等),允许更多的灵活性,我想写一个插件-in for Anki(版本2),它将实现一些后端功能并使卡的JavaScript脚本能够调用它们.
例如,假设我的插件中有一个(Python)函数与Anki的对象进行交互:
def myFunc():
# use plug-in's ability to interact with Anki's objects to do stuff
Run Code Online (Sandbox Code Playgroud)
我希望能够允许卡的JavaScript调用该功能,例如在卡中有这样的东西:
<script>
myFunc(); // This should invoke the plug-in's myFunc().
</script>
Run Code Online (Sandbox Code Playgroud)
我知道如何添加钩子,以便各种Anki事件调用我的插件的功能,但我想允许卡内的JavaScript这样做.这可以完成,如果是这样,那么如何?谢谢!
读了后通过@Louis联系,并与一些同事讨论了这个问题,并四处乱尝试不同的东西出来,我终于成功地想出了一个解决方案:
这个想法可以归纳为这两个关键点(和两个子关键点):
插件可以创建一个或多个对象,这些对象将"暴露"到卡片的JavaScript脚本中,这样卡片脚本就可以访问这些对象 - 它们的字段和方法 - 就像它们是脚本范围的一部分一样.
和
PyQt提供了将这些对象"注入"webview的功能.
以下代码显示了如何实现此目的.它为卡片脚本提供了检查当前状态("问题"或"答案")的方法,并提供了一种访问(读取,更重要的是 - 写入)笔记字段的方法.
from aqt import mw # Anki's main window object
from aqt import mw QObject # Our exposed object will be an instance of a subclass of QObject.
from aqt import mw pyqtSlot # a decorator for exposed methods
from aqt import mw pyqtProperty # a decorator for exposed properties
from anki.hooks import wrap # We will need this to hook to specific Anki functions in order to make sure the injection happens in time.
# a class whose instance(s) we can expose to card scripts
class CardScriptObject(QObject):
# some "private" fields - card scripts cannot access these directly
_state = None
_card = None
_note = None
# Using pyqtProperty we create a property accessible from the card script.
# We have to provide the type of the property (in this case str).
# The second argument is a getter method.
# This property is read-only. To make it writeable we would add a setter method as a third argument.
state = pyqtProperty(str, lambda self: self._state)
# The following methods are exposed to the card script owing to the pyqtSlot decorator.
# Without it they would be "private".
@pyqtSlot(str, result = str) # We have to provide the argument type(s) (excluding self),
# as well as the type of the return value - with the named result argument, if a value is to be returned.
def getField(self, name):
return self._note[name]
# Another method, without a return value:
@pyqtSlot(str, str)
def setField(self, name, value):
self._note[name] = value
self._note.flush()
# An example of a method that can be invoked with two different signatures -
# pyqtSlot has to be used for each possible signature:
# (This method replaces the above two.
# All three have been included here for the sake of the example.)
@pyqtSlot(str, result = str)
@pyqtSlot(str, str)
def field(self, name, value = None): # sets a field if value given, gets a field otherwise
if value is None: return self._note[name]
self._note[name] = value
self._note.flush()
cardScriptObject = CardScriptObject() # the object to expose to card scripts
flag = None # This flag is used in the injection process, which follows.
# This is a hook to Anki's reviewer's _initWeb method.
# It lets the plug-in know the reviewer's webview is being initialised.
# (It would be too early to perform the injection here, as this method is called before the webview is initialised.
# And it would be too late to do it after _initWeb, as the first card would have already been shown.
# Hence this mechanism.)
def _initWeb():
global flag
flag = True
# This is a hook to Anki's reviewer's _showQuestion method.
# It populates our cardScriptObject's "private" fields with the relevant values,
# and more importantly, it exposes ("injects") the object to the webview's JavaScript scope -
# but only if this is the first card since the last initialisation, otherwise the object is already exposed.
def _showQuestion():
global cardScriptObject, flag
if flag:
flag = False
# The following line does the injection.
# In this example our cardScriptObject will be accessible from card scripts
# using the name pluginObject.
mw.web.page().mainFrame().addToJavaScriptWindowObject("pluginObject", cardScriptObject)
cardScriptObject._state = "question"
cardScriptObject._card = mw.reviewer.card
cardScriptObject._note = mw.reviewer.card.note()
# The following hook to Anki's reviewer's _showAnswer is not necessary for the injection,
# but in this example it serves to update the state.
def _showAnswer():
global cardScriptObject
cardScriptObject._state = "answer"
# adding our hooks
# In order to already have our object injected when the first card is shown (so that its scripts can "enjoy" this plug-in),
# and in order for the card scripts to have access to up-to-date information,
# our hooks must be executed _before_ the relevant Anki methods.
mw.reviewer._initWeb = wrap(mw.reviewer._initWeb, _initWeb, "before")
mw.reviewer._showQuestion = wrap(mw.reviewer._showQuestion, _showQuestion, "before")
mw.reviewer._showAnswer = wrap(mw.reviewer._showAnswer, _showAnswer, "before")
Run Code Online (Sandbox Code Playgroud)
就是这个!安装了这样的插件后,来自卡内的JavaScript脚本可以使用pluginObject.state来检查它是作为问题的一部分运行还是作为答案的一部分运行(也可以通过将问题部分包装在答案模板中来实现)使用设置变量的脚本,但这是更整洁的),pluginObject.field(name)从注释中获取字段的值(也可以通过使用Anki的预处理器将字段直接注入JavaScript代码来实现)和pluginObject.field(name,value)来设置注释中字段的值(据我所知,到目前为止还无法完成).当然,许多其他功能可以编程到我们的CardScriptObject中,以允许卡脚本执行更多操作(读取/更改配置,实现另一个问题/答案机制,与调度程序交互等).
如果有人能提出改进建议,我有兴趣听听.具体来说,我感兴趣的是: