新規作成、開く、名前をつけて保存、上書き保存、終了をトレイトで

この記事は、Play or Scala Advent Calendar 2012 の24日目の記事です。
http://qiita.com/advent-calendar/2012/play-or-scala

概要

GUIアプリケーションを作るときに、ファイルメニューに「新規作成」「開く」「上書き保存」
「名前をつけて保存」というメニューを用意することがよくあります。
なんとか共通化できないかと思っていたのですが、
トレイトを使うといい感じに整理できたので紹介します。

前提・環境

ここでは、同時に複数のファイルが扱えないSDI(Single Document Interface)を想定しています。
メモ帳やペイントなどがそれにあたります。
また、GUIは javax.swingパッケージを使っています。
(scala.swingパッケージではありません)。

トレイトを使わない場合

単に「開く」と言ってもファイル選択するだけではなく、すでに現在のドキュメントが
編集されていたら、先に保存するかどうか問い合わせなければなりません。
保存する場合でも、編集中のドキュメントがファイルと関連付けられていないときには
保存するファイル名を入力させる必要があります。
擬似的なコードを書くと以下のようになります。

    /** [開く]のときの処理 */
    def actionPerformed(e:ActionEvent) {
      // 編集されていたら、
      if (modified) {
        // 保存しますか?と問い合わせる。
        // キャンセルならreturn
        if (Yesなら) {
          // ファイルと関連付けられてないとき、
          if (file == null) {
            // 保存用ファイル選択ダイアログを表示。
          }
          save(file)      // ★ココがアプリ固有の処理
          modified = false
        }
      }
      // オープン用ファイル選択ダイアログを表示。
      // 開くボタンを押したら
      open(file)          // ★ココがアプリ固有の処理
      modified = false
      // GUIを更新。
    }

これらの処理のうち、大半は紋切り型のコードです。
アプリ固有の部分は、★の箇所のopen()とsave()だけです。

なんとか共通化できないかと思っていたのですが、
トレイトを使うといい感じに整理できたので紹介します。
まぁ、共通部分をstaticなメソッドにしたりすればある程度共通化できるのですが、
どうもしっくりきませんでした。

トレイトを使った場合

以下がそのトレイトになります。
GUIアプリケーションには、ビューアとエディタの2種類が考えられるので、
SingleDocumentViewerとSingleDocumentEditorの2つのトレイトに分けました。

trait SingleDocumentViewer {
	self: JFrame =>  // 自分型の指定

	var file:File = null
	def open(file:File)
	def updateFrame()

	val fileOpenAction:Action = new FileOpenAction()
	val fileExitAction:Action = new FileExitAction()

	class FileOpenAction extends AbstractAction {
		// 中略
	}

	class FileExitAction extends AbstractAction {
		// 中略
	}
}


trait SingleDocumentEditor extends SingleDocumentViewer {
	self: JFrame =>  // 自分型の指定
	var modified = false

	def init()
	def save(file:File)

	val fileNewAction = new FileNewAction()
	override val fileOpenAction = new FileOpenAction()
	val fileSaveAction = new FileSaveAction()
	val fileSaveAsAction = new FileSaveAsAction()
	override val fileExitAction = new FileExitAction()

	def inquireSave(parentComponent:JFrame):Boolean = {
		// 中略
	}

	class FileNewAction extends AbstractAction {
		// 中略
	}

	class FileOpenAction extends AbstractAction {
		// 中略
	}

	class FileSaveAction extends AbstractAction {
		// 中略
	}

	class FileSaveAsAction extends AbstractAction {
		// 中略
	}

	class FileExitAction extends AbstractAction {
		// 中略
	}
}


まだ荒削りで、考慮すべき点は多いですが、おおむねイイ感じにできています。
ソースはこちら。
http://www.hcn.zaq.ne.jp/no-ji/memo-scala/Advent2012.zip
サンプルとして、テキストパッドと画像ビューアを用意しました。
短いソースですが、ファイルメニューに関しては充実しています。

まとめ

GUIアプリでのトレイトの使い方ですが、
他にも、クリップボード3兄弟、最近使ったファイル、多言語対応、アプリの起動&終了、
などにも適用できるのではないかと考えてます。

ちなみに

ちなみに、今日は私の誕生日です。(*^_^*)

Scala Advent Calendar 2012

めも。
去年、初めてこーいうイベントがあることを知って、今年は参加することにしてみました。 一番上のやつ。
でも、探したらほかにもあった。


Play or Scala Advent Calendar 2012
http://qiita.com/advent-calendar/2012/play-or-scala

Scala Advent Calendar jp 2012
http://partake.in/events/3407d5d2-435c-4354-a96e-1ab9b18bef73

Scalaz Advent Calendar
http://partake.in/events/7211abc9-ebb8-4670-b912-3089dc5e0edd

[scala]文字コードの自動認識

"JISAutoDetect"だと、シフトJISとかEUCは自動認識してくれるのだけれど、"UTF-8"だと認識してくれません。
なので、候補となる文字セットをすべて試して、文字化けが一番少ないのを探すようにしました。
もちろん、間違える可能性もあります。

    /**
     * 文字セットを推測する。
     * @param   bytes               バイト配列
     * @param   candidates  推測する文字セットの候補。要素は1つ以上であること。
     * @return      推測した文字セット。candidatesのうちの1つを返す。
     */
    def guessCharset(bytes:Array[Byte], candidates:Seq[String]=Seq("UTF-8", "MS932", "EUC-JP")):String = {
        assert(candidates.size > 0, candidates)
        candidates.map{cs =>
            // 文字列に変換し、65533の文字数を数える。(文字セット,個数)
            (cs, new String(bytes, cs).count(_.toInt == 65533))
        }.minBy(_._2)._1
    }

一応動くけれど、65533というマジックナンバーが入っていたり、タプルの._1や._2が出ているのがいまいち。

洗濯機(日常)

ベランダにある洗濯機に、朝、黒ぬこがフタの上でよく ひなたぼっこをしています。
最近よく来る。
朝陽があたり、すべすべのフタの上は、お気に入りの場所のようだ。
目を細め、尻尾をゆっくりフリフリしている。


この前、窓を開けて寝たら、次の朝、その黒ぬこが部屋の中の洗濯物の上で、あたかも自分の家のようにくつろいでた。
起きたばかりの私は頭が回らず、しばらく黒ぬこと「にらめっこ」してた。
(んん???なんで黒ぬこがココにいるんだ??? 窓・・・か? オイオイ、勘弁してくれよー。)
私はぬこに話してみた。「えっと、これから会社だから、出てってくれない?」
一瞬、首を傾げたが、窓からベランダに出て行った。
なんだ。言葉通じるじゃん。