Squeak5.0で日本語表示をするシリーズ: (寄り道編)ワークスペース内容をHTMLで保存する機能追加

Squeak5.0の日本語フォント表示周りをぼちぼち直していこうかと件のTTなんたらクラスの動きをちまちま調べている途中、ワークスペースにメソッド内容などをコピペしてメモを作ったりしているうちに、シンタックスハイライトできれいに色づけされた文字属性も保持しつつ保存したくなってきたので、思い切ってそういう機能を追加することにしました。そこで、トリビアルなテクニックなどをご紹介しつつ、作業内容を記したいと思います。


もともとワークスペースの内容は、ウインドウメニュー(以前はマウスの第三ボタンでしたが、今はウインドウタイトルバー右手に設置された青いメニューボタンでプルダウン)から save contents to file... を選択したり、黄ボタン(通常は右クリック)メニューからも more... 経由でポップアップできるシフト黄ボタン(同、右クリック)メニューから同名メニュー項目を選択する(ワークスペースを含む TextMorphForEditView なら可能)ことで保存自体はできるようになっています。

http://squab.no-ip.com/collab/uploads/saveContentsInFile01.png


ちなみにワークスペースでは、内容の保存のつもりで accept (alt/cmd + s) しても、これはビューなど UI コンポーネントでの編集内容がモデル(a Workspace)に反映されるだけで、ファイルへの保存がされているわけではないという点に注意を要します。やっかいなことに、こうしてモデルにビューの内容が反映されて一致した状態になってしまうと、両者に不一致がある状態のままインドウを閉じようとした場合に出る警告(Changes have not been saved. Is it OK to cancel those changes?)が出なくなり、当該ウインドウを容易に閉じてしまうことができるというワナももれなくついてくるのでこれまた要注意です。(もっとも、うっかり閉じてしまったとしても、ガベコレされる前なら、Workspace allInstances を inspect it (alt/cmd + i) して、閉じてしまったと思われるワークスペースのモデルをその内容(contents)からなんとか探し、それに openLabel: 'Workspace' などのメッセージを送ってやれば復活は可能です。念のため。)


さて、ワークスペースの内容の保存に話を元に戻すと、たしかに save contents to file... でファイルとして保存することは可能なのですが、保存されるのは文字列のみで、カラー情報などは抜け落ちてしまいます。そこで、保存時の拡張子を .html にしたときにカラー情報などを保持したまま HTML ファイルとしてはき出す細工をします。


まず、メニューから save contents to file... を選んだときにコールされるメソッド(当該機能の実体)を探します。本来であれば StringHolder(ワークスペースの実体)のソースを読むべきなのですが、操作のトリガーとなるメニュー項目をとっ捕まえて訊ねるのが一番手っ取り早いので、ここではそうします。

適当なワークスペースのウインドウメニューかシフト黄ボタンメニューをポップアップさせて、alt/cmd + シフトキーを押しながら当該メニュー項目をクリックしてモーフ(オブジェクト)として選択します。

http://squab.no-ip.com/collab/uploads/saveContentsInFile02.png


他のモーフ同様、メニュー項目オブジェクト(a MenuItemMorph)も選択すると、ハロー(小さな丸いボタン)に囲まれるので、右手にある灰色のデバッグハローをシフトボタンを押しながらクリック(あるいはただクリックしてポップアップするメニューから inspect morph )して選択されたメニュー項目のインスペクターを呼び出します。

http://squab.no-ip.com/collab/uploads/saveContentsInFile03.png


左側のペインからインスタンス変数である selector をクリックして内容を確認すると、#saveContentsInFile であることがわかります。“#”を含めずに save〜以下を選択して implementors of it (alt/cmd + m) すると、同名メソッドの一覧を呼び出せるので、その中から関連がありそうな TextEditor>>#saveContentsInFile をクリックして選択してそのコードを呼び出します。

http://squab.no-ip.com/collab/uploads/saveContentsInFile04.png


最後のところを下に示す青字の部分を赤字のようにちょっと手直しして置き換え、accept (alt/cmd + s) します。いつもどおり、初回の accept でイニシャルを求められたら教えてあげてください。

TextEditor >> saveContentsInFile
    "Save the receiver's contents string to a file, prompting the user for a
    file-name. Suggest a reasonable file-name."
    | fileName stringToSave parentWindow labelToUse suggestedName |
    stringToSave := paragraph text string.
    stringToSave size = 0
        ifTrue: [^ self inform: 'nothing to save.'].
    parentWindow := model dependents
                detect: [:dep | dep isKindOf: SystemWindow]
                ifNone: [].
    labelToUse := parentWindow
                ifNil: ['Untitled']
                ifNotNil: [parentWindow label].
    suggestedName := nil.
    #(#('Decompressed contents of: ' '.gz' ) )
        do: [:leaderTrailer | | lastIndex | "can add more here..."
            (labelToUse beginsWith: leaderTrailer first)
                ifTrue: [suggestedName := labelToUse copyFrom: leaderTrailer first size + 1 to: labelToUse size.
                    (labelToUse endsWith: leaderTrailer last)
                        ifTrue: [suggestedName := suggestedName copyFrom: 1 to: suggestedName size - leaderTrailer last size]
                        ifFalse: [lastIndex := suggestedName
                                        lastIndexOf: $.
                                        ifAbsent: [0].
                            (lastIndex = 0
                                    or: [lastIndex = 1])
                                ifFalse: [suggestedName := suggestedName copyFrom: 1 to: lastIndex - 1]]]].
    suggestedName
        ifNil: [suggestedName := labelToUse , '.text'].
    fileName := UIManager default request: 'File name?' initialAnswer: suggestedName.
    fileName isEmptyOrNil
        ifFalse: [(fileName endsWith: '.html')
                ifTrue: [FileStream
                        newFileNamed: fileName
                        do: [:file | paragraph text printHtmlOn: file]]
                ifFalse: [FileStream
                        newFileNamed: fileName
                        do: [:file | file nextPutAll: stringToSave]]]
        ifFalse: [(FileStream newFileNamed: fileName) nextPutAll: stringToSave;
                 close]


これで #saveContentsInFile で色情報などを保ったまま HTML での保存が可能になります。上のは実は TextEditor>>>#saveContentsInFile の versions の diff(下のペインの内容)を当該メソッドを用いて HTML保存し、それをコピペしたものです。と、書いていて気がついたのですが打ち消し線とか抜け落ちてしまうのですね。残念。


さて、スタイルを保ったまま保存はできましたが、これを読み込んでワークスペースとして表示できないとうれしさは半減です。そこで、ファイルリストをちょっといじって読み込みもできるようにしましょう。

Squeak 組み込みのファイラであるファイルリスト(Tools メニュー → File List などで呼び出し可能)には、ファイル名のリストにあるファイル(たとえば .html ファイル)を右クリックして選択すると同時に黄ボタンメニューを呼び出すとそこに「workspace with contents」という項目が見つかります。

http://squab.no-ip.com/collab/uploads/saveContentsInFile05.png


これを選ぶと選択したテキストファイルの内容を新しいワークスペースとして開けます。ただ、この機能で注意しないといけないのは、ワークスペースとして表示されたのはあくまでファイルの中身のコピーであり、これを編集して accept したからといって、元ファイルに変更が保存されるわけではない、ということです。なお、ファイルの中身を編集して更新したければ、ファイルリストの下のペインか、spawn (alt/cmd + o) して1ペインのエディタスタイルのウインドウを開いて編集し、accept → overwrite that file する必要があります。


話を戻して、workspace with contents ですが、先の方法でこのメニュー項目のインスペクターを開いて selector を見ても #performServiceFor: とあるだけで、今回は残念ながら機能の実体のメソッド名らしき情報を得ることができません。こういう場合は arguments とか target も順に見ていくとよいです。すると target に SimpleServiceEntry: (a FileList --- viewContentsInWorkspace) とあり、セレクター(メソッド名の実体のシンボル)ではありませんが、それっぽい viewContentsInWorkspace というメソッド名らしき情報を得ることができます。

http://squab.no-ip.com/collab/uploads/saveContentsInFile06.png


試しに、選択して implementors of it すると、はたして興味の対象であるファイルの内容をワークスペースとして表示する機能の実体である FileList>>#viewContentsInWorkspace のコードを呼び出すことができます。

http://squab.no-ip.com/collab/uploads/saveContentsInFile07.png


これを次のように編集して accept してコンパイルします。

FileList >> viewContentsInWorkspace
    
"View the contents of my selected file in a new workspace"
    
    
| aStringOrText aName |
    
directory readOnlyFileNamed: self fullName do: [:file |
        
file setConverterForCode.
        
aStringOrText := (file localName endsWith: '.html') ifFalse: [
            
file contentsOfEntireFile
        
] ifTrue: [
            
(HtmlReadWriter on: file) nextText
        
].
        
aName := file localName.
    
].
    
UIManager default edit: aStringOrText withSqueakLineEndings label: 'Workspace from ', aName


この後、ファイルリストで .html を選択し workspace with contents すると、元のカラー属性などを保ったままワークスペースに内容を復元できるはずです。