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
を使う必要がありそうでした。
- General Threading Rules | IntelliJ Platform Plugin SDK
- Modality and invokeLater() | General Threading Rules | IntelliJ Platform Plugin SDK
なお、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.
実装
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
の中に一階層下のlabel
やbutton
があるため、name
がmyLabel
のものに絞り込み、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