IntelliJで寿司を回す

はじめに

この記事はJetBrains Advent Calendar 2016の記事です。

最近Twitterではエディタで寿司を流すのが流行っているそうです。私の観測した範囲ではvimemacsで寿司を流している方が居るみたいです。
そんな最近の流行に乗ってIntelliJでも寿司を流すことにしました。

寿司を流す方法

当然ですがデフォルトのIntelliJのままでは寿司を流すことは出来ません。なので、プラグインを自作するしか無さそうです。
今回はステータスバーに寿司を流すプラグインを自作してみることにしました。

プラグインを作る

IntelliJ Platform Pluginプロジェクトでプラグインを開発することが出来ます。詳しいことは色々な方が解説記事を書かれているのでそちらを参照されると良いと思います。
今回やることはステータスバーを拡張することです。plugin.xmlのextensionsタグの中で拡張ポイントを指定して独自の機能を追加できるので、ステータスバーを拡張できるようなポイントを探してみます。
非推奨になっていますがstatusBarComponentが使えそうです。

<extensions defaultExtensionNs="com.intellij">
  <!-- Add your extensions here -->
  <statusBarComponent implementation="net.orekyuu.sushi.SushiStatusBarComponentFactory"/>
</extensions>

implementationにはcom.intellij.openapi.wm.StatusBarCustomComponentFactoryを継承したクラスを指定します。 あとは、追加したいコンポーネントをStatusBarCustomComponentFactory#createComponentで返すだけです。

雑な実装ですが以下のようなコードを書きました。

public class SushiStatusBarComponentFactory extends StatusBarCustomComponentFactory {
 
    private Image image;
    private final int size = 20;
    private final int laneWidth = 300;
    private List<Sushi> sushiList = new LinkedList<>();
    private static Thread thread = null;
    private JPanel root;
 
    public SushiStatusBarComponentFactory() {
        try (InputStream sushiStream = this.getClass().getResourceAsStream("/sushi.png")){
            this.image = ImageIO.read(sushiStream);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
 
        int sushiCount = (laneWidth / size / 2) + 1;
        for (int i = 0; i < sushiCount; i++) {
            sushiList.add(new Sushi(i * size * 2 - size, laneWidth, image));
        }
 
        if (thread == null) {
            thread = new Thread(() -> {
                while (true) {
                    try {
                        if (root != null) {
                            sushiList.forEach(Sushi::update);
                            root.repaint();
                        }
                        thread.sleep(16);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.setDaemon(true);
            thread.start();
        }
    }
 
    @Override
    public JComponent createComponent(@NotNull StatusBar statusBar) {
 
        root = new JPanel() {
            @Override
            protected void paintComponent(Graphics graphics) {
                Graphics2D g = (Graphics2D) graphics;
                g.setBackground(statusBar.getComponent().getBackground());
                g.clearRect(0, 0, getWidth(), getHeight());
                sushiList.forEach(sushi -> sushi.drawSushi(g));
            }
        };
        root.setPreferredSize(new Dimension(laneWidth, statusBar.getComponent().getHeight()));
        return root;
    }
}
 
public class Sushi {
    private final Image image;
    private int x;
    private int y = 0;
    private int size = 20;
    private int laneWidth;
    private int speed = 1;
 
    public Sushi(int x, int laneWidth, Image image) {
        this.x = x;
        this.image = image;
        this.laneWidth = laneWidth;
    }
 
    public void drawSushi(Graphics2D g) {
        g.drawImage(image, x, y, size, size, null);
    }
 
    public void update() {
        x += speed;
        //端までいったら-sizeまで戻す
        if (laneWidth < x) {
            x = -size;
        }
    }
}

無事寿司を流すことが出来ました!