Firefox 內的 Flash 播放器和其他嵌入式應用程序要求使用自己的掛鉤(hook)獲得鍵盤和滑鼠輸入。多年來,Flash 一直佔用 Firefox 的按鍵事件,使人們無法使用鍵盤進行導航、創建新標籤甚至退出 Flash 焦點。通過本文了解如何創建一個可以與 Firefox 擴展和 cnee 交互的 Perl 程序,幫助您取回鍵盤功能。
Firefox 內的 Flash 播放器和其他嵌入式應用程序要求使用自己的掛鉤(hook)獲得鍵盤和滑鼠輸入。自 2001 年 5 月以來,如果 Flash 佔用 Firefox 的按鍵,那麼您就無法使用鍵盤進行導航、創建標籤甚至退出 Flash 焦點(參見 參考資料 獲得 Mozilla bug No. 78414)。
本文提供的工具和代碼使 Linux® 上運行的 Firefox 能夠在嵌入式 Flash 播放器持有焦點的情況下響應 Ctrl+t(打開新標籤)等熱鍵。使用本文的代碼幫助 Firefox 應用程序重新控制鍵盤。本文並沒有修復底層問題,但是為 Linux 用戶提供了一種針對 Mozilla bug No. 78414 的解決方案。
通過使用 cnee 監視系統的鍵盤事件,並使用 Perl 跟蹤 Firefox 應用程序的狀態,即使在 Flash 播放器持有的焦點的情況下,也可以恢復 Firefox 熱鍵功能。
硬體和軟體需求
Linux 是必需的,還會用到 Firefox V2 或更高版本。需要使用 libXnee 和 cnee 組件監視系統內外的鍵盤事件,並使用 Perl 處理演算法。需要用到某些 Perl 模塊處理 cnee 輸出併發送 X Window System 事件:threads、Thread::Queue、X11:GUITest 和 Time::HiRes。有關這些模塊和 libXnee 軟體包的更多信息,請參閱 參考資料 小節。
儘管本文在 Linux 之上提供實現,但是這裡介紹的一般概念適用於多個操作系統,比如 Microsoft® Windows®。只需要使用其他軟體包替代 cnee 來可靠地輸出系統的鍵盤事件,因為 Firefox 擴展和 Perl 代碼是跨平台的。(如果您是一名聰明的 Windows 應用程序開發人員並且開發出類似本文提供內容的開源補丁,請將您的代碼通過電子郵件發送給我,我們將根據您的代碼改進這篇文章)。
熟悉如何編寫 Firefox 擴展以及安裝 Extension Developer 的 Extension 將很有用處(參見 參考資料)。
![]() ![]() |
創建一個 Firefox 鍵盤報告擴展
要判斷 Flash 播放器何時佔用了 Firefox 熱鍵(比如 Ctrl+t),可通過兩個步驟實現。首先記錄 Firefox 中的地址欄文本何時發生改變。地址欄文本在每打開一個標籤、訪問一個新頁面或顯示不同的標籤時發生變化。其次在整個系統內監視鍵盤事件。如果 cnee 識別出一個鍵盤組合鍵(比如 Ctrl+t),但是最後一次地址欄文本變化發生在 X 秒之前,那麼 Flash 播放器已獲得鍵盤焦點。
Mozilla Developer 的 Center Progress Listeners 頁面給出一個簡單的方法,用於確定地址欄的變化。要實現類似的代碼,從 Google Calendar Encryption 文章下載預構建擴展(參見 參考資料)。提取擴展目標並修改 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="[email protected]_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> |
使用清單 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 文件,插入下面所示的內容。
//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(); }, }; |
上面的內容基本上是直接從 Progress Listeners 示例複製過來的,myExt_urlBarListener 函數查詢可用的介面以確保 onLocationChange 功能是可用的。每當地址欄發生變化時,myExtension.updateFile() 函數將得到調用。清單 4 展示了 updateFile 和 init/unint 函數,這兩個函數被添加到 overlay.js 文件的底部。
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(); } }; |
通過向 /tmp/locationBarChange 文件寫入最後更新時間(UNIX® 之後的幾秒內),可以實現簡單的進程間通信。添加清單 5 中的代碼行確保擴展被正確載入和卸載。
window.addEventListener("load", function() {myExtension.init()}, false); window.addEventListener("unload", function() {myExtension.uninit()}, false); |
為了完成擴展更新,需要用以下的內容替換 chrome/content/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。在 Firefox 載入 xpi(也可從 下載 小節獲得)並重新啟動瀏覽器。
重新載入完成後,發出以下命令 perl -e 'while(1){print `cat /tmp/locationBarChange` . "\n";sleep(1)}'。將 Firefox 窗口變為可查看,並在不同的標籤中創建和載入頁面。您將發現如果修改標籤、載入不同的頁面或修改地址欄中文本,每隔一秒就會輸出不斷增加的數字。
![]() ![]() |
flashUngrabber.pl 程序
現在已經確定了 Firefox 何時會響應一個熱鍵按下動作,現在我們可以編寫一個程序,監視它應該響應而實際上沒有響應的情況。清單 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 ); |
上面的代碼包含了必要的模塊並定義了變數。在 Firefox 應用程序名稱中顯示的某些 “特殊” 字元,比如 “—”(印刷中的 “m-dash”),可能會引起 FindWindowLike 函數失敗。如果 flashUngrabber.pl 無法找到您的 Firefox 應用程序 ID 的話,嘗試載入不同的頁面或切換到不同的標籤。清單 8 繼續展示了程序的設置。
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 散列中定義特殊的鍵碼。稍後將在程序中搜索這些鍵,並且它們的最後記錄時間將作為 “event” 散列元素的值存儲。清單 9 展示了主程序循環的開始部分。
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 處理。與此相反,cnee 可能會在 Firefox 有機會處理擊鍵事件之前輸出鍵盤事件。這種特殊的無限循環和微休眠(micro-sleep)方法目的是允許 cnee 和 firefox 有機會處理事件,同時保持足夠的 UI 性能。
在每次執行主循環時,cnee 輸出(如果有的話)將進行處理以查找控制鍵。如果控制鍵被找到,並且在 %keys 散列中指定了下一個按下的鍵,那麼該鍵的事件時間將被記錄下來。清單 10 展示了讀取 cnee 事件後主處理循環的後面部分。
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 ); |
將處理每一個鍵碼,以確定檢測到事件之後是否至少經過了半秒時間。如果 Firefox 當前持有焦點,/tmp/locationBarChange 文件將退出,並且自最後一個合成事件發出后至少過了 2 秒,下面繼續顯示處理。
# 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 播放器將捕獲按鍵事件。最可靠的方法是移動滑鼠,單擊窗口(例如,在 Google Search 框內)併發送 Ctrl+t,確保擊鍵事件由 Firefox 處理。在按鍵前將滑鼠移回初始位置可以確保將滑鼠放回您離開它的位置。
超負荷的系統載入和各種其他因素會影響 Firefox 接收滑鼠移動和鍵盤事件。減少或去除 usleep 函數調用可以提高發送按鍵事件的速度,但是如果系統響應變慢的話會引起其他問題。
如果有一個非標準的 Firefox 工具欄設置或希望確保合成單擊事件被發送到瀏覽器中的不同位置,那麼可能需要修改 ClickWindow 坐標。清單 12 展示了 createPipe 和 getTimeStr 支持子常式。
sub createPipe { my $cmd = shift; my $queue = new Thread::Queue; async{ my $pid = open my $pipe, $cmd or die $!; $queue->enqueue(Firefox 內的 Flash 播放器和其他嵌入式應用程序要求使用自己的掛鉤(hook)獲得鍵盤和滑鼠輸入。多年來,Flash 一直佔用 Firefox 的按鍵事件,使人們無法使用鍵盤進行導航、創建新標籤甚至退出 Flash 焦點。通過本文了解如何創建一個可以與 Firefox 擴展和 cnee 交互的 Perl 程序,幫助您取回鍵盤功能。
創建一個 Firefox 鍵盤報告擴展 要判斷 Flash 播放器何時佔用了 Firefox 熱鍵(比如 Ctrl+t),可通過兩個步驟實現。首先記錄 Firefox 中的地址欄文本何時發生改變。地址欄文本在每打開一個標籤、訪問一個新頁面或顯示不同的標籤時發生變化。其次在整個系統內監視鍵盤事件。如果 cnee 識別出一個鍵盤組合鍵(比如 Ctrl+t),但是最後一次地址欄文本變化發生在 X 秒之前,那麼 Flash 播放器已獲得鍵盤焦點。 Mozilla Developer 的 Center Progress Listeners 頁面給出一個簡單的方法,用於確定地址欄的變化。要實現類似的代碼,從 Google Calendar Encryption 文章下載預構建擴展(參見 參考資料)。提取擴展目標並修改 install.rdf 文件的內容,如下所示。 清單 1. install.rdf
使用清單 2 的內容替換 chrome.manifest 文件。 清單 2. chrome.manifest
注意,反斜杠(\)只是用來續行,因此不應放到文件中。如上所述替換了擴展元數據后,刪除 chrome/content/overlay.js 文件,插入下面所示的內容。 清單 3. overlay.js myExt_urlBarListener 函數
上面的內容基本上是直接從 Progress Listeners 示例複製過來的,myExt_urlBarListener 函數查詢可用的介面以確保 onLocationChange 功能是可用的。每當地址欄發生變化時,myExtension.updateFile() 函數將得到調用。清單 4 展示了 updateFile 和 init/unint 函數,這兩個函數被添加到 overlay.js 文件的底部。 清單 4. overlay.js myExtension 函數
通過向 /tmp/locationBarChange 文件寫入最後更新時間(UNIX® 之後的幾秒內),可以實現簡單的進程間通信。添加清單 5 中的代碼行確保擴展被正確載入和卸載。 清單 5. overlay.js addEventListeners
為了完成擴展更新,需要用以下的內容替換 chrome/content/overlay.xul 文件中的內容。 清單 6. overlay.xul
擴展現在已經準備好載入到 Firefox 了。一個簡單方法是切換到擴展的根目錄併發出命令 zip -r flashUngrabber.xpi *,以創建一個 xpi。在 Firefox 載入 xpi(也可從 下載 小節獲得)並重新啟動瀏覽器。 重新載入完成後,發出以下命令 perl -e 'while(1){print `cat /tmp/locationBarChange` . "\n";sleep(1)}'。將 Firefox 窗口變為可查看,並在不同的標籤中創建和載入頁面。您將發現如果修改標籤、載入不同的頁面或修改地址欄中文本,每隔一秒就會輸出不斷增加的數字。
flashUngrabber.pl 程序 現在已經確定了 Firefox 何時會響應一個熱鍵按下動作,現在我們可以編寫一個程序,監視它應該響應而實際上沒有響應的情況。清單 7 所示的 flashUngrabber.pl 程序可以實現這個過程。 清單 7. flashUngrabber.pl 頭部
上面的代碼包含了必要的模塊並定義了變數。在 Firefox 應用程序名稱中顯示的某些 “特殊” 字元,比如 “—”(印刷中的 “m-dash”),可能會引起 FindWindowLike 函數失敗。如果 flashUngrabber.pl 無法找到您的 Firefox 應用程序 ID 的話,嘗試載入不同的頁面或切換到不同的標籤。清單 8 繼續展示了程序的設置。 清單 8. 繼續展示 flashUngrabber.pl
在鍵盤監視模式下創建了到 cnee 程序的連接后,將在 %keys 散列中定義特殊的鍵碼。稍後將在程序中搜索這些鍵,並且它們的最後記錄時間將作為 “event” 散列元素的值存儲。清單 9 展示了主程序循環的開始部分。 清單 9. flashUngrabber.pl 主程序循環的開始部分
考慮到系統載入和各種其他因素,鍵盤事件在到達 cnee 程序之前可以由 Firefox 處理。與此相反,cnee 可能會在 Firefox 有機會處理擊鍵事件之前輸出鍵盤事件。這種特殊的無限循環和微休眠(micro-sleep)方法目的是允許 cnee 和 firefox 有機會處理事件,同時保持足夠的 UI 性能。 在每次執行主循環時,cnee 輸出(如果有的話)將進行處理以查找控制鍵。如果控制鍵被找到,並且在 %keys 散列中指定了下一個按下的鍵,那麼該鍵的事件時間將被記錄下來。清單 10 展示了讀取 cnee 事件後主處理循環的後面部分。 清單 10. 繼續 flashUngrabber.pl 主程序循環
將處理每一個鍵碼,以確定檢測到事件之後是否至少經過了半秒時間。如果 Firefox 當前持有焦點,/tmp/locationBarChange 文件將退出,並且自最後一個合成事件發出后至少過了 2 秒,下面繼續顯示處理。 清單 11. flashUngrabber.pl 主程序循環的結束部分
此時,需要發送一個合成事件,因此將記錄當前的滑鼠位置和窗口位置。此時僅僅發送 Ctrl+t 並不能獲得想要的行為,因為 Flash 播放器將捕獲按鍵事件。最可靠的方法是移動滑鼠,單擊窗口(例如,在 Google Search 框內)併發送 Ctrl+t,確保擊鍵事件由 Firefox 處理。在按鍵前將滑鼠移回初始位置可以確保將滑鼠放回您離開它的位置。 超負荷的系統載入和各種其他因素會影響 Firefox 接收滑鼠移動和鍵盤事件。減少或去除 usleep 函數調用可以提高發送按鍵事件的速度,但是如果系統響應變慢的話會引起其他問題。 如果有一個非標準的 Firefox 工具欄設置或希望確保合成單擊事件被發送到瀏覽器中的不同位置,那麼可能需要修改 ClickWindow 坐標。清單 12 展示了 createPipe 和 getTimeStr 支持子常式。 清單 12. flashUngrabber.pl 子常式
createPipe 子常式實現了從 cnee 程序的非阻塞管道讀取,而 getTimeStr 提供了一個長度一致的高精度時間串。將以上清單保存為 flashUngrabber.pl 程序並使用以下命令運行程序:perl flashUngrabber.pl。
使用 通過載入 Flash 內容播放器測試您的配置,比如 YouTube 視頻。如果單擊 Flash 播放器 — 比如音量控制 — 並按下 Ctrl+t,您將看到滑鼠將移動到 Google 搜索框,創建了一個新標籤,然後滑鼠返回到其原來的位置。
結束語 通過使用本文提供的代碼和工具,您可以從 Flash 手中重新奪回最喜愛的 Firefox 熱鍵功能。考慮向 flashUngrabber.pl 程序添加 cnee 鍵碼,實現更進一步的鍵盤導航,比如按下 Ctrl+tab 將移動到下一個標籤,或按下 Ctrl+l 將訪問地址欄。從 Flash 播放器取回 PgUp 和 PgDn 鍵來滾動整個頁面,或添加 cnee --record --mouse 選項重新支持滾輪。(責任編輯:A6) ) 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 搜索框,創建了一個新標籤,然後滑鼠返回到其原來的位置。
![]() ![]() |
結束語
通過使用本文提供的代碼和工具,您可以從 Flash 手中重新奪回最喜愛的 Firefox 熱鍵功能。考慮向 flashUngrabber.pl 程序添加 cnee 鍵碼,實現更進一步的鍵盤導航,比如按下 Ctrl+tab 將移動到下一個標籤,或按下 Ctrl+l 將訪問地址欄。從 Flash 播放器取回 PgUp 和 PgDn 鍵來滾動整個頁面,或添加 cnee --record --mouse 選項重新支持滾輪。(責任編輯:A6)
[火星人 ] 如何將 Firefox 熱鍵從 Flash 播放器釋放出來已經有715次圍觀