calfw の設計と利用方法

はじめに

前回の記事は calfw が使えるようになるところまででした。今回の記事では calfw をもう少し詳しく説明してみたいと思います。

今回の記事の目標は2点あります。一つはカレンダーの情報源である「情報ソース(cfw:source)」の作り方、もう一つは calfw 自体を他のアプリケーションに組み込む方法です。前者の情報ソースを作れるようになれば、自分でカレンダーに表示する内容を作れるようになります。後者が分かれば、「ここにカレンダーがあるとすごく便利なのに」と思うような場面で calfw を組み込むことが出来るようになります。

calfw の全体のアーキテクチャMVC になっています。 Emacs のアプリケーションでも、ある程度大きなアプリケーションでは OOP のテクニックはよく使われています。 calfw では、それほど複雑なクラス構成は出てきていないため、構造体と名前のルールによるシンプルなモジュール化を使っています。

cfw:source について

cfw:source はカレンダーの情報源を定義するオブジェクトです。 cfw:open-calendar-buffer などのカレンダー構築用関数に :contents-sources の名前付き引数で渡します。以下にコンポーネント構築のコード例を示します。

(cfw:open-calendar-buffer
   :contents-sources
   (list 
    (cfw:org-create-source)  ; org 用のソース生成関数
    (cfw:howm-create-source) ; howm 用のソース生成関数
    (cfw:ical-create-source "ical" ; icalendar 用のソース生成関数
      "https://../basic.ics" "Red")))

ここでは org, howm, icalendar の情報ソースをつくってカレンダーを構築しています。このように、任意の情報源を組み合わせてカレンダーを構築することが出来ます。

この情報ソースオブジェクトを自分で用意できれば、同じようにカレンダーに表示させることが出来ます。
まず、 cfw:source の詳細について簡単に説明します。その後、具体的なソースの作り方を説明します。

cfw:source 詳細

cfw:source は defstruct で定義された構造体です。各スロットの詳細は以下のようです。

名前 説明
name [必須] 情報源の名前。ステータスバーに表示される。
data [必須] 情報源の内容を返す関数。詳細は次の「 cfw:source-data 仕様」で説明。
update [オプション] 情報源の内容を更新する必要があるときに calfw 本体から呼ばれる。
ここでキャッシュクリアなどを行う。
color [オプション] この情報源の色。文字列で指定。
M-x list-colors-display で出てくる色名や、"#abcdef"のような6桁の16進。
主に通常のスケジュール表示用の色。
period-fgcolor [オプション] 期間スケジュールの前景色。省略すると白か黒。
period-bgcolor [オプション] 期間スケジュールの背景色。省略すると color を使う。
opt-face [オプション] face の色以外に指定したい項目。
フォントをイタリックにするとか太字にするとか。
:opt-face '(:weight bold) のように指定。
opt-period-face [オプション] opt-face と同じく、期間スケジュールに適用する face 項目。

name と data が必須であり、 name, date, update 以外は見た目の設定です。

ちなみに、なぜ直接 face を指定するようになってないかというと、 color だけ指定すればあとはうまく calfw が表示してくれることを想定しているからです。まず、 color だけでやってみて、気に入らないところがあれば追加で見た目を細かく調整できるように考えています。

cfw:source-data 仕様とコードサンプル

次に、cfw:source-data の関数の定義や返すべき値について説明します。

cfw:source-data 関数は、 開始日・終了日を受け取って、(日付 . (内容のリスト)) の alist を返す関数です。具体例で書くと以下のようです。(簡単のために、引数の開始日・終了日は使っていません)

;; cfw:source-data の簡単な例
(defun sample-data1 (b e)
  '(
    ((1  1 2011) . ("内容1"))
    ((1 10 2011) . ("内容2" "内容2行目"))
    ))

(cfw:open-calendar-buffer
  :contents-sources
   (list 
     (make-cfw:source
      :name "test1" :data 'sample-data1)))

このコードを scratch バッファなどで実行して2011年1月を表示させると、以下のような画面になります。



単純なスケジュールの例

日付は (list 月 日 年 ) で指定します。この形式は、 calendar.el や orgmode で広く使われている形式です。 Emacs の TIME 型やその他の形式との相互変換については、日付変換まとめ を参照してください。

期間スケジュールは以下のコードのように periods の項目を作って、そこに (list 開始日 終了日 内容) のリストを入れます。*1

;; cfw:source-data 期間スケジュールの例
(defun sample-data2 (b e)
  '(
    ((1  8 2011) . ("内容1"))
     (periods
      ((1 8 2011) (1 9 2011) "期間1")
      ((1 11 2011) (1 12 2011) "次の期間"))
    ))

(cfw:open-calendar-buffer
  :contents-sources
   (list 
     (make-cfw:source
      :name "test2" :data 'sample-data2)))

このコードの結果は以下のようになります。



期間スケジュールの例

以下、もう少し細かい仕様です。

  • 関数の引数の開始日・終了日は表示したい期間(両端含む)
  • 関数の引数の開始日・終了日の範囲外のものが含まれてもかまわない
  • 描画が必要な度に呼ばれるので、計算に時間がかかる場合はデータをキャッシュしておくべき
  • リスト内は日付順に並んでなくてもよい。periods もリストのどこにあってもよい。重複している場合はまとめられる。
  • 日付内での並び順はデフォルトではアルファベット順

ここまでは固定の日付データを使いましたが、実際には何らかのプログラムで結果リストを出力することになると思います。 org, howm, icalendar の実際のコードが参考になると思います。

calfw コンポーネント

次は、 calfw を他のアプリケーションで使う方法について説明します。

calfw はカレンダー表示の方法として次の3つの形態を用意しています。

  • 独立したバッファとして表示
  • リージョン内に挿入
  • テキスト出力

それぞれ簡単に説明します。

バッファ

通常の Emacs のアプリと同じように、独立したバッファを作ってカレンダーを表示します。

関数 cfw:open-calendar-buffer を呼ぶと、カレンダー用のバッファを作成し、 switch-to-buffer でバッファを表示します。バッファは cfw:calendar-mode のメジャーモードがセットされ、キーバインドなどもバッファ全体で cfw:calendar-mode-map がセットされます。

ふつうのよくある使い方です。アプリケーションの境界はバッファ単位であり、ユーザーは通常のバッファ操作を行うことが出来ます。

リージョン

別のアプリケーションバッファに埋め込みたい場合に使います。具体例としては howm のメニューへの埋め込みがあります。

どういうことなのか分かりにくいかもしれないので、 scratch バッファで以下の式を評価してみてください。

;; scratch バッファで以下の式を評価してみる
(cfw:create-calendar-component-region :height 10)



scratch バッファで実行 → そのままカレンダーの操作ができる

アンドゥで元に戻ります。

カレンダーのリージョン以外には影響をほとんど与えず、キーバインドも自前で用意(テキストの keymap プロパティにセットされている)するので、手軽に他のアプリケーションに組み込むことが出来ます。

テキスト

カレンダーが描画された純粋なテキストが欲しい場合に使います。 cfw:get-calendar-text でテキストを返します。
まだ具体的に活用された例はないのですが、外部への export とか、ツールチップ用の文字列準備などで考えています。

「描画先」と「ビュー」

以上、3つの形態を説明しました。これらの見た目や構築方法は全然違いますが、calfw を使うプログラムからは統一的に操作することが出来ます。

今後、これらの形態の違いは「描画先」と呼ぶことにします。似た単語に「ビュー」が出てきますが、これは月や週などの描画方法を指します。分かりにくくてすみません。

オブジェクトの構成

まず、概要をつかむために calfw のオブジェクト達について説明して、その後、各詳細とどう使うのかを説明します。

オブジェクト概要

大きく分けて以下のオブジェクトがあります。

  • 全体の入れ物である「 calfw コンポーネント(cfw:component)
  • 全データを管理する「モデル」 (cfw:model)
  • 各カレンダーのデータを定義する「情報ソース」 (cfw:source)
  • 「描画先」の抽象化 (cfw:dest)

UMLのクラス図で描くと以下のようです。*2



オブジェクトの概要図

calfw コンポーネントMVC の Controller の役目を担い、モデル描画先をつなげて全体を制御します。また、外界とのインタフェースとして各種オブジェクトへのアクセスを提供します。

モデル情報ソースMVC の Model に当たります。情報の管理とロジックを担当します。

描画先MVC の V に当たり、描画先を抽象化して切り替えられるような仕組みになっています。

それぞれもう少し詳しく説明します。

全体、見た目の操作 cfw:component

calfw コンポーネントは、全体の統括と見た目の制御等を担当しています。

calfw コンポーネントは以下のデータを保持しています。

  • 描画先 (dest) とモデル (model) への参照
  • 現在選択されている日付 (selected)
  • 現在のビュー (view)
  • 各種フック
    • 描画時 (update-hooks)
    • 選択 (selection-change-hooks)
    • クリック (click-hooks)

操作については以下のような項目があります。

  • 描画先、モデル、所属バッファなどの取得
  • 選択日の取得、更新 (get-selected-date / set-selected-date)
  • ビューの取得、変更 (get-view / set-view)
    • 内容は month, two-weeks, week, day などのシンボル
  • 描画サイズの変更 (resize)
  • 再描画 (update)
  • フックの追加 (add-xxx-hook)

なお、コンポーネント構築後の描画先の変更などは動的に出来ません。

見た目の要となるビューは、今のところただの関数として定義されていて、このコンポーネントのなかで描画担当の関数が振り分けられています。 (cfw:cp-dispatch-view-impl)

calfw コンポーネントインスタンスは、描画先によって保持される場所が違います。

  • バッファ → バッファローカル変数 cfw:component
  • リージョン → 範囲内のテキストプロパティ cfw:component
  • テキスト → なし

cfw:cp-get-component 関数を使うと、現在のカーソール位置から適切なインスタンスを取得できます。イベントハンドラなど、ステートレスなロジックはこの方法でインスタンスを取得すると良いと思います。

ステートフルな操作のためには、コンポーネントを使用する側がインスタンスを保持したほうが良いと思います。

カレンダーの内容へのアクセス cfw:model

情報ソースのインタフェースで情報の定義を統一し、モデルが情報ソースを束ねて管理します。

主に以下のようなデータを管理します。

  • 内容の情報ソース (contents-sources)
  • 注釈の情報ソース (annotation-sources)
  • 内容のソート関数 (sorter)

モデルは、描画先やビューについての情報は一切持っていません。純粋に、日付や期間に対応する内容だけを管理します。祝日に関しては calendar.el と同様に、 calendar-holiday-list 関数から取得します。

大抵の場合、モデルの内容は構築時に決定されますが、後で動的に情報ソース自体を操作したりするような場合にモデルを経由してアクセスします。

現在の実装では描画処理の過程で、ビュー固有のデータ(いわゆる ViewModel)が付加されます。このあたりの実装は個人的にも微妙な感じがしています。。。

描画先の抽象化 cfw:dest

描画先の操作を抽象化して、ビュー関数が描画先に依存せずに描画できるようにする仕組みです。

描画先は以下のデータや操作を持っています。

  • 描画先バッファ (buffer)
  • 描画範囲取得 (min-func, max-func)
  • サイズ (width, height)
  • 削除関数 (clear-func)
  • 更新関数 (before-update-func, after-update-func)
  • オーバーレイ管理 (select-ol, today-ol)

上の calfw の利用形態のところで少し書いたように、現在の実装ではバッファ、リージョン、テキストがあります。リージョンがやりたくてこの仕組みを作ったと言っても過言ではありません。描画に必要な情報をすべて持っていますので、一つのバッファに複数のカレンダーコンポーネントを入れることも出来ます。 *3

組み込み方針

calfw 活用の方法としては、アプリケーションのUIとして使ったり (howmの形態)、日付の入力用として一時的に表示させるということが考えられます。

まず、描画先としてバッファを丸ごと使うか、既にあるバッファに組み込むかという選択があります。切り替えるのは簡単ですので、いろいろ試してみて使いやすい方を採用したらいいと思います。
データの表示は、単純に情報ソースを作れば表示できます。

次に、ユーザーからの入力ですが、日付の選択やクリックなどの単純なアクションは、コンポーネントのフックで簡単に拾うことが出来ます。また、選択された日付は、 cfw:cursor-to-nearest-date か cfw:cursor-to-date で取得できます。現在は、単一の日付しか取得できません。

上記以外のイベント(エンターキーや任意のボタンクリックなど)は、構築時の :custom-map 引数に適当なキーマップオブジェクトを渡すことで取得することが出来ます。さらに細かく調整したい場合は、情報ソースで付加された大抵のテキストプロパティはそのまま表示まで持って行くことが出来ますので、文字単位でキーマップや face を設定することで対応できると思います。

上記のアクションでデータを更新したら、コンポーネントの再描画関数を呼ぶことで表示を更新できます。

これで一通りの入力・表示の操作が出来るのではないかなと思います。以上をまとめると下のような絵になります。



アプリケーションから calfw を使うイメージ

具体的には、 howm 連携のコードが参考になると思います。

今後の予定

以下、 calfw の今後についてのメモです。 orgmode の ML が熱いので、そちらを watch すると良いかもしれません。

  • viewの追加、改善
    • 3,4日表示とか
    • ガントチャートっぽく横長表示とか
    • もっとコンパクトな表示や、オーバーレイでポップアップとか
    • もっとスペースを有効活用できるような表示方法
  • 期間選択
  • 時間の扱い
    • 週表示や日表示
    • 時間の持ち方について検討
  • orgとの連携の強化
  • 他の連携の追加
    • diary とか

*1: ちなみに「(A . (B C) )」は「(A B C)」と同じになります。念のため。

*2: このクラス図はいつもお世話になっている Astah で描いています。データは githubリポジトリに入っています。

*3:この仕組みを拡張して、カレンダー以外の汎用的な「一つのバッファに複数入れて組み合わせることが出来るコンポーネント」の仕組みが出来ないかなということも考えています。ちょうど、近年の Web 上の JavaScript がお行儀よくなって、一つの document 内でも複数の JavaScript コンポーネントやライブラリが同居出来るようになってきた感じにならないかなと、勝手に考えています。