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 文件的内容,如下所示。
清单 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_cnnew1@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> |
使用清单 2 的内容替换 chrome.manifest 文件。
清单 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(); }, }; |
上面的内容基本上是直接从 Progress Listeners 示例复制过来的,myExt_urlBarListener 函数查询可用的接口以确保 onLocationChange 功能是可用的。每当地址栏发生变化时,myExtension.updateFile() 函数将得到调用。清单 4 展示了 updateFile 和 init/unint 函数,这两个函数被添加到 overlay.js 文件的底部。
清单 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(); } }; |
通过向 /tmp/locationBarChange 文件写入最后更新时间(UNIX® 之后的几秒内),可以实现简单的进程间通信。添加清单 5 中的代码行确保扩展被正确加载和卸载。
清单 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。在 Firefox 加载 xpi(也可从 下载 小节获得)并重新启动浏览器。
重新加载完成后,发出以下命令 perl -e 'while(1){print `cat /tmp/locationBarChange` . "\n";sleep(1)}'。将 Firefox 窗口变为可查看,并在不同的标签中创建和加载页面。您将发现如果修改标签、加载不同的页面或修改地址栏中文本,每隔一秒就会输出不断增加的数字。
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 ); |
上面的代码包含了必要的模块并定义了变量。在 Firefox 应用程序名称中显示的某些 “特殊” 字符,比如 “—”(印刷中的 “m-dash”),可能会引起 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 散列中定义特殊的键码。稍后将在程序中搜索这些键,并且它们的最后记录时间将作为 “event” 散列元素的值存储。清单 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 处理。与此相反,cnee 可能会在 Firefox 有机会处理击键事件之前输出键盘事件。这种特殊的无限循环和微休眠(micro-sleep)方法目的是允许 cnee 和 firefox 有机会处理事件,同时保持足够的 UI 性能。
在每次执行主循环时,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 ); |
将处理每一个键码,以确定检测到事件之后是否至少经过了半秒时间。如果 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 播放器将捕获按键事件。最可靠的方法是移动鼠标,单击窗口(例如,在 Google Search 框内)并发送 Ctrl+t,确保击键事件由 Firefox 处理。在按键前将鼠标移回初始位置可以确保将鼠标放回您离开它的位置。
超负荷的系统加载和各种其他因素会影响 Firefox 接收鼠标移动和键盘事件。减少或去除 usleep 函数调用可以提高发送按键事件的速度,但是如果系统响应变慢的话会引起其他问题。
如果有一个非标准的 Firefox 工具栏设置或希望确保合成单击事件被发送到浏览器中的不同位置,那么可能需要修改 ClickWindow 坐标。清单 12 展示了 createPipe 和 getTimeStr 支持子例程。
清单 12. flashUngrabber.pl 子例程
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)