「キーワードの追加・削除」機能の実装

今回も、Thunderbird用アドオンのSubjectHelperの改造をする。

実装後のファイル構成はこんな感じ。

subjecthelper
 ├ install.rdf
 ┠ chrome.manifest
 ┠ defaults
 │ └ preferences
 │  └ preferences.js
 └ content
   ┠ overlay.xul
   ┠ subjectHelper.css
   ┠ subjectHelper.xml
   ┠ subjectHelper.js
   └ img
    └ icon.png

defaults配下を追加、contentのsubjectHelper.js、overlay.xulを変更した。

キーワードの保持方法

今回は、キーワードを保持するために、Mozilla設定システムを利用した。

Mozilla設定システムとは、FireFoxのアドレスバーに「about:config」を入力した際に、表示されるアレのこと。

今回追加したdefaults配下のpreferences.jsは、初回起動時に、初期値を設定するための定義が記述されている。

設定した初期値はこんな感じ。

pref( "extensions.subjecthelper.keyword.list", "要返信,年休連絡,時間外" );

※これは、[編集]->[設定]->[詳細]->「設定エディタ] から確認できる。

この「extensions.subjecthelper.keyword.list」の値を書き換えることで、キーワードの追加・削除を行う。

overlay.xulの修正

修正後のoverlay.xulは以下のような感じ。

<?xml version="1.0"?>

<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script type="application/x-javascript" src="chrome://subjecthelper/content/subjectHelper.js"/>

  <textbox id="msgSubject">
    <button popup="_child" type="menu" id="subject-helper-button"
     style="-moz-binding: url('chrome://subjecthelper/content/subjectHelper.xml#subjectHelper');">
      <menupopup>
        <menuitem label="設定" accesskey="0" oncommand="SubjectHelper.setMenu();"/>
      </menupopup>
    </button>
  </textbox>
</overlay>

Mozilla設定システムにキーワードを保持しているため、予めmenuitem要素で記述していた既存のキーワードを除去した。

そのかわりに、Mozilla設定システムのキーワードを操作するための関数をcommandイベントに付加したmenuitem要素を追加した。

subjectHelper.jsの修正

今回の実装では、SubjectHelperオブジェクトに以下の関数を追加した。
・createMenu
・createMenuItem
・getKeywordList
・init
・setKeywordList
・setMenu

まず、取得したキーワードでメニューを新しく作成するcreateMenu。

    createMenu: function( keywordList ) {
        const delimiter = ",";

        var button = document.getElementById( "subject-helper-button" );
        var menu   = button.firstChild;

        // メニューの削除
        for( var index = menu.childNodes.length - 1; index >= 1; index-- ) {
            menu.removeChild( menu.childNodes[index] );
        }

        // メニューの追加
        var keywords = keywordList.split( delimiter );

        for( index in keywords ) {
            // 前後の空白を削除
            keywords[index] = keywords[index].replace( /^\s+|\s+$/g, "" );

            if( keywords[index].length > 0 ) {
                var menuItem = this.createMenuItem( keywords[index] );

                menu.appendChild( menuItem );
            }
        }
    }

メニューの削除部分は、わざわざ後ろから順番に消去しているが、これはコードの可読性を考慮している。

これを前から順番に消していく処理にすると、

        for( var index = 1; index <= menu.childNodes.length - 1; index++ ) {
            menu.removeChild( menu.childNodes[1] );
        }

となり、一目ではmenu.childNodes[1]あたり何をしているかわかりにくい。

JavaScriptにあまり慣れていないせいかもしれないが、removeChildによるダルマ落とし的な削除では、forをインクリメントで回す場合、どうしてもこういう書き方になる。

インデックスが1の要素を消していくより、後ろから順に消していく方が、自分としてはわかりやすかったので、そうした。

次は、menuitem要素を作成するcreateMenuItem関数。

    createMenuItem: function( keyword ) {
        var menuItem = document.createElement( "menuitem" );

        menuItem.setAttribute( "label", keyword );
        menuItem.setAttribute( "oncommand", "SubjectHelper.insert( this );" );

        return menuItem;
    }

これは、名前のまんま、引数で与えられたキーワードをラベルとしてmenuitem要素を作成する。

次、getKeywordListはMozilla設定システムからキーワードを取得する関数で中身はこんな感じ。

    getKeywordList: function() {
        var prefs = Components.classes["@mozilla.org/preferences-service;1"]
                        .getService( Components.interfaces.nsIPrefService );

        prefs = prefs.getBranch( "extensions.subjecthelper." );

        var keywordList = prefs.getComplexValue( "keyword.list",
                            Components.interfaces.nsISupportsString ).data;

        return keywordList;
    }

initは、メール作成画面起動時にメニューを作成するための関数。

    init: function() {
        var keywordList = this.getKeywordList();

        this.createMenu( keywordList );
    }

setKeywordListは、Mozilla設定システムにキーワードを設定するための関数。

    setKeywordList: function( keywordList ) {
        var prefs = Components.classes["@mozilla.org/preferences-service;1"]
                        .getService( Components.interfaces.nsIPrefService );

        prefs = prefs.getBranch( "extensions.subjecthelper." );

        var str = Components.classes["@mozilla.org/supports-string;1"]
                    .createInstance( Components.interfaces.nsISupportsString );

        str.data = keywordList;

        prefs.setComplexValue( "keyword.list"
                                , Components.interfaces.nsISupportsString, str );
    }

setMenuは、メニューの「設定」を押した場合に、キーワードを入力するダイアログを表示させる関数。

    setMenu: function() {
        var keywordList = this.getKeywordList();

        keywordList = prompt( "プロジェクト名をカンマ区切りで設定", keywordList );

        this.createMenu( keywordList );

        this.setKeywordList( keywordList );
    }

以上、6つの関数を追加した。

ソースをgithubに公開しようと思ったけど、フォークやらライセンスやら、わからないことがあるので、やめとく。

前よりは、コードをきれいにかけた気がする・・・気のせいかもしれない。

addEventListenerの引数の部分で、「function(){SubjectHelper.init();}」と書く場合と、「SubjectHelper.init」と書く場合で、挙動が違い、なぜ違うのかイマイチわからない。なんでだろ?


JavaScript難しい><

参考

・Preferences/MDN

「キーワードの変更時に前のキーワードを削除」機能の実装

今後の追加で、XULファイルがカオスになりそう・・・な気がしたので、

とりあえず、XULファイルから、処理が記述してある部分をJSファイルに抜き出した。

なので、ファイル構成は以下のようになった。

subjecthelper
 ├ install.rdf
 ┠ chrome.manifest
 └ content
   ┠ overlay.xul
   ┠ subjectHelper.css
   ┠ subjectHelper.xml
   ┠ subjectHelper.js
   └ img
    └ icon.png

削除処理を実装

subjectHelper.jsの中身はこんな感じ。

var SubjectHelper = { 
    insert: function( evt ) { 
        const keyHead = "【"; 
        const keyTail = "】"; 
 
        var key         = keyHead + evt.label + keyTail;            // キーワード作成 
        var msgSubject  = document.getElementById( "msgSubject" ); 
        var subjectLine = msgSubject.value;                         // 件名取得 
 
        // 件名からキーワードを除外 
        subjectLine = subjectLine.substr( subjectLine.indexOf( keyTail ) + 1 ); 
 
        msgSubject.value = key + subjectLine; 
         
        // 件名にフォーカスをあてる 
        msgSubject.focus(); 
 
        gContentChanged = true; 
 
        // ウインドウタイトルを更新 
        SetComposeWindowTitle(); 
    } 
}; 

削除処理は、件名取得して、件名からキーワードの末尾から後ろを取り出し、

選択したキーワードと、さっき取り出した件名をくっつけて新しい件名にする。

これで、キーワードを変更時に以前のキーワードが削除される。

元からあるコードの理解

ということで、元からあるコードを読んでみると、件名を変更する処理以外に次の3つの処理がされている。

・msgSubject.focus();
・gContentChanged = true;
・SetComposeWindowTitle();

gContentChangedは、不明のままだったけど、他の2つは何とかわかった。

gContentChangedがグローバル変数であることはわかるけど、それが何を意味するのか不明。

msgSubject.focus()

なんとなく使われてる関数の名前でわかるけど、msgSubject.focus()は件名を入力する箇所にフォーカスを当てている。

これによって、キーワードを変更した際に、件名部分にカーソルが合わさるようになっている。

SetComposeWindowTitle()

これは、最初まったく意味がわからなかったけど、ソースコードを見るとなんとなくわかった。*1

function SetComposeWindowTitle()
{
  var newTitle = GetMsgSubjectElement().value;

  var bundle = document.getElementById("bundle_composeMsgs");
  if (newTitle == "" )
    newTitle = bundle.getString("defaultSubject");

  newTitle += GetCharsetUIString();
  document.title = bundle.getString("windowTitlePrefix") + " " + newTitle;
}

最終行のdocument.titleで、ウインドウタイトルを変更していることがわかる。

実際に、キーワード変更時のウインドウタイトルの挙動に注目してみると、確かに変わっていた。

また、SetComposeWindowTitle()をコメントアウトして、キーワードを変更した場合、ウインドウタイトルに変化はなかった。

つまり、SetComposeWindowTitle()は、ウインドウタイトルに件名を反映させるための関数であることが推測できる。

感想

プロトタイプは、まだよくわかってないけど・・・とりあえず、こんなんでいいのかな?

あと関数とか、グローバル変数を知るためのリファレンスが欲しい・・・。

*1:後から考えると関数名でバレバレなんだけど^^;

SubjectHelperの改造案

SubjectHelperを使っていて、こんなのあったらいいなー、と思ってた機能があったので、それを実装してみる。

欲しい機能は次のとおり。

・キーワードの追加、削除
・キーワード変更時に前のキーワードを削除

削除、削除、削除で、削除ばっかだけど、こんな感じの機能が欲しい。

キーワードの追加、削除

まずは、「キーワードの追加、削除」について。

今のSubjectHelperは「返不要」「要返信」など10個のキーワードが用意されている。

仕事の中では、件名にプロジェクト名を使うことが多いので、キーワードは後から追加や削除できるとうれしい。

なので、キーワードの追加、削除を実装したい。

キーワードの変更時に前のキーワードを削除

次は、「キーワードの変更時に前のキーワードを削除」について。

これは、キーワード変更時に前のキーワードが残ってしまうので、選択すればするほど、件名がどんどん長くなる。

回答:返不要:要返信:返不要:回答:回答:

こんなことになることは、たぶんないと思うけど。

自分で使う場合は、キーワードを連続させて使うことはないので、キーワードを選択したら、前のキーワードは消えてくれた方がうれしい。

なので、「キーワードの変更時に前のキーワードを削除」する機能が欲しい。


ということで、この2つをとりあえず、実装してみる。
(以前、実装したのは「キーワードの追加、削除」)*1

*1:実装方法は、あまり覚えてない^^;

SubjectHelperをthunderbird3.1.13で動かしてみる

thunderbirdのアドオンでけっこーお世話になってるのが、id:mallowlabsさんが作ったSubjectHelperというアドオン。
*1

thunderbird2.0用のアドオンなので、thunderbird3.0以上でも動くようにしたいので、ちょっと修正する。

開発環境

・Ubuntu 11.04
・Thunderbird 3.1.13

修正箇所

以下の2つのファイルを修正する。

・install.rdf
・subjectHelper.css

install.rdfの修正

maxVersion要素を2.0.0.*から4.0.0.*に変更する。

<?xml version="1.0"?>

    〜省略〜

    <em:targetApplication>
      <!-- Thunderbird -->
      <Description>
        <em:id>{3550f703-e582-4d05-9a08-453d09bdfdc6}</em:id>
        <em:minVersion>2.0b1</em:minVersion>
        <em:maxVersion>2.0.0.*</em:maxVersion>
      </Description>
    </em:targetApplication>
  </Description>

</RDF>

これでバージョン4.0でもアドオンをインストールできるようになる。

subjectHelper.cssの修正

あと、見た目大事!ということで、search-arrow.gifをsearch-arrow.pngに変更する。

.subject-helper-button-dropmarker {
  -moz-margin-start: 7px;
  margin-top: 5px;
  list-style-image: url("chrome://messenger/skin/icons/search-arrow.gif");
}

2.0からバージョンアップしたときにgifからpngに変更したのかな?

こっちの環境では矢印が表示されなく、pngに変更したら表示された。(偶然)

このあと

職場で使ってたときは、もうちょっといじって、追加・削除できるようにしてたんだけど。

・・・社外持ち出し禁止令によりソースコードはモチカエレナカッタので、また作ってみようかな、と。

前のはごちゃごちゃしてしまったので、今度のはもうちょっとがんばってスマートなコードを書きたい:)

*1:mallowlabsさん、ありがとうございます。

xpiファイルの構成要素について

先日、作成したBookSearcherを例として、xpiファイルのファイル構成を見てみる。

BookSearcherのファイル構成は次のようになっている。

booksearcher
 ┠─ chrome.manifest
 ┠─ install.rdf
 ┠─ content
 │ ┠─ booksearcher.xul
 │ └─ booksearcher.js
 └─ locale
   ┠ja-JP
   │ └ booksearcher.dtd
   └en-US
     └booksearcher.dtd

以下、構成ファイルの簡単な説明。

chrome.manifest

chromeはツールバー、メニューバー、ウインドウタイトルなど、アプリケーションのUIを構成する要素のセットのことです。

chrome.manifestは、その要素を拡張するための情報を記している。

主に、ファイルの位置情報や、どの画面を拡張するのか、といった情報を定義している。

install.rdf

install.rdf(インストールマニュフェスト)は、アドオンがアプリケーションにインストールされるために必要な情報を記している。

主に、アドオンの作成者情報や、利用可能なバージョンの情報などが定義されている。

contentディレクトリ

contentディレクトリの中には、FireFoxのUIを定義するXULとJavaScriptが入っています。

booksearcher.xul

XULファイルは、ボタン、メニュー、ツールバーなど、UIの部品がXMLの文法で定義されてます。

booksearcherの場合は、コンテキストメニュー(右クリック時に表示されるメニュー)に2つの要素や、booksearcher.jsを読み込むためのscriptタグを追加しています。

booksearcher.js

XULで定義した部品の処理について記述している。

booksearcherの場合は、BookSearcherオブジェクトを定義しています。

localeディレクトリ

localeディレクトリには、複数言語をサポートするために、言語別のサブディレクトリが入っており、サブディレクトリ内には、localeパッケージのソースファイルが入っています。

ja-JPディレクトリ

日本語をサポートするために必要なDTDファイルを配置するディレクトリ。

en-USディレクトリ

英語をサポートするために必要なDTDファイルを配置するディレクトリ。

booksearcher.dtd

DTDファイルは、XULファイル内に定義された文字列を各言語へローカライズするために、文字列の実体参照が定義されている。

参考

・文字実体参照
・第5回 localeパッケージによるローカライズ
・Install Manifests/MDN
・Chrome Registration/MDN

Gmail Managerの不具合を修正してみよう

ここ最近、GmailManagerが頻繁にログインを求めてくるわ、アドオンバーからGmailに飛ぼうとしたらクッキーがどうのこうので表示してくれないわ、で調子悪そうだった。

いいかげん、どうにかしないとな、と思ったので調べた。

原因は、Gmailの仕様変更らしく、Gmail Managerがそれに対応していない、ということだったみたい。

以下、ココを見ながら対処した方法。

概要

GmailManagerのxpiファイルを解凍、中にあるソースコードを修正して、再インストールする。

環境

・Ubuntu 11.04
・FireFox 6.0

手順

1. xpiファイルを展開(解凍)
2. instal.rdfを修正
3. gmServiceGmail.jsを修正
4. 1で展開したファイルをzip形式で圧縮
5. GmailManagerの再インストール

※環境によっては、xpiファイルが展開済みなこともあるみたい

1. xpiファイルを展開

デフォのプロファイルを使ってる前提で進めると、GmailManagerのxpiファイルは以下の場所にある。

/home/ユーザ名/.mozilla/firefox/ハッシュ値.default/extensions/

で、ファイル名は「{582195F5-92E7-40a0-A127-DB71295901D7}.xpi」

これをunzipで展開する。

$ mkdir gmailmanager
$ cp {582195F5-92E7-40a0-A127-DB71295901D7}.xpi gmailmanager/
$ cd gmailmanager
$ unzip {582195F5-92E7-40a0-A127-DB71295901D7}.xpi

展開すると、ファイルが散乱するので、ディレクトリを作成して、その中で展開する。

2. install.rdfを修正

        <!-- Firefox -->
        <em:targetApplication>
            <Description>
                <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
                <em:minVersion>2.0</em:minVersion>
                <em:maxVersion>4.0.*</em:maxVersion>
            </Description>
        </em:targetApplication>

「maxVersion」の「4.0.*」を「8.0.*」に変更する。

これでFireFox8.0.*までのバージョンでGmailManagerがインストールできるようになる。

3. gmServiceGmail.jsの修正

gmServiceGmail.jsはcomponentsの配下にある。

    else
    {
      this._loginURL = "https://www.google.com/accounts/ServiceLoginAuth";
      this._checkURL = "https://mail.google.com/mail/?";
      this._atomURL = "https://mail.google.com/mail/feed/atom/";
    }

224行目のwww.google.com/accountsをaccounts.google.comに置換する。

これで修正は完了。

4. 1で展開したファイルをzip形式で圧縮

展開したファイル、ディレクトリをzip形式で圧縮し、再インストール用のxpiファイルを作成する。

zip -r gmailmanager.xpi chrome.manifest install.rdf license.txt chrome/ components/ defaults/ install.js

5. GmailManagerの再インストール

あとは、作成したxpiファイルを再インストールするだけ。

[アドオンメニュー]->[ツールアイコン]->[ファイルからアドオンをインストール]

で、作成したxpiファイルを選択して終了。

Q. アドオンを削除してから、でなくて大丈夫か?
A. 大丈夫だ、問題ない。


かんりょー、これで頻繁にログイン求められることもなくなり、平和になった。

ファイルの移動

gitでは、ファイルの移動を追跡してくれないらしく、自身でファイルの移動をgitに示さなければならない。

具体的には、
1. ファイルの移動
2. 「git rm」コマンドにより、移動前のファイルの削除をステージ
3. 「git add」コマンドにより、移動後のファイルをステージ
これら3の操作を行うことで、gitはファイルの移動を認識する。

毎回、この操作を行うのはめんどうなので、gitには「git mv」コマンドが用意されており、これを使うことで上記の操作を1つのコマンドで行うことができる。

$ git mv 移動前 移動後

今回は、「git mv」コマンドでBookSearcherのディレクトリ構成を変更した。

参考

・変更内容のリポジトリへの記録 ファイルの移動