14

Anki を使用すると、カードで JavaScript を使用できます。たとえば、カードには次のようなものを含めることができます。

<script>
//JavaScript code here
</script>

カードが表示されたときに JavaScript コードが実行されます。

このようなスクリプトが Anki バックエンドとやり取りできるようにすることで柔軟性を高めるために (たとえば、メモのフィールドの値を変更したり、タグを追加したり、スケジューリングに影響を与えたりするためなど)、プラグインを作成したいと思います。いくつかのバックエンド関数を実装し、カードの JavaScript スクリプトがそれらを呼び出せるようにする Anki (バージョン 2) 用の -in。

たとえば、プラグインに Anki のオブジェクトとやり取りする (Python) 関数があるとします。

def myFunc():
# use plug-in's ability to interact with Anki's objects to do stuff

カードの JavaScript がその関数を呼び出せるようにしたいと考えています。たとえば、カードに次のようなものを含めることができます。

<script>
myFunc(); // This should invoke the plug-in's myFunc().
</script>

さまざまな Anki イベントがプラグインの機能を呼び出すようにフックを追加する方法は知っていますが、カード内の JavaScript がそうできるようにしたいと考えています。これを行うことはできますか?ありがとう!

4

1 に答える 1

13

@Louis によってリンクされた投稿を読み、同僚と問題について話し合い、さまざまなことを試してみて、最終的に解決策を思いつくことができました。

このアイデアは、次の 2 つのキー ポイント (および 2 つのサブキー ポイント) に要約できます。

  • プラグインは、カードの JavaScript スクリプトに「公開」される 1 つまたは複数のオブジェクトを作成できるため、カード スクリプトは、これらのオブジェクト (フィールドとメソッド) に、スクリプトのスコープの一部であるかのようにアクセスできます。

    • それを行うには、オブジェクトは特定のクラス (またはそのサブクラス) のインスタンスである必要があり、カード スクリプトに公開される各メソッドとプロパティは、適切な PyQt デコレータを使用して宣言する必要があります。

  • PyQt は、そのようなオブジェクトを webview に「注入」する機能を提供します。

    • プラグインは、Anki のレビュアーの Web ビューが (再) 初期化されるたびに、この挿入が確実に行われるようにする必要があります。

次のコードは、これを達成する方法を示しています。現在の状態 (「質問」または「回答」) を確認する方法と、メモのフィールドにアクセス (読み取り、さらに重要なことに、書き込み) する方法をカード スクリプトに提供します。

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")

これです!このようなプラグインがインストールされている場合、カード内の JavaScript スクリプトは pluginObject.state を使用して、それが質問の一部として実行されるか、回答の一部として実行されるかを確認できます (回答テンプレートで質問部分をラップすることによっても実現できます)。変数を設定するスクリプトを使用しますが、これはより適切です)、 pluginObject.field(name) を使用してメモからフィールドの値を取得します (Anki のプリプロセッサを使用してフィールドを JavaScript コードに直接挿入することによっても実現できます)。および pluginObject.field(name, value) を使用して、メモのフィールドの値を設定します (私の知る限り、これまではできませんでした)。もちろん、他の多くの機能を CardScriptObject にプログラムして、カード スクリプトがさらに多くのことを実行できるようにすることもできます (構成の読み取り/変更、別の質問/回答メカニズムの実装、スケジューラとの対話など)。

誰かが改善を提案できるなら、私は聞きたいです。具体的には、次のことに興味があります。

  • 署名の柔軟性を高めるために、メソッドとプロパティを公開するためのより適切な方法があるかどうか。と
  • 注入を実行するためのより面倒でない方法があるかどうか。
于 2014-04-26T11:57:40.830 に答える