前回、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
をイチから作成することで、どのようにして画面上に各部品を置けばよいか把握できたことから、その時のメモを残します。
目次
- 環境
- ToolWindowを自作する
- Kotlin UI DSL Version 2を使って、画面に色々なコンポーネントを表示してみる
- rowの引数にテキストを渡す
- Indentを使う
- separatorを使う
- Kotlin UI DSL Version 2で使えるSwingのコンポーネントについて
- Cellを使い、Kotlin UI DSL Version 2に無いSwingのコンポーネント (JBTable) を扱う
- checkBoxの状態により、有効/無効や表示/非表示を切り替える
- checkBox + rowsRange で、ON/OFFの状態をまとめて切り替える
- ラジオボタン + component + addActionListener で、選択によりラベルのテキストを切り替える
- groupにより、項目をグループ化する
- groupRowsRangeで、グループ単位の有効/無効・表示/非表示を切り替える
- collapsibleGroupで開閉できるグループを作る
- Cell.commentでコメントを追加する
- Kotlin UI DSL Version 2で、コンポーネントのレイアウトを調整する
- 参考資料
- ソースコード
環境
- 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
とあるように、Javaの Swing
で行えるようです。
また、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が閉じた状態での表示になってしまいます。
ただ、これだと開発で動作確認を繰り返すときに不便なので、色々試してみました。
すると、
を行うことで、常に表示状態にできました。
どこかに設定があるのかもしれませんが、ひとまずこれで良さそうです。
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
の他にも textField
や checkBox
なども、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のコンポーネント、例えば JBScrollPane
な JBTable
を扱う方法を探したところ、 Row.cell
がありました。
https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html#rowcell
そこで、DSLにない JBTable
を Cell
で表示してみます。
// データ定義 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の状態により、有効/無効や表示/非表示を切り替える
checkBox
と visibleIf
・ enabledIf
を組み合わせることで、有効/無効や表示/非表示を切り替えられます。
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でコンポーネントの上の余白を調整する
上下の余白を調整したい場合、 topGap
や bottomGap
で調整できます。
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