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