Firefox のホットキーを Flash プレイヤーから奪い返す方法

Perl と cnee と通信する独自の拡張機能を作成し、組み込みの Flash プレイヤーから Firefox のホットキーを奪い返す

Firefox では、Flash プレイヤーなどの組み込みアプリケーションにはキーボード入力用とマウス入力用に独自のフックが必要です。これまで長年の間、Firefox でのキー入力が Flash に奪われてしまっていたため、ナビゲーションや新しいタブの作成、さらには Flash の外にフォーカスを移すことでさえ、キーボードを使って行うことはできませんでした。この記事では、Firefox の拡張機能と cnee と通信してキーボードの機能を取り戻すための Perl プログラムを作成する方法を学びましょう。

Firefox では、Flash プレイヤーなどの組み込みアプリケーションにはキーボード入力用とマウス入力用に独自のフックが必要です。2001年5月以来、Flash によって Firefox のキー入力が奪われてしまっていたため、ナビゲーションや新しいタブの作成、さらには Flash の外にフォーカスを移すことでさえ、キーボードを使って行うことはできませんでした (「参考文献」に挙げた Mozilla のバグ番号 78414 を参照してください)。

この記事は、たとえ組み込みの Flash プレイヤーにフォーカスがある場合でも Linux® 上で実行する Firefox を Ctrl+t (新しいタブを開く) などのホットキーに応答させるためのツールとコードについて説明します。ここで紹介するコードを使用すると、Firefox でキーボードを使って再びアプリケーションを制御できるようになります。この記事は根本的な問題を解決するものではありませんが、Linux ユーザーにとっては Mozilla のバグ番号 78414 の回避手段となります。

cnee を使ってシステムのキーボード・イベントを監視し、また Perl を使って Firefox アプリケーションのステータスを追跡することによって、Flash プレイヤーにフォーカスがある場合であっても Firefox のホットキー機能を回復することができます。

ハードウェアとソフトウェアの要件

Firefox V2 以降をインストールした Linux が必要です。システム全体にわたってのキーボード・イベントをモニターするために libXnee コンポーネントと cnee コンポーネントが必要であり、また Perl がアルゴリズムを処理します。cnee 出力の処理と X ウィンドウ・システムのイベントの送信用に、特定の Perl モジュールが必要です (threads、Thread::Queue、X11:GUITest、Time::HiRes)。これらのモジュールと libXnee ソフトウェア・パッケージについては「参考文献」を参照してください。

ここでは Linux に実装しますが、ここで紹介する一般的な概念は Microsoft® Windows® など他のオペレーティング・システムにも適用することができます。Firefox の拡張機能と Perl コードはクロス・プラットフォームなので、必要なものは cnee に代わってシステム全体にわたるキーボード・イベントを確実に出力できるものだけです。(もし読者が賢明な Windows アプリケーション開発者であり、ここで紹介するものと同様のオープンソースの修正を Windows 用に作成した場合には、そのコードを添付して私に E メールを送ってください。そうすればその読者の功績を讃えた上で、この記事を修正するつもりです。)

この記事を読む上では Firefox の拡張機能に関するプログラミングに慣れていると役に立ち、また拡張機能開発者のための拡張機能 (「参考文献」を参照) をインストールしてあると便利です。


Firefox でのキーボード操作をレポートする拡張機能を作成する

Flash プレイヤーが Firefox のホットキー (Ctrl+t など) をいつ奪ったのかを、2 つの部分で判断します。最初の部分では、Firefox のロケーション・バーのテキストがいつ変更されたかを記録します。ロケーション・バーのテキストが変更されるのは、タブが開かれた場合、新しいページにアクセスした場合、または異なるタブを表示した場合です。2 番目の部分ではシステム全体にわたってキーボード・イベントをモニターします。キー入力の組み合わせ (Ctrl+t など) が cnee によって認識されているにもかかわらず、ロケーション・バーのテキストが最後に変更されたのは X 秒前だとすると、Flash プレイヤーがキーボードのフォーカスを奪っていることになります。

ロケーション・バーのテキストが変更されたかどうかを判断するための単純な方法が Mozilla Developer Center の Progress Listeners ページに紹介されています。それと同様のコードを実装するために、既に作成された拡張機能を Google カレンダーでの暗号化に関する記事 (「参考文献」を参照) からダウンロードします。拡張機能のディレクトリーを抽出し、install.rdf ファイルの内容を以下のように変更します。

リスト1. install.rdf
<?xml version="1.0"?>
<RDF:RDF xmlns:em="http://www.mozilla.org/2004/em-rdf#"
         xmlns:NC="http://home.netscape.com/NC-rdf#"
         xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
  <RDF:Description RDF:about="urn:mozilla:install-manifest"
                   em:id="flashUngrabber@devWorks_IBM.com"
                   em:name="flashUngrabber"
                   em:version="1.0.0"
                   em:creator="Nathan Harrington"
                   em:description="flashUngrabber">
    <em:targetApplication RDF:resource="rdf:#$9ttCz1"/>
  </RDF:Description>
  <RDF:Description RDF:about="rdf:#$9ttCz1"
                   em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
                   em:minVersion="1.5"
                   em:maxVersion="3.0.5" />
</RDF:RDF>

chrome.manifest ファイルをリスト 2 の内容と置き換えます。

リスト 2. chrome.manifest
content flashUngrabber  chrome/content/

overlay chrome://browser/content/browser.xul  \
  chrome://flashUngrabber/content/overlay.xul

locale  flashUngrabber  en-US   chrome/locale/en-US/

skin    flashUngrabber  classic/1.0     chrome/skin/
style   chrome://global/content/customizeToolbar.xul  \
  chrome://flashUngrabber/skin/overlay.css

バックスラッシュ文字 (\) は行が続いていることを示すために入れてあるだけで、ファイルの中に含めてはならないことに注意してください。拡張機能のメタデータを上記のように置き換えた後、chrome/content/overlay.js ファイルを削除し、以下の内容を挿入します。

リスト 3. overlay.js myExt_urlBarListener 関数
//overlay.js for flash "ungrabber" borrows heavily from
//https://developer.mozilla.org/en/Code_snippets/Progress_Listeners

var myExt_urlBarListener = {
  QueryInterface: function(aIID)
  {
   if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
       aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
       aIID.equals(Components.interfaces.nsISupports))
     return this;
   throw Components.results.NS_NOINTERFACE;
  },

  // switching through tabs changes the location bar
  onLocationChange: function(aProgress, aRequest, aURI)
  {
    myExtension.updateFile();
  },

};

myExt_urlBarListener 関数は Progress Listeners のサンプルをほとんどそのままコピーしたものですが、どんなインターフェースが利用できるかを照会して onLocationChange 機能が利用できることを確認します。ロケーション・バーが変更されるたびに、myExtension.updateFile() 関数が呼び出されます。リスト 4 は overlay.js ファイルの最後に追加される updateFile 関数と init/unint 関数を示しています。

リスト 4. overlay.js myExtension 関数
var myExtension = {

  init: function() {
    // add the listener on web page loaded
    gBrowser.addProgressListener(myExt_urlBarListener,
        Components.interfaces.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
  },

  uninit: function() {
    // remove the listener when page is unloaded
    gBrowser.removeProgressListener(myExt_urlBarListener);
  },

  updateFile: function() {

    // write the epoch seconds + precision when the location bar was changed
    locTime = new Date().getTime();

    var fileOut = Components.classes["@mozilla.org/file/local;1"]
                        .createInstance(Components.interfaces.nsILocalFile);
    fileOut.initWithPath("/tmp/locationBarChange");
    var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
                            .createInstance(Components.interfaces.nsIFileOutputStream);
    foStream.init(fileOut, 0x02 | 0x08 | 0x20, 0666, 0);
    foStream.write(locTime.toString(), locTime.toString().length);
    foStream.close();
  }

};

簡単なプロセス間通信を行うために、最後に更新された時刻を (UNIX® エポックからの秒数で) /tmp/locationBarChange ファイルに書き出します。リスト 5 の 2 行を追加し、拡張機能のロードとアンロードが適切に行われるようにします。

リスト 5. overlay.js addEventListeners
window.addEventListener("load", function() {myExtension.init()}, false);
window.addEventListener("unload", function() {myExtension.uninit()}, false);

拡張機能の更新を完了するために、chrome/content/overlay.xul ファイルの内容を以下の内容で置き換えます。

リスト 6. overlay.xul
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://quickgooglecal/skin/overlay.css" type="text/css"?>
<!DOCTYPE overlay SYSTEM "chrome://quickgooglecal/locale/overlay.dtd">
<overlay id="helloworld-overlay"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script src="overlay.js"/>
</overlay>

これで、この拡張機能を Firefox にロードする準備が整いました。ロードするための簡単な方法としては、カレント・ディレクトリーをこの拡張機能のルート・ディレクトリーに変更してコマンド zip -r flashUngrabber.xpi * を実行し、xpi を作成します。この xpi (これも「ダウンロード」セクションにあります) を Firefox にロードしてブラウザーを再起動します。

リロードが完了したら、コマンド perl -e 'while(1){print `cat /tmp/locationBarChange` . "\n";sleep(1)}' を実行します。Firefox のウィンドウが表示されるようにし、異なるタブでいくつかのページを作成してロードします。タブを変更したり別のページをロードしたり、あるいはロケーション・バーのテキストを変更したりすると、1 秒に 1 度数字が出力され、その数字が増加していくことがわかるはずです。


flashUngrabber.pl プログラム

ホットキー入力に Firefox が応答するタイミングを判断できるようになったので、「応答する必要があったのに応答しなかった」場合を検出するためのプログラムを作成します。リスト 7 のflashUngrabber.pl プログラムはこのプロセスの処理を行います。

リスト 7. flashUngrabber.pl の先頭部分
#!/usr/bin/perl -w
# flashUngrabber.pl - monitor keyboard events, send firefox key combos
use strict;  
use X11::GUITest qw( :ALL );                # make sure firefox app has focus
use Time::HiRes  qw( gettimeofday usleep ); # sub second timings
use threads;                                # for asynchronous pipe reads
use Thread::Queue;                          # for asynchronous pipe reads
  
my $padLen = 16;       # epoch significant digits
my $foundControl = 0;  # loop control variable
my $setLocalTime = 0;  # last recorded synthetic event 
my %keys = ();         # key codes and times
  
my ($currWind) = FindWindowLike( 'Mozilla Firefox' );
die "can't find Mozilla Firefox window\n" if ( !$currWind );

上記のコードは必要なモジュールをインクルードし、いくつかの変数を定義しています。このコードは、ある「特別な」文字、例えば「—」(印刷用語での「m ダッシュ」) などが Firefox アプリケーションの名前の中に含まれていると、FindWindowLike 関数が失敗する場合があります。flashUngrabber.pl が Firefox アプリケーションの ID を見つけられない場合には、別のページをロードするか、あるいは別のタブに切り換えてみます。リスト 8 はこのプログラムのセットアップの続きです。

リスト 8. flashUngrabber.pl の続き
my $cneeCmd  = qq{ cnee --record --keyboard | };
my $pipeCnee = createPipe( $cneeCmd ) or die "cannot create cnee pipe\n";

$keys{ "ctrl-t" }{ cneeCode } = '0,2,0,0,0,28';
$keys{ "ctrl-t" }{ sendKeys } = '^(t)';
$keys{ "ctrl-t" }{ event } = 0;

$keys{ "ctrl-w" }{ cneeCode } = '0,2,0,0,0,25';
$keys{ "ctrl-w" }{ sendKeys } = '^(w)';
$keys{ "ctrl-w" }{ event } = 0;

キーボード・モニター・モードで cnee プログラムに接続した後、特別なキー・コードを %keys ハッシュの中で定義します。後でプログラムの中でこれらのキーが検索され、これらのキーが最後に記録された時刻が「イベント」ハッシュ要素の値として保存されます。リスト 9 はメイン・プログラムのループの最初の部分を示しています。

リスト 9. flashUngrabber.pl のメイン・プログラムのループの最初の部分
while( 1 )
{
  # read all data off the cnee output queue, process each line.  cnee data 
  # needs to be control first, then the very next line be the key like: ctlr-t
  my $cneeData =  "";
  while( $pipeCnee->pending ){ $cneeData .= $pipeCnee->dequeue or next }

  for my $line ( split "\n", $cneeData )
  {
    if( $foundControl == 1 )
    { 
      $foundControl = 0;
      for my $name( keys %keys )
      { 
        next unless ( $line =~ /$keys{$name}{"cneeCode"}/ );
        $keys{$name}{"event"} = getTimeStr();

      }#for each key

    }#if control pressed

    if( ($line =~ /0,2,0,0,0,37/) || ($line =~ /0,2,0,0,0,109/) )
    {
      # control pressed
      $foundControl = 1;

    }elsif( ($line =~ /0,3,0,0,0,37/) || ($line =~ /0,3,0,0,0,109/) )
    {
      #control released
      $foundControl = 0;

    }#if control pressed

  }#for each line

システムの負荷やその他多種多様な要因によって変わりますが、キーボード・イベントは cnee プログラムに達するかなり前に Firefox によって処理されます。また逆に、Firefox がキー入力を処理する前に cnee によってキーボード・イベントを出力することもできます。無限ループと非常に短期間のスリープによるこの方法は、cnee と Firefox によってイベントを処理する一方、UI のパフォーマンスを適切に維持するように設計されています。

メイン・ループを通るたびに、(cnee 出力がある場合には) cnee 出力を処理してコントロール・キーを見つけます。コントロール・キーが見つかり、次のキー押下が %keys ハッシュに指定されると、そのキーに対するイベント時刻が記録されます。リスト 10 は cnee イベントが読み取られた後のメインの処理ループの続きを示しています。

リスト 10. flashUngrabber.pl のメイン・プログラムのループの続き
  my $curTime = getTimeStr();

  for my $name ( keys %keys )
  { 
    # require the event to have .5 second to bubble up to cnee
    next unless ( ($curTime - $keys{$name}{"event"} ) > 500000 &&
                  $keys{$name}{"event"} != 0 );

    # reset the event time
    $keys{$name}{"event"} = 0;

    next unless ( $currWind == GetInputFocus() ); # skip if firefox not focused

    next unless( -e "/tmp/locationBarChange" );   # skip if no address bar data

    open( FFOUT, "/tmp/locationBarChange" ) or die "no location bar file";
      my $ffTime = <FFOUT>;
    close(FFOUT);

    # determine if firefox has written a location bar change recently 
    $ffTime = $ffTime . "0" x ( $padLen - length($ffTime) );
    if( $ffTime > $setLocalTime ){ $setLocalTime = $ffTime }

    # if it's been more than two seconds since last event
    next unless( ($curTime - $setLocalTime)  > 2000000 );

それぞれのキー・コードを処理し、そのイベントが検出されてから少なくとも 0.5 秒経過したかどうかを判断します。もし現在のフォーカスが Firefox にあり、/tmp/locationBarChange ファイルが存在し、しかも合成されたイベントが最後に送信されてから少なくとも 2 秒が経過していると、以下の処理が続いて実行されます。

リスト 11. flashUngrabber.pl のメイン・プログラムのループの最後
    # record original mouse position
    my($origX,$origY) = GetMousePos();
    my( $x, $y, $width, $height ) = GetWindowPos( $currWind );

    # highly subjective, clicks in google search box on default firefox 
    # installation.  Sleeps are ugly, but help ensure inputs trigger 
    # correctly on a heavily loaded machine
    ClickWindow( $currWind, $width-150, $height-($height-40) );
    usleep(200000);
    SendKeys( $keys{$name}{"sendKeys"} );
    usleep(200000);
    MoveMouseAbs( $origX, $origY );
    usleep(200000);
    $setLocalTime = $curTime;

  }#for each key combo to look for

  usleep(100000); # wait a tenth of a second

}#while main loop

この時点では合成イベントを送信する必要があるため、現在のマウスの位置とウィンドウの位置のデータが記録されます。この時点で Ctrl+t を送信しただけでは Flash プレイヤーが単にそのキー入力を奪ってしまうため、希望の動作を実現することはできません。確実に Firefox でキー入力を処理するための最も信頼性の高い方法として、マウスを移動して (例えば Google の検索ボックスの中で) ウィンドウをクリックし、それから Ctrl+t を送信します。そしてキー入力をする前の元の位置にマウスを戻し、マウスの位置を記録した場所に確実にマウスが戻るようにします。

Firefox がマウス移動イベントとキーボード・イベントをいつ受信するかは、システム負荷の重さなどさまざまな要因の影響を受けます。usleep 関数の呼び出しを減らしたり削除したりすることでキー入力イベントが送信される速さを改善することはできますが、そうするとシステムの応答が遅い場合には他の問題が起こる可能性があります。

Firefox のツールバーの設定が標準的ではない場合、あるいは合成されたクリックを必ずブラウザーの別の場所に送信したい場合には、ClickWindow 座標を変更する必要があるかもしれません。リスト 12 は createPipegetTimeStr というサポート・サブルーチンを示しています。

リスト 12. flashUngrabber.pl のサブルーチン
sub createPipe
{ 
  my $cmd = shift;
  my $queue = new Thread::Queue;
  async{
      my $pid = open my $pipe, $cmd or die $!;
      $queue->enqueue( $_ ) while <$pipe>;
      $queue->enqueue( undef );
  }->detach;

  #detach causes the threads to be silently terminated on exit (sometimes)
  return $queue;

}#createPipe

sub getTimeStr
{
  # i suppose the idea of not providing standard length time strings makes
  # sense... somewhere, this is not one of those times
  my   ($seconds, $microseconds) = gettimeofday;
  my $text = "$seconds$microseconds";
  return( $text . "0" x ($padLen - length($text)) );

}#getTimeStr

createPipe サブルーチンは cnee プログラムから読み取るための非ブロック型のパイプを作成し、また getTimeStr は高精度で時刻を表す固定長のストリングを提供します。これまでに挙げたリストを flashUngrabber.pl プログラムとして保存し、このプログラムを perl flashUngrabber.pl というコマンドにより、実行します。


使い方

Flash コンテンツ・プレイヤーをロードして (YouTube のビデオなどをロードして) 構成をテストしてみます。Flash プレイヤー上 (例えば音量コントロールの上) でクリックし、そして Ctrl+t を入力すると、マウスが Google の検索ボックスに移動し、新しいタブが作成され、そしてマウスが元の座標に戻るはずです。


まとめと、さらに別の例

ここで紹介したコードとツールを使うと、Firefox で使用するお気に入りのホットキーを Flash から奪い返すことができます。さらに別の例として、flashUngrabber.pl プログラムに cnee キー・コードを追加し、キーボードによる他のナビゲーションを実現する方法を考えてみてください (Ctrl+tab を使って次のタブに移動する、あるいは Ctrl+l を使ってアドレス・バーにアクセスする、など)。また Flash プレイヤーから PgUp キーと PgDn キーを奪い返してページ全体をスクロールする方法、あるいは cnee --record --mouse オプションを追加してスクロール・ホイールを再び使えるようにする方法なども考えてみてください。


ダウンロード

内容ファイル名サイズ
Sample codeos-78414-firefox-flash-Ungrabber.0.1.zip17KB

参考文献

学ぶために

製品や技術を入手するために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source
ArticleID=366909
ArticleTitle=Firefox のホットキーを Flash プレイヤーから奪い返す方法
publish-date=12162008