You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1668 lines
51 KiB

/*
* This Source Code is subject to the terms of the Mozilla Public License
* version 2.0 (the "License"). You can obtain a copy of the License at
* http://mozilla.org/MPL/2.0/.
*/
#filter substitution
/**
* @fileOverview Application integration module, will keep track of application
* windows and handle the necessary events.
*/
var EXPORTED_SYMBOLS = ["AppIntegration"];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import(baseURL + "TimeLine.jsm");
Cu.import(baseURL + "Utils.jsm");
Cu.import(baseURL + "Prefs.jsm");
Cu.import(baseURL + "ContentPolicy.jsm");
Cu.import(baseURL + "FilterListener.jsm");
Cu.import(baseURL + "FilterStorage.jsm");
Cu.import(baseURL + "FilterNotifier.jsm");
Cu.import(baseURL + "FilterClasses.jsm");
Cu.import(baseURL + "SubscriptionClasses.jsm");
Cu.import(baseURL + "RequestNotifier.jsm");
Cu.import(baseURL + "Synchronizer.jsm");
/* Cu.import(baseURL + "Sync.jsm"); */
/**
* Wrappers for tracked application windows.
* @type Array of WindowWrapper
*/
let wrappers = [];
/**
* Stores the selected hotkeys, initialized when the first browser window opens.
*/
let hotkeys = null;
/**
* Object observing add-on manager notifications about add-on options being initialized.
* @type nsIObserver
*/
let optionsObserver =
{
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
observe: function(subject, topic, data)
{
if (data != Utils.addonID)
return;
initOptionsDoc(subject.QueryInterface(Ci.nsIDOMDocument));
}
};
/**
* Initializes app integration module
*/
function init()
{
// Process preferences
reloadPrefs();
// Listen for pref and filters changes
Prefs.addListener(function(name)
{
if (name == "enabled" || name == "showinstatusbar" || name == "defaulttoolbaraction" || name == "defaultstatusbaraction")
reloadPrefs();
});
FilterNotifier.addListener(function(action)
{
if (/^(filter|subscription)\.(added|removed|disabled|updated)$/.test(action) || action == "load")
reloadPrefs();
});
Services.obs.addObserver(optionsObserver, "addon-options-displayed", true);
}
/**
* Exported app integration functions.
* @class
*/
var AppIntegration =
{
/**
* Adds an application window to the tracked list.
*/
addWindow: function(/**Window*/ window)
{
let hooks = window.document.getElementById("abp-hooks");
if (!hooks)
return;
TimeLine.enter("Entered AppIntegration.addWindow()")
// Execute first-run actions
if (!("lastVersion" in Prefs))
{
Prefs.lastVersion = Prefs.currentVersion;
// Show subscriptions dialog if the user doesn't have any subscriptions yet
if (Prefs.currentVersion != Utils.addonVersion)
{
Prefs.currentVersion = Utils.addonVersion;
if ("nsISessionStore" in Ci)
{
// Have to wait for session to be restored
let observer =
{
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
observe: function(subject, topic, data)
{
Services.obs.removeObserver(observer, "sessionstore-windows-restored");
timer.cancel();
timer = null;
addSubscription();
}
};
Services.obs.addObserver(observer, "sessionstore-windows-restored", false);
// Just in case, don't wait more than a second
let timer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer);
timer.init(observer, 1000, Ci.nsITimer.TYPE_ONE_SHOT);
}
else
addSubscription();
}
}
TimeLine.log("App-wide first-run actions done")
let wrapper = new WindowWrapper(window, hooks);
wrappers.push(wrapper);
TimeLine.leave("AppIntegration.addWindow() done")
},
/**
* Retrieves the wrapper object corresponding to a particular application window.
*/
getWrapperForWindow: function(/**Window*/ wnd) /**WindowWrapper*/
{
for each (let wrapper in wrappers)
if (wrapper.window == wnd)
return wrapper;
return null;
},
/**
* Toggles the value of a boolean preference.
*/
togglePref: function(/**String*/ pref)
{
Prefs[pref] = !Prefs[pref];
},
/**
* Toggles the pref for the Adblock Plus sync engine.
* @return {Boolean} new state of the sync engine
*/
/** toggleSync: function()
{
let syncEngine = Sync.getEngine();
syncEngine.enabled = !syncEngine.enabled;
return syncEngine.enabled;
},
*/
/**
* Adds or removes the Adblock Plus toolbar icon.
* @return {Boolean} new state of the toolbar button
*/
toggleToolbarIcon: function()
{
if (!wrappers.length)
return false;
let newVal = !wrappers[0].isToolbarIconVisible();
for (let i = 0; i < wrappers.length; i++)
{
if (newVal)
wrappers[i].installToolbarIcon();
else
wrappers[i].hideToolbarIcon();
}
return wrappers[0].isToolbarIconVisible();
},
/**
* If the given filter is already in user's list, removes it from the list. Otherwise adds it.
*/
toggleFilter: function(/**Filter*/ filter)
{
if (filter.subscriptions.length)
{
if (filter.disabled || filter.subscriptions.some(function(subscription) !(subscription instanceof SpecialSubscription)))
filter.disabled = !filter.disabled;
else
FilterStorage.removeFilter(filter);
}
else
FilterStorage.addFilter(filter);
},
/**
* Opens ABP menu.
*/
openMenu: function(window)
{
let wrapper = AppIntegration.getWrapperForWindow(window.top);
if (!wrapper)
{
// Maybe we got a content window
window = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
if (window.wrappedJSObject)
window = window.wrappedJSObject;
wrapper = AppIntegration.getWrapperForWindow(window);
}
if (!wrapper)
{
// Try to find any known window
let enumerator = Utils.windowMediator.getZOrderDOMWindowEnumerator(null, true);
if (!enumerator.hasMoreElements())
{
// On Linux the list returned will be empty, see bug 156333. Fall back to random order.
enumerator = Utils.windowMediator.getEnumerator(null);
}
while (enumerator.hasMoreElements())
{
window = enumerator.getNext().QueryInterface(Ci.nsIDOMWindow);
wrapper = AppIntegration.getWrapperForWindow(window);
if (wrapper)
break;
}
}
if (wrapper)
Utils.runAsync(wrapper.openMenu, wrapper);
}
};
/**
* Removes an application window from the tracked list.
*/
function removeWindow()
{
let wnd = this;
for (let i = 0; i < wrappers.length; i++)
if (wrappers[i].window == wnd)
wrappers.splice(i--, 1);
}
/**
* Class providing various functions related to application windows.
* @constructor
*/
function WindowWrapper(window, hooks)
{
TimeLine.enter("Entered WindowWrapper constructor")
this.window = window;
this.initializeHooks(hooks);
TimeLine.log("Hooks element initialized")
this.fixupMenus();
TimeLine.log("Context menu copying done")
this.configureKeys();
TimeLine.log("Shortcut keys configured")
this.initContextMenu();
TimeLine.log("Context menu initialized")
let browser = this.getBrowser();
if (browser && browser.currentURI)
{
this.updateState();
}
else
{
// Update state asynchronously, the Thunderbird window won't be initialized yet for non-default window layouts
Utils.runAsync(this.updateState, this);
}
TimeLine.log("Icon state updated")
// Some people actually switch off browser.frames.enabled and are surprised
// that things stop working...
window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.allowSubframes = true;
this.registerEventListeners();
TimeLine.log("Added event listeners")
this.executeFirstRunActions();
TimeLine.log("Window-specific first-run actions done")
TimeLine.leave("WindowWrapper constructor done")
}
WindowWrapper.prototype =
{
/**
* Application window this object belongs to.
* @type Window
*/
window: null,
/**
* Current state as displayed for this window.
* @type String
*/
state: null,
/**
* Methods that can be defined at attributes of the hooks element.
* @type Array of String
*/
customMethods: ["getBrowser", "addTab", "getContextMenu", "getToolbox", "getDefaultToolbar", "toolbarInsertBefore"],
/**
* Progress listener used to watch for location changes, if any.
* @type nsIProgressListener
*/
progressListener: null,
/**
* Filter corresponding with "disable on site" menu item (set in fillPopup()).
* @type Filter
*/
siteWhitelist: null,
/**
* Filter corresponding with "disable on site" menu item (set in fillPopup()).
* @type Filter
*/
pageWhitelist: null,
/**
* Data associated with the node currently under mouse pointer (set in updateContextMenu()).
* @type RequestEntry
*/
nodeData: null,
/**
* The document node that nodeData belongs to.
*/
currentNode: null,
/**
* Data associated with the background image currently under mouse pointer (set in updateContextMenu()).
* @type RequestEntry
*/
backgroundData: null,
/**
* Data associated with the frame currently under mouse pointer (set in updateContextMenu()).
* @type RequestEntry
*/
frameData: null,
/**
* The frame that frameData belongs to.
*/
currentFrame: null,
/**
* Window of the detached list of blockable items (might be null or closed).
* @type Window
*/
detachedSidebar: null,
/**
* Binds a function to the object, ensuring that "this" pointer is always set
* correctly.
*/
_bindMethod: function(/**Function*/ method) /**Function*/
{
let me = this;
return function() method.apply(me, arguments);
},
/**
* Retrieves an element by its ID.
*/
E: function(/**String*/ id)
{
let doc = this.window.document;
this.E = function(id) doc.getElementById(id);
return this.E(id);
},
/**
* Initializes abp-hooks element, converts any function attributes to actual
* functions.
*/
initializeHooks: function(hooks)
{
for each (let hook in this.customMethods)
{
let handler = hooks.getAttribute(hook);
this[hook] = hooks[hook] = (handler ? this._bindMethod(new Function(handler)) : null);
}
},
/**
* Makes a copy of the ABP icon's context menu for the toolbar button.
*/
fixupMenus: function()
{
function fixId(node, newId)
{
if (node.nodeType == node.ELEMENT_NODE)
{
if (node.hasAttribute("id"))
node.setAttribute("id", node.getAttribute("id").replace(/abp-status/, newId));
for (let i = 0, len = node.childNodes.length; i < len; i++)
fixId(node.childNodes[i], newId);
}
return node;
}
let menuSource = this.E("abp-status-popup");
let paletteButton = this.getPaletteButton();
let toolbarButton = this.E("abp-toolbarbutton");
let menuItem = this.E("abp-menuitem");
if (toolbarButton)
toolbarButton.appendChild(fixId(menuSource.cloneNode(true), "abp-toolbar"));
if (paletteButton && paletteButton != toolbarButton)
paletteButton.appendChild(fixId(menuSource.cloneNode(true), "abp-toolbar"));
if (menuItem)
menuItem.appendChild(fixId(menuSource.cloneNode(true), "abp-menuitem"));
},
/**
* Attaches event listeners to a window represented by hooks element
*/
registerEventListeners: function()
{
// Palette button elements aren't reachable by ID, create a lookup table
let paletteButtonIDs = {};
let paletteButton = this.getPaletteButton();
if (paletteButton)
{
function getElementIds(element)
{
if (element.hasAttribute("id"))
paletteButtonIDs[element.getAttribute("id")] = element;
for (let child = element.firstChild; child; child = child.nextSibling)
if (child.nodeType == Ci.nsIDOMNode.ELEMENT_NODE)
getElementIds(child);
}
getElementIds(paletteButton);
}
// Go on and register listeners
this.window.addEventListener("unload", removeWindow, false);
for each (let [id, event, handler] in this.eventHandlers)
{
handler = this._bindMethod(handler);
let element = this.E(id);
if (element)
element.addEventListener(event, handler, false);
if (id in paletteButtonIDs)
paletteButtonIDs[id].addEventListener(event, handler, false);
}
let browser = this.getBrowser();
browser.addEventListener("click", this._bindMethod(this.handleLinkClick), true);
// Register progress listener as well if requested
if (!("isDummy" in this.updateState))
{
let dummy = function() {};
this.progressListener =
{
onLocationChange: this._bindMethod(this.updateState),
onProgressChange: dummy,
onSecurityChange: dummy,
onStateChange: dummy,
onStatusChange: dummy,
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference])
};
browser.addProgressListener(this.progressListener);
}
},
/**
* Retrieves the current location of the browser (might return null on failure).
*/
getCurrentLocation: function() /**nsIURI*/
{
if ("currentHeaderData" in this.window && "content-base" in this.window.currentHeaderData)
{
// Thunderbird blog entry
return Utils.unwrapURL(this.window.currentHeaderData["content-base"].headerValue);
}
else if ("currentHeaderData" in this.window && "from" in this.window.currentHeaderData)
{
// Thunderbird mail/newsgroup entry
try
{
let headerParser = Cc["@mozilla.org/messenger/headerparser;1"].getService(Ci.nsIMsgHeaderParser);
let emailAddress = headerParser.extractHeaderAddressMailboxes(this.window.currentHeaderData.from.headerValue);
return Utils.makeURI("mailto:" + emailAddress.replace(/^[\s"]+/, "").replace(/[\s"]+$/, "").replace(/\s/g, "%20"));
}
catch(e)
{
return null;
}
}
else
{
// Regular browser
return Utils.unwrapURL(this.getBrowser().currentURI.clone());
}
},
/**
* Executes window-specific first-run actions if necessary.
*/
executeFirstRunActions: function()
{
// Only execute first-run actions for this window once
if ("doneFirstRunActions " + this.window.location.href in Prefs)
return;
Prefs["doneFirstRunActions " + this.window.location.href] = true;
// Check version we previously executed first-run actions for;
let hooks = this.E("abp-hooks");
let lastVersion = hooks.getAttribute("currentVersion") || "0.0";
if (lastVersion != Prefs.currentVersion)
{
hooks.setAttribute("currentVersion", Prefs.currentVersion);
this.window.document.persist("abp-hooks", "currentVersion");
let needInstall = (Utils.versionComparator.compare(lastVersion, "0.0") <= 0);
if (!needInstall)
{
// Before version 1.1 we didn't add toolbar icon in SeaMonkey, do it now
needInstall = Utils.appID == "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}" &&
Utils.versionComparator.compare(lastVersion, "1.1") < 0;
}
// Add ABP icon to toolbar if necessary
if (needInstall)
Utils.runAsync(this.installToolbarIcon, this);
}
},
/**
* Finds the toolbar button in the toolbar palette.
*/
getPaletteButton: function()
{
let toolbox = (this.getToolbox ? this.getToolbox() : null);
if (!toolbox || !("palette" in toolbox) || !toolbox.palette)
return null;
for (var child = toolbox.palette.firstChild; child; child = child.nextSibling)
if (child.id == "abp-toolbarbutton")
return child;
return null;
},
/**
* Updates displayed state for an application window.
*/
updateState: function()
{
let state = (Prefs.enabled ? "active" : "disabled");
if (state == "active")
{
let location = this.getCurrentLocation();
if (location && Policy.isWhitelisted(location.spec))
state = "whitelisted";
}
this.state = state;
function updateElement(element)
{
if (!element)
return;
if (element.tagName == "statusbarpanel")
element.hidden = !Prefs.showinstatusbar;
else
{
if (element.hasAttribute("context") && Prefs.defaulttoolbaraction == 0)
element.setAttribute("type", "menu");
else
element.setAttribute("type", "menu-button");
}
element.setAttribute("abpstate", state);
};
let status = this.E("abp-status");
if (status)
{
updateElement.call(this, status);
if (Prefs.defaultstatusbaraction == 0)
status.setAttribute("popup", status.getAttribute("context"));
else
status.removeAttribute("popup");
}
let button = this.E("abp-toolbarbutton");
if (button)
updateElement.call(this, button);
updateElement.call(this, this.getPaletteButton());
},
/**
* Sets up hotkeys for the window.
*/
configureKeys: function()
{
if (!hotkeys)
{
hotkeys = {__proto__: null};
let validModifiers =
{
accel: 1,
shift: 2,
ctrl: 4,
control: 4,
alt: 8,
meta: 16,
__proto__: null
};
try
{
let accelKey = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch).getIntPref("ui.key.accelKey");
if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_CONTROL)
validModifiers.ctrl = validModifiers.control = validModifiers.accel;
else if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_ALT)
validModifiers.alt = validModifiers.accel;
else if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_META)
validModifiers.meta = validModifiers.accel;
}
catch(e)
{
Cu.reportError(e);
}
// Find which hotkeys are already taken, convert them to canonical form
let existing = {};
let keys = this.window.document.getElementsByTagName("key");
for (let i = 0; i < keys.length; i++)
{
let key = keys[i];
let keyChar = key.getAttribute("key");
let keyCode = key.getAttribute("keycode");
if (!keyChar && !keyCode)
continue;
let modifiers = 0;
let keyModifiers = key.getAttribute("modifiers");
if (keyModifiers)
{
for each (let modifier in keyModifiers.match(/\w+/g))
{
modifier = modifier.toLowerCase();
if (modifier in validModifiers)
modifiers |= validModifiers[modifier]
}
let canonical = modifiers + " " + (keyChar || keyCode).toUpperCase();
existing[canonical] = true;
}
}
// Find available keys for our prefs
for (let pref in Prefs)
{
let match = /_key$/.exec(pref);
if (match && typeof Prefs[pref] == "string")
{
try
{
let id = match.input.substr(0, match.index);
let result = this.findAvailableKey(id, Prefs[pref], validModifiers, existing);
if (result)
hotkeys[id] = result;
}
catch (e)
{
Cu.reportError(e);
}
}
}
}
// Add elements for all configured hotkeys
for (let id in hotkeys)
{
let [keychar, keycode, modifierString] = hotkeys[id];
let element = this.window.document.createElement("key");
element.setAttribute("id", "abp-key-" + id);
element.setAttribute("command", "abp-command-" + id);
if (keychar)
element.setAttribute("key", keychar);
else
element.setAttribute("keycode", keycode);
element.setAttribute("modifiers", modifierString);
this.E("abp-keyset").appendChild(element);
}
},
/**
* Finds an available hotkey for a value defined in preferences.
*/
findAvailableKey: function(/**String*/ id, /**String*/ value, /**Object*/ validModifiers, /**Object*/ existing) /**Array*/
{
let command = this.E("abp-command-" + id);
if (!command)
return;
for each (let variant in value.split(/\s*,\s*/))
{
if (!variant)
continue;
let modifiers = 0;
let keychar = null;
let keycode = null;
for each (let part in variant.split(/\s+/))
{
if (part.toLowerCase() in validModifiers)
modifiers |= validModifiers[part.toLowerCase()];
else if (part.length == 1)
keychar = part.toUpperCase();
else if ("DOM_VK_" + part.toUpperCase() in Ci.nsIDOMKeyEvent)
keycode = "VK_" + part.toUpperCase();
}
if (!keychar && !keycode)
continue;
let canonical = modifiers + " " + (keychar || keycode);
if (canonical in existing)
continue;
let modifierString = "";
for each (let modifier in ["accel", "shift", "control", "alt", "meta"])
{
if (modifiers & validModifiers[modifier])
{
modifierString += modifier + " ";
modifiers &= ~validModifiers[modifier];
}
}
return [keychar, keycode, modifierString];
}
return null;
},
/**
* Initializes window's context menu.
*/
initContextMenu: function()
{
let contextMenu = this.getContextMenu();
if (contextMenu)
{
contextMenu.addEventListener("popupshowing", this._bindMethod(this.updateContextMenu), false);
contextMenu.addEventListener("popuphidden", this._bindMethod(this.clearContextMenu), false);
// Make sure our context menu items are at the bottom
contextMenu.appendChild(this.E("abp-removeWhitelist-menuitem"));
contextMenu.appendChild(this.E("abp-frame-menuitem"));
contextMenu.appendChild(this.E("abp-object-menuitem"));
contextMenu.appendChild(this.E("abp-media-menuitem"));
contextMenu.appendChild(this.E("abp-image-menuitem"));
}
},
/**
* Checks whether the toolbar icon is currently displayed.
*/
isToolbarIconVisible: function()
{
let tb = this.E("abp-toolbarbutton");
if (!tb || tb.parentNode.localName == "toolbarpalette")
return false;
if (tb.parentNode.collapsed)
return false;
return true;
},
/**
* Makes sure the toolbar button is displayed.
*/
installToolbarIcon: function()
{
let tb = this.E("abp-toolbarbutton");
if (!tb || tb.parentNode.localName == "toolbarpalette")
{
let toolbar = (this.getDefaultToolbar ? this.getDefaultToolbar() : null);
if (!toolbar || typeof toolbar.insertItem != "function")
return;
let insertBefore = (this.toolbarInsertBefore ? this.toolbarInsertBefore() : null);
if (insertBefore && insertBefore.parentNode != toolbar)
insertBefore = null;
toolbar.insertItem("abp-toolbarbutton", insertBefore, null, false);
toolbar.setAttribute("currentset", toolbar.currentSet);
this.window.document.persist(toolbar.id, "currentset");
}
tb = this.E("abp-toolbarbutton");
if (tb && tb.parentNode.collapsed)
{
tb.parentNode.setAttribute("collapsed", "false");
this.window.document.persist(tb.parentNode.id, "collapsed");
}
},
/**
* Removes toolbar button from the toolbar.
*/
hideToolbarIcon: function()
{
let tb = this.E("abp-toolbarbutton");
if (!tb || tb.parentNode.localName != "toolbar")
return;
let toolbar = tb.parentNode;
toolbar.currentSet = toolbar.currentSet.split(",").filter(function(id) id != "abp-toolbarbutton").join(",");
toolbar.setAttribute("currentset", toolbar.currentSet);
this.window.document.persist(toolbar.id, "currentset");
},
/**
* Opens Adblock Plus menu.
*/
openMenu: function()
{
this.installToolbarIcon();
let button = this.E("abp-toolbarbutton");
if (!button)
return;
let toolbar = button.parentNode;
if (toolbar.collapsed)
{
toolbar.setAttribute("collapsed", "false");
this.window.document.persist(toolbar.id, "collapsed");
}
Utils.runAsync(function()
{
button.open = true;
});
},
/**
* Handles browser clicks to intercept clicks on abp: links. This can be
* called either with an event object or with the link target (if it is the
* former then link target will be retrieved from event target).
*/
handleLinkClick: function (/**Event*/ event, /**String*/ linkTarget)
{
if (event)
{
// Ignore right-clicks
if (event.button == 2)
return;
// Search the link associated with the click
let link = event.target;
while (link && !(link instanceof Ci.nsIDOMHTMLAnchorElement))
link = link.parentNode;
if (!link || link.protocol != "abp:")
return;
// This is our link - make sure the browser doesn't handle it
event.preventDefault();
event.stopPropagation();
linkTarget = link.href;
}
let match = /^abp:\/*subscribe\/*\?(.*)/i.exec(linkTarget);
if (!match)
return;
// Decode URL parameters
let title = null;
let url = null;
let mainSubscriptionTitle = null;
let mainSubscriptionURL = null;
for each (let param in match[1].split('&'))
{
let parts = param.split("=", 2);
if (parts.length != 2 || !/\S/.test(parts[1]))
continue;
switch (parts[0])
{
case "title":
title = decodeURIComponent(parts[1]);
break;
case "location":
url = decodeURIComponent(parts[1]);
break;
case "requiresTitle":
mainSubscriptionTitle = decodeURIComponent(parts[1]);
break;
case "requiresLocation":
mainSubscriptionURL = decodeURIComponent(parts[1]);
break;
}
}
if (!url)
return;
// Default title to the URL
if (!title)
title = url;
// Main subscription needs both title and URL
if (mainSubscriptionTitle && !mainSubscriptionURL)
mainSubscriptionTitle = null;
if (mainSubscriptionURL && !mainSubscriptionTitle)
mainSubscriptionURL = null;
// Trim spaces in title and URL
title = title.replace(/^\s+/, "").replace(/\s+$/, "");
url = url.replace(/^\s+/, "").replace(/\s+$/, "");
if (mainSubscriptionURL)
{
mainSubscriptionTitle = mainSubscriptionTitle.replace(/^\s+/, "").replace(/\s+$/, "");
mainSubscriptionURL = mainSubscriptionURL.replace(/^\s+/, "").replace(/\s+$/, "");
}
// Verify that the URL is valid
url = Utils.makeURI(url);
if (!url || (url.scheme != "http" && url.scheme != "https" && url.scheme != "ftp"))
return;
url = url.spec;
if (mainSubscriptionURL)
{
mainSubscriptionURL = Utils.makeURI(mainSubscriptionURL);
if (!mainSubscriptionURL || (mainSubscriptionURL.scheme != "http" && mainSubscriptionURL.scheme != "https" && mainSubscriptionURL.scheme != "ftp"))
mainSubscriptionURL = mainSubscriptionTitle = null;
else
mainSubscriptionURL = mainSubscriptionURL.spec;
}
// Open dialog
let subscription = {url: url, title: title, disabled: false, external: false,
mainSubscriptionTitle: mainSubscriptionTitle, mainSubscriptionURL: mainSubscriptionURL};
this.window.openDialog("chrome://@ADDON_CHROME_NAME@/content/subscriptionSelection.xul", "_blank",
"chrome,centerscreen,resizable,dialog=no", subscription, null);
},
/**
* Updates state of the icon tooltip.
*/
fillTooltip: function(/**Event*/ event)
{
let node = this.window.document.tooltipNode;
if (!node || !node.hasAttribute("tooltip"))
{
event.preventDefault();
return;
}
// Prevent tooltip from overlapping menu
for each (let id in ["abp-toolbar-popup", "abp-status-popup"])
{
let element = this.E(id);
if (element && element.state == "open")
{
event.preventDefault();
return;
}
}
let type = (node.id == "abp-toolbarbutton" ? "toolbar" : "statusbar");
let action = parseInt(Prefs["default" + type + "action"]);
if (isNaN(action))
action = -1;
let actionDescr = this.E("abp-tooltip-action");
actionDescr.hidden = (action < 0 || action > 3);
if (!actionDescr.hidden)
actionDescr.setAttribute("value", Utils.getString("action" + action + "_tooltip"));
let statusDescr = this.E("abp-tooltip-status");
let statusStr = Utils.getString(this.state + "_tooltip");
if (this.state == "active")
{
let [activeSubscriptions, activeFilters] = FilterStorage.subscriptions.reduce(function([subscriptions, filters], current)
{
if (current instanceof SpecialSubscription)
return [subscriptions, filters + current.filters.filter(function(filter) !filter.disabled).length];
else if (!current.disabled && !(Prefs.subscriptions_exceptionscheckbox && current.url == Prefs.subscriptions_exceptionsurl))
return [subscriptions + 1, filters];
else
return [subscriptions, filters]
}, [0, 0]);
statusStr = statusStr.replace(/\?1\?/, activeSubscriptions).replace(/\?2\?/, activeFilters);
}
statusDescr.setAttribute("value", statusStr);
let activeFilters = [];
this.E("abp-tooltip-blocked-label").hidden = (this.state != "active");
this.E("abp-tooltip-blocked").hidden = (this.state != "active");
if (this.state == "active")
{
let stats = RequestNotifier.getWindowStatistics(this.getBrowser().contentWindow);
let blockedStr = Utils.getString("blocked_count_tooltip");
blockedStr = blockedStr.replace(/\?1\?/, stats ? stats.blocked : 0).replace(/\?2\?/, stats ? stats.items : 0);
if (stats && stats.whitelisted + stats.hidden)
{
blockedStr += " " + Utils.getString("blocked_count_addendum");
blockedStr = blockedStr.replace(/\?1\?/, stats.whitelisted).replace(/\?2\?/, stats.hidden);
}
this.E("abp-tooltip-blocked").setAttribute("value", blockedStr);
if (stats)
{
let filterSort = function(a, b)
{
return stats.filters[b] - stats.filters[a];
};
for (let filter in stats.filters)
activeFilters.push(filter);
activeFilters = activeFilters.sort(filterSort);
}
if (activeFilters.length > 0)
{
let filtersContainer = this.E("abp-tooltip-filters");
while (filtersContainer.firstChild)
filtersContainer.removeChild(filtersContainer.firstChild);
for (let i = 0; i < activeFilters.length && i < 3; i++)
{
let descr = filtersContainer.ownerDocument.createElement("description");
descr.setAttribute("value", activeFilters[i] + " (" + stats.filters[activeFilters[i]] + ")");
filtersContainer.appendChild(descr);
}
}
}
this.E("abp-tooltip-filters-label").hidden = (activeFilters.length == 0);
this.E("abp-tooltip-filters").hidden = (activeFilters.length == 0);
this.E("abp-tooltip-more-filters").hidden = (activeFilters.length <= 3);
},
/**
* Updates state of the icon context menu.
*/
fillPopup: function(/**Event*/ event)
{
let popup = event.target;
// Submenu being opened - ignore
let match = /^(abp-(?:toolbar|status|menuitem)-)popup$/.exec(popup.getAttribute("id"));
if (!match)
return;
let prefix = match[1];
let sidebarOpen = this.isSidebarOpen();
this.E(prefix + "opensidebar").hidden = sidebarOpen;
this.E(prefix + "closesidebar").hidden = !sidebarOpen;