IntelliJ IDEA の progress bar を 犬にする plugin

「はてなエンジニア Advent Calendar 2024」の2025/01/07 の記事です。

- [はてなエンジニア - Qiita Advent Calendar 2024 - Qiita](https://qiita.com/advent-calendar/2024/hatena)
- [はてなエンジニア Advent Calendar 2024 - Hatena Developer Blog](https://developer.hatenastaff.com/advent-calendar-2024)


### イメージ

![fds.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700755/7ac58a3e-4666-019a-0cc2-c5c373b186e1.gif)

モデルはペットのおこげです。 ゆえに okoge-progress-barです。かわいい。


### Marketplace

https://plugins.jetbrains.com/plugin/26236-okoge-progress-bar/

### ソース

https://github.com/kk-house-777/okoge-progress-bar

# IntelliJ Pluginの 仕組み
###  IntelliJ Platform SDK
IntelliJ Platform SDKは、JetBrains製IDE（IntelliJ IDEA、PyCharm、WebStormなど）用のプラグインを開発するためのツールとAPIを提供する開発者向けキットです。

SDKを使用することで、次のようなことが実現できます：
- カスタムUIの追加（今回のプログレスバーのような機能）
- アクションやリスナーを通じてイベントを処理
- メニューやツールウィンドウの拡張

### intellij-platform-plugin-template
https://github.com/JetBrains/intellij-platform-plugin-template

intellij-platform-plugin-template は、JetBrainsが提供するプラグイン開発のためのテンプレートリポジトリです。プラグイン開発の初期設定とサンプルコード、公開のための設定が入ってます。

### plugin.xml

plugin.xmlは、プラグインの設定や動作を定義するファイルです。

IntelliJ Platform では イベント駆動型アーキテクチャを採用しているようで、さまざまなイベントに応じてリスナーを登録できます。

このファイルでは、そのListenerを登録することで、IntelliJプラットフォームのイベント（アプリケーション、プロジェクト、UI変更など）にフックしてカスタム処理を実行できます。

```plugin.xml
<idea-plugin>
    <id>kk.kkhouse777.okogeprogressbar</id>
    <name>okoge-progress-bar</name>
    <vendor>kk__777</vendor>

    <depends>com.intellij.modules.platform</depends>

    <description><![CDATA[
      Pretty progress bars featuring a dog named Okoge, designed for IJ-based IDEs.
    ]]></description>

    <applicationListeners>
        <listener class="kk.kkhouse777.okogeprogressbar.listeners.OkogeProgressListener"
                  topic="com.intellij.ide.ui.LafManagerListener"/> <!--    おそらく不要    -->
        <listener class="kk.kkhouse777.okogeprogressbar.listeners.OkogeProgressListener"
                  topic="com.intellij.openapi.application.ApplicationActivationListener"/>
    </applicationListeners>
</idea-plugin>

```
ほかにも PluginのWindowで表示される説明文等のmetadataの設定もここで行います。

# 実装

### 素材を用意

知り合い作。32*32 と 64*64 で 準備。

![rsz_okoge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700755/3e0a4fcc-8de1-c426-1a4b-f2eb045a6f40.png)


サイズ感は先駆者👇にならいました。
[NyanProgressBar](https://github.com/batya239/NyanProgressBar)


### javax.swing.BasicProgressBarUIを拡張する

IntelliJのプログレスバーはjavax.swing.JProgressBarをベースにしているため、[BasicProgressBarUI](https://docs.oracle.com/javase/jp/8/docs/api/javax/swing/plaf/basic/BasicProgressBarUI.html#createUI-javax.swing.JComponent-)を拡張してカスタマイズします。

参考： [javax.swing](https://docs.oracle.com/javase/jp/8/docs/api/javax/swing/package-summary.html) 

```java
import com.intellij.openapi.ui.GraphicsConfig;
import com.intellij.util.ui.GraphicsUtil;
import com.intellij.util.ui.JBUI;
import kk.kkhouse777.okogeprogressbar.okoge.OkogeProgressBarState;

import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicProgressBarUI;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.geom.RoundRectangle2D;

public class OkogeProgressBarUI extends BasicProgressBarUI {

    @SuppressWarnings({"MethodOverridesStaticMethodOfSuperclass", "UnusedDeclaration"})
    public static ComponentUI createUI(JComponent c) {
        return new OkogeProgressBarUI();
    }

    @Override
    protected void paintIndeterminate(Graphics g2d, JComponent c) {
        // ~~略
    }

    @Override
    protected void paintDeterminate(Graphics g, JComponent c) {
        // ~~略
    }
    // ...
}

```

- paintIndeterminate

    - プログレスバーが進行状態を特定できない場合に描画される

<br>

- paintDeterminate
    - プログレスバーの進行状況が特定できる場合に描画される

<br>


描画部分は javax.swing に倣えばよいので、例えばシンプルにオレンジ色のバーを表示するならこんな感じになります。
```java
@Override
protected void paintDeterminate(Graphics g, JComponent c) {
    Graphics2D g2 = (Graphics2D) g.create();
    try {
        // 背景の描画
        g2.setColor(Color.LIGHT_GRAY);
        g2.fillRect(0, 0, c.getWidth(), c.getHeight());

        // 進捗バーの描画
        int progress = progressBar.getValue();
        int width = (int) (c.getWidth() * (progress / 100.0));
        g2.setColor(Color.ORANGE);
        g2.fillRect(0, 0, width, c.getHeight());
    } finally {
        g2.dispose();
    }
}
```

### 拡張したProgressBar を Listener で 登録する

```kotlin
import com.intellij.ide.ui.LafManager
import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.application.ApplicationActivationListener
import com.intellij.openapi.wm.IdeFrame
import kk.kkhouse777.okogeprogressbar.OkogeProgressBarUI
import javax.swing.UIManager

class OkogeProgressListener :
    LafManagerListener,
    ApplicationActivationListener {
    override fun applicationActivated(ideFrame: IdeFrame) {
        updateProgressBar()
    }

    override fun lookAndFeelChanged(p0: LafManager) {
        updateProgressBar()
    }

    companion object {
        private const val PROGRESS_BAR_UI_KEY = "ProgressBarUI"
        private const val PROGRESS_BAR_UI_CLASS_NAME = "kk.kkhouse777.okogeprogressbar.ui.OkogeProgressBarUI"

        private fun updateProgressBar() {
            UIManager.put(PROGRESS_BAR_UI_KEY, PROGRESS_BAR_UI_CLASS_NAME)
            UIManager.getDefaults().put(PROGRESS_BAR_UI_CLASS_NAME, OkogeProgressBarUI::class.java)
        }
    }
}


```
- LafManagerListener
    - テーマが変更されたときに、カスタムプログレスバーの外観や設定が反映されるようにします
    - （一応 入れているのですが おそらく不要）

- ApplicationActivationListener
    - IDEがアクティブ化された際に、UI設定を再適用することで、プログレスバーが正しく表示されるようにします

UIManagerで指定するkey は 以下を参考にしました。
https://github.com/kagof/intellij-pokemon-progress/blob/master/src/main/java/com/kagof/intellij/plugins/pokeprogress/PokemonProgressListener.java

### plugin.xmlにListener を宣言する

```plugin.xml
<idea-plugin>
    <id>kk.kkhouse777.okogeprogressbar</id>
    <name>okoge-progress-bar</name>
    <vendor>kk__777</vendor>

    <depends>com.intellij.modules.platform</depends>

    <description><![CDATA[
      Pretty progress bars featuring a dog named Okoge, designed for IJ-based IDEs.
    ]]></description>

    <applicationListeners>
        <listener class="kk.kkhouse777.okogeprogressbar.listeners.OkogeProgressListener"
                  topic="com.intellij.ide.ui.LafManagerListener"/> <!--    おそらく不要    -->
        <listener class="kk.kkhouse777.okogeprogressbar.listeners.OkogeProgressListener"
                  topic="com.intellij.openapi.application.ApplicationActivationListener"/>
    </applicationListeners>
</idea-plugin>

```


# 公開

intellij-platform-plugin-template が すでにいろいろ準備してくれています。

### credential周り
intellij plugin の公開タスク等をまとめている gradle plugin  `org.jetbrains.intellij.platform`が すでに入っていて以下設定があります。

```build.gradle.kts
...
intellijPlatform {
    pluginConfiguration {
    ...
    signing {
        certificateChain = providers.environmentVariable("CERTIFICATE_CHAIN")
        privateKey = providers.environmentVariable("PRIVATE_KEY")
        password = providers.environmentVariable("PRIVATE_KEY_PASSWORD")
    }

```

この環境変数へ それぞれのcredential に設定します。

https://plugins.jetbrains.com/docs/intellij/plugin-signing.html#generate-private-key

詳細は上記 を参照で良さそうですが、さっくり 

- private key を作成
```terminal
openssl genpkey\
  -aes-256-cbc\
  -algorithm RSA\
  -out private_encrypted.pem\
  -pkeyopt rsa_keygen_bits:4096
```

- 自己署名証明書を作成
```terminal
openssl rsa -in private_encrypted.pem -out private.pem
openssl req -key private.pem -new -x509 -days 365 -out chain.crt
```

- IntelliJ MarketPlaceのAPIトークンを取得

https://plugins.jetbrains.com/docs/intellij/publishing-plugin.html#providing-your-personal-access-token-to-gradle


- 環境変数（GithubAction含む）に登録する

となります。

### github action
templateには ワークフローが いくつか定義されています。

https://github.com/JetBrains/intellij-platform-plugin-template/tree/main/.github/workflows

- main への 変更をフックに releaseのドラフトが作られる
- 上記を publish する と MarketPlaceに公開される（初回のみ手動で公開する必要がある

https://plugins.jetbrains.com/docs/intellij/publishing-plugin.html?from=IJPluginTemplate


### その他必要なこと

ReadMeの修正 や MarketPlaceへのmeta dataの登録