FireGestures ボツネタ ~ 複数機能のポップアップ

※このエントリの内容は FireGestures のボツネタ機能に関する説明です。

機能

通常マウスジェスチャでは、ひとつのジェスチャにはひとつの機能しか割り当てることができないが、ひとつのジェスチャに対して複数の機能割り当てを許可し、そのジェスチャを行うとポップアップを表示して実行したい機能を選択できるという機能。

例えば、↑のジェスチャに対して、「新しいウィンドウ」「新しいタブ」の2つの機能を割り当てる。
すると、↑のジェスチャを行うと下図のようなポップアップが表示され、本当に実行したい機能を選択できる。

あまり使わない機能群に対して同一ジェスチャを割り当てておけば、覚えなければならないジェスチャの数も減らせて効果的である。

実装

とりあえず実装してみたところ、小1時間ほどでできた。改修したポイントは以下の5点。

  • 機能のタイプの定義へ、新たにポップアップ型を追加。(現状は標準装備型とユーザスクリプト型の2種類)
  • 設定画面にて、複数機能に対して同一ジェスチャの割り当てを許可する(現状は同一ジェスチャを割り当てようとするとダイアログ表示)。
  • マッピング管理では、同一ジェスチャに対して複数の機能が割り当てられている場合、それらの機能のIDをタブで連結した文字列にて保持する。
  • ユーザが行ったジェスチャに対してポップアップ型機能が割り当てられている場合、ポップアップを表示する(1度目の機能実行)。
    さらにポップアップからメニュー項目を選択すると、そのメニュー項目に割り当てられているコマンドを実行する(2度目の機能実行)。つまり、2段階で目的の機能を実行する形となる。
  • 実際にポップアップを表示する処理は、元々あるホイールジェスチャなどから表示するポップアップの実装を流用。

問題点

「リンクをタブで開く」のようにジェスチャの始点のリンクに対して何かするような機能をポップアップから実行する場合に痛い問題が発生。ジェスチャ開始位置のDOM要素への参照は、ジェスチャ実行開始~終了までの間は保持するものの、ジェスチャに割り当てられた機能を実行後にメモリリーク対策として破棄してしまう。よって、2度目の機能実行(ポップアップのメニュー項目を選択)をした時点では、ジェスチャ始点にあるDOM要素はもはや保持されておらず、リンク先に対する処理が実行できない。

ジェスチャ開始位置のDOM要素への参照を破棄するタイミングを変えることができるように修正しようとしたが、ロジックがだいぶ複雑化しそうなので早々に諦めた。

TOP

[userChrome.js] 軽量マウスジェスチャをMacに対応させる

9/1 訂正 やはり以前作った Linux 対応版でも、マウスジェスチャのみのバージョンは Mac にも対応していたようである。ただ、ロッカージェスチャ・ホイールジェスチャ機能追加版の方は Mac では動作せず、その原因は Mac で右クリックしたときの mouseup イベントが「event.button == 2」で判断不可で、代わりに「event.button == 0 && event.ctrlKey」としなければならないためであるようだ。

以前 userChrome.js 版マウスジェスチャを Linux 対応させたとき、勢い余って Mac にも対応と書いてしまっていたが実際はロッカージェスチャ・ホイールジェスチャ機能追加版の方は Mac では動作していなかった。ところが、 Windows / Linux で右クリックの mouseup イベントを「event.button == 2」として判断している箇所を、Mac ではCtrlキー+左クリックされたと読み替えて「event.button == 0 && event.ctrlKey」とすることでうまくいくようだ(筆者は未確認ですが)。そこで、下記の青字で示した部分が追加される。

ロッカージェスチャ・ホイールジェスチャ対応版

    _lastX: 0,
    _lastY: 0,
    _directionChain: "",
    _isMac: false,    // for Mac

    init: function()
    {
        this._isMac = navigator.platform.indexOf("Mac") == 0;
        gBrowser.mPanelContainer.addEventListener("mousedown", this, false);
        gBrowser.mPanelContainer.addEventListener("mousemove", this, false);
        gBrowser.mPanelContainer.addEventListener("mouseup", this, false);
        gBrowser.mPanelContainer.addEventListener("contextmenu", this, true);
            case "mouseup": 
                // [3] ジェスチャ終了~アクション実行
                if ((this._isMouseDownR && event.button == 2) || 
                    (this._isMouseDownR && this._isMac && event.button == 0 && event.ctrlKey)) {
                    this._isMouseDownR = false;
                    if (this._directionChain)
                        this._suppressContext = true;

TOP

ロッカージェスチャの実装 Part 2

ロッカージェスチャの実装における問題点(ページロード遷移中の mouseup イベントが認識されない)への対策として、一度ロッカージェスチャを実行した後に少しでもマウスを動かすとロッカー状態を強制解除することに決めた。
つまり、マウスポインタを移動させないという制限付きで、左クリック(mousedown)→右クリック→右クリック→右クリック→・・・→左クリック(mouseup)での連続ロッカージェスチャを実行可能とする。

メリット

上記問題点が発生しても、ちょっとでもマウスを動かせば正常な状態に戻る。

デメリット

マウスを移動させながらの連続ロッカージェスチャが不可能。

TOP

All-in-One Gestures のマウストレイルを改造

All-in-One Gestures のマウスジェスチャ実装を改造し、マウスポインタを水平または垂直に移動させた場合、すぐ隣り合う座標へ新しく aioTrailDot 要素を配置するのではなく、現在の aioTrailDot 要素の大きさを縦や横に広げていくことで直線を描画するようにしてみた。下図の右が通常の実装方式、左が上記の修正を施したものである。わかりやすくするために各 aioTrailDot 要素へ outline: 1px solid blue で枠を付けている。

Mouse Trail

描く曲線にもよるが、これでジェスチャ中のメモリ消費量が削減できると思う。

TOP

All-in-One Gestures のマウストレイルで DOM Inspector が固まる問題

マウストレイルの実装 の中で、 All-in-One Gestures のマウストレイル実装方式の場合に DOM Inspector で Webページの DOM を調べながらマウスジェスチャをすると、Firefox がハングアップすると記したが、その原因はマウストレイル消去時に aioTrailContainer 要素へ appendChild された aioTrailDot 要素(マウストレイルの線を描画するための点)を removeChild せず、 aioTrailContainer 要素自体をいきなり removeChild しているためだとわかった。そこで、

while (elt.lastChild)
    elt.removeChild(elt.lastChild);
elt.parentNode.removeChild(elt);

みたいにして、まず aioTrailDot 要素を削除し、その後に aioTrailContainer 要素を削除するように修正したら DOM Inspector が固まる問題が発生しなくなった。

TOP

マウストレイルの実装

マウストレイルとは

マウスを右クリックして動かすと、その軌跡を表示する機能。右クリックを放すと軌跡は消滅する。

All-in-One Gestures の場合

DOM のレベルで実現している。右クリックの mousedown でジェスチャを開始すると、HTML の body 要素直下に aioTrailContainer 要素が生成される。その後、マウスを動かすにしたがってサイズが1×1の aioTrailDot 要素を複製して aioTrailContainer 下に appendChild する。この aioTrailDot 要素はサイズが1×1で、マウスポインタと同じ座標へ絶対配置されており、背景色がある。この aioTrailDot 要素を並べることで、あたかも一本のつながった線に見えるようになる。
この方式には以下のような問題点がある。
・縦長のページでマウストレイルを行うと重たい
・曲線が滑らかに描画されない
・DOM Inspector で Webページの DOM を調べながらマウスジェスチャをすると、Firefox がハングアップする。

Optimoz Mouse Gestures の場合

Windows では、C++製?の独自 XPCOM コンポーネント (mgMouseService.dll) を用い、 Windows ネイティブな?実装によってマウストレイルの描画を実現している。
この XPCOM は mgIMouseService というインタフェースを有し、 initTrails と stopTrails の2つのメソッドによって XUL からマウストレイルの描画を制御することができる。

Windows 以外のプラットフォームでは All-in-One Gestures と同じ実装方式である。

他にうまい方法はあるか?

マウストレイル開始時に ブラウザ上に canvas 要素をかぶせ、 dot の stroke を連続で行うことで実現可能。この方法では、 All-in-One Gestures の実装方式の問題点が解決され、曲線が滑らかに描画でき、縦長のページで動作が重たくなることも無い。しかし、以下のような問題点がある。
・canvas 要素が邪魔をしてマウスが通過したリンクを調べることができない。
・一時的にメモリ使用量が激増する。

TOP

ロッカージェスチャの実装 Part 1

[userChrome.js] 軽量マウスジェスチャーでの問題点

ロッカージェスチャを使って「戻る」をして、ページロード中に右クリックを放すとそれが認識されない(mouseup イベントが発生しない)。その後通常の左クリックをしただけでロッカージェスチャが実行されてしまう。

All-in-One Gestures の場合

上記とまったく同様のバグが発生することがわかった。ただ、AiOG ではロッカージェスチャ実行後にタイマーが仕掛けられ、3秒間何もしないとロッカージェスチャの待ち状態が解除される(つまり右クリックを放したと見なされる)という仕組みがある。もしかするとこのタイマーはバグが発生したときの被害を最小限に食い止めるための意味があるのかも?つまり、右クリック放しが認識されずにバグ状態に陥っても、3秒間我慢すれば勝手に正常な状態へと戻る。

Optimoz Mouse Gestures の場合

Windows の場合はバグ発生しない。 Linux の場合はバグ発生する。
なぜ Windows の場合にバグが発生しない(mouseup イベントが必ず発生する)かを調べたところ、 OMG では独自に実装したC++製?のXPCOMコンポーネントによって、 DOM のレイヤーで発生する mouseup イベントとは別にもっと上のレイヤー?でマウスの動きを検知しているためである。この XPCOM コンポーネントは mgIMouseService というインタフェースを有し、マウストレイル(マウスジェスチャ中の軌跡描画)が主な仕事であるが、それ以外にもマウスの動きやクリックに応じて nsIObserverService による通知を行う仕組みをもっている。 XUL 側ではこの通知を監視し、クリックの放し (mozgestButtonUp) が発生すると initMouseEvent によって DOM のレイヤーでの mouseup イベントを生成する処理になっている。

結論

マウスジェスチャとホイールジェスチャは mousedown や mousemove といった DOM イベントを捕捉することで実装できた。しかし、まともなロッカージェスチャを DOM のレイヤーのみで実装するのは無理っぽい。

TOP

[userChrome.js] 軽量マウスジェスチャー(ホイールジェスチャ・ロッカージェスチャ対応版) ~途中経過~

6/30 追記
ロッカージェスチャ有効時、選択範囲をテキストボックスへドラッグ&ドロップすると右クリックが効かなくなるバグを修正。

「Operaユーザは「右押しながら左クリック」をよく使う」というアンケート結果に衝撃を受けたわけではないですが、軽量マウスジェスチャへ、以下の2機能を追加したバージョンを作成しました。
 ・ホイールジェスチャ (右クリックしながらホイール回転でタブ切り替え)
 ・ロッカージェスチャ (右クリックしながら左クリックで戻る、左クリックしながら右クリックで進む)

ホイールジェスチャおよびロッカージェスチャ実行時には、 _performAction メソッドへ以下のような文字列が渡りますので、実行するアクションをカスタマイズ可能です。
 ・ホイールジェスチャ(下に回転): W+
 ・ホイールジェスチャ(上に回転): W-
 ・ロッカージェスチャ(右クリックしながら左クリック): L<R
 ・ロッカージェスチャ(左クリックしながら右クリック): L>R

ただし、ロッカージェスチャに未解決の問題点があって、例えば右クリックしながら左クリックを連続で何回か押して連続で戻る操作をしているとき、ページのローディングが行われている瞬間(くるくるアイコンが回転している瞬間)を狙って右クリックを放すと、放したことが認識されずに右クリックが続いていると誤認識され、その後の動作がおかしくなる。bfcache が効いていると再現しにくいので、いったんキャッシュをクリアするとページ遷移のたびにローディングされ、上記問題が再現しやすくなる。

とはいえ、そもそも All-in-One Gestures ではロッカージェスチャは連続でできない(右クリック押しながら一回左クリックをするとその時点で終了する)ため、 userChrome.js 版マウスジェスチャでもその動きにすれば上記問題点は解決すると思われる。反面、 Optimoz Mouse Gestures や本家 Opera では連続クリックが可能である。

ロッカージェスチャを望んでいる方は、この連続クリック機能が必須であるか、無くてもいい程度のものか、あるいはまったくもって不要かなどについて教えてくださるとありがたいです。ただ、連続クリックが必須という意見が大半であったとしても、今後がんばっても上記問題を解決できないままである可能性が高いです。

TOP

[userChrome.js] 軽量マウスジェスチャをWindows/Linuxに対応させる

通りすがりさんによるパッチをベースに、[userChrome.js] 軽量マウスジェスチャを Windows/Linux に対応させました。以下は今回修正した内容についてメモです。

以前はマウスジェスチャ中の状態遷移を数値型のフラグ _state を使って以下のように制御していた。

イベント _state フラグの変化 意味
mousedown 0 → 1 右クリック開始
mousemove (1のまま変化なし) 右クリックしたままマウス移動中
mouseup 1 → 2 ジェスチャ認識あり
1 → 3 ジェスチャ認識なし(マウスの移動量が微小)
contextmenu 2 → 0 コンテキストメニューの表示を抑止する
3 → 0 コンテキストメニューの表示を抑止しない

しかしながら、 Windows では上記のように mousedown→mousemove→mouseup→contextmenu という順序でイベントが発生するものの、 Linux では mousedown→contextmenu→mousemove→mouseup という順序で発生するため、制御がうまくいかなかった。そこで、数値型のフラグを廃止し、代わりに以下のような3つの真偽値フラグを使うようにした。

フラグ 意味
_isMouseDown 右クリックが押されているかどうか。
mousedown イベントで true になり、 mouseup イベントで false になる。
_suppressContext この後の contextmenu イベントを抑止するかどうか。
mouseup イベント発生時にジェスチャの認識があれば true にし、その後の contextmenu イベントを抑止する。
_shouldFireContext 後で contextmenu イベントを擬似的に発生させる必要があるかどうか。 Linux 専用。
Linux の場合は mousedown イベント直後に contextmenu イベントが発生するが、これを抑止した際にフラグを true にしておき、その後の mouseup イベント発生時にフラグが立っていれば擬似的に contextmenu を発生させる。

これによってスクリプトの一部は以下のように変更された。青色の部分が Linux 専用となる処理である。

    _isMouseDown: false,
    _suppressContext: false,
    _shouldFireContext: false,

    handleEvent: function(event)
    {
        switch (event.type) {
            case "mousedown": 
                // [1] ジェスチャ開始
                if (event.button == 2) {
                    this._isMouseDown = true;
                    this._startGesture(event);
                }
                break;
            case "mousemove": 
                // [2] ジェスチャ継続中
                if (this._isMouseDown) {
                    this._progressGesture(event);
                }
                break;
            case "mouseup": 
                // [3] ジェスチャ終了~アクション実行
                if (this._isMouseDown) {
                    this._isMouseDown = false;
                    this._suppressContext = !!this._directionChain;
                    this._stopGesture(event);
                    // [Linux] Win32を真似てmouseup後にcontextmenuを発生させる
                    if (this._shouldFireContext) {
                        this._shouldFireContext = false;
                        this._displayContextMenu(event);
                    }
                }
                break;
            case "contextmenu": 
                // [4-1] アクション実行後のコンテキストメニュー表示を抑止する
                // [4-2] 方向が認識されない微小な動きの場合は抑止しない
                // [Linux] mousedown直後のcontextmenuを抑止して...
                if (this._suppressContext || this._isMouseDown) {
                    this._suppressContext = false;
                    event.preventDefault();
                    event.stopPropagation();
                    // [Linux] ...代わりにmouseup後にcontextmenuを発生させる
                    if (this._isMouseDown) {
                        this._shouldFireContext = true;
                    }
                }
                break;
        }
    },

    _displayContextMenu: function(event)
    {
        var evt = event.originalTarget.ownerDocument.createEvent("MouseEvents");
        evt.initMouseEvent(
            "contextmenu", true, true, event.originalTarget.defaultView, 0,
            event.screenX, event.screenY, event.clientX, event.clientY,
            false, false, false, false, 2, null
        );
        event.originalTarget.dispatchEvent(evt);
    },

TOP

マウスジェスチャについてのアンケート結果

マウスジェスチャについてのアンケートにご協力ありがとうございました。

Q1. あなたはマウスジェスチャの拡張機能を使っていますか?

All-in-One Gestures を使っている 77
Optimoz Mouse Gestures を使っている 9
userChrome.js 用マウスジェスチャを使っている 99
使っていない 14

Q2. マウスジェスチャ機能は必要ですか?

はい 182
いいえ 8
わからない 7

Q3. ロッカージェスチャ機能(右クリックしながら左クリック)は必要ですか?

はい 56
いいえ 116
わからない 27

Q4. ホイールジェスチャ機能(右クリックしながらマウスホイール)は必要ですか?

はい 87
いいえ 93
わからない 20

Q5. ミドルクリックでのホイールジェスチャ機能(ミドルクリックしながらマウスホイール)は必要ですか?

はい 12
いいえ 158
わからない 30

Q6. タブバー上でのマウスホイールによってタブを切り替える機能は必要ですか?

はい 85
いいえ 98
わからない 17

Q7. マウスジェスチャ中の軌跡描画(マウストレイル)は必要ですか?

はい 36
いいえ 154
わからない 10

Q8. マウスジェスチャ中のステータスバー表示は必要ですか?

はい。「LR」のように、現在の方向を表示すべき。 31
はい。「LR (タブを開く)」のように、現在の方向と機能名称を表示すべき。 123
いいえ 41
わからない 5

Q9. ジェスチャの方向の表現方法はどれが最適ですか?

L、R、U、D 71
左、右、上、下 7
←、→、↑、↓ 111
わからない 11

Q10. 斜め方向のジェスチャの認識は必要ですか?

はい 18
いいえ 163
わからない 18

Q11. ジェスチャのタイムアウト(ジェスチャ中に数秒間じっとしているとジェスチャの認識を停止する機能)は必要ですか?

はい 114
いいえ 55
わからない 29

Q12. ジェスチャ中に通過したすべてのリンクをタブで開く機能は必要ですか?

はい 41
いいえ 120
わからない 38

Q13. 「タブを閉じる」機能に最適なジェスチャの割り当ては?

↓→ 68
46
↓↑ 10

Q14. 「新しいタブを開く」機能に最適なジェスチャの割り当ては?

25
19
←→ 7

Q15. 「閉じたタブを元に戻す」機能に最適なジェスチャの割り当ては?

↓← 20
↓↑ 18
↑↓ 11

Q16. マウスジェスチャに求めるものは?

動作の軽さ 168
機能の豊富さ 32
設定のしやすさ 100
ユーザスクリプトへの対応 52

TOP