From d6a48df60162a2f0b4465c4bea5b477cde06cadd Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 4 Jun 2015 08:27:32 -0400 Subject: [PATCH] this fixes #246 -- FF tab management re-factored --- platform/firefox/vapi-background.js | 678 ++++++++++++++++------------ 1 file changed, 400 insertions(+), 278 deletions(-) diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index 0fbdf0b..8e5c52f 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -129,6 +129,41 @@ window.addEventListener('unload', function() { /******************************************************************************/ +vAPI.browserSettings = { + + setBool: function(branch, setting, value) { + try { + Services.prefs + .getBranch(branch + '.') + .setBoolPref(setting, value); + } catch (ex) { + } + }, + + set: function(details) { + for ( var setting in details ) { + if ( details.hasOwnProperty(setting) === false ) { + continue; + } + switch ( setting ) { + case 'prefetching': + this.setBool('network', 'prefetch-next', !!details[setting]); + break; + + case 'hyperlinkAuditing': + this.setBool('browser', 'send_pings', !!details[setting]); + this.setBool('beacon', 'enabled', !!details[setting]); + break; + + default: + break; + } + } + } +}; + +/******************************************************************************/ + // API matches that of chrome.storage.local: // https://developer.chrome.com/extensions/storage @@ -392,277 +427,70 @@ vAPI.storage = (function() { /******************************************************************************/ -var windowWatcher = { - onReady: function(e) { - if ( e ) { - this.removeEventListener(e.type, windowWatcher.onReady); - } - - var wintype = this.document.documentElement.getAttribute('windowtype'); - - if ( wintype !== 'navigator:browser' ) { - return; - } - - var tabBrowser = getTabBrowser(this); - if ( !tabBrowser || !tabBrowser.tabContainer ) { - return; - } - - var tabContainer = tabBrowser.tabContainer; - - tabContainer.addEventListener('TabClose', tabWatcher.onTabClose); - tabContainer.addEventListener('TabSelect', tabWatcher.onTabSelect); - tabBrowser.addTabsProgressListener(tabWatcher); - vAPI.contextMenu.register(this.document); - - // when new window is opened TabSelect doesn't run on the selected tab? - }, - - observe: function(win, topic) { - if ( topic === 'domwindowopened' ) { - win.addEventListener('DOMContentLoaded', this.onReady); - } - } -}; - -/******************************************************************************/ - -var tabWatcher = { - SAME_DOCUMENT: Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT, - - onTabClose: function({target}) { - // target is tab in Firefox, browser in Fennec - var tabId = vAPI.tabs.getTabId(target); - if ( tabId === vAPI.noTabId ) { - return; - } - vAPI.tabs.onClosed(tabId); - delete vAPI.toolbarButton.tabs[tabId]; - }, - - onTabSelect: function({target}) { - var tabId = vAPI.tabs.getTabId(target); - if ( tabId === vAPI.noTabId ) { - return; - } - vAPI.setIcon(tabId, getOwnerWindow(target)); - }, - - onLocationChange: function(browser, webProgress, request, location, flags) { - if ( !webProgress.isTopLevel ) { - return; - } - - var tabId = vAPI.tabs.getTabId(browser); - - // LOCATION_CHANGE_SAME_DOCUMENT = "did not load a new document" - if ( flags & this.SAME_DOCUMENT ) { - vAPI.tabs.onUpdated(tabId, {url: location.asciiSpec}, { - tabId: tabId, - url: browser.currentURI.asciiSpec - }); - return; - } - - // https://github.com/gorhill/uBlock/issues/105 - // Allow any kind of pages - vAPI.tabs.onNavigation({ - tabId: tabId, - url: location.asciiSpec - }); - } -}; - -/******************************************************************************/ - -vAPI.isBehindTheSceneTabId = function(tabId) { - return tabId.toString() === '-1'; -}; - -vAPI.noTabId = '-1'; - -/******************************************************************************/ - var getTabBrowser = function(win) { return win.gBrowser || null; }; /******************************************************************************/ -var getBrowserForTab = function(tab) { - if ( !tab ) { - return null; - } - return tab.linkedBrowser || null; -}; - -/******************************************************************************/ - var getOwnerWindow = function(target) { if ( target.ownerDocument ) { return target.ownerDocument.defaultView; } - return null; }; /******************************************************************************/ -vAPI.tabs = {}; - -/******************************************************************************/ - -vAPI.tabs.registerListeners = function() { - // onNavigation and onUpdated handled with tabWatcher.onLocationChange - // onClosed - handled in tabWatcher.onTabClose - - for ( var win of this.getWindows() ) { - windowWatcher.onReady.call(win); - } - - Services.ww.registerNotification(windowWatcher); - - cleanupTasks.push(function() { - Services.ww.unregisterNotification(windowWatcher); - - for ( var win of vAPI.tabs.getWindows() ) { - vAPI.contextMenu.unregister(win.document); - win.removeEventListener('DOMContentLoaded', windowWatcher.onReady); - - var tabContainer; - var tabBrowser = getTabBrowser(win); - if ( !tabBrowser ) { - continue; - } - - if ( tabBrowser.tabContainer ) { - tabContainer = tabBrowser.tabContainer; - tabBrowser.removeTabsProgressListener(tabWatcher); - } - - tabContainer.removeEventListener('TabClose', tabWatcher.onTabClose); - tabContainer.removeEventListener('TabSelect', tabWatcher.onTabSelect); - - // Close extension tabs - for ( var tab of tabBrowser.tabs ) { - var browser = getBrowserForTab(tab); - if ( browser === null ) { - continue; - } - var URI = browser.currentURI; - if ( URI.schemeIs('chrome') && URI.host === location.host ) { - vAPI.tabs._remove(tab, getTabBrowser(win)); - } - } - } - }); +vAPI.isBehindTheSceneTabId = function(tabId) { + return tabId.toString() === '-1'; }; -/******************************************************************************/ - -vAPI.tabs.stack = new WeakMap(); -vAPI.tabs.stackId = 1; +vAPI.noTabId = '-1'; /******************************************************************************/ -vAPI.tabs.getTabId = function(target) { - if ( !target ) { - return vAPI.noTabId; - } - if ( target.linkedPanel ) { - target = target.linkedBrowser; // target is a tab - } - if ( target.localName !== 'browser' ) { - return vAPI.noTabId; - } - var tabId = this.stack.get(target); - if ( tabId ) { - return tabId; - } - tabId = '' + this.stackId++; - this.stack.set(target, tabId); - - // https://github.com/gorhill/uMatrix/issues/189 - // If a new tabid-tab pair is created, tell the client code about it. - if ( this.onNavigation ) { - this.onNavigation({ - tabId: tabId, - url: target.currentURI.asciiSpec - }); - } - - return tabId; -}; +vAPI.tabs = {}; /******************************************************************************/ -// If tabIds is an array, then an array of tabs will be returned, -// otherwise a single tab - -vAPI.tabs.getTabsForIds = function(tabIds) { - var tabs = []; - var singleTab = !Array.isArray(tabIds); - if ( singleTab ) { - tabIds = [tabIds]; - } - for ( var tab of this.getAllSync() ) { - var tabId = this.stack.get(getBrowserForTab(tab)); - if ( !tabId ) { - continue; - } - if ( tabIds.indexOf(tabId) !== -1 ) { - if ( singleTab ) { - return tab; - } - tabs.push(tab); - } - if ( tabs.length >= tabIds.length ) { - break; - } - } - return tabs.length !== 0 ? tabs : null; +vAPI.tabs.registerListeners = function() { + tabWatcher.start(); }; /******************************************************************************/ vAPI.tabs.get = function(tabId, callback) { - var tab, win; + var browser; if ( tabId === null ) { - win = Services.wm.getMostRecentWindow('navigator:browser'); - tab = getTabBrowser(win).selectedTab; - tabId = this.getTabId(tab); + browser = tabWatcher.currentBrowser(); + tabId = tabWatcher.tabIdFromTarget(browser); } else { - tab = this.getTabsForIds(tabId); - if ( tab ) { - win = getOwnerWindow(tab); - } + browser = tabWatcher.browserFromTabId(tabId); } // For internal use if ( typeof callback !== 'function' ) { - return tab; + return browser; } - if ( !tab ) { + if ( !browser ) { callback(); return; } - var windows = this.getWindows(); - var browser = getBrowserForTab(tab); + var win = getOwnerWindow(browser); var tabBrowser = getTabBrowser(win); - var tabIndex = tabBrowser.browsers.indexOf(browser); - var tabTitle = tab.label; + var windows = this.getWindows(); callback({ id: tabId, - index: tabIndex, + index: tabWatcher.indexFromTarget(browser), windowId: windows.indexOf(win), - active: tab === tabBrowser.selectedTab, + active: browser === tabBrowser.selectedBrowser, url: browser.currentURI.asciiSpec, - title: tabTitle + title: browser.contentTitle }); }; @@ -693,24 +521,22 @@ vAPI.tabs.getAllSync = function(window) { /******************************************************************************/ vAPI.tabs.getAll = function(callback) { - var tabs = []; - var win, tab; + var tabs = [], tab; - for ( win of this.getWindows() ) { - var tabBrowser = getTabBrowser(win); - if ( tabBrowser === null ) { + for ( var browser of tabWatcher.browsers() ) { + tab = tabWatcher.tabFromBrowser(browser); + if ( tab === null ) { continue; } - for ( tab of tabBrowser.tabs ) { - if ( tab.hasAttribute('pending') ) { - continue; - } - tabs.push({ - id: this.getTabId(tab), - url: getBrowserForTab(tab).currentURI.asciiSpec - }); + if ( tab.hasAttribute('pending') ) { + continue; } + tabs.push({ + id: tabWatcher.tabIdFromTarget(browser), + url: browser.currentURI.asciiSpec + }); } + callback(tabs); }; @@ -749,13 +575,13 @@ vAPI.tabs.open = function(details) { details.url = vAPI.getURL(details.url); } - var win, tab, tabBrowser; + var tab; if ( details.select ) { var URI = Services.io.newURI(details.url, null, null); for ( tab of this.getAllSync() ) { - var browser = getBrowserForTab(tab); + var browser = tabWatcher.browserFromTarget(tab); // Or simply .equals if we care about the fragment if ( URI.equalsExceptRef(browser.currentURI) === false ) { @@ -772,15 +598,15 @@ vAPI.tabs.open = function(details) { } if ( details.tabId ) { - tab = this.getTabsForIds(details.tabId); + tab = tabWatcher.browserFromTabId(details.tabId); if ( tab ) { - getBrowserForTab(tab).loadURI(details.url); + tabWatcher.browserFromTarget(tab).loadURI(details.url); return; } } - win = Services.wm.getMostRecentWindow('navigator:browser'); - tabBrowser = getTabBrowser(win); + var win = Services.wm.getMostRecentWindow('navigator:browser'); + var tabBrowser = getTabBrowser(win); if ( details.index === -1 ) { details.index = tabBrowser.browsers.indexOf(tabBrowser.selectedBrowser) + 1; @@ -805,9 +631,9 @@ vAPI.tabs.replace = function(tabId, url) { targetURL = vAPI.getURL(targetURL); } - var tab = this.getTabsForIds(tabId); - if ( tab ) { - getBrowserForTab(tab).loadURI(targetURL); + var browser = tabWatcher.browserFromTabId(tabId); + if ( browser ) { + browser.loadURI(targetURL); } }; @@ -820,50 +646,46 @@ vAPI.tabs._remove = function(tab, tabBrowser) { /******************************************************************************/ vAPI.tabs.remove = function(tabIds) { - if ( !Array.isArray(tabIds) ) { - tabIds = [tabIds]; - } - var tabs = this.getTabsForIds(tabIds); - if ( !tabs ) { + var browser = tabWatcher.browserFromTabId(tabId); + if ( !browser ) { return; } - for ( var tab of tabs ) { - this._remove(tab, getTabBrowser(getOwnerWindow(tab))); + var tab = tabWatcher.tabFromBrowser(browser); + if ( !tab ) { + return; } + this._remove(tab, getTabBrowser(getOwnerWindow(browser))); }; /******************************************************************************/ vAPI.tabs.reload = function(tabId) { - var tab = this.get(tabId); - - if ( !tab ) { + var browser = tabWatcher.browserFromTabId(tabId); + if ( !browser ) { return; } - getBrowserForTab(tab).webNavigation.reload( - Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE - ); + browser.webNavigation.reload(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE); }; /******************************************************************************/ vAPI.tabs.select = function(tab) { - tab = typeof tab === 'object' ? tab : this.get(tab); - + if ( typeof tab !== 'object' ) { + tab = tabWatcher.tabFromBrowser(tabWatcher.browserFromTabId(tab)); + } if ( !tab ) { return; } - getTabBrowser(getOwnerWindow(tab)).selectedTab = tab; + var tabBrowser = getTabBrowser(getOwnerWindow(tab)).selectedTab = tab; }; /******************************************************************************/ vAPI.tabs.injectScript = function(tabId, details, callback) { - var tab = this.get(tabId); - - if ( !tab ) { + var browser = tabWatcher.browserFromTabId(tabId); + if ( !browser ) { return; } @@ -872,7 +694,7 @@ vAPI.tabs.injectScript = function(tabId, details, callback) { } details.file = vAPI.getURL(details.file); - getBrowserForTab(tab).messageManager.sendAsyncMessage( + browser.messageManager.sendAsyncMessage( location.host + ':broadcast', JSON.stringify({ broadcast: true, @@ -891,6 +713,309 @@ vAPI.tabs.injectScript = function(tabId, details, callback) { /******************************************************************************/ +// Firefox: +// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Tabbed_browser +// +// browser --> ownerDocument --> defaultView --> gBrowser --> browsers --+ +// ^ | +// | | +// +------------------------------------------------------------------- +// +// browser (browser) +// contentTitle +// currentURI +// ownerDocument (XULDocument) +// defaultView (ChromeWindow) +// gBrowser (tabbrowser OR browser) +// browsers (browser) +// selectedBrowser +// selectedTab +// tabs (tab.tabbrowser-tab) +// +// Fennec: (what I figured so far) +// +// tab --> browser windows --> window --> BrowserApp --> tabs --+ +// ^ window | +// | | +// +---------------------------------------------------------------+ +// +// tab +// browser +// [manual search to go back to tab from list of windows] + +var tabWatcher = (function() { + // TODO: find out whether we need a janitor to take care of stale entries. + var browserToTabIdMap = new Map(); + var tabIdToBrowserMap = new Map(); + var tabIdGenerator = 1; + + var indexFromBrowser = function(browser) { + var win = getOwnerWindow(browser); + if ( !win ) { + return -1; + } + var tabbrowser = getTabBrowser(win); + if ( !tabbrowser ) { + return -1; + } + // This can happen, for example, the `view-source:` window, there is + // no tabbrowser object, the browser object sits directly in the + // window. + if ( tabbrowser === browser ) { + return 0; + } + // Fennec + // https://developer.mozilla.org/en-US/Add-ons/Firefox_for_Android/API/BrowserApp + if ( vAPI.fennec ) { + return tabbrowser.tabs.indexOf(tabbrowser.getTabForBrowser(browser)); + } + return tabbrowser.browsers.indexOf(browser); + }; + + var indexFromTarget = function(target) { + return indexFromBrowser(browserFromTarget(target)); + }; + + var tabFromBrowser = function(browser) { + var i = indexFromBrowser(browser); + if ( i === -1 ) { + return null; + } + var win = getOwnerWindow(browser); + if ( !win ) { + return null; + } + var tabbrowser = getTabBrowser(win); + if ( !tabbrowser ) { + return null; + } + if ( !tabbrowser.tabs || i >= tabbrowser.tabs.length ) { + return null; + } + return tabbrowser.tabs[i]; + }; + + var browserFromTarget = function(target) { + if ( !target ) { + return null; + } + if ( vAPI.fennec ) { + if ( target.browser ) { // target is a tab + target = target.browser; + } + } else if ( target.linkedPanel ) { // target is a tab + target = target.linkedBrowser; + } + if ( target.localName !== 'browser' ) { + return null; + } + return target; + }; + + var tabIdFromTarget = function(target) { + var browser = browserFromTarget(target); + if ( browser === null ) { + return vAPI.noTabId; + } + var tabId = browserToTabIdMap.get(browser); + if ( tabId === undefined ) { + tabId = '' + tabIdGenerator++; + browserToTabIdMap.set(browser, tabId); + tabIdToBrowserMap.set(tabId, browser); + } + return tabId; + }; + + var browserFromTabId = function(tabId) { + var browser = tabIdToBrowserMap.get(tabId); + if ( browser === undefined ) { + return null; + } + // Verify that the browser is still live + if ( indexFromBrowser(browser) !== -1 ) { + return browser; + } + removeBrowserEntry(tabId, browser); + return null; + }; + + var currentBrowser = function() { + var win = Services.wm.getMostRecentWindow('navigator:browser'); + return browserFromTarget(getTabBrowser(win).selectedTab); + }; + + var removeBrowserEntry = function(tabId, browser) { + if ( tabId && tabId !== vAPI.noTabId ) { + vAPI.tabs.onClosed(tabId); + delete vAPI.toolbarButton.tabs[tabId]; + tabIdToBrowserMap.delete(tabId); + } + if ( browser ) { + browserToTabIdMap.delete(browser); + } + }; + + // https://developer.mozilla.org/en-US/docs/Web/Events/TabOpen + var onOpen = function({target}) { + var tabId = tabIdFromTarget(target); + var browser = browserFromTabId(tabId); + vAPI.tabs.onNavigation({ + frameId: 0, + tabId: tabId, + url: browser.currentURI.asciiSpec, + }); + }; + + // https://developer.mozilla.org/en-US/docs/Web/Events/TabShow + var onShow = function({target}) { + tabIdFromTarget(target); + }; + + // https://developer.mozilla.org/en-US/docs/Web/Events/TabClose + var onClose = function({target}) { + // target is tab in Firefox, browser in Fennec + var browser = browserFromTarget(target); + var tabId = browserToTabIdMap.get(browser); + removeBrowserEntry(tabId, browser); + }; + + // https://developer.mozilla.org/en-US/docs/Web/Events/TabSelect + var onSelect = function({target}) { + vAPI.setIcon(tabIdFromTarget(target), getOwnerWindow(target)); + }; + + var onWindowLoad = function(ev) { + if ( ev ) { + this.removeEventListener(ev.type, onWindowLoad); + } + + var wintype = this.document.documentElement.getAttribute('windowtype'); + if ( wintype !== 'navigator:browser' ) { + return; + } + + var tabBrowser = getTabBrowser(this); + if ( !tabBrowser ) { + return; + } + + var tabContainer; + if ( tabBrowser.deck ) { + // Fennec + tabContainer = tabBrowser.deck; + } else if ( tabBrowser.tabContainer ) { + // desktop Firefox + tabContainer = tabBrowser.tabContainer; + vAPI.contextMenu.register(this.document); + } else { + return; + } + tabContainer.addEventListener('TabOpen', onOpen); + tabContainer.addEventListener('TabShow', onShow); + tabContainer.addEventListener('TabClose', onClose); + tabContainer.addEventListener('TabSelect', onSelect); + + // when new window is opened TabSelect doesn't run on the selected tab? + }; + + var onWindowUnload = function() { + vAPI.contextMenu.unregister(this.document); + this.removeEventListener('DOMContentLoaded', onWindowLoad); + + var tabBrowser = getTabBrowser(this); + if ( !tabBrowser ) { + return; + } + + var tabContainer = null; + if ( tabBrowser.deck ) { + // Fennec + tabContainer = tabBrowser.deck; + } else if ( tabBrowser.tabContainer ) { + tabContainer = tabBrowser.tabContainer; + } + if ( tabContainer ) { + tabContainer.removeEventListener('TabOpen', onOpen); + tabContainer.removeEventListener('TabShow', onShow); + tabContainer.removeEventListener('TabClose', onClose); + tabContainer.removeEventListener('TabSelect', onSelect); + } + + // Close extension tabs + var browser, URI, tabId; + for ( var tab of tabBrowser.tabs ) { + browser = tabWatcher.browserFromTarget(tab); + if ( browser === null ) { + continue; + } + URI = browser.currentURI; + if ( URI.schemeIs('chrome') && URI.host === location.host ) { + vAPI.tabs._remove(tab, getTabBrowser(this)); + } + browser = browserFromTarget(tab); + tabId = browserToTabIdMap.get(browser); + if ( tabId !== undefined ) { + tabIdToBrowserMap.delete(tabId); + } + browserToTabIdMap.delete(browser); + } + }; + + // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowWatcher + var windowWatcher = { + observe: function(win, topic) { + if ( topic === 'domwindowopened' ) { + win.addEventListener('DOMContentLoaded', onWindowLoad); + } + } + }; + + // Initialize map with existing active tabs + var start = function() { + var tabBrowser, tab; + for ( var win of vAPI.tabs.getWindows() ) { + onWindowLoad.call(win); + tabBrowser = getTabBrowser(win); + if ( tabBrowser === null ) { + continue; + } + for ( tab of tabBrowser.tabs ) { + if ( vAPI.fennec || !tab.hasAttribute('pending') ) { + tabIdFromTarget(tab); + } + } + } + + Services.ww.registerNotification(windowWatcher); + }; + + var stop = function() { + Services.ww.unregisterNotification(windowWatcher); + + for ( var win of vAPI.tabs.getWindows() ) { + onWindowUnload.call(win); + } + + browserToTabIdMap.clear(); + tabIdToBrowserMap.clear(); + }; + + cleanupTasks.push(stop); + + return { + browsers: function() { return browserToTabIdMap.keys(); }, + browserFromTabId: browserFromTabId, + browserFromTarget: browserFromTarget, + currentBrowser: currentBrowser, + indexFromTarget: indexFromTarget, + start: start, + tabFromBrowser: tabFromBrowser, + tabIdFromTarget: tabIdFromTarget + }; +})(); + +/******************************************************************************/ + vAPI.setIcon = function(tabId, iconId, badge) { // If badge is undefined, then setIcon was called from the TabSelect event var win; @@ -899,7 +1024,7 @@ vAPI.setIcon = function(tabId, iconId, badge) { } else { win = Services.wm.getMostRecentWindow('navigator:browser'); } - var curTabId = vAPI.tabs.getTabId(getTabBrowser(win).selectedTab); + var curTabId = tabWatcher.tabIdFromTarget(getTabBrowser(win).selectedTab); var tb = vAPI.toolbarButton; // from 'TabSelect' event @@ -963,7 +1088,7 @@ vAPI.messaging.onMessage = function({target, data}) { var sender = { tab: { - id: vAPI.tabs.getTabId(target) + id: tabWatcher.tabIdFromTarget(target) } }; @@ -1314,7 +1439,7 @@ var httpObserver = { .notificationCallbacks .getInterface(Ci.nsILoadContext); if ( loadContext.topFrameElement ) { - return vAPI.tabs.getTabId(loadContext.topFrameElement); + return tabWatcher.tabIdFromTarget(loadContext.topFrameElement); } aWindow = loadContext.associatedWindow; } catch (ex) { @@ -1330,7 +1455,8 @@ var httpObserver = { .associatedWindow; } if ( aWindow ) { - return vAPI.tabs.getTabId(aWindow + return tabWatcher.tabIdFromTarget( + aWindow .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShell) .rootTreeItem @@ -1891,7 +2017,7 @@ vAPI.contextMenu.create = function(details, callback) { } callback(details, { - id: vAPI.tabs.getTabId(gContextMenu.browser), + id: tabWatcher.tabIdFromTarget(gContextMenu.browser), url: gContextMenu.browser.currentURI.asciiSpec }); }; @@ -1924,7 +2050,7 @@ var optionsObserver = { Services.obs.addObserver(this, 'addon-options-displayed', false); cleanupTasks.push(this.unregister.bind(this)); - var browser = getBrowserForTab(vAPI.tabs.get(null)); + var browser = tabWatcher.currentBrowser(); if ( browser && browser.currentURI && browser.currentURI.spec === 'about:addons' ) { this.observe(browser.contentDocument, 'addon-enabled', this.addonId); } @@ -1973,12 +2099,8 @@ vAPI.lastError = function() { // the web pages before uBlock was ready. vAPI.onLoadAllCompleted = function() { - for ( var tab of this.tabs.getAllSync() ) { - // We're insterested in only the tabs that were already loaded - if ( tab.hasAttribute('pending') ) { - continue; - } - getBrowserForTab(tab).messageManager.sendAsyncMessage( + for ( var browser of tabWatcher.browsers() ) { + browser.messageManager.sendAsyncMessage( location.host + '-load-completed' ); }