IntelliJ Platform Pluginの開発にて、ApplicationManagerやToolWindowManagerを使って、Actionの中でToolWindow上のlabelを更新してみた

IntelliJ Platform Pluginの開発にて、「ToolWindow上のボタンをクリックすると、ラベルの値が更新される」みたいなことを実装したくなりました。

ボタンとラベルを定義したのと同じファイル内であれば、 actionListener を使って以下のように実装できます。

class UpdateInActionContent {
    var contentPanel : DialogPanel
    lateinit var myLabel: Cell<JLabel>

    init {
        contentPanel = panel {
            row {
                // ラベルの定義
                myLabel = label("Initial content")
                myLabel.component.name = "myLabel"

                // ボタンを定義し、actionListenerにクリックしたときに handleClick メソッドを実行
                button("Run Function") { event -> handleClick(event) }
            }
        }
    }

    private fun handleClick(event: ActionEvent) {
        myLabel.component.text = "handle click"
    }
}

 
ただ、 button()オーバーロードとして、 AnAction を受け取ることもできます。

この場合、AnActionは別クラスとして定義することになるため、 actionListener と同じように実装することができません。

 
そこで、どのようにすれば実現できるか試してみたときのメモを残します。

なお、動作するようにはなったもののこれで正しいのかは自信がないため、詳しい方がいれば教えていただけるとありがたいです。

 
目次

 

環境

 

調査

UIスレッドにあるコンポーネントへの書き込む方法について

公式ドキュメントによると、ToolWindow上のコンポーネントの更新を行う場合、 ApplicationManager.getApplication().invokeLater を使う必要がありそうでした。

 
なお、Pluginのスレッドモデルは 2023.3 の前後で変わったようです。

2023.3 Threading Model Changes Threading model has changed in 2023.3, please make sure to choose the correct version in the tabs below

https://plugins.jetbrains.com/docs/intellij/general-threading-rules.html#read-access

 

ToolWindow上にあるラベルコンポーネントを取得する方法について

公式ドキュメントによると、 ToolWindowManager.getToolWindow() を使うことで、ToolWindowにアクセスできそうでした。
Accessing Tool Window | Tool Windows | IntelliJ Platform Plugin SDK

 

ToolWindowManagerを使うため、ActionEvent ではなく AnActionEvent から project をもらう

ToolWindowManagerインスタンスを取得する getInstance() では project の値が必要です。
Project | IntelliJ Platform Plugin SDK

 
今回、ボタンを押したときにToolWindow上のlabelの値を更新する際、

button("Run Function") { event -> handleClick(event) }

と定義した場合、 handleClick のシグネチャ

private fun handleClick(event: ActionEvent) {}

となります。

ただ、ActionEventクラスには project の値が含まれません。

 
一方、Actionを使うときにオーバーライドが必要な actionPerformed の引数は AnActionEvent 型になることから、 project の値が含まれます。

そのため、以下のような実装で project の値が取得できます。

override fun actionPerformed(e: AnActionEvent) {
    val project = e.project ?: return
}

 
ちなみに e.project でprojectを取得する場合、 null が返ってくることもあります。

nullを回避するためには AnActionEvent にある getRequiredData を使うこともできます。

ただ、ソースコードコメントにある通り、 update メソッドで null ではないかのチェックをする前提で使えます。

Returns not null data by a data key. This method assumes that data has been checked for {@code null} in {@code AnAction#update} method.

 

https://github.com/JetBrains/intellij-community/blob/e3561dcaf64652cafb7ebca270d4a2cb536e5558/platform/editor-ui-api/src/com/intellij/openapi/actionSystem/AnActionEvent.java#L189

 

実装

ToolWindow上のコンテンツ

まずは、ToolWindow上に表示するコンテンツを用意します。

今回は

  • myLabel という name を持つラベル
  • actionListener を定義した Run Function ボタン
  • AnAction を定義した Run Action ボタン

を用意します。

kage com.github.thinkami.hellojetbrainsplugin.ui

import com.github.thinkami.hellojetbrainsplugin.actions.UpdateLabelAction
import com.intellij.openapi.ui.DialogPanel
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.panel
import java.awt.event.ActionEvent
import javax.swing.JLabel

class UpdateInActionContent {
    var contentPanel : DialogPanel
    lateinit var myLabel: Cell<JLabel>

    init {
        contentPanel = panel {
            row {
                myLabel = label("Initial content")
                myLabel.component.name = "myLabel"

                button("Run Function") { event -> handleClick(event) }
                button("Run Action", UpdateLabelAction())
            }
        }
    }

    private fun handleClick(event: ActionEvent) {
        myLabel.component.text = "handle click"
    }
}

 

ToolWindowFactory

続いて、用意したコンテンツを表示するToolWindowFactoryを用意します。

package com.github.thinkami.hellojetbrainsplugin.toolWindow

import com.github.thinkami.hellojetbrainsplugin.ui.UpdateInActionContent
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.ui.content.ContentFactory

class UpdateInActionToolWindowFactory : ToolWindowFactory {
    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        val content = ContentFactory.getInstance().createContent(UpdateInActionContent().contentPanel, null, false)
        toolWindow.contentManager.addContent(content)
    }
}

 

Action

Actionでは、ToolWindow上にある myLabel コンポーネントにアクセスし、そのラベルのテキストを更新します。

流れとしては

  • ToolWindowへアクセスするところは ApplicationManager.getApplication().invokeLater で囲む
  • toolWindow.component.components でルートのDialogPanel を取得
  • DialogPanel の components の中に一階層下の labelbutton があるため、 namemyLabel のものに絞り込み、 text を更新

となります。

package com.github.thinkami.hellojetbrainsplugin.actions

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.testFramework.requireIs
import javax.swing.JLabel

class UpdateLabelAction : AnAction() {
    override fun actionPerformed(e: AnActionEvent) {
        // AnActionEventに含まれる project の値を取得する
        val project = e.project ?: return

        // UIスレッドで非同期に処理を実行する
        ApplicationManager.getApplication().invokeLater {
            // project を元に ToolWindowを取得する
            val toolWindow = ToolWindowManager.getInstance(project).getToolWindow("UpdateInActionToolWindow")
                // ToolWindowが存在しない場合は何も処理しない
                ?: return@invokeLater

            // DialogPanelは1つしかないはず
            val panelComponent = toolWindow.component.components.filterIsInstance<DialogPanel>().first()

            // myLabelというnameを持ったlabelは1つしかないはず
            val labelComponent = panelComponent.components.filterIsInstance<JLabel>().find {
                it.name == "myLabel"
            }

            if (labelComponent != null) {
                labelComponent.text = "updated"
            }
        }
    }
}

 

plugin.xml

extensions に、今回追加した ToolWindow を登録します。

<extensions defaultExtensionNs="com.intellij">
    <toolWindow factoryClass="com.github.thinkami.hellojetbrainsplugin.toolWindow.UpdateInActionToolWindowFactory" id="UpdateInActionToolWindow"/>
</extensions>

 

動作確認

初期表示

initial content が表示されてます。

 

Run Function ボタンをクリック

ラベルが handle click に更新されました。

 

Run Action ボタンをクリック

ラベルが updated に更新されました。

 

その他参考資料

 

ソースコード

Githubにあげました。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin

今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin/pull/12