WebOS Goodies

WebOS の未来を模索する、ゲームプログラマあがりの Web 開発者のブログ。

WebOS Goodies へようこそ! WebOS はインターネットの未来形。あらゆる Web サイトが繋がり、共有し、協力して創り上げる、ひとつの巨大な情報システムです。そこでは、あらゆる情報がネットワーク上に蓄積され、我々はいつでも、どこからでも、多彩なデバイスを使ってそれらにアクセスできます。 WebOS Goodies は、さまざまな情報提供やツール開発を通して、そんな世界の実現に少しでも貢献するべく活動していきます。
Subscribe       

Opera ウィジェットの作り方

前回の記事のとおり、最新の Opera Labs Release にて Opera ウィジェットが大幅に強化され、 Web / デスクトップのハイブリッド・アプリケーション開発プラットフォームとして非常に興味深い存在になりました。

Opera ウィジェットの特徴は、なんといってもその開発の容易さです。最低限必要なのは Opera と表示内容となる HTML ファイル、それにウィジェット定義の XML ファイルだけ。コンパイルも必要なく、定義ファイルを Opera ブラウザに放り込むだけでウィジェットを表示できます。デバッグも Opera の JavaScript デバッガ「Dragonfly」が利用できるので、 Web ページとまったく同じ感覚でデスクトップ・アプリの開発が行えます。

本日は、簡単な HTML エディタを作成しながら、この Opera ウィジェットの作り方をご紹介します。 HTML / CSS / JavaScript の基礎知識があれば、この記事を読むだけですぐに開発がはじめられます。これまで Opera に触れたことのない方も、ぜひこの機会に Opera ウィジェットの世界を体験してみてください!

環境を整える

この記事では最新の Opera Labs Release を前提にします。以下のページの最後にインストーラへのリンクがありますので、環境にあったものをダウンロードし、インストールしてくださいOpera 10.2 alpha が公開されたので、そちらを利用してください。

http://labs.opera.com/news/2009/10/15/

前述のとおり Opera ウィジェットの開発に必要なのは Opera ブラウザのみですので、これで準備完了です。強いていえばテキストエディタも必要ですが、そちらも Opera のソース編集機能で代用できます。手慣れたテキストエディタが無い場合はご利用ください。

Opera ウィジェットのスケルトンを作る

本題の HTML エディタの前に、どの Opera ウィジェットでも必要となる基本機能を備えたスケルトン・ウィジェットを作りましょう。以下のように中身は白紙で、タイトルバーと設定・クローズボタン、リサイズボックスのみを表示するウィジェットです。

機能はなにもありませんが、ウィジェットの移動・リサイズ、クローズといった基本機能はすべて動作します。以下の場所に完全なソースファイルとパッケージ(Opera でリンクをクリックするとウィジェットがインストールできる)があるので、ご利用ください。

http://webos-goodies.googlecode.com/svn/trunk/blog... (ソースファイル)
http://webos-goodies.googlecode.com/svn/trunk/blog... (パッケージ)

まずは適当なディレクトリを作成し、以下の内容を config.xml というファイル名で保存します。これがウィジェットの定義ファイルになります。

<?xml version="1.0" encoding="utf-8"?>
<widget network="public" defaultmode="widget">
  <widgetname>Skeleton Widget</widgetname>
  <width>320</width>
  <height>240</height>
</widget>

ウィジェットの動作に必要な最低限の項目のみを記述しています。各項目の機能は以下のようになります。

widget タグの network 属性
XMLHttpRequest などでアクセスする IP アドレスの範囲です。 "private" ならプライベート IP アドレス(192.168.x.x など)のみ、 "public" なら公開 IP アドレスのみ、 "private public" なら両方にアクセス可能。それ以外だと完全にアクセス禁止になるので注意してください。
widget タグの defaultmode 属性
ウィジェットの表示モード。 "widget" なら従来通りすべてをウィジェット側で描画、 "application" なら OS 標準のタイトルバーとリサイズボックスを表示。
widgetname
ウィジェットの名前です。インストール時の実行ファイル名にもなります。
width, height
ウィジェットのデフォルトの縦・横サイズです。

これら以外にも多くの項目があるので、詳細は Opera ウィジェットの仕様を参照してください。

ウィジェットに表示される内容を、 index.html に記述します。

<!DOCTYPE html>
<html>

<head>
  <title>Skeleton Widget</title>
  <script type="text/javascript" src="scripts/chrome.js"></script>
  <link href="basic.skin/skin.css" rel="stylesheet" type="text/css" media="screen" />
</head>

<body></body>

</html>

見てのとおり、ごく普通の HTML ファイルです。 body タグの内部に要素を追加していけば、それがそのままウィジェット内に表示されます。 Web ページを作る要領で Opera ウィジェットを開発できることがおわかりいただけると思います。

最後に、 index.html で読み込んでいるスクリプトやスタイルシートを準備します。これらは Widget Chrome Library (WCL) と呼ばれるライブラリで、これを読み込むだけでタイトルバーやリサイズボックスなどの基本的なパーツの面倒をみてくれます。以下のページにドキュメントがあります。

http://dev.opera.com/libraries/chrome/

ページの最初の方にある「Download the template」のリンクからダウンロードできますので、展開して scripts と basic.skin を index.html と同じディレクトリにコピーしてください。

これでスケルトンが完成です。 config.xml を Opera ブラウザにドラッグ&ドロップすれば、ウィジェットが表示され、同時に Dragonfly が起動してデバッグ可能な状態になります。

この時点では Opera ブラウザ上で動作していますが、後述のパッケージングを行ってインストールすればスタンドアローンで動作するようになります。

HTML エディタを作成する

スケルトンが完成したところで、それを元にして HTML エディタウィジェットを作りましょう。

使い方は、まず設定ボタン(タイトルバーの歯車のボタン)を押して設定画面に切り替え、「ディレクトリを選択」ボタンを押して編集したい HTML があるディレクトリを選択します。そのディレクトリ内の HTML がリストアップされますので、編集したいファイルを選択して、設定ボタンで通常画面に切り替えます。すると、上下 2 ペインの上側にソース、下側にプレビューが表示されますので、上側のペインでソースを編集してください。編集内容は 3 秒後にプレビューに反映され、同時に元のファイルに保存されます。

完全なソースとパッケージが以下の場所にあるので、ご利用ください。

http://webos-goodies.googlecode.com/svn/trunk/blog... (ソースファイル)
http://webos-goodies.googlecode.com/svn/trunk/blog... (パッケージ)

スケルトンからの変更作業としては、 config.xml, index.html の編集とアプリケーションのロジック (JavaScript) を記述した application.js の追加です。まずはそれぞれの全体を見ていきましょうか。まずは config.xml 。

<?xml version="1.0" encoding="utf-8"?>
<widget network="" defaultmode="widget">
  <widgetname>HTML Editor</widgetname>
  <width>500</width>
  <height>400</height>
  <feature name="http://xmlns.opera.com/fileio"></feature>
</widget>

各パラメータは変更されているものの、内容はスケルトンとほぼ同じです。 feature タグが追加されていますが、それについては後述。

次に index.html です。

<!DOCTYPE html>
<html>

<head>
  <title>HTML Editor</title>
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.min.js"></script>
  <script type="text/javascript" src="scripts/chrome.js"></script>
  <script type="text/javascript" src="application.js"></script>
  <link href="basic.skin/skin.css" rel="stylesheet" type="text/css" media="screen" />
  <style type="text/css">
    .half_height { box-sizing: border-box; height: 50%; }
    .upper_pain { padding: 4px 4px 2px; }
    .lower_pain { padding: 2px 4px 4px; }
    .border { box-sizing: border-box; width: 100%; height: 100%; border: solid 1px #ccc; }
    #source_edit { border: none; padding: 0px; margin: 0px; width: 100%; height: 100%; white-space: nowrap; }
    #preview { border: none; padding: 0px; margin: 0px; width: 100%; height: 100%; }
    #config { padding: 4px; }
    #files { width: 100%; }
    .btn_dir_div { text-align: right; }
  </style>
</head>

<body>

  <div id="main" style="display: none;">
    <div class="half_height upper_pain">
      <div class="border"><textarea id="source_edit"></textarea></div>
    </div>
    <div class="half_height lower_pain">
      <div class="border"><iframe id="preview" src="opera:blank"></iframe></div>
    </div>
  </div>

  <div id="config" style="display: none;">
    <select id="files" size="8"></select>
    <div class="btn_dir_div"><input id="btn_dir" type="button" value="ディレクトリを選択" /></p>
  </div>

</body>

</html>

通常画面 (id="main") と設定画面 (id="config") の内容を、それぞれ別の DIV に記述しているのがわかると思います。設定ボタンが押されたときに、これらの DIV の表示を ON/OFF することで、画面を切り替えているわけです。

そして新たに追加するのが application.js です。

var currentFilename = widget.preferenceForKey('filename');
var currentUrl      = 'opera:blank';
var currentText     = '';
var updateTimer     = null;

function onChangeSource(event) {
  updateTimer && clearTimeout(updateTimer);
  updateTimer = setTimeout(saveFile, 3000);
}

function onClickConfig() {
  $('#main, #config').toggle();
}

function onClickBtnDir() {
  var olddir = null;
  try { olddir = opera.io.filesystem.mountPoints.resolve('current'); } catch(e) {}
  opera.io.filesystem.browseForDirectory('current', '', function(dir) {
    if(dir) {
      olddir && opera.io.filesystem.removeMountPoint(olddir);
      refreshFiles();
    }
  }, true);
}

function onChangeFiles() {
  var fname = $('#files').attr('value');
  fname && loadFile(fname);
}

function isValidFile(file) {
  return file.exists && file.isFile && !file.readOnly && /\.html?$/.test(file.name);
}

function refreshFiles() {
  var files = $('#files').empty();
  try {
    var dir = opera.io.filesystem.mountPoints.resolve('current');
    dir.refresh();
    for(var i = 0 ; i < dir.length ; ++i) {
      var file = dir[i];
      if(isValidFile(file))
        files.append($('<option />').attr('value', file.name).text(decodeURIComponent(file.name)))
    }
  } catch(e) {}
}

function loadFile(fname) {
  currentFilename = null;
  currentUrl      = 'opera:blank';
  currentText     = '';
  try {
    if(fname) {
      var file = opera.io.filesystem.mountPoints.resolve('current/' + fname)
      if(isValidFile(file)) {
        var stream = file.open(null, opera.io.filemode.READ);
        try {
          currentText     = stream.read(stream.bytesAvailable);
          currentFilename = fname;
          currentUrl      = file.path;
        } finally { stream.close(); }
      }
    }
  } catch(e) {}
  $('#source_edit').attr('value', currentText);
  $('#preview').attr('src', currentUrl);
  widget.setPreferenceForKey(currentFilename, 'filename');
}

function saveFile() {
  var text = $('#source_edit').attr('value')
  if(currentFilename && text != currentText) {
    try {
      var file   = opera.io.filesystem.mountPoints.resolve('current/' + currentFilename)
      var stream = file.open(null, opera.io.filemode.WRITE);
      try {
        stream.write(text);
        currentText = text;
      } finally { stream.close(); }
      var iframe = $('#preview').get(0);
      var parent = iframe.parentNode;
      parent.removeChild(iframe);
      parent.appendChild(iframe);
      iframe.src = file.path;
    } catch(e) {throw e;}
  }
}

function initApp() {
  WidgetChrome.ButtonConfig.onclick = onClickConfig;
  WidgetChrome.autoResize('main');
  $('#main').show();
  $('#source_edit').keyup(onChangeSource);
  $('#btn_dir').click(onClickBtnDir);
  $('#files').change(onChangeFiles);
  refreshFiles();
  loadFile(currentFilename);
}

$(window).load(initApp);

このウィジェット特有の JavaScript はすべてここに記述してあります。もちろん index.html に直接書くことも可能ですが、メンテナンスなどを考えると別ファイルすることをお勧めします。

アプリケーションのロジックかなり単純なので、ソースを見ていただければ一目瞭然でしょう。そこで、以降では Opera ウィジェットに特有の部分に的を絞って説明します。

jQuery を利用する

JavaScript でアプリケーションを組むときは、基本的な処理を実装したライブラリを利用するのが一般的です。もちろん Opera ウィジェットの開発でもそれらのライブラリをそのまま利用できます。 index.html で script タグを使って普通に読み込めば OK です。

今回の HTML エディタウィジェットも、そのようなライブラリのひとつである jQuery を利用しています。ちょっと手抜きをして、以下のように Google AJAX Libraries API から読み込んでいます。

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.min.js"></script>

しかし、実はこの方法はあまり好ましくありません。ネットワークに繋がっていないときはウィジェットが動作しなくなりますし、外部ネットワークからコードを読み込むのはセキュリティー上も問題があるからです。皆さんがウィジェットを開発するときは、 index.html と同じフォルダに jQuery のスクリプトファイルを配置し、 WCL と同様に相対パスで読み込むようにしてください。

設定画面を実装する

スケルトンの状態では設定ボタンは表示されるものの機能しないので、イベントハンドラを実装してクリック時の処理を指定してやる必要があります。そのためには、 onload イベント時に以下のようにしてイベントハンドラを設定します。

function onClickConfig() {
  $('#main, #config').toggle();
}

function initApp() {
  // ...
  WidgetChrome.ButtonConfig.onclick = onClickConfig;
  // ...
}

$(window).load(initApp);

WidgetChrome.ButtonConfig で設定ボタンに対応する button 要素が取得できるので、その onclick 要素にイベントハンドラを設定してやるわけです。イベントの設定には addEventListener を使うことも可能ですが、それだと WCL が設定したデフォルトのハンドラ(エラーコンソールにメッセージを出力するだけ)も同時に実行されてしまうので、上記のように onclick に設定するのが良いかと思います。

ローカルファイルにアクセスする

Opera ウィジェットからローカルファイルにアクセスするには、 File I/O API を利用します(これは最新の Labs Release から追加された機能です)。この部分はちょっと複雑なので、エッセンスを抽出して手短に説明します。より詳細な使い方はドキュメントを参照してください。

さて、 File I/O API を有効にするには、まず config.xml に以下の feature タグを追加する必要があります。

<feature name="http://xmlns.opera.com/fileio"></feature>

これで JavaScript から File I/O API が利用できるようになります。しかし、無制限にファイルシステム全体へのアクセスが許されるわけではありません。まずユーザーにアクセスを許可するディレクトリを選択させ、それを Opera ウィジェットの仮想ファイルシステムにマウントする必要があります。

そのためには、 opera.io.filesystem.browseForDirectory メソッドを以下のように呼び出します(このメソッドは onclick イベントなど、ユーザー・アクションで起動された処理内でしか呼び出せないので注意してください)。

opera.io.filesystem.browseForDirectory('current', '', function(dir) {
  if(dir) {
    // ディレクトリが選択された際の処理
  }
}, true);

アクセスを許可するディレクトリが選択されると、第一引数で指定したマウントポイント(上記の場合は '/current')にそのディレクトリをマウントし、それを示す File オブジェクトを引数にしてコールバック関数が呼ばれます。もしダイアログがキャンセルされたらコールバックの引数として null が渡されるので、それぞれ適切な処理を実行してください。

なお、ここでマウントされたディレクトリは、通常はウィジェットがクローズされるまで有効です。しかし、メソッドの第 4 引数に true を渡すとマウント状態が記憶され、次回のウィジェット実行時に自動でマウントされるようになります。

ディレクトリをマウントした後に、そこにあるファイルを列挙するには以下のようにします。

var dir = opera.io.filesystem.mountPoints.resolve('current');
dir.refresh();
for(var i = 0 ; i < dir.length ; ++i) {
  // dir[i] が各ファイルに対応する File オブジェクトになる
}

opera.io.filesystem.mountPoints がルートディレクトリを示す File オブジェクトなので、その resolve メソッドを使って '/current' のオブジェクトを取得します(これは先ほどの browseForDirectory メソッドでコールバックに渡されるのと同じものです)。そして、 refresh メソッドで情報を更新した後に、配列と同様の方法でディレクトリ内の各ファイル・ディレクトリに対応する File オブジェクトが取得できます。

操作したいファイルに対応する File オブジェクトが取得できたら、 open メソッドで FileStream オブジェクトを取得し、その内容を読み書きできます。例えば、 file が示すテキストファイルの内容をすべて読み込むには、以下のようにします。

var stream = file.open(null, opera.io.filemode.READ);
try {
  text = stream.read(stream.bytesAvailable);
} finally {
  stream.close();
}

open メソッドの第一引数はオープンするファイル名ですが、 null を指定するとオブジェクトに対応するファイルそのものが対象になります。第二引数はモードで、 opera.io.filemode を OR (|) で組み合わせて指定します。 FileStream オブジェクトが取得できたら、そのメソッドを使ってファイルにアクセスできます。上記の例では、 read メソッドでファイルの内容を読み込んでいます。もちろん WRITE モードで開けば write メソッドなどでファイルに書き込むことができますし、 Base64 や ByteArray でバイナリファイルの読み書きも可能です。

FileStream を使う際の注意点として、処理が終了したら必ず close メソッドでファイルを閉じてください。例のように finally を使って確実に呼び出すようにするのが良いでしょう。

以上で File I/O API の使い方をざっと説明しましたが、この API にはほかにもまだまだ機能があり、例えばファイルをマウントするためのメソッドは browseForDirectory 以外にも単一のファイルに限定してアクセスできる browseForFile やファイルを新規作成するための browseForSave があります。 File I/O APIOpera Unite でもキーとなる API なので、別の機会に詳しくご紹介したいと思っています。

mountpoint:// スキーム

File I/O API に関連する機能として、 mountpoint URL スキームがあります。これは File I/O API でマウントしたファイルにアクセスするための URL で、例えば "/current/sample.html" を示す URL は以下のようになります。

mountpoint://current/sample.html

この URL を使うと、 Web 上のリソースと同じ方法でローカルファイルの内容を表示したり、アクセスできるようになります。例えば img タグの src 要素に設定すればローカルの画像ファイルを表示できますし、 XMLHttpRequest でファイル内容を読み込むことも可能です。

HTML エディタウィジェットでは、 iframesrc 属性に編集中のファイルの URL を設定することで、プレビュー表示を実現しています。

Preference Store を利用する

こちらは Opera 9 の頃からある機能なのでご存知の方も多いと思いますが、一応説明しておきます。 Opera ウィジェットには、ウィジェットの設定などを記憶するための領域として、 Preference Store というものが用意されています。これは一種の key-value 型ストレージで、特定の文字列キーに対して文字列の値を結びつけて保存することができます。

利用方法はとても単純で、以下の 2 つのメソッドを使うだけです。

preferenceForKey(key)
key に結びつけられた文字列を返します。
setPreferenceForKey(value, key)
key に文字列 value を結びつけて保存します。

HTML エディタウィジェットでは、編集ファイルが指定されたときに setPreferenceForKey でそのファイル名を保存しています。

widget.setPreferenceForKey(currentFilename, 'filename');

そして、ウィジェット起動時に preferenceForKey を呼び出してその値を復帰させています。

var currentFilename = widget.preferenceForKey('filename');

Preference Store についてはこちらの記事にも解説とサンプルがありますので、参考にしてください。

パッケージングし、配布する

開発したウィジェットは、 .wgt ファイルとしてパッケージングすれば Web サイトで簡単に配布できます。また、この記事が前提としている最新の Labs Release ではスタンドアローンのデスクトップ・アプリとしてインストールされるようになります。

パッケージングの方法は簡単で、 config.xml のあるディレクトリの内容を zip ファイルとしてアーカイブし、その拡張子を .wgt にするだけです。例えば "~/html_editor" に config.xml がある場合、コマンドラインなら以下のようにすることで "~/html_editor.wgt" にパッケージが作成されます(要 zip コマンド)。

cd ~/html_editor
zip -r ~/html_editor.wgt *

ポイントは、 config.xml が所属しているディレクトリ (~/html_editor) はアーカイブに含めないことです。つまり、 html_editor.wgt を展開したときに、 html_editor ディレクトリを作成せず、直下に config.xml ができるようにしなければなりません。

.wgt ファイルができたら、widgets.opera.comアップロードページに申請することで同サイト経由で広く配布することができます。

ただ、内輪だけで使うウィジェットだったり、開発中のウィジェットを限定公開する場合など、広く一般配布はしたくないこともあるでしょう。そんなときは .wgt ファイルを自分の Web サーバーにアップロードし、 Content-Type が "application/x-opera-widgets" になるように設定します。 Apache であれば、 .htaccess に以下の一行を書けば OK です。

AddType application/x-opera-widgets .wgt

ほかに、オープンソースなウィジェットであれば Google Project Hosting を使用する方法もあります。この記事でリンクしている .wgt ファイルもこの方法で公開しています。

役立つ資料など

Opera ウィジェットの開発に必要な情報はすべて Dev.Opera で公開されています。しかし、数多くのドキュメントがあって最初は戸惑うと思うので、デスクトップ版の Opera ウィジェット開発に特に有用と思われるものを抜き出しました。これらを押さえておけば、 Opera ウィジェットの開発はバッチリです。

Opera Widgets Specification 1.0 fourth edition
config.xml の詳細、 widget オブジェクトのメソッド概要など、 Opera ウィジェットの仕様がまとめられています。
Opera Widgets security model
Opera ウィジェットから XMLHttpRequest を投げられるプロトコル・ホスト・ポート番号などをカスタマイズする方法が解説されています。
Widget modes: docked, widget, and more
Opera ウィジェットの各種モードの違いやモードごとにスタイルシートを切り替える方法が解説されています。ただし、デスクトップ版の Opera で利用できるモードは最新の Labs Release でも widget と application のみ、それ以前では widget のみです。
Introduction to Opera Dragonfly日本語訳
デバッガである Opera Dragonfly の使い方が解説されています。
Widget Object API
Opera ウィジェット内で使える widget オブジェクトのドキュメントです。
Widget Chrome Library
スケルトンで基本 UI の実装に使用した Widget Chrome Library のドキュメントです。
File I/O API
この記事でもご紹介した File I/O API のドキュメントです。
Opera Widgets Resources
Opera ウィジェットで多用されるアイコン、ボタン、背景などの素材が配布されています。

以上、本日は HTML エディタウィジェットを例にして Opera ウィジェットの開発方法をご紹介しました。個人的に、今回の Labs Release で Opera ウィジェットはとても有用なプラットフォームになってきたと思っているので、今後も暇をみて詳しい解説記事を書いてみたいと思っています。皆さんも、ぜひオリジナルの Opera ウィジェットを作ってみてください。

関連記事

この記事にコメントする

Recommendations
Books
「Closure Library」の入門書です。
詳しくはこちらの記事をどうぞ!
Categories
Recent Articles