IntelliJ Platform Pluginの開発にて、ToolWindow上で、テーブルの絞り込み条件入力で使うコンポーネントを SearchTextField にしてみた

以前の記事では、テーブルの絞り込み条件を入力する項目として TextField を使っていました。
IntelliJ Platform Pluginの開発にて、ToolWindow上で、TextFieldの入力値に従って絞り込み可能なテーブル(JBTable)を表示してみた - メモ的な思考的な

そんな中、JetBrains の Platform UI Guidelines を見ていたところ、 SearchTextField という項目がありました。
Search field | IntelliJ Platform UI Guidelines

公式ドキュメント User Interfrace Components には記載がなかったものの、「ガイドラインに記載があるならライブラリとして用意されているかもしれない」と思い調べてみたところ、 com.intellij.ui パッケージに SearchTextField コンポーネントがありました。
https://github.com/JetBrains/intellij-community/blob/89df267e41664d91b114af8d1c57a78abe2c8456/platform/platform-api/src/com/intellij/ui/SearchTextField.java

 
そこで、今回 SearchTextField をためしてみたことから、メモを残します。

 
目次

 

環境

 
なお、前回の記事ソースコードをもとに、今回の実装を加えています。

 

TextField を SearchTextField に置き換える

Contentで定義していたTextFieldをSearchTextFieldに差し替えます。

もともとのTextField版はこちらです。

searchText = textField()
searchText.component.document.addDocumentListener(object: DocumentListener { /* 略 */ })

 
続いて、SearchTextFieldに差し替えた版はこちらです。

SearchTextFieldの場合、 component プロパティに対して addDocumentListener を設定できるようです。

searchText = cell(SearchTextField())
searchText.component.addDocumentListener(object: DocumentListener { /* 略 */ })

 
以上で実装は完了です。

   

動作確認

TextFieldのときはこのような表示でした。

 
SearchTextFieldに差し替えるとこのような感じになります。

入力欄の右側にある x をクリックすると、入力値をクリアできます。

 
また、何回かSearchTextFieldに入力後に虫めがねアイコンをクリックすると、入力履歴が表示されます。

 
その履歴の中で適用したい値をクリックすると、SearchTextField に値が入力されます。

 
これらの機能を自分で実装すると大変なので、検索用の入力項目を作るときは SearchTextField を使っていけばよさそうです。

 

ソースコード

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

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

IntelliJ Platform Pluginの開発にて、ToolWindow上で、DialogWrapperとPropertiesComponentを使って、設定の保存と読み込みを行ってみた

以前、TextFieldの入力値により、テーブルの値を絞り込むことをためしてみました。
IntelliJ Platform Pluginの開発にて、ToolWindow上で、TextFieldの入力値に従って絞り込み可能なテーブル(JBTable)を表示してみた - メモ的な思考的な

以前は部分一致のみの絞り込みを実装しました。

ただ、設定により

  • 部分一致
  • 前方一致

を切り替えたくなったため、ためしてみたときのメモを残します。

 
目次

 

環境

 
なお、ソースコードは前回の記事の続きに実装していきます。
IntelliJ Platform Pluginの開発にて、ToolWindow上で、CellRendererを使ってテーブル(JBTable)の列に画像を表示してみた - メモ的な思考的な

 

仕様について

今回は以下の仕様としました。

  • ToolWindow上のボタンをクリックすると、設定ダイアログが開き、絞り込み方法をコンボボックスで選択する
  • OKボタンを押すと、コンボボックスの選択値が保存される
  • その状態でTextFieldに入力すると、テーブルの絞り込み方法がコンボボックスの選択値により変化する

 
上記の仕様を実現するため、1つずつ段階を追って実装していきます。

 

ToolWindow上のボタンをクリックすると、設定ダイアログを開くようにする

IntelliJ Platform Pluginでは、 DialogWrapper を使うことでダイアログを表示できます。
Dialogs | IntelliJ Platform Plugin SDK

 

DialogWrapperを継承したダイアログクラスを用意する

設定用のダイアログを用意します。

まずは Hello とだけ表示するダイアログにします。

package com.github.thinkami.hellojetbrainsplugin.ui

import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.dsl.builder.panel
import javax.swing.JComponent

class SettingsDialog: DialogWrapper(true) {
    init {
        title = "My Settings"
        init()
    }

    override fun createCenterPanel(): JComponent {
        return panel {
            row {
                label("Hello")
            }
        }
    }
}

 

ダイアログを開くActionを用意する

ダイアログを開くためのActionを用意します。
Actions | IntelliJ Platform Plugin SDK

dialog.showAndGet() でダイアログでOKを押したときの挙動も定義できます。

今回はコンソールへHelloと出力してみます。

package com.github.thinkami.hellojetbrainsplugin.actions

import com.github.thinkami.hellojetbrainsplugin.ui.SettingsDialog
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent

class SettingsAction: AnAction() {
    override fun actionPerformed(e: AnActionEvent) {
        val dialog = SettingsDialog()
        if (dialog.showAndGet()) {
            // OKボタンをクリックした場合に、コンソールへHelloを出力する
            println("Hello")
        }
    }
}

 

ToolWindow上にボタンを用意し、クリックするとActionを起動する

今までの記事で作ってきているので、説明は省略します。

row {
    button("Settings", SettingsAction())
}

 

動作確認

ToolWindowを開くと、設定ダイアログを開くボタンが表示されます。

 
クリックすると、ダイアログが表示されます。

 

ここでの実装

以下のコミットです。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin/pull/16/commits/7b616431a27f632f1a31fd39ac27242ae73fccf0

 

ダイアログにコンボボックスを表示する

続いて、ダイアログにコンボボックスを表示してみます。

 

Swingでコンボボックスを表示する方法を調査

調べてみたところ、以下の記事がありました。
コンボボックス JComboBox の使い方 - Swing のコンポーネント - Java 入門

IntelliJ Platform PluginのToolWindowでも使えそうです。

 

ダイアログにコンボボックスを表示する

Swingの実装とほぼ同じです。

プロパパティ comboBox を用意し、ダイアログを利用する側でも参照できるようにします。

また、 init の中でコンボボックスの選択肢などの設定を行い、 createContentPanel の中でコンボボックスを定義します。

class SettingsDialog: DialogWrapper(true) {
    val comboBox: JComboBox<String>

    init {
        title = "My Settings"

        // 追加
        comboBox = JComboBox<String>()
        comboBox.addItem("Preamble match") // 前方一致
        comboBox.addItem("Partial match")  // 部分一致
        comboBox.name = "matchType"

        init()
    }

    override fun createCenterPanel(): JComponent {
        return panel {
            row {
                label("Hello")
            }
            // 追加
            row {
                cell(comboBox)
            }
        }
    }
}

 

Actionでコンボボックスの選択値を確認する

actionPerformed メソッドの中で、OKボタンをクリックしたときにコンソールへコンボボックスの選択値を出力してみます。

なお、コンボボックスの getItemsAtselectedIndex を組み合わせることで、コンボボックスの選択値を取得できます。

override fun actionPerformed(e: AnActionEvent) {
    val dialog = SettingsDialog()
    if (dialog.showAndGet()) {
        // OKボタンをクリックしたときに、コンソールへコンボボックスの選択値を出力する
        val combo = dialog.comboBox
        val selectedType = combo.getItemAt(combo.selectedIndex)
        println(selectedType)
    }
}

 

動作確認

ダイアログにコンボボックスが表示されました。

 
OKボタンをクリックすると、コンソールへ選択値が出力されました。

 

ここでの実装

以下のコミットです。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin/pull/16/commits/626379719771a00324ac457e6ccc9efcbe2cb170

 

コンボボックスの選択肢を PropertiesComponent で永続化する

永続化する方法を調査

公式ドキュメントでは、以下のページに値の永続化について記載されていました。
Persisting State of Components | IntelliJ Platform Plugin SDK

今回はお手軽に値を永続化したかったため、 PropertiesComponent を使うことにしました。
Using PropertiesComponent for Simple Non-Roamable Persistence | Persisting State of Components | IntelliJ Platform Plugin SDK

 

ダイアログでOKボタンをクリック後、PropertiesComponent で永続化する

actionPerformed の中で PropertiesComponent を使ってコンボボックスの選択値を永続化します。

なお、必要かどうかは分かりませんが、UIコンポーネントを操作しているため、今回は ApplicationManager.getApplication().invokeLater で囲っておきました。

override fun actionPerformed(e: AnActionEvent) {
    ApplicationManager.getApplication().invokeLater {
        val dialog = SettingsDialog()
        if (dialog.showAndGet()) {
            // OKボタンをクリックしたときに、コンソールへコンボボックスの選択値を出力する
            val combo = dialog.comboBox
            val selectedType = combo.getItemAt(combo.selectedIndex)
            println(selectedType)

            // コンボボックスの選択値を永続化する
            val properties = PropertiesComponent.getInstance()
            properties.setValue("matchType", selectedType)
        }
    }
}

 

永続化した設定をコンボボックスのデフォルト選択値にする

init の中で PropertiesComponent を使い、永続化情報を取り出してコンボボックスのデフォルト選択値にします。

init {
    // ...
    val properties = PropertiesComponent.getInstance()
    val matchType = properties.getValue("matchType")
    if (!matchType.isNullOrBlank()) {
        comboBox.selectedItem = matchType
    }
}

 

動作確認

ダイアログのコンボボックスで、初期値から Partial match へと選択値を変更します。

 
再度ダイアログを開くと、初期値が Partial match へと変更となっています。

 

ここでの実装

以下のコミットです。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin/pull/16/commits/b04092157dd3d4faef12696d929acab65fb5888f

 

コンボボックスの選択肢により、テーブルの絞り込み方法を変更する

最後の仕上げです。

 

実装

filterChanged メソッドにてテーブルの絞り込みを行う際、 PropertiesComponent から値を取り出して使用します。

fun filterChanged() {
    tableData = allData.filter {
        val name = it[1] // Nameで絞り込むため、列番号を指定

        // 絞り込み条件を設定から取得して使用する
        val matchType = PropertiesComponent.getInstance().getValue("matchType")
        if (matchType == "Partial match") {
            name.contains(this.tableFilter.filterText)
        } else {
            name.startsWith(this.tableFilter.filterText)
        }
    }.toList() // allDataとは別オブジェクトにするため toList する

    // filterが更新されたことを通知する
    this.fireTableDataChanged()
}

 

動作確認

絞り込みを行わない場合のテーブル状態です。

 
ダイアログで Preamble match を選択した場合の絞り込みです。

という文字を入力しても、前方一致で検索するため、該当行はありません。

 
続いて、ダイアログで Partial match を選択した場合です。

この場合は部分一致であるため、 を入力すると該当行が表示されます。

 

ここでの実装

以下のコミットです。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin/pull/16/commits/99a4988dd4ba8f26b193accdd85de2a2a9a56042

 

ソースコード

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

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

IntelliJ Platform Pluginの開発にて、ToolWindow上で、CellRendererを使ってテーブル(JBTable)の列に画像を表示してみた

前回、ToolWindow上にテーブル(JBTable)を表示してみました。
IntelliJ Platform Pluginの開発にて、ToolWindow上で、TextFieldの入力値に従って絞り込み可能なテーブル(JBTable)を表示してみた - メモ的な思考的な

今回は、テーブルの列に画像を表示できるかためしてみたので、メモを残します。

 
目次

 

環境

 
なお、ソースコードは前回の記事のものを引き続き使います。

 

表示する方法を調査

SwingのJTableにおける列の表示方法を調べたところ、以下の記事がありました。
JTable でのデータの表示方法を決める セルレンダラー - Swing の JTable の使い方 - Java の Swing を用いた GUI - Java 入門

上記記事では、セルレンダラーにより列の表示方法を決めていました。

そこで、IntelliJ Platform Plugin の場合も同じ方法で実現できるかもしれないと考え、ためしてみます。

 

実装

画像ファイルを resources/icons ディレクトリの中に置く

まずは描画する画像ファイルを配置します。

 
画像ファイルをどこに置くのが適切なのか、公式ドキュメントには見つかりませんでした。

そこで今回は、リソースっぽいファイルの置き場である resources/icons の中に shinanogold.png ファイルを置くことにしました。

 

ImageIcon で画像ファイルを参照するobjectの作成

続いて、画像ファイルを参照するobjectを作成します。

画像ファイルを参照できるようにする場合、kotlinでは ImageIcon クラスを使うのが良さそうでした。

そこで、 ui ディレクトリの中に AppleTableCellIcon objectを用意し、リソースディレクトリの中に置いたファイルを参照します。

package com.github.thinkami.hellojetbrainsplugin.ui

import javax.swing.ImageIcon

object AppleTableCellIcon {
    val ShinanoGold = ImageIcon(requireNotNull(javaClass.getResource("/icons/shinanogold.png")))
}

 

DefaultTableCellRenderer を継承したセルレンダラーを作成

今回は、3列目が の場合のみ、画像ファイルを表示してみます。

package com.github.thinkami.hellojetbrainsplugin.ui

import javax.swing.table.DefaultTableCellRenderer

class AppleTableCellRenderer: DefaultTableCellRenderer() {
    override fun setValue(value: Any?) {
        if (value == "黄") {
            // 黄色の場合だけ、シナノゴールドのアイコンに差し替える
            icon = AppleTableCellIcon.ShinanoGold
            text = ""
            return
        }

        icon = null
        text = value.toString()
    }
}

 

JBTableのcellRendererに、セルレンダラーを指定

今回は3列目に上記で作成したセルレンダラーを設定します。

row {
    val table = JBTable()
    table.model = appleTableModel
    table.columnModel.getColumn(2).cellRenderer = AppleTableCellRenderer()
    myTableModel = scrollCell(table)
}

 
以上で実装は完了です。

 

動作確認

Run Pluginしてみると、3列目が の値になっている場合のみ、画像が表示されました。

 
前回作成した絞り込み機能を使った場合でも、正しく動作しました。

 

ソースコード

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

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

IntelliJ Platform Pluginの開発にて、ToolWindow上で、TextFieldの入力値に従って絞り込み可能なテーブル(JBTable)を表示してみた

以前、「ToolWindow上に色々なコンポーネントを表示してみる」ことを試してみました。
IntelliJ Platform Pluginの開発にて、Kotlin UI DSL Version 2 や Swing を使って、ToolWindow上にコンポーネントを表示してみた - メモ的な思考的な

そんな中、「TextFieldの入力値に従って絞り込み可能なテーブル」を実現できるか気になったことから、試してみたときのメモを残します。

 
目次

 

環境

 

IntelliJ Platform PluginのToolWindowにテーブルを表示するには

どのようにすればテーブルを表示できるか分からなかったため、まずはそこから調べてみます。

 
IntelliJ Platform Pluginの公式ページには、テーブルをどのように表示すればよいかは記載されていませんでした。

一方、IntelliJ Platform PluginのToolWindow上にはJavaのSwingコンポーネントを表示できることは分かっています。

そのため、Swingのテーブル(JTable)と同じような感じで作っていけるのではと考えました。

 
次に、ToolWindowsで使えるコンポーネントIDE上でながめていると、 com.intellij.ui.table.JBTable がありました。

Github上の実装を見たところ、 JTable を継承していることが分かりました。
https://github.com/JetBrains/intellij-community/blob/master/platform/platform-api/src/com/intellij/ui/table/JBTable.java

 
そこで今回は、 JBTable コンポーネントを使ってテーブルを表示してみることにしました。

 

絞り込み機能がないテーブルを実装

まずは、単純にテーブルを表示してみます。

JTableはどのようにしてデータを表示しているのか調べたところ、以下の記事に詳しく書かれていました。
JTable でデータとビューをつなぐテーブルモデル - Swing の JTable の使い方 - Java の Swing を用いた GUI - Java 入門

 
そこで、

  • データ置き場として、TableModel インタフェースを実装した AbstractModel」を継承したクラスを定義
  • Contentに JBTable を定義
  • ContentFactoryを定義
  • plugin.xmlを定義

の順に実装していきます。

 

AbstractModelを継承したクラスを定義

まず、テーブルに関する値を AbstractModel を継承したクラスに定義します。

今回は以下を定義しています。

  • allData に、テーブルに表示するためのデータを2次元のリストで定義する
    • 今回はデータを3行用意
  • colkumns に、テーブルの列を定義する
  • テーブルを表示するために必要最低限のメソッドをオーバーライドする
    • getRowCount
    • getColumnCount
    • getValueAt
package com.github.thinkami.hellojetbrainsplugin.ui

import javax.swing.table.AbstractTableModel

class AppleTableModel: AbstractTableModel() {
    val allData: List<List<String>> = listOf(
        listOf("1", "シナノゴールド", "黄"),
        listOf("2", "シナノホッペ", "赤"),
        listOf("3", "ジョナゴールド", "赤"),
    )

    val columns: List<String> = listOf("No", "Name", "Color")

    override fun getRowCount(): Int {
        return allData.size
    }

    override fun getColumnCount(): Int {
        return columns.size
    }

    override fun getValueAt(rowIndex: Int, columnIndex: Int): Any {
        return allData[rowIndex][columnIndex]
    }
}

   

Contentを定義

続いて、テーブルを表示するための Contentを定義します。

ここでのポイントは以下でした。

package com.github.thinkami.hellojetbrainsplugin.ui

import com.intellij.openapi.ui.DialogPanel
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.table.JBTable

class AppleTableContent {
    var contentPanel : DialogPanel
    lateinit var myTableModel: Cell<JBTable>

    init {
        contentPanel = panel {
            row {
                val table = JBTable()
                val model = AppleTableModel()
                table.model = model
                myTableModel = cell(table)
            }
        }
    }
}

 

ToolWindowFactoryを定義

上記で作った Content を ToolWindowFactoryを使って ToolWindow へ表示します。

package com.github.thinkami.hellojetbrainsplugin.toolWindow

import com.github.thinkami.hellojetbrainsplugin.ui.AppleTableContent
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 AppleTableToolWindowFactory: ToolWindowFactory {
    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        val content = ContentFactory.getInstance().createContent(AppleTableContent().contentPanel, null, false)
        toolWindow.contentManager.addContent(content)
    }
}

 

plugin.xmlに、ToolWindowを追加

ToolWindowとして表示できるよう、 plugin.xml に定義を追加します。

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

 

動作確認

ToolWindowを表示してみると、想定した通りのデータが表示されました。

 

ここでの実装

以下のコミットです。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin/commit/b292c08a124df79a642b334008574d39dbc1c94f

 

データを固定で絞り込んだテーブルを実装

次に、データを絞り込んだテーブルを表示してみます。

 

AbstractModelを継承したクラスを修正

まずは絞り込み後のデータを入れておくプロパティ tableData を用意します。

lateinit var tableData: List<List<String>>

 
次に

  • getRowCount()
  • getValueAt()

が参照するプロパティを tableData へと変更します。

override fun getRowCount(): Int {
    return tableData.size
}

override fun getValueAt(rowIndex: Int, columnIndex: Int): Any {
    return tableData[rowIndex][columnIndex]
}

 
続いて、実際に絞り込みを行うメソッド filterChanged() を追加し、以下を行います。

 

fun filterChanged() {
    tableData = allData.filter {
        val name = it[1] // Nameで絞り込むため、列番号を指定
        val regex = Regex("シナノ")
        regex.containsMatchIn(name)
    }.toList() // allDataとは別オブジェクトにするため toList する

    // filterが更新されたことを通知する
    this.fireTableDataChanged()
}

 
最後に、 init の処理の中で filterChanged() メソッドを呼び出します。

init {
    // 初期段階で絞り込みを実行する
    filterChanged()
}

 

動作確認

再度 ToolWindow を表示してみると、シナノ が含まれるデータのみテーブルに表示されました。

 

ここでの実装

以下のコミットになります。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin/commit/3dabe5194dde2c265af98ce75ab2ba25246b9d7e

 

テーブルのヘッダ行があるテーブルを実装

ここまでのスクリーンショットをよく見ると、テーブルにヘッダ行が表示されていません。

そこで、ヘッダ行を表示するよう修正します。

 

Content で JBScrollPane (scrollCell) を使う

Swingとは異なり、IntelliJ Platform Plugin でテーブルのヘッダ行を表示するには JBScrollPane (もしくは scrollCell) を使う必要があります。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#rowscrollcell-component

ここでは JBScrollPane を使うように修正します。

lateinit var myTableModel: Cell<JBScrollPane>

init {
    contentPanel = panel {
        row {
            // ...
            myTableModel = cell(JBScrollPane(table))
        }
    }
}

 

AbstractModelを継承したクラスで getColumnName をオーバーライド

ヘッダ行を表示するため、 getColumnName メソッドをオーバーライドします。

override fun getColumnName(column: Int): String {
    return columns[column]
}

 

動作確認

再度 ToolWindow を表示すると、ヘッダ行が表示されました。

 

ここでの実装

以下のコミットになります。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin/commit/91df3491d049d07b003f5383eac88924d6e6a486

 

TextFieldの入力値で絞り込めるテーブルを実装

今回は以下の流れになるよう実装します。

  1. 画面で絞り込み文字列を入力すると、絞り込み文字列を管理するクラスへ通知
  2. 絞り込み文字列を管理するクラスが、AbstractModel を継承したクラスへ絞り込み条件が変更になったことを通知
  3. AbstractModel を継承したクラスが、データを絞り込んだ結果を画面のJBTableへ通知
  4. 画面が再描画

 

画面で絞り込み文字列を入力すると、絞り込み文字列を管理するクラスへ通知

AppleTableContent クラスを修正します。

なお、「絞り込み文字列を入力する」というイベントを捕捉するため、今回は addDocumentListener を使っています。

class AppleTableContent {
    var contentPanel : DialogPanel
    lateinit var myTableModel: Cell<JBScrollPane>
    lateinit var searchText: Cell<JBTextField>

    // Modelオブジェクトを保持できるようプロパティを追加
    val appleTableModel: AppleTableModel

    init {
        // 追加したプロパティに初期値を設定
        appleTableModel = AppleTableModel()

        contentPanel = panel {
            // 絞り込み条件を入力するTextFieldを追加するとともに、イベントリスナーも追加
            row {
                label("Search text")
                searchText = textField()
                searchText.component.document.addDocumentListener(object: DocumentListener {
                    override fun insertUpdate(e: DocumentEvent?) {
                        handleChange()
                    }

                    override fun removeUpdate(e: DocumentEvent?) {
                        handleChange()
                    }

                    override fun changedUpdate(e: DocumentEvent?) {
                        handleChange()
                    }
                })
            }
            row {
                val table = JBTable()
                table.model = appleTableModel
                myTableModel = cell(JBScrollPane(table))
            }
        }
    }

    private fun handleChange() {
        appleTableModel.tableFilter.filterText = searchText.component.text
    }
}

 

絞り込み文字列を管理するクラスが、AbstractModel を継承したクラスへ絞り込み条件が変更になったことを通知

絞り込み文字列を管理するクラス AppleTableFilter を追加します。

なお、

AbstractModel を継承したクラスへ絞り込み条件が変更になったことを通知

を実現するため、コンストラクタで AbstractModel を継承したクラスを受け取っています。

package com.github.thinkami.hellojetbrainsplugin.ui

// modelをコンストラクタで受け取りつつプロパティとして定義
class AppleTableFilter(val model: AppleTableModel){
    var filterText: String = ""
    set(value) {
        if (value != filterText) {
            // 絞り込み文字列に変更があった場合のみ、プロパティを更新してモデルに変更があったことを通知する
            field = value
            this.model.filterChanged()
        }
    }
}

 

AbstractModel を継承したクラスが、データを絞り込んだ結果を画面のJBTableへ通知

画面の JBTable へ通知する処理はすでに実装しているため、データを絞り込むところのみ実装します。

クラスのプロパティとして、絞り込み文字列を管理するクラスを追加します。

val tableFilter: AppleTableFilter

 
続いて、 init の中で、各種初期値を反映します。

init {
    // 初期条件による絞り込みを実行するため、初期値を設定しておく
    tableData = allData.toList()

    tableFilter = AppleTableFilter(this)
    filterChanged()
}

 
あとは、絞り込み文字列を tableFilter から取得するようにします。

fun filterChanged() {
    tableData = allData.filter {
        val name = it[1] // Nameで絞り込むため、列番号を指定
        name.contains(this.tableFilter.filterText)
    }.toList() // allDataとは別オブジェクトにするため toList する

    // ...
}

 

動作確認

絞り込み文字列を何も入力していないときは、全件表示されます。

 
絞り込み文字列を入力すると、それに応じた内容へと表示が変わります。

 
もし、絞り込み文字列に該当する行が存在しない場合、テーブルの枠だけが表示されます。

 
以上でやりたいことが実現できました。

 

ここでの実装

以下のコミットになります。
https://github.com/thinkAmi-sandbox/hello_jetbrains_plugin/commit/a9d699048c1ad626be487de52ff3615aa401d184

 

ソースコード

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

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

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

IntelliJ Platform Pluginの開発にて、RubyMineなどのIntelliJ IDEA以外のIDEがRun Pluginで起動するようにしてみた

IntelliJ Platform Plugin Template を使って IntelliJ Platform Pluginを開発している場合、 デフォルトでは Run Plugin すると

な状態で IDE の別インスタンスが起動します。

ただ、

などはどうすればよいか気になったことから、調べたときのメモを残します。

 
目次

 

環境

  • プラグインの開発環境
    • Widnows 11
    • IntelliJ IDEA 2023.3.4 Ultimate Edition
    • IntelliJ Platform Plugin Template 1.12.0
    • Kotlinで実装
  • 起動するIDE
    • RubyMine 2023.2.6、IntelliJ IDEA 2023.2.6 Ultimate
      • Windows上に、JetBrains Toolboxでインストール済
      • 開発環境とは異なるプラットフォームのバージョンを指定できるか確認するため、 2023.2.6 にしています

 
また、今回の実装は、前回の記事の続きとして実装していきます。
IntelliJ Platform Pluginの開発にて、Kotlin UI DSL Version 2 や Swing を使って、ToolWindow上にコンポーネントを表示してみた - メモ的な思考的な

 

起動するIDEをRubyMineに変更する

Run Scriptで起動するIDEをRubyMineへ変更する方法については、IntelliJ Platform Plugin SDKの公式ドキュメントに記載がありました。

 
これらによると、

とすれば良さそうでしたので、ためしてみます。

 

build.gradle.kts の修正

build.gradle.kts を見たところ、 intellij.plugins の設定は以下のようになっていました。

intellij {
    // ...
    // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file.
    plugins = properties("platformPlugins").map { it.split(',').map(String::trim).filter(String::isNotEmpty) }
}

 
ソースコードのコメントに

Uses platformPlugins property from the gradle.properties file.

とあるように、定義済のキーに対する値を gradle.properties へ設定すれば良さそうでした。

 
一方、 runIde.ideDir の定義は存在しなかったため、

  • キーを末尾に追加
    • ただし、値を何も設定しない場合は、デフォルトのIntelliJ IDEAが起動できるようにする
  • intellij.plugins 同様、値については gradle.properties へ設定

とします。

 
なお、 runIde はドキュメントによると intellij と同階層で定義してあったのでその通りにしたところ

[FUNCTION_EXPECTED] Expression 'runIde' cannot be invoked as a function. The function 'invoke()' is not found

というエラーが出て定義できませんでした。

そこで、stackoverflowの回答にある通り、 tasks の中に入れたところエラーが解消されました。
creating Android Studio plugin using IntelliJ runIde can not be invoked - Stack Overflow

 
ちなみに、 build.gradle.kts の書式は

  • Kotlin
    • ideDir.set(file(properties("ideDir")))
  • Groovy
    • ideDir = file(properties("ideDir"))

のどちらでも問題なく動作しました。

 
まとめるとこんな感じになります。

tasks {
    // ...
    runIde {
        // executable other IDE
        // キーに対する値が存在するときだけ、 ideDir を設定
        if (properties("ideDir").get().isNotEmpty()) {
            // Kotlin
            ideDir.set(file(properties("ideDir")))
            // Groovy
            // ideDir = file(properties("ideDir"))
        }
    }
}

 

gradle.properties の修正

続いて、build.gradle.kts で定義したキーに対応する値を gradle.properties へと記載します。

 

platformPlugins の設定

すでに platformPlugins = としてキーが存在しているため、値だけ設定します。

設定する値は、以下に掲載されているRubyプラグインのバージョンになります。
https://plugins.jetbrains.com/plugin/1293-ruby/versions

なお、RubyプラグインのバージョンはRubyMineのバージョンとは異なるので注意が必要です。

 
今回は、RubyMine 2023.2.6 で動作するバージョン 232.10203.2 とともに、以下のように指定します。

platformPlugins = org.jetbrains.plugins.ruby:232.10203.2

 

ideDirの設定

こちらは、RubyMineをインストールしたディレクトリを指定します。

今回の場合、インストール済の RubyMine 2023.2.6 があるディレクトリを指定します。

 
なお、インストールしたディレクトリはバージョン表記ではなくビルド表記なので注意が必要です。

バージョン表記とビルド表記の比較は、以下のページにて確認できます。
https://www.jetbrains.com/ja-jp/ruby/download/other.html

今回使うバージョン 2023.2.6 に対するビルド表記は 232.10300.41 でした。

 
また、runIde.ideDir にて指定すべきディレクトリ表記ですが、macOSの場合は以下のページに記載があります。
https://plugins.jetbrains.com/docs/intellij/rubymine.html#configuring-plugin-projects-targeting-rubymine

一方、今回はWindows環境なので調べてみたところ、手元のJetBrains ToolboxによるRubyMine 2023.2.6 のインストール先は以下でした。
(<USER_NAMEch-N は環境により読み替えてください。)

C:\Users\<USER_NAME>\AppData\Local\JetBrains\Toolbox\apps\RubyMine\<ch-N>\232.10300.41

 
このパスをgradleでビルドするためには、バックスラッシュ \エスケープして設定します。

ideDir =  C:\\Users\\<USER_NAME>\\AppData\\Local\\JetBrains\\Toolbox\\apps\\RubyMine\\<ch-N>\\232.10300.41

 

plugin.xml の修正

最後に、 plugin.xml にてRubyプラグインが必要な旨を末尾に追記します。

<idea-plugin>
    <!-- ファイル末尾 -->
    <depends>com.intellij.modules.ruby</depends>
</idea-plugin>

 

動作確認

以上で準備ができたので、 Run Plugin にて別インスタンスIDEを起動します。

すると、HelloToolWindowプラグインのあるRubyMine 2023.2.6 が起動しました。

 
また、IDEのRuntime versionは、IDEに同梱されている 17.0.9+8-b1166.2 amd64 でした。

 

特定のJBRでRubyMineを起動する

続いて、IDEのRuntime versionを特定のバージョンにして、Run Plugin できるよう設定を変更します。

特定のRuntime versionで実行には、 runIde.jbrVersion を指定すると良さそうです。

 
そこで

  • build.gradle.kts
  • gradle.properties

の2ファイルを修正します。

 

build.gradle.kts に runIde.jbrVersion キーを追加

先ほど追加した runIde キーの末尾に、jbrVersionキーを追加し、値を gradle.properties から取得するようにします。

runIde {
    // 末尾に追加
    if (properties("ideDir").get().isNotEmpty()) {
        jbrVersion.set(properties("jbrVersion"))
    }
}

 

gradle.properties に jbrVersion キーと値を追加

こちらも末尾に追加します。

指定可能なJBRのバージョンは、READMEに記載されています。
https://github.com/JetBrains/JetBrainsRuntime/releases

 
今回は、 2023.2 系の JDK 17 ベースで起動したいことから、Latest JBR にある 17.0.10-b1000.48 のリンクをクリックします。

 
すると Realeses ページに飛ぶので、先頭の Release の次にある 17.0.10b1000.48 を使用します。

jbrVersion = 17.0.10b1000.48

 

動作確認

再度 Run Plugin してRubyMineを起動すると、JBRのバージョンが切り替わっていました。

 

起動するIDEの構成を IntelliJ IDEA Ultimate + 自作プラグイン + Rubyプラグインにする

最後に、IntelliJ IDEA Ultimate でも起動できるよう設定します。

 

gradle.properties の修正

起動するIDEをRubyMineからIntelliJ IDEA Ultimateへ切り替えるだけなので、 gradle.propertiesideDirIntelliJ IDEA Ultimate 2023.2.6 がインストールされているパスへと変更します。

ideDir = C:\\Users\\<UserName>\\AppData\\Local\\JetBrains\\Toolbox\\apps\\IDEA-U\\<ch-N>\\232.10300.40

 

動作確認

再度 Run Pliugin を実行すると、IntelliJ IDEA Ultimate 2023.2.6 が起動しました。

 
プラグインページを見ると、自作プラグインRubyプラグインがインストールされていることがわかります。

 

その他参考資料

JetBrains のビルド番号の情報

Build Number Ranges | IntelliJ Platform Plugin SDK

Gradle IntelliJ Pluginの情報

Gradle IntelliJ Plugin | IntelliJ Platform Plugin SDK

 

ソースコード

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

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

 
なお、gradle.properties の中に、個人のローカルPCのパスを指定しているため、今回のプルリクから gradle.propertiesgradle.properties.example へと差し替えています。

その結果、 gradle.properties が無い影響でCIがエラーになっています

そこで、もし実際に動かす場合には、 gradle.properties.examplegradle.properties ヘリネームし、自分の環境に合わせた設定へと変更する必要があります。

 
(2024/03/19 追記)

この後、gradle.properties.example を使う代わりに、 local.properties ファイルを使うことにしました。詳細は以下の記事に記載しています。
IntelliJ Platform Pluginの開発にて、開発環境にある公開したくない情報を local.properties に定義してビルドする - メモ的な思考的な

(2024/03/19 追記 ここまで)

 
また、「RubyMineのライセンスがないとサンプルコードが動かせない」という事態を避けるため、 plugin.xml の定義はコメント化してあります。

IntelliJ Platform Pluginの開発にて、Kotlin UI DSL Version 2 や Swing を使って、ToolWindow上にコンポーネントを表示してみた

前回、IntelliJ Platform Plugin Templateを使って、JetBrains系IDEプラグイン開発の Hello, world 的なことをやってみました。
IntelliJ Platform Plugin Templateを使って、「エディタのコンテキストメニューからダイアログを表示する」だけのJetBrains系IDEプラグインを作ってみた - メモ的な思考的な

その IntelliJ Platform Plugin Template には、 ToolWindow を表示するサンプルコードが同梱されていました。

サンプルコード を見ると

fun getContent() = JBPanel<JBPanel<*>>().apply {
    val label = JBLabel(MyBundle.message("randomLabel", "?"))

    add(label)
    add(JButton(MyBundle.message("shuffle")).apply {
        addActionListener {
            label.text = MyBundle.message("randomLabel", service.getRandomNumber())
        }
    })
}

という実装だけで、画面を伴う ToolWindow が表示されていました。

一方、これを見ただけではどうやって画面を作ればよいか分かりませんでした。

 
そこで、自分で ToolWindow をイチから作成することで、どのようにして画面上に各部品を置けばよいか把握できたことから、その時のメモを残します。

 
目次

 

環境

  • Widnows 11
  • IntelliJ IDEA 2023.3.4 Ultimate Edition
  • IntelliJ Platform Plugin Template 1.12.0
  • Kotlinで実装

 

ToolWindowを自作する

IntelliJ Platform Plugin SDKのドキュメントを見たところ、以下に ToolWindow の情報がありました。
Tool Windows | IntelliJ Platform Plugin SDK

ただ、ToolWindowの概念的な内容が多く、実際にどのような順で定義していけばよいか分かりませんでした。

 
次に、ToolWindowのページからリンクされていたサンプルコードを見てみました。
https://github.com/JetBrains/intellij-sdk-code-samples/tree/main/tool_window

サンプルコードはJavaでしたが、

  • ToolWindowFactory を継承したFactoryクラス
  • plugin.xml へのエントリ追加

をすれば良さそうでしたので、実際にためしてみます。

 
なお、サンプルコードではFactoryクラスに画面の構成も定義されていました。

ただ、画面の部品が増えると

  • 画面の定義
  • Factoryの定義

が混在して分かりづらくなりそうと考え、今回は画面とFactoryで定義を分けることにします。

 

Kotlin UI DSL Version 2で、画面を定義する

まず、Factoryから呼び出される画面の定義から行います。

IntelliJ Platform Pluginでの画面定義は

The IntelliJ Platform includes a large number of custom Swing components. Using those components in your plugins will ensure that your plugin looks and works consistently with the UI of the rest of the IDE, and can often reduce the code size compared to using the default Swing components.

 
https://plugins.jetbrains.com/docs/intellij/user-interface-components.html

とあるように、JavaSwing で行えるようです。

また、Figmaで画面レイアウトを描くためのUIKitも用意されていました。 https://jetbrains.design/intellij/resources/UI_kit/

引き続き公式ドキュメントを読んでいたところ、 Kotlin UI DSL Version 2 を使うことで、KotlinでSwingを簡潔に書けるような記載がありました。
Kotlin UI DSL Version 2 | IntelliJ Platform Plugin SDK

 
そこで、今回の画面定義は

  • src/main/kotlin/com/github/thinkami/hellojetbrainsplugin/ui/HelloContent.kt ファイルに定義する
  • Kotlin UI DSL Version 2 を使う
  • Factoryに画面定義を渡すため、 JComponent を返す関数を定義する

という方針で実装します。

方針に従った実際のコードは以下で、ToolWindowを開いた時に Hello が表示される想定です。

fun getHelloContent(): JComponent {
    return panel {
        row {
            label("Hello")
        }
    }
}

 

ToolWindowFactory を継承したFactoryクラスを作成

画面定義ができたので、次は IntelliJ Platform Plugin Template で生成されたサンプルコードを参考にして、Factory HelloToolWindowFactory を実装します。

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

 

plugin.xml への追記

最後に plugin.xml へ追記し、IDEにHelloToolWindowを表示します。

今回は、 IntelliJ Platform Plugin Template で生成された extensions の下にある toolWindow を使って定義しました。

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

 

動作確認

Run Plugin を実行し、HelloToolWindow を開くと、 Hello が表示されました。良さそうです。

 

常に自作のToolWindowを表示状態にする

ここまででToolWindowの表示はできたものの、Run Plugin を再度実行すると自作のToolWindowが閉じた状態での表示になってしまいます。

ただ、これだと開発で動作確認を繰り返すときに不便なので、色々試してみました。

すると、

  • Run PluginでIDEが起動する
  • Hello ToolWindow を開く
  • IDEx ボタンで閉じる

を行うことで、常に表示状態にできました。

どこかに設定があるのかもしれませんが、ひとまずこれで良さそうです。

 

JBScrollPane を使い、 ToolWindowをスクロール可能にする

今の実装では、1つのToolWindowに多数のコンポーネントを定義した場合、ToolWindowにスクロールバーが表示されないことから、すべてのコンポーネントを表示できません。

そこで、 JBScrollPane を使い、ToolWindowをスクロール可能にします。

// DSLで生成したものを `p` に入れる
val p = panel {
    row {
        label("Hello")
    }
    // ...
}

// `p` を JBScrollPane でラップして関数の呼び出し元に渡す
return JBScrollPane(p)

 
これにより、スクロールバーが表示されました。以下の画像では、右下に小さくなったスクロールバーが見えます。

 

Kotlin UI DSL Version 2を使って、画面に色々なコンポーネントを表示してみる

ここまでで、ToolWindowの画面に Hello という文字列を表示できるようになりました。

引き続き、 Kotlin UI DSL Version 2 を使って、画面へのコンポーネント表示を色々試してみます。

   

rowの引数にテキストを渡す

Githubにあるサンプルコードの中には row の引数にテキストを渡している箇所もありました。
https://github.com/JetBrains/intellij-community/blob/idea/233.14475.28/platform/platform-impl/src/com/intellij/internal/ui/uiDslShowcase/DemoBasics.kt

そこで、 label とどう違うのかを見てみます。

row("With row args") {
    label("Row args")
}

 
行の冒頭に指定したテキストが表示されるようです。

 

Indentを使う

Indent の有無による表示の違いを見てみます。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#panelindent

indent {
    row {
        label("Indented row")
    }
}

 
今回追加した行は、インデントが設定されていました。

 

separatorを使う

続いて、 separator を使って、コンポーネントの区切りを表示してみます。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#panelseparator

今回、「rowに引数あり」と「インデントあり」の間に separator() を追加してみます。

// rowに引数あり
row("With row args") {
    label("Row args")
}

// 追加
separator()

// インデントあり
indent {
    row {
        label("Indented row")
    }
}

 
その結果、横線が追加されました。

青矢印で横線を指してみましたが、背景が暗めなので見づらいですね。。

 

Kotlin UI DSL Version 2で使えるSwingのコンポーネントについて

ここまでは label だけを使ってきました。

ただ、以下のデモコードを見たところ、label の他にも textFieldcheckBox なども、DSLにてSwingコンポーネントを表示できそうでした。
https://github.com/JetBrains/intellij-community/blob/idea/233.14475.28/platform/platform-impl/src/com/intellij/internal/ui/uiDslShowcase/DemoComponents.kt

 

Cellを使い、Kotlin UI DSL Version 2に無いSwingのコンポーネント (JBTable) を扱う

Kotlin UI DSL Version 2に無いSwingのコンポーネント、例えば JBScrollPaneJBTable を扱う方法を探したところ、 Row.cell がありました。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#rowcell

 
そこで、DSLにない JBTableCell で表示してみます。

// データ定義
val data = arrayOf(
    arrayOf("1", "Fuji", "Red"),
    arrayOf("2", "Shinano Gold", "Yellow"),
    arrayOf("3", "Bramley", "Green")
)

val columnNames = arrayOf("ID", "Name", "Color")
val tableModel = DefaultTableModel(data, columnNames)
val table = JBTable(tableModel)
val scrollableTablePanel = JBScrollPane(table)

// cellで表示
row {
    cell(scrollableTablePanel)
}

 
すると、テーブルが表示できました。

 
なお、 JBScrollPane の代わりに Row.scrollCell を使うこともできそうです。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#rowscrollcell-component

 

checkBoxの状態により、有効/無効や表示/非表示を切り替える

checkBoxvisibleIfenabledIf を組み合わせることで、有効/無効や表示/非表示を切り替えられます。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#rowvisibleifenabledif

lateinit var myCheckBox: com.intellij.ui.dsl.builder.Cell<JBCheckBox>
row {
    myCheckBox = checkBox("Enable option if checked.")
    checkBox("Option by enable").enabledIf(myCheckBox.selected)
    checkBox("Option by visible").visibleIf(myCheckBox.selected)
}

 
チェックが無い状態では、無効・非表示です。

 
チェックすると、有効・表示へと切り替わります。

 

checkBox + rowsRange で、ON/OFFの状態をまとめて切り替える

checkBoxにより切り替わる項目が複数ある場合、 rowsRange を使うと定義が1度で済みます。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#panelrowsrange

ためしに、

  • rowごとに visibleIf を定義する
  • rowsRangeでまとめて visibleIf を定義する

の2つを比べてみます。

// rowsRangeなしの場合
row {
    label("1st row label without rowsRange")
}.visibleIf(myCheckBoxForRowsRange.selected)

row {
    label("2nd row label without rowsRange")
}.visibleIf(myCheckBoxForRowsRange.selected)

// rowsRangeありの場合
rowsRange {
    row {
        label("1st row label with rowsRange")
    }
    row {
        label("2nd row label with rowsRange")
    }
}.visibleIf(myCheckBoxForRowsRange.selected)

 
チェックが無い場合、何も表示されていません。

 
チェックを入れると、複数行が表示されました。

挙動は同じだったため、 rowsRange を使ったほうがまとめて定義できて便利そうでした。

 

ラジオボタン + component + addActionListener で、選択によりラベルのテキストを切り替える

公式ドキュメントの Panel.buttonsGroup には、ラジオボタンをグループ化できるコードが掲載されていました。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#panelbuttonsgroup

 
そこで、「ラジオボタンの選択を変えたときにラベルのテキストを切り替える」ような機能を実装してみます。

実際には

  • 公式ドキュメントに従い、 buttonsGroup を使ってラジオボタンをグループ化する
  • addActionListenerラジオボタンにイベントを割り当てる
    • イベントの中でテキストを差し替える

とすれば良さそうです。

 
ちなみに、 radioButton DSLで生成されるオブジェクトは Cell でラップされているため、 イベントの割り当ては

  • component プロパティからラップされていないSwingのオブジェクトを取り出す
  • そのSwingオブジェクトに addActionListener でイベントを割り当てる

という手順になります。

なお、 component プロパティから取り出せることが公式ドキュメントには書かれていないため、これも正しいやり方なのかはわかりません。。

 
では順に実装していきます。

まずは、buttonsGroup の中にラジオボタンを定義します。

lateinit var firstRadio: Cell<JBRadioButton>
lateinit var secondRadio: Cell<JBRadioButton>
var selectedRadioValue = "Default message"
buttonsGroup("Select Radio Button") {
    row {
        firstRadio = radioButton("1st choice", "1st selected")
    }
    row {
        secondRadio = radioButton("2nd choice", "2nd selected")
    }
}.bind( {selectedRadioValue}, { selectedRadioValue = it } ) // bindしないと表示できない

 
次にラベルコンポーネントを用意します。あとでイベントを割り当てられるよう、ラベルコンポーネントlateinit の変数に入れておきます。

lateinit var labelForRadioButton: Cell<JLabel>
row {
    labelForRadioButton = label(selectedRadioValue)
}

 
続いて、 ActionListener を使って、イベント発火時の処理を記載します。

今回は、選択したラジオボタンのテキストをラベルに割り当てます。

val myListener = ActionListener { e ->
    val rb = e.source as JBRadioButton
    labelForRadioButton.component.text = rb.text
}

 
最後にラジオボタンcomponent プロパティでラジオボタンオブジェクトを取り出して、 addActionListener でイベントを割り当てます。

// Cell<JBRadioButton> 型なので、 component を使って <> の中を取り出す
firstRadio.component.addActionListener(myListener)
secondRadio.component.addActionListener(myListener)

 
実装が終わったので動作確認します。

最初はデフォルトの文字列が表示されています。

 
ラジオボタンを選択すると、ラジオボタンのテキストがラベルに表示されます。

 

groupにより、項目をグループ化する

続いて、 group によるグループ化を見てみます。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#panelgroup

なお、 group のネストも可能です。

group("My Group Title") {
    row("1st row") {
        textField()
    }
    row("2nd row") {
        textField()
    }
}

group("Root Group") {
    row("Root row") { textField() }

    group("Nested Group") {
        row("Nested row") { textField() }
    }
}

 
タイトル付きでグループ化されました。また、ネストしたグループも表示されています。

 

groupRowsRangeで、グループ単位の有効/無効・表示/非表示を切り替える

groupRowsRange を使うことで、「checkBoxkによる有効/無効や表示/非表示の切り替え」をグループ単位でも行なえます。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#panelgrouprowsrange

lateinit var groupCheckbox: com.intellij.ui.dsl.builder.Cell<JBCheckBox>
row {
    groupCheckbox = checkBox("Checkbox for groups")
}
groupRowsRange {
    group("1st Group") {
        row("1st row") { textField() }
    }
    group("2nd Group") {
        row("2nd row") { textField() }
    }
}.visibleIf(groupCheckbox.selected)

 
チェックが無い場合、グループは表示されません。

 
チェックを入れると、グループが表示されるようになりました。

 

collapsibleGroupで開閉できるグループを作る

必要に応じて開閉したいグループを作りたい時は、 collapsibleGroup が使えます。 デフォルトだと最初は閉じた状態になります。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#panelcollapsiblegroup

もし、最初から開きたい場合は、 expanded = true を設定すれば良いようです。ドキュメントやサンプルコードでは事例を見つけられなかったので、これが正しいやり方かは不明ですが。。

// デフォルトだと、最初は閉じている
collapsibleGroup("Collapsible Group") {
    group("My Group") {
        row("My row") { textField() }
    }
}

// 最初から開いておく
collapsibleGroup("Collapsible Open Group") {
    group("Open Group") {
        row("Open row") { textField() }
    }
}.expanded = true

 
上は閉じていて、下は開いています。

 

Cell.commentでコメントを追加する

ラベル以外で文字列を表示したい場合、 Cell.comment が使えます。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#cellcomment

row {
    label("With comment").comment("My comment")
}

row {
    // .text() で textField のデフォルト値を設定する
    textField().comment("Text field comment").text("text property")
}

row("Row args") {
    textField().comment("Text field comment").text("text property")
}

row {
    label("Label args")
    textField().comment("Text field comment").text("text property")
}

 
comment では、コメントの追加対象のコンポーネントの下に、定義したコメントが表示されるようです。

 

Kotlin UI DSL Version 2で、コンポーネントのレイアウトを調整する

ここまででコンポーネントの定義を見てきました。

ここからは、コンポーネントの表示位置などのレイアウトを調整する方法を見ていきます。

 

Row.topGap/bottomGapでコンポーネントの上の余白を調整する

上下の余白を調整したい場合、 topGapbottomGap で調整できます。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#rowtopgapbottomgap

設定可能な値は

  • None (何も設定しない)
  • SMALL
  • MEDIUM

の3種類あるので、それぞれ見てみます。

row {
    label("Default top gap")
}

row {
    label("Small top gap")
}.topGap(TopGap.SMALL)

row {
    label("Medium top gap")
}.topGap(TopGap.MEDIUM)

 
前のコンポーネントとの余白がそれぞれ調整されています。

 

Cell.alignでCellの中のコンポーネントの位置を調整する

Cellの中のコンポーネントの表示位置を調整するには align が使えます。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#cell-align

以下の例では、ラベルの表示位置を調整しています。

row {
    label("With alignY-Top").align(AlignY.TOP)
    textField()
}

row {
    label("With alignX and alignY").align(AlignX.RIGHT).align(AlignY.BOTTOM)
    textField()
}

 
ラベルの縦の表示位置が

  • AlignY.TOP は上揃え
  • AlignY.BOTTOM は下揃え

になっています。

 

Cell.resizableColumn で水平方向の余白を自動調整する

resizableColumn の有無による表示差異を見てみます。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#cellresizablecolumn

row("Without Resizable Column") {
    textField()
    link("My link") {}
}
row("With Resizable Column") {
    textField().resizableColumn()
    link("My link") {}
}

 
表示してみると、「MyLink」の表示位置が調整されていました。

また、ToolWindowの横幅を変えてみると、「MyLink」の位置がいい感じに調整されます。

 

Cell.gap で、同じ行のコンポーネント間の余白を調整する

余白の幅は

  • 指定しない
  • SMALL
  • COLUMNS

の3つを指定できそうだったため、それぞれためしてみます。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#cellgap

row {
    label("Gap nothing")
    textField()
}

row {
    label("Gap small__").gap(RightGap.SMALL)
    textField()
}

row {
    label("Gap columns").gap(RightGap.COLUMNS)
    textField()
}

 
SMALL よりも何も指定しないほうが、横の余白が広くなるようです。

 

Row.layoutによる表示位置の調整

ここでは、 RowLayout 定数ごとに、

  • Rowの引数がある時
  • Rowの中にlabelがある時

の表示差異を確認してみます。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#rowlayout

row("Row args") {
    textField().text("default")
}

row("Row args") {
    textField().text("independent")
}.layout(RowLayout.INDEPENDENT)

row("Row args") {
    textField().text("label aligned")
}.layout(RowLayout.LABEL_ALIGNED)

row("Row args") {
    textField().text("parent grid")
}.layout(RowLayout.PARENT_GRID)

// rowに引数はないが、labelコンポーネントがある場合
row {
    label("Label args")
    textField().text("default")
}

row {
    label("Label args")
    textField().text("independent")
}.layout(RowLayout.INDEPENDENT)

row {
    label("Label args")
    textField().text("label aligned")
}.layout(RowLayout.LABEL_ALIGNED)

row {
    label("Label args")
    textField().text("parent grid")
}.layout(RowLayout.PARENT_GRID)

 
以下のように調整されました。今回の場合、 INDEPENDENT による調整が一番わかりやすいです。

より良い例があるかもしれませんが、今回はいったんここまでにします。

 

Cell.labelの第2引数で、ラベルの表示位置を調整する

Cell.label では、ラベルのテキストの他、第2引数でラベルの表示位置を調整できます。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#celllabel

実際に、第2引数の値を変えてみて、どのように表示が変わるか見てみます。

row {
    textField().label("Cell label")
}
row {
    textField().label("Cell label", LabelPosition.TOP)
}
row {
    textField().label("Cell label", LabelPosition.LEFT)
}

 
LabelPosition.TOP を指定した時だけ、続くコンポーネントの上にラベルが表示されました。

 

textFieldの横幅を columns で調整する

ドキュメントには記載されていませんでしたが、入力補完を使っていたときに気づきました。

columns には引数の値として

  • COLUMNS_TINY
  • COLUMNS_SHORT
  • COLUMNS_MEDIUM
  • COLUMNS_LARGE

が取れたので、それぞれためしてみます。

row {
    textField().columns(COLUMNS_TINY)
}
row {
    textField().columns(COLUMNS_SHORT)
}
row {
    textField().columns(COLUMNS_MEDIUM)
}
row {
    textField().columns(COLUMNS_LARGE)
}

 
columsに指定した値ごとに、TextBoxの幅が異なりました。

 

参考資料

Githubの uiDslShowcase

Githubの以下のディレクトリの下に、色々なサンプルコードがありました。
https://github.com/JetBrains/intellij-community/tree/idea/233.14475.28/platform/platform-impl/src/com/intellij/internal/ui/uiDslShowcase

 

リポジトリ intellij-sdk-code-samples

この中には、IntelliJ Platform Plugin SDKのサンプルコードがあります。
https://github.com/JetBrains/intellij-sdk-code-samples

例えば、ToolWindowのサンプルコードはこちらです。
https://github.com/JetBrains/intellij-sdk-code-samples/tree/main/tool_window

ただ、中にはJavaで書かれているサンプルコードもあるので、Kotlinへと変換する必要があります。

 

JetBrains の Platform Blog

深く読んでいませんが、参考になる記事があるかも。
JetBrains Platform | The JetBrains Platform Blog

 

Swingまわり

昔からある機能なので、Web上にもいろいろ記事がありました。

 

ソースコード

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

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