2010-10-24

ページ切り替え機構の切り替え - Blogger Glass

今回はちょっと実験。2 種類のページ切り替えの仕組みを用意し、どちらが使いやすいかを確かめる実験だ。

「ページ切り替え」と言うのは、Blogger Glass の一覧表示系の画面の下部に出てくるもののこと。リストする項目が多いときには複数のページに分割する。そのページを切り替えるための仕組みだ。

現状では、Google の検索結果に出てくるモノ(Goooooooooogle! のようになるアレだ)を真似ている。これをもっとシンプルな「Next」「Prev」ボタンのものと比較してみようと思ったのだ。

2 つの方式の比較

実験というからには、2 つを比較できなければ意味がない。旧来の方式と新しい方式を両方使えるようにしておいて、実際に使ってみて試すわけだ。

幸い、一覧表示系の画面(機能)は「一覧画面」と「検索結果画面」の 2 種類がある。このうち、「一覧画面」のみを新方式にして、「検索結果画面」の方は従来のままにしておく。そうすれば、どちらの方式も試すことができる。

まあ、配備ずみの正式版(appspot で稼動しているもの)と開発中のローカル版(SDK で動かしているもの)で比較するという手もあるんだがね。

リファクタリング

「ページ切り替え機構」を簡単に取り替えられるようにするため、src/info.py、さらに src/main.pysrc/searchview.py にも手を入れる。

具体的には、「ページ切り替え機構」そのものを ViewInfo のインスタンスで抱え、表示対象のページ番号や全ページ数は「ページ切り替え機構」の属性に押し込める。リクエストハンドラは「ページ切り替え機構」を選び、ViewInfo のインスタンスにセットすることになる。

「ページ切り替え機構」は、util.Pager クラスを基底として派生させる。旧方式は util.GlassPager と名付けた。

(src/util.py より)
class Pager(object):
    PAGESIZE = 25
    def __init__(self, page_num, total_posts):
        self.current = page_num
        self.total_pages = 0
        if total_posts > 0:
            self.total_pages = (total_posts - 1) // Pager.PAGESIZE + 1
        else:
            self.total_pages = 1

class GlassPager(Pager):
    class Page(object):
        def __init__(self, number, current=False):
            self.number = number
            self.current = current

    def __init__(self, page_num, total_posts):
        Pager.__init__(self, page_num, total_posts)
        self.pages = []
        for p in range(self.total_pages):
            self.pages.append(GlassPager.Page(p, p == page_num))

src/main.pyMainHandler で「ページ切り替え機構」をセットする付近のコードを示す。

(src/main.py)
            total_posts = int(feed.total_results.text)
            self.viewinfo.pager = util.PrevNextPager(page, total_posts)
            self.viewinfo.start_index = start_index

結果として、ページ切り替えに関する情報の生成と詰め直しを追い出したことになり、少しすっきりした。

シンプルなページ切り替え

ごく単純な「Prev」「Next」の 2 つのボタン(リンク)のある方式で、最初と最後へのショートカットを持っている。つまり、こんな感じになる。

[Top] [Prev] <current page #> [Next] [Last]

また、現在のページが最初のときは「Top」と「Prev」は表示せず、最後のときは同様に「Next」と「Last」を表示しない。

これを実現するための util.Pager の派生クラスは以下のようになる。

(src/util.py より)
class PrevNextPager(Pager):
    def __init__(self, page_num, total_posts):
        Pager.__init__(self, page_num, total_posts)

        if self.total_pages > 0:
            self.last = self.total_pages - 1

        if self.current != 0:
            self.is_top = False
            self.prev = self.current - 1
        else:
            self.is_top = True
            self.prev = 0

        if self.current != self.last:
            self.is_last = False
            self.next = self.current + 1
        else:
            self.is_last = True
            self.next = self.last

で、これを表示するテンプレートはこう。

(src/listview.html より)
{% block view_footer %}
<nav>
  <div class="pager">
    {% if not view.pager.is_top %}
    <span class="l-button"><a href="/?page=0">Top</a></span>
    <span class="l-button"><a href="/?page={{ view.pager.prev }}">Prev</a></span>
    {% endif %}
    <span id="pager-current">{{ view.pager.current }}</span>
    {% if not view.pager.is_last %}
    <span class="r-button"><a href="/?page={{ view.pager.next }}">Next</a></span>
    <span class="r-button"><a href="/?page={{ view.pager.last }}">Last</a></span>
    {% endif %}
  </div>
</nav>
{% endblock %}

info.Menu クラスと同様に、util.Pager クラス(とそのサブクラス)は info モジュールに移すべきかな。

「Goooooooooogle!」方式と「前後」方式

実際に使ってみたところ、「Goooooooooogle!」方式にはとくにメリットがないと感じた。この方式では途中のページにダイレクトに飛べるわけだが、そこに何がリストされているかがわかっていない限りほとんど意味がない。リストを順にたどって見るようなときには「前後」方式の方が切り替えやすい(マウスを動かさなくても良いから)。まとめて何ページも先を開くなら「Goooooooooogle!」方式の方が便利だが、実用上そういう場面が思い浮かばない。

特定の時期に書かれた記事を探すなら大きく前後に移動できた方が便利だが、そういう便利さを実現したいなら月や週で検索できる画面(ビュー)を用意した方が良い。

iPhone のような画面の狭いデバイスの場合も「前後」方式が適している。特定のページを直接開くというユーザ体験を実現するなら、それ専用の画面(ビュー)を作るべきだ。

「一覧」や「検索」といった機能を実行した結果が複数のページに分かれる場合、シンプルな前後方式の方が汎用性が高い(画面のサイズを選ばないから)。そもそも(大量の)複数ページに分かれているという点でユーザ体験としては減点だろう。ユーザが意識して「全部を順番に見たい」というのでない限り、適切に絞り込む手段を提供すべき。そして、適切に絞り込めるなら(ページ数にしてせいぜい 2 〜 3)「前後」方式が(ユーザに余分なことを考えさせないという意味で)直感的だ。

関連リンク

関連記事

2010-10-23

Blogger Glass、ラベルで検索したい

一覧から記事を表示できるようにはなった。しかし、一覧だけから特定の記事を探すのはシンドい。Google の存在に慣れた身にとって「探す == 検索」であって欲しい。Blogger Glass にも「検索」機能が必要だ。

Blogger Data API では、データのいわゆる全文検索には対応していない(→ Blogger API Reference Guide)。可能なのはカテゴリを指定してフィードの内容を絞り込むことだけ。ここでいうカテゴリとは Blogger (の投稿画面など)ではラベルと呼ばれているもの。

たとえラベルによる検索だけだったとしても、何もないよりはマシだ。適切にラベル付けがされていれば効果的な絞り込みができるし、適切でない場合も多少の助けにはなる。

そんなわけで、今回はラベル検索を実装してみる。

調査

カテゴリによる検索

Google Data API (プロトコル) では、フィードを要求する際のパラメータとしてカテゴリを指定できる。これまでにも実装の中で使ってきた max-resultsstart-index などと同様に URL に埋め込む形で利用する。

これはそのまま GData ライブラリでも利用できる。具体的にはこうなる。

(カテゴリによる絞り込み)
        client = gdata.blogger.client.BloggerClient()
        q = gdata.blogger.client.Query(categories=[label.encode('utf-8')])
        feed = client.get_posts(blog_id, query=q)

この例では label にラベル文字列が入っている。複数のラベルで絞り込む場合(AND 結合)は gdata.blogger.client.Query 生成の際に categories に複数のラベルを詰めれば良い。OR 結合で指定する場合はラベルを "|" でつないた文字列を詰める。たとえば、'imac' と 'macbook' のどちらか一方をラベルに持つ記事を探すのであれば categories=['imac|macbook'] をわたす。

デザイン(意匠と設計)

検索結果画面

一般に検索結果は複数の記事になるから、その表示も一覧表示が適している。欲を言えば Google の検索結果のように記事の抜粋も付けたいところだが、現段階ではそこまでは望まない。よって、画面そのものは「一覧画面」と同様になる。また、結果が多数の場合は複数のページに分かれることになるから、これも「一覧画面」と同様にページ切り替えの仕組みも必要だ。唯一の違いは、「検索結果画面」にはどんな検索語(ラベル)で検索したかを表示すべきだという点だ。これは「view-header」の部分に表示する。

リクエスト形式

リクエスト形式として /search/ を追加する。以下に、これをふくめた定義ずみ全リクエスト形式を示す。

Blogger Glass 全リクエスト形式
リクエスト 機能
/ page=0 を指定した場合と同様。
/?page=<number> number で指定したページの一覧を表示する。
/post/ 最新の記事の内容を表示する。
/post/?id=<id> id で指定した記事の内容を表示する。
/search/?label=<string> ラベル string で記事を絞り込んむ。結果が複数ページにわたる場合は、最初のページ(ページ番号 0)を開く。
/search/?label=<string>&page=<number> ラベル string で記事を絞り込んだ結果のうち、number で指定したページを開く。
/settings/ 設定変更のための画面を開く。

実装

新規に追加したのは、SearchViewHandler (後述)を定義する src/searchview.py と「検索結果画面」の src/searchview.html だ。

src/app.yaml には新しいリクエスト形式 (/search/) を追加。src/info.py には「検索結果画面」に表示する情報を詰めるためのクラス(SearchViewInfo)を追加。また、「一覧画面」と「検索結果画面」で共通になるページ切り替えの仕組みは src/util.py に移動させた。src/postview.html への変更はラベルに検索リクエストへのリンクを張るためのものだ。

SearchViewHandler

結果として、ほぼ「一覧画面」の MainHandler と同じコードになった。違いはクエリ(gdata.blogger.client.Query)生成時にラベルを指定している程度だ。fill_viewinfo() をリファクタリングしてくくり出すことを考えても良いかも。

この実装における注意点は、1 つは GData Python ライブラリのバグ(後述)で、もう 1 つは、クエリにラベル文字列を追加する際にエンコードを指定する、という点だ。つまり、label がラベル文字列だったとして、このままクエリにわたすのではなく、label.encode('utf-8') でわたさなければならない、ということ。さもないと、GData ライブラリの奥で以下のようなエラーが出る。

UnicodeEncodeError: 'ascii' codec can't encode characters in position 6-13: ordinal not in range(128)

そう言えば、以前同じような現象に遭遇したことがあった(→ Ruby で書いたフィルタを Python で書き直す #2)。文字列のエンコードの問題には、こっちが忘れた頃に出くわすなあ。

GData Python ライブラリのバグ?

実は、今回の実装の過程で、GData Python ライブラリのバグだと思われるものにぶつかった。それは上述のようにクエリにカテゴリを指定しても絞り込みが行われないというものだ。原因は gdata/client.py 中にあった。

(gdata/client.py のオリジナル; 827 行付近)
 759: class Query(object):
[...]   [...snip...]
 824:   def modify_request(self, http_request):
 825:     _add_query_param('q', self.text_query, http_request)
 826:     if self.categories:
 827:       http_request.uri.query['categories'] = ','.join(self.categories)

この 827 行目は正しくはこうなるようだ。

(gdata/client.py 修正版; 827 行目)
 827:       http_request.uri.query['category'] = ','.join(self.categories)

要は http_request.uri.query に詰められたキーと値が最終的に Google Data API にわたるが、このキー名が間違っているのだ。複数形の "categories" ではなく単数形の "category" がプロトコルとして正しいクエリ文字列だ。

ま、次のバージョンでは直ってくるだろう。

追記 (@2010-10-24)

このバグはすでに問題として報告されていて、かつすでに修正されているようだ。といっても修正されたのは最近(この 10 月に入ってから)のようだけど。

  • Issue 315 (gdata-python-client; Project Hosting on Google Code)

今後の展望

Blogger Glass はブログを「見る」ことに特化したアプリだ。その意味では「検索」機能は重要な機能だと言える。今回実装した「検索」機能は、とくにそのユーザ体験において必要最低限のものにも到達していない。なにしろ、個々の記事を開いたときにしか検索機能を使うことができないのだ、ユーザにとって使いやすいものではない。せめて、ラベルの一覧表示は欲しい。できれば一覧の他にタグクラウドのような表示も欲しい。また、ラベルを複数指定できればさらに実用性が高まるだろう。GData ライブラリ(と API)ではカテゴリを複数指定できる。これに関して足りないのは単純にアプリ側の作り込みなのだ。

一歩前進すれば、より遠くが見えるようになる。ゴールのように見えていた場所は、単なる中継地点でしかなかったことがわかる。

とはいえ、記事の一覧と個々の記事の内容が独自のスタイルで表示できるようになったことで、Blogger Glass は実用性のあるアプリになった。今回、指定できるラベルが 1 つに限定されているとは言え、ラベルによる絞り込み(検索)ができるようになり、さらに実用性は高まったと言える。もう記事の投稿の時以外にオリジナルの Blogger の方を開く必要はないかも。

さっき数えたら、Blogger Glass に関係する記事が 28 本になっていた。そろそろ全体の「まとめ記事」を書いて整理した方が良いな。前ばっかり見ていると、いつまでたっても近付かないゴールに(なにしろゴールが動くからな)やる気を削がれかねない。来し方を振り返り積み重ねたものを実感することで、また一歩を踏み出すモチベーションになる。

関連リンク

関連記事

2010-10-22

Blogger Glass でスタイルを定義する #2

前回からの続き。今回は、実際に CSS によるスタイルを定義する。スタイルのベースになるのは、Style Repository で LOG+REPO 用に提供しているものだ。

画面(ビュー)の構造: 詳細編

共通部分 (src/base.html) の構造は前回示した通り。今回は、画面ごとに変化する領域の構造を示す。

個別画面の構造は、ソース上、以下のファイルで定義されている。

ちなみに、前回は書かなかったが、以下に示す HTML の構造図では、主に CSS のセレクタに記述する書式で HTML の要素を書いている。h1blockquote はそのまま HTML の要素名で、pre.code のように . (ピリオド) に続くのはクラス名だ。また # (シャープ) に続くのは ID を表している。ただし、div 要素については、他と混同する恐れがない限り .pager#contents のように要素名を省略して書くことにする。

一覧表示画面

テンプレート (src/listview.html) を見てもわかる通り、この画面の特徴となる構成要素は、ブログ記事のタイトル一覧(内容表示画面へのリンク付き)とページ切り替え用のリンクだ。

#view-header
|   +-- h2
|   +-- p
#contents
|   +-- ol
|       +-- li
|       +-- ...
#view-footer
    +-- nav
        +-- .pager
            +-- span
            +-- span.current-page
            +-- span.other-page
            |   +-- a
            +-- span
内容表示画面

内容表示画面では、#contents 領域にブログの記事がそのまま流し込まれる。よって、この画面の HTML の構造は src/postview.html と記事の部分の構造を融合したものになる。以下の構造図では、典型的な記事の構造を挿入してある。

#view-header
|   +-- h3
|       +-- a
#contents
|   +-- h4
|   +-- p
|   +-- blockquote
|   +-- h4
|   +-- h5
|   +-- .right
|   |   +-- img
|   +-- p
|   +-- h5
|   +-- p
|   +-- table
|   |   +-- caption
|   |   +-- thead
|   |   +-- tbody
|   +-- h4
|   +-- pre.code
|   +-- pre.terminal
|   +-- ul
|   +-- ...
#view-footer
    +-- #labels
        +-- span.label
        +-- ...
設定画面

主要素の formtable 内に配置されている。table は 第 1 行 (theadに収められている) と 2 行目以降の第 1 列が見出しになる標準的な構造だ。form の要素 (input) は2 行目以降の第 2 列に置かれている。

#view-header
|   +-- h3
#contents
|   +-- form [action='/settings/', method='post']
|   |   +-- table
|   |       +-- caption
|   |       +-- thead
|   |       |   +-- tr
|   |       |   |   +-- th
|   |       |   |   +-- th
|   |       +-- tbody
|   |       |   +-- tr
|   |       |   |   +-- th
|   |       |   |   +-- td
|   |       |   |       +-- input [name='blog_id']
|   +-- form [action='/settings/', method='post']
|       +-- form-buttons
|           +-- input [type='hidden', name='delete', value='yes']
|           +-- input [type='submit', value='Delete Settings']
#view-footer
    +-- (text)
        +-- span.timestamp

デフォルトスタイル

いずれ、デバイスによるスタイルの切り替えを実装する(BG 内に作り込むか、または Style Repository で提供する)が、まずは Mac (または PC) 用のものをデフォルトスタイルとして BG 内に用意することにした。また、画面ごとに専用のスタイルを用意するかどうかは検討中で、デフォルトスタイルは 3 つの画面すべてに共通のものにしている。

スタイルの定義は Style Repository で LOG+REPO 用に提供しているものをほぼすべて流用している。HTML の構造の違いを調整した他は、フォントサイズ、色などはそのまま使っている。全体の横幅と要素ごとのマージンとパディングについてはまだ調整していない。

関連リンク

関連記事

twitter より (2010-10-21)

  • 12:00  何だか良くわかんないラインナップだなあ > new MacBook Air。11インチだけなら「世界で一番薄くて軽いMac」でわかりやすかっただろうに。 → http://bit.ly/a7VMie
  • 12:03  あと、バッテリーの駆動時間が MacBook の半分ってどういうことだ? 計測の基準が違うのか? いくら薄くて軽くてもバッテリーが持たないんじゃ持ち歩きにくいだろうに。やっぱ、良くわからん。
  • 12:15  Lion はまだ1年近く先か。Mac App Storeを別にすれば、LaunchPadもフルスクリーンアプリもMission Controlも、ぜんぶ画面の小さなMac用に思えるよ。→ http://www.apple.com/jp/macosx/lion/
Powered by twtr2src.

2010-10-21

Blogger Glass でスタイルを定義する #1

Blogger Glass (以下、BG)は Blogger で作ったブログの記事を表示するためのアプリだ。その目的は、Blogger の表示用テンプレートを変更することなく、独自の外観でブログ記事を見ることにある。Blogger の提供する(複雑きわまりない構造の)テンプレートに手を入れることなく、記事の見た目を変える。それが BG を作った動機だ。

記事の一覧から内容の表示を作り込み、ブログ閲覧用アプリとしては最小限の機能を備えるようになった。まだ追加したい機能もあるが、このあたりで最初の目的に立ち返り、独自の外観を与えることにしたい。つまり、スタイルを定義するということだ。

テンプレートのリファクタリング

現在、BG には、一覧表示、内容表示、設定という 3 つの画面が定義されている。それぞれ、listview.htmlpostview.html そして settingsview.html というテンプレートから作られている。まずは、このテンプレートたちをリファクタリングすることから始める。

GAE に組み込まれているのテンプレートシステムの Django では、基本となる共通のテンプレートを定義しておき、画面ごとに差分を定義するという方法を使うことができる。ちょうど、(クラスベースの)オブジェクト指向プログラミング言語における、継承による実装の共有のようなことができるのだ。

以下のように、共通となるテンプレートに {% block foo %}{% endblock %} で上書き可能なブロックを定義しておく。

(src/base.html より)
[...snip...]
<div id="contents">
{% block content %}
<p>main content area</p>
{% endblock %}
</div>
[...snip...]

個別のテンプレート側では上書きしたいブロックを定義すれば良い。共通テンプレートの内容から、そのブロックだけを置き換えたものが実際のテンプレートになる。

(src/listview.html より)
{% extends 'base.html' %}
[...snip...]
{% block content %}
<ol>
{% for entry in view.content %}
  <li><a href="/post/?id={{ entry.get_post_id }}">{{ entry.title.text }}</a></li>
{% endfor %}
</ol>
{% endblock %}

画面(ビュー)の構造

続いて、画面の構造の見直し。以下がその概要だ。/html/body/section が、個別の画面により変化する領域で、他は(少なくとも構造は)共通になる。

html
+-- head
|   +-- meta[charset]
|   +-- title
|   +-- link[stylesheet]
+-- body
    +-- header
    |   +-- nav
    |   |   #top-menu
    |   |   +-- span.menu-item
    |   |   +-- ...
    |   +-- h1
    +-- section
    |   +-- #view-header
    |   +-- #contents
    |   +-- #view-footer
    +-- footer

個別画面で変化する画面要素と内容の対応を以下に示す。一部、未実装の内容もふくまれている。

画面要素と表示内容
画面 view-header contents view-footer
一覧表示 ページ番号 記事一覧 ページ切り替えリンク
内容表示 記事タイトル 記事本文 ラベル (未実装)
最終更新日時 (未実装)
設定 「Settings」 設定項目 最終更新日時 (未実装)

今日はここまで。実際の CSS の定義は次回に。

関連リンク

関連記事