週刊 git GUIクライアントを作る [5] 初めてのデスクトップアプリ


月刊にはなってないからセーフ。
基本的にSwingの話。

# 前号までのまとめ

## [ \[1\] stage/unstage基礎知識編](https://qiita.com/guignol/items/f19d05b8db5390b9b647)

行単位のstage/unstageのためにやるべきこと5つ

`1. 元になるpatchとしてdiffを出力する`
`2. stage/unstageしたい行を選ぶ`
`3. hunkのボディを編集する`
`4. hunkのヘッダーを再計算する`
`5. 最終的なpatchをapplyする`

##  [ \[2\] stage/unstage不完全攻略編](https://qiita.com/guignol/items/3463d8f569c390be6801)

JGitを使ってみたら無理だったので、git.exeを使おうというお話でした。

## [ \[3\] stage/unstageパッチ編集編](https://qiita.com/guignol/items/c8d0cea9f6833942eaf2)

`3. hunkのボディを編集する`
`4. hunkのヘッダーを再計算する`

## [ \[4\] diff et al.](https://qiita.com/guignol/items/b59dce0137c4922553eb)

`1. 元になるpatchとしてdiffを出力する`

# 今号は

`2. stage/unstageしたい行を選ぶ`

![進捗.gif](https://qiita-image-store.s3.amazonaws.com/0/202327/2cf8c186-09ef-f0d1-ab4a-20a6666962f2.gif)

ですが、選ぶためには表示しなければいけません。
今回は、表示の話です。

# UIを考えるの難しい

GUIアプリケーションとしては

+ 何をクリックできるのか
+ 何を選択できるのか

等をデザインに反映させる必要がありそうですが、
ここでは基本的に考えてません。
「ぼくのかんがえたさいきょうのgitくらいあんと」を作りたいので

+ どう表示されると自分が嬉しいか
+ どう操作できると自分が嬉しいか

だけ考えて、各位幸せになりましょう。

## 例えばUITableView

まず最初にイメージしたのは、
iOSのUITableViewのようなセクションのあるコレクション系のViewです。
しかし、あなたとJava、ここはJVMです。

てっとりばやくSwingでdiffの部分選択をするためのUIとして、
JListがいちばん楽そうなので採用しました。
パッチの元となるdiffを1行ずつリストに表示して、選択させる、というわけです。

行単位の更新をしたくなったのでJTableにしましたが（後述）、
1列だけのJTableなので、基本的な使い方は同じです。

## モ並感

ところで、リストで表示するとしても、diffを1行ずつ表示する必要はありませんね。
リストの各行の内部にテキスト表示して、そこで選択させればいいわけです。
選択の単位はリストの行だと、自然に考えてしまっていました。

このあたり、モバイルアプリケーション開発から抜け切れてない感じです。
（私は基本的にiOSやAndroidのネイティブアプリ開発を生業としています）

## 選択後のstage/unstageのトリガー

選択してから、stage/unstageさせるトリガーとなるアクションが必要です。
SourceTreeを見ると、ボタンと右クリックのメニューがありますね。
右クリックは常用するには面倒です。
ボタンの位置や形を考えるのも面倒です。

というわけで、diffのヘッダー部分をクリックさせることにしました。

# Diffの表示

## hunk

`git diff`でもらえるデータをそのまま表示したほうが楽ですので、hunk単位で表示しましょう。

ここで言うdiffのヘッダーは、それぞれのファイルごとに複数行あり、
`diff --git`から始まる行からhunkのヘッダー行の手前までのことです。
ここで言うhunkのヘッダーは、`@@`から始まる1行のことです。

```
<diff-per-file>
	<diff-header />
	<hunk>
		<hunk-header />
		<hunk-body />
	</hunk>
	<hunk>
		<hunk-header />
		<hunk-body />
	</hunk>
</diff-per-file>

<!-- 画像など、hunkが無い場合もある -->
<diff-per-file>
	<diff-header />
</diff-per-file>

```

gitのdiffは上記のような構造になっていますが、

1. どのファイルのdiffなのかをhunk単位で分かると嬉しい
2. ヘッダー部分をクリックすることでstage/unstageするようにしているので、広いほうが嬉しい

という理由から、diffのヘッダーをそれぞれのhunkと一緒に表示します。

```
<diff-per-file>
	<diff-header />
	<hunk>
		<hunk-header />
		<hunk-body />
	</hunk>
	<diff-header />
	<hunk>
		<hunk-header />
	    <hunk-body />
	</hunk>
</diff-per-file>

<diff-per-file>
	<diff-header />
	<hunk>
		<hunk-header dummy="new file" />
		<hunk-body img="icon.png" />
	</hunk>
</diff-per-file>
```

同じdiffの複数のhunkをまとめるとdiffのヘッダーが重複しますが、
まとめてパッチに出力しても特に問題は無いようです。
（ただ、私は基本的に`git apply`はhunk単位でしか行っていないので、隠れた問題があるかもしれません）


## hunk ヘッダー

+ ヘッダーの情報を個別に選択させる必要はない
+ ヘッダーのクリックをそのhunkのstage/unstageのトリガーとしたい

ので、リストの1行に、テキストを複数行表示させてしまいます。
JLabelなどはhtml（のようなもの？）を`<html>``</html>`でまとめて突っ込めます。

```
Collector<CharSequence, ?, String> toHtml = Collectors.joining(
            "</nobr><br><nobr>",
            "<html><nobr>",
            "</nobr></html>");
Arrays.stream(ヘッダー部分の文字列.split("\n")).collect(toHtml)
```

`<nobr></nobr>`は、現在または将来どの環境でも使えるのか怪しいですが、便利です。

## hunk ボディ

`git diff`の出力そのままです。
追加行、削除行、コンテクスト行で色が違うと分かりやすいです。

## 画像の非同期取得

画像（などバイナリファイル）の追加や削除の場合、ファイル単位で扱うので、
hunkという単位は存在せず、`git diff`の出力にも現れません。

でも、せっかくなので画像は表示させたいですね。
[前号](https://qiita.com/guignol/items/b59dce0137c4922553eb#%E7%94%BB%E5%83%8F)では、その方法や注意点などについて言及しました。

リストで表示する場合、

+ 他のhunkと違い、`git diff`時点では画像ファイルを得ていない
+ 画像オブジェクトを作成するのに多少は時間がかかる

という点を考慮して、表示方法が2つあります。

+ 画像の準備を待ってからリストを表示する
+ リストに各hunkのテキストを表示し、画像だけ後から更新する

待つのは嬉しくないので、表示できるものは先に表示させましょう。

（まさか実装が面倒になると思わなかったので、当たり前のように後者を選びましたが、ネットワーク経由ではなく、ローカルマシンの性能依存なので、待つのも全然アリだとは思います。）

## リスト表示を行単位で更新する

ところがJListは行単位の更新ができないようです。
なので列が1つだけのJTableを使います。

いろいろ試した結果、テキストだけの行と画像だけの行に分けて、
以下の順序で更新すると、望ましい感じのパフォーマンスとなりました。

1. テキスト行の高さを先に決める
2. 画像行の画像の非同期取得を開始する
3. 画像を取得出来たら、画像からその行の高さを決める

以下は、Swingの内部実装に依存したhackを含みますので、
バージョンによっては同じ発想、同じコードが通用しない可能性はあります。
確認しているのは`javac 1.8.0_144`です。
（javacのバージョンと同じ、という理解であってます？）
（ところで今後、Swingの既存コンポーネントに劇的に手が入る予定ってあるんですか？）

### テキスト行の高さを先に決める

JTableは各行の表示・更新が必要になると各列ごとに設定したTableCellRendererが呼ばれます。
TableCellRendererはinterfaceです。

```
public interface TableCellRenderer {

	Component getTableCellRendererComponent(JTable table, 
	                                        Object value,
	                                        boolean isSelected, 
	                                        boolean hasFocus,
	                                        int row, 
	                                        int column);
}
```

各行の更新する方法はいくつかあるはずですが、
ここでは`JTable#setRowHeight(int row, int rowHeight)`を使います。
行ごとに高さを変えるために必須なのです。

テキストの高さはJLabelにテキストを入れたら分かりますので、
TableCellRendererが呼ばれる前にJLabelを作って高さを設定します。

各行の高さを更新するのは、テーブルに紐づくデータに変更があったタイミングです。
サンプルコードです。

```
// テキスト行のsetRowHeightを先にまとめてやる
preRender = () -> {
    // 以下のいずれかを実行しないと、テーブルの最大行数が最初に表示した行数に依存してしまう
    // おそらく、JTable内部でrowModel = null;が必要なのだと思う
    this.table.setRowHeight(1);
//    setRowSorter(null);
//    tableChanged(new TableModelEvent(getModel()));
    final int size = dataModel.getSize();
    for (int i = 0; i < size; i++) {
        final ListItem item = dataModel.getElementAt(i);
        final ImageHolder imageHolder = item.imageHolder();
        // 画像行ではない場合
        if (imageHolder == null || !imageHolder.hasLoader()) {
            // 大したコストではないはずなのでキャッシュしない
            final JLabel label = new JLabel();
            this.renderer.render(label, item, false, null);
            final int height = label.getPreferredSize().height;
            table.setRowHeight(i, height);
        }
    }
};
```

サンプルコード内に書いていますが、注意点としては、

+ this.table.setRowHeight(1);
+ setRowSorter(null);
+ tableChanged(new TableModelEvent(getModel()));

などを呼ばないと、テーブルの最大行数が最初に表示した行数に依存してしまいます。
最初に3行表示すると、そのテーブルは3行しか表示できません。
何故こんなことになるのかはよく分かっていませんが、
JTableの`private SizeSequence rowModel;`を初期化すると想定通りの挙動になるようです。

同じ行に対して`JTable#setRowHeight(int row, int rowHeight)`を複数回呼んではいけない、
ということなのかもしれません。

### 画像行の画像の非同期取得を開始する

テキストと同じく、事前に開始してもいいのですが、
特にパフォーマンスに問題は感じなかったので、
TableCellRenderer#getTableCellRendererComponentで行います。

いずれにせよ、
少なくとも周辺行の更新時にTableCellRenderer#getTableCellRendererComponentは呼ばれてしまうので、
画像取得と取得後のコールバック呼び出しが1度しか行われないよう実装する必要があります。

### 画像を取得出来たら、画像からその行の高さを決める

TableCellRenderer#getTableCellRendererComponentの外部で表示を変更することができないので、
画像を取得したら、紐づくアイテムのほうがに画像を持たせて、
`JTable#setRowHeight(int row, int rowHeight)`を呼びます。
そうすると、再びTableCellRenderer#getTableCellRendererComponentが呼ばれます。

```
final TableColumn tableColumn = this.table.getColumnModel().getColumn(0);
tableColumn.setCellRenderer((table, value, isSelected, hasFocus, row, column) -> {
        final ListItem item = (ListItem) value;
        final JLabel label = new JLabel();
        // ここで画像取得を開始する、あるいは画像をセットする
        this.renderer.render(label, item, isSelected, imageIcon -> {
                // 画像取得後
                final int iconHeight = imageIcon.getIconHeight();
                table.setRowHeight(row, iconHeight);
        });
        return label;
});
```

# 進捗

毎度おなじみ、投稿にあんまり関係ない進捗gifです。

![進捗_3.gif](https://qiita-image-store.s3.amazonaws.com/0/202327/ab6424f9-91af-991a-8735-89aa6adad122.gif)

まだバグや課題はありますが、コミットまではできます。
そろそろ履歴を表示できるようにしたいなぁ、というところです。

# あとがき

## 次回予告

書き始めたら、

+ 頑張った工夫が無くても動くことが分かったり
+ 日本語ファイル名を扱えないことに気づいたり

なんだかんだで疲れてきたので、選択は次号に回しました。

## 近況

このシリーズとアプリを書き始めたのは、
退職して無職をエンジョイしてたからでした。

まだしばらく働くつもりはなかったんですが、
空から仕事が降ってきたので、流れでフリーランサーになりました。
プログラマになったのも流れだったので、このまま流れていくのでしょう。

直近ではiOSアプリの改修です。
まだコードは書いていませんが、自作のアプリケーションで`git commit`するはずです。
（まだmacで動かしてないけど、俺はJVMを信じてる）

手元に業務があると、業務外でツール作るモチベーションも上がりますね。

## Swing

Swingで書いてるのは特に理由も無く、
「CSSを書きたくない」ぐらいしかコダワリも無くて、
デザインのための仮実装ぐらいのつもりだったんですが、
特に問題も無いので、このまま続ける予定です。

配布する場合は、どういう形がいいのかなぁ。