Spec2入門(その6)

これはSmalltalk Advent Calendar 2019の記事です。

Spec2は、Pharo Smalltalk で採用されているUIフレームワークであるSpec の新しいバージョンです。Pharo 7.0 では Spec が使われていましたが、Pharo 8 では(ほぼ?)全てのUIが Spec2 で書き直されるようです。この記事では Spec2 を使った UI の実装方法について簡単に紹介します。

Squeak/Pharo Smalltalk では、UIの実現のために長らく Morphic を用いてきました。Morphic はシンプルで強力なフレームワークですが、複数のコンポーネントを組み合わせていくとアナーキーな実装をしがちで、リファクタリングや再利用の際に泣きたくなるような状況に陥りがちです。Spec/Spec2 はUIの構築に秩序をもたらし、コンポーネントの再利用がしやすく設計されています。

前回の記事では、Scratch 1.4風のファイル選択ダイアログに、親ディレクトリへ移動するボタンと、ディレクトリ階層がわかるドロップリストを追加しました。


前回つくったドロップリストはガワだけで機能がないので、今回はディレクトリ階層を表示したり、途中の親ディレクトリに移動できるような機能を追加します。

ドロップリストの内容を設定する

スクラッチではドロップリストをクリックすると以下のようにディレクトリ階層が表示されます。途中のディレクトリをクリックすると、そのディレクトリ内容がファイル一覧に表示されます。

まず、現在のディレクトから、ディレクトリ階層の文字列を取り出すメソッド directoryHierarchy を作成します。

directoryHierarchy
     ^ directory path segments
         withIndexCollect:
             [ :each :index | (String new: index withAll: Character space) , each ]

directory は FileReference のインスタンスなので、path で AbsolutePath オブジェクトに変換し、segments でパスを部分文字列の配列に変換します。その後、インデックスを使ってインデントの空白を作ります。

次にドロップリストにディレクトリ階層を設定するメソッド listCurrentContents: を作ります。現在のディレクトリ名が表示されるよう、インデックス位置を設定します。

listCurrentContents: aCollection
     listCurrent selectIndex: 0.
     listCurrent items: aCollection.
     listCurrent selectIndex: aCollection size

ディレクトリ階層は、ディレクトリが変更されたタイミングで設定するべきなので、directory: メソッドの中で設定するようにします。

directory: aFileReference
     directory := aFileReference.
     listEntries
         unselectAll;
         items: #();
         items: self getEntries.
     self listCurrentContents: self directoryHierarchy

ドロップリストで選択できるようにする

表示されたディレクトリ階層を選んだら、その親(系列の)ディレクトリに移動するようにしましょう。そのためには、ドロップリストが変更された時の処理を connectPresenters に追加する必要があります。

connectPresenters
     listEntries
         display: [ :m | self showEntry: m ];
         whenActivatedDo: [ :selection | self entriesChanged: selection ].
     buttonParent action: [ self changeParentDirectory ].
     listCurrent
         whenSelectedItemChangedDo: [ :selection | self currentChanged: selection ]

ここでは、ブロック引数をつけて whenSelectedItemChangedDo: メッセージを送っています。これで、ドロップリストの項目が選択されたタイミングで 、選択された項目の文字列を引数として currentChanged: メッセージが送られるようになります。

currentChanged: メソッドは以下のように定義します。

currentChanged: aString
     | dir |
     listCurrent selectedIndex = 0
         ifTrue: [ ^ self ].
     dir := directory.
     directory path segments size - listCurrent selectedIndex
         timesRepeat: [ dir := dir parent ].
     self directory: dir

ディレクトリのパスの個数から選択された位置を引いた回数だけ親ディレクトリをたどって、選ばれたディレクトリを特定しています。不格好なやり方ですが、とりあえず動きます。

ドリップリストの項目を選択すれば親系列のディレクトリに移動できるのですが、ドロップリストにリストを設定するたびに上のメッセージが送られることになるため、初期化などの際にも不用意なメッセージ送信が発生してしまいます。そこで、ドロップリストの生成時に startWithoutSelection を送ることで、これを回避します。

initializePresenters
     listEntries := self newList
         beSingleSelection;
         activateOnDoubleClick;
         items: self getEntries.
     buttonParent := self newButton
         icon: (Smalltalk ui icons iconNamed: #up).
     listCurrent := self newDropList startWithoutSelection.
     (以下略)

初期化のための調整

ドロップリストに関する最後の調整をします。 initialize 時に directory 変数を初期化していますが、これではドロップリストの内容が設定されないので、初期化のタイミングを後にずらすことにします。

まず、最初に作成した initialize メソッドを削除します。その上で、 initializePresenters に以下の変更を加えます。

initializePresenters
     listEntries := self newList
         beSingleSelection;
         activateOnDoubleClick.
     buttonParent := self newButton
         icon: (Smalltalk ui icons iconNamed: #up).
     listCurrent := self newDropList
         startWithoutSelection.
     self directory: FileSystem workingDirectory.
     self focusOrder
         add: listCurrent;
         add: buttonParent;
         add: listEntries

directory: メッセージを使うことで、ファイル一覧もディレクトリ階層もあわせて初期化されることになります。