如何创建用于在 Firefox(WebExtensions)中打开 "browserAction" 弹出窗口的全局热键?

How to create a global hotkey for opening the "browserAction" popup in Firefox (WebExtensions)?

似乎 Chrome 没有 API 来打开弹出窗口,但是有一个专门的系统可以用热键来打开它:_execute_browser_action key in commands.

Firefox 不支持(1) commands_execute_browser_action 的特殊功能。

我关心的弹出窗口类型是 browserAction,而不是 pageAction

如何在按下键盘 shortcut/hotkey 组合键时打开 browserAction 弹出窗口?

_exectue_browser_action 的支持正在进行中:https://bugzilla.mozilla.org/show_bug.cgi?id=1246034

同时我很确定这是不可能的。

在 >= 52 的 Firefox 版本中本地可用

此功能将在 Firefox 52 中原生可用,目前为 Firefox Developer Edition(即 Firefox 52.0a2)。 如您所知,对于 WebExtensions,您可以使用为 commands 键提供的对象中的 _execute_browser_action 键创建一个全局热键。例如:

"commands":{
    "_execute_browser_action": {
        "suggested_key": {
            "default": "Alt+Shift+J"
        }
    }
}

打开一个伪弹出窗口(在旧版本的 Firefox 中填充此功能)

虽然显式功能在 Firefox 52 之前不可用,但您可以通过定义名为 "_execute_browser_action" 的自定义命令在当前版本的 Firefox 中填充此功能。它看起来与您的普通弹出窗口有点不同,但它会起作用。它将位于一个面板中,您可能需要考虑一些关联的样式,这些样式仅在面板而不是弹出窗口中应用。当您的面板打开时,活动选项卡的内容也可能存在一些差异。但是,下面的代码至少在使用 chrome.tabs.query()browser.tabs.query() 执行查询时考虑到了这一点,方法是使响应成为在真正的弹出窗口而不是面板中打开时的预期结果。

相同的代码将继续在 Firefox 52+ 上运行。在 Firefox 52+ 上,"_execute_browser_action" 直接激活浏览器操作点击或弹出窗口。

因为当您不使用弹出窗口时,最主要的是您不为 browserAction.onClicked listener. This allows the functionality to also be called by the commands.onCommand 侦听器使用匿名函数。 commands.onCommand 是在 Firefox 48 中引入的,因此这应该适用于 48+ 的任何版本。

在使用此 polyfill 时,您 可能 需要 activeTab 以外的权限。究竟需要什么,如果有的话,将取决于您的代码。

以下是一个扩展,当您按下键盘快捷键 Alt-Shift-J 时,它会导致通过浏览器操作按钮调用的功能被执行。它将激活 doActionButton() 函数,或者,如果定义了弹出窗口,它将作为面板打开弹出窗口,其行为与弹出窗口的正常行为类似,但并不完美。它从当前为当前活动选项卡定义的文件中获取弹出文件的名称,就像单击 browserAction 按钮的情况一样。

manifest.json:

{
    "description": "Polyfill browserAction keyboard shortcut, including popups.",
    "manifest_version": 2,
    "name": "Polyfill browserAction keyboard shortcut",
    "version": "0.1",

    "background": {
        "scripts": [
            "background.js"
        ]
    },

    "browser_action": {
        "default_icon": {
            "32": "myIcon.png"
        },
        "default_title": "Open popup",
        "default_popup": "popup.html"
    },
    
    "commands": {
        "_execute_browser_action": {
            "suggested_key": {
                "default": "Alt+Shift+J"
            }
        }
    }
}

background.js:

chrome.browserAction.onClicked.addListener(doActionButton);

function doActionButton(tab){
    console.log('Action Button clicked. Tab:',tab);
}

chrome.commands.onCommand.addListener(function(command) {
    //Polyfill the Browser Action button
    if(command === '_execute_browser_action') {
        chrome.tabs.query({active:true,currentWindow:true},function(tabs){
            //Get the popup for the current tab
            chrome.browserAction.getPopup({tabId:tabs[0].id},function(popupFile){
                if(popupFile){
                    openPopup(tabs[0],popupFile);
                } else {
                    //There is no popup defined, so we do what is supposed to be done for
                    //  the browserAction button.
                    doActionButton(tabs[0]);
                }
            });
        });
        return;
    } //else
});

//popupWindowId can be true, false, or the popup's window Id.
var popupWindowId = false;
var lastFocusedWin;
var lastActiveTab;
function openPopup(tab,popupFile){
    chrome.windows.getLastFocused(function(win){
        lastFocusedWin=win;
        if(popupWindowId === false){
            //This prevents user from pressing the button quickly multiple times in a row.
            popupWindowId = true;
            lastActiveTab = tab;
            chrome.windows.create({ 
                url: popupFile, 
                type: 'popup',
            },function(win){
                popupWindowId = win.id;
                //Poll for the view of the window ID. Poll every 50ms for a
                //  maximum of 20 times (1 second). Then do a second set of polling to
                //  accommodate slower machines.
                //  Testing on a single moderately fast machine indicated the view 
                //  was available after, at most, the second 50ms delay.
                waitForWindowId(popupWindowId,50,20,actOnPopupViewFound,do2ndWaitForWinId);
            });
            return;
        }else if(typeof popupWindowId === 'number'){
            //The window is open, and the user pressed the hotkey combo.
            //  Close the window (as happens for a browserAction popup).
            closePopup();
        }
    });
}

function closePopup(){
    if(typeof popupWindowId === 'number'){
        chrome.windows.remove(popupWindowId,function(){
            popupWindowId = false;
        });
    }
}

chrome.windows.onRemoved.addListener(function(winId){
    if(popupWindowId === winId){
        popupWindowId = false;
    }
});

chrome.windows.onFocusChanged.addListener(function(winId){
    //If the focus is no longer the popup, then close the popup.
    if(typeof popupWindowId === 'number'){
        if(popupWindowId !== winId){
            closePopup();
        }
    } else if(popupWindowId){
    }
});

function actOnPopupViewFound(view){
    //Make tabs.query act as if the panel is a popup.
    if(typeof view.chrome === 'object'){
        view.chrome.tabs.query = fakeTabsQuery;
    }
    if(typeof view.browser === 'object'){
        view.browser.tabs.query = fakeTabsQuery;
    }
    view.document.addEventListener('DOMContentLoaded',function(ev){
        let boundRec = view.document.body.getBoundingClientRect();
        updatePopupWindow({
            width:boundRec.width + 20,
            height:boundRec.height + 40
        });
    });
    updatePopupWindow({});
}

function updatePopupWindow(opt){
    let width,height;
    if(opt){
        width =typeof opt.width  === 'number'?opt.width :400;
        height=typeof opt.height === 'number'?opt.height:300;
    }
    //By the time we get here it is too late to find the window for which we
    //  are trying to open the popup.
    let left = lastFocusedWin.left + lastFocusedWin.width - (width +40);
    let top = lastFocusedWin.top + 85; //Just a value that works in the default case.
    let updateInfo = {
        width:width,
        height:height,
        top:top,
        left:left
    };
    chrome.windows.update(popupWindowId,updateInfo);
}

function waitForWindowId(id,delay,maxTries,foundCallback,notFoundCallback) {
    if(maxTries--<=0){
        if(typeof notFoundCallback === 'function'){
            notFoundCallback(id,foundCallback);
        }
        return;
    }
    let views = chrome.extension.getViews({windowId:id});
    if(views.length > 0){
        if(typeof foundCallback === 'function'){
            foundCallback(views[0]);
        }
    } else {
        setTimeout(waitForWindowId,delay,id,delay,maxTries,foundCallback,notFoundCallback);
    }
}

function do2ndWaitForWinId(winId,foundCallback){
    //Poll for the view of the window ID. Poll every 500ms for a
    //  maximum of 40 times (20 seconds). 
    waitForWindowId(winId,500,40,foundCallback,windowViewNotFound);
}

function windowViewNotFound(winId,foundCallback){
    //Did not find the view for the window. Do what you want here.
    //  Currently fail quietly.
}

function fakeTabsQuery(options,callback){
    //This fakes the response of chrome.tabs.query and browser.tabs.query, which in
    //  a browser action popup returns the tab that is active in the window which
    //  was the current window when the popup was opened. We need to emulate this
    //  in the popup as panel.
    //The popup is also stripped from responses if the response contains multiple
    //  tabs.
    let origCallback = callback;
    function stripPopupWinFromResponse(tabs){
        return tabs.filter(tab=>{
            return tab.windowId !== popupWindowId;
        });
    }
    function stripPopupWinFromResponseIfMultiple(tabs){
        if(tabs.length>1){
            return stripPopupWinFromResponse(tabs);
        }else{
            return tabs;
        }
    }
    function callbackWithStrippedTabs(tabs){
        origCallback(stripPopupWinFromResponseIfMultiple(tabs));
    }
    if(options.currentWindow || options.lastFocusedWindow){
        //Make the query use the window which was active prior to the panel being
        //  opened.
        delete options.currentWindow;
        delete options.lastFocusedWindow;
        options.windowId = lastActiveTab.windowId;
    }
    if(typeof callback === 'function') {
        callback = callbackWithStrippedTabs;
        chrome.tabs.query.apply(this,arguments);
        return;
    }else{
        return browser.tabs.query.apply(this,arguments)
                                 .then(stripPopupWinFromResponseIfMultiple);
    }
}

WebExtensions 仍在开发中:

WebExtensions API 仍在开发中。每个版本的 Firefox 都改进了工作。现在,您可能最好使用 Firefox Developer EditionFirefox Nightly(对于 _execute_browser_action)开发和测试您的 WebExtension 附加组件。您还应该仔细记下您希望使用的功能需要哪个版本的 Firefox。此信息包含在 MDN 文档页面的“浏览器兼容性”部分。


这个问题中的部分代码来自copied/modified我的其他各种答案。

_exectue_browser_action_execute_page_action_execute_sidebar_action 实施:Special shortcuts.