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