diff options
Diffstat (limited to 'application/palemoon/modules')
19 files changed, 4541 insertions, 0 deletions
diff --git a/application/palemoon/modules/AboutHomeUtils.jsm b/application/palemoon/modules/AboutHomeUtils.jsm new file mode 100644 index 000000000..1d4070eaf --- /dev/null +++ b/application/palemoon/modules/AboutHomeUtils.jsm @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ "AboutHomeUtils" ]; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +this.AboutHomeUtils = { + /** + * Returns an object containing the name and searchURL of the original default + * search engine. + */ + get defaultSearchEngine() { + let defaultEngine = Services.search.defaultEngine; + let submission = defaultEngine.getSubmission("_searchTerms_", null, "homepage"); + + return Object.freeze({ + name: defaultEngine.name, + searchURL: submission.uri.spec, + postDataString: submission.postDataString + }); + }, + + /* + * showKnowYourRights - Determines if the user should be shown the + * about:rights notification. The notification should *not* be shown if + * we've already shown the current version, or if the override pref says to + * never show it. The notification *should* be shown if it's never been seen + * before, if a newer version is available, or if the override pref says to + * always show it. + */ + get showKnowYourRights() { + // Look for an unconditional override pref. If set, do what it says. + // (true --> never show, false --> always show) + try { + return !Services.prefs.getBoolPref("browser.rights.override"); + } catch (e) { } + // Ditto, for the legacy EULA pref. + try { + return !Services.prefs.getBoolPref("browser.EULA.override"); + } catch (e) { } + +#ifndef MOZILLA_OFFICIAL + // Non-official builds shouldn't show the notification. + return false; +#endif + + // Look to see if the user has seen the current version or not. + var currentVersion = Services.prefs.getIntPref("browser.rights.version"); + try { + return !Services.prefs.getBoolPref("browser.rights." + currentVersion + ".shown"); + } catch (e) { } + + // Legacy: If the user accepted a EULA, we won't annoy them with the + // equivalent about:rights page until the version changes. + try { + return !Services.prefs.getBoolPref("browser.EULA." + currentVersion + ".accepted"); + } catch (e) { } + + // We haven't shown the notification before, so do so now. + return true; + } +}; diff --git a/application/palemoon/modules/BrowserNewTabPreloader.jsm b/application/palemoon/modules/BrowserNewTabPreloader.jsm new file mode 100644 index 000000000..719a9e05e --- /dev/null +++ b/application/palemoon/modules/BrowserNewTabPreloader.jsm @@ -0,0 +1,436 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["BrowserNewTabPreloader"]; + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window%20id='win'/>"; +const NEWTAB_URL = "about:newtab"; +const PREF_BRANCH = "browser.newtab."; + +// The interval between swapping in a preload docShell and kicking off the +// next preload in the background. +const PRELOADER_INTERVAL_MS = 600; +// The initial delay before we start preloading our first new tab page. The +// timer is started after the first 'browser-delayed-startup' has been sent. +const PRELOADER_INIT_DELAY_MS = 5000; +// The number of miliseconds we'll wait after we received a notification that +// causes us to update our list of browsers and tabbrowser sizes. This acts as +// kind of a damper when too many events are occuring in quick succession. +const PRELOADER_UPDATE_DELAY_MS = 3000; + +const TOPIC_TIMER_CALLBACK = "timer-callback"; +const TOPIC_DELAYED_STARTUP = "browser-delayed-startup-finished"; +const TOPIC_XUL_WINDOW_CLOSED = "xul-window-destroyed"; + +function createTimer(obj, delay) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init(obj, delay, Ci.nsITimer.TYPE_ONE_SHOT); + return timer; +} + +function clearTimer(timer) { + if (timer) { + timer.cancel(); + } + return null; +} + +this.BrowserNewTabPreloader = { + init: function Preloader_init() { + Initializer.start(); + }, + + uninit: function Preloader_uninit() { + Initializer.stop(); + HostFrame.destroy(); + Preferences.uninit(); + HiddenBrowsers.uninit(); + }, + + newTab: function Preloader_newTab(aTab) { + let win = aTab.ownerDocument.defaultView; + if (win.gBrowser) { + let utils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + let {width, height} = utils.getBoundsWithoutFlushing(win.gBrowser); + let hiddenBrowser = HiddenBrowsers.get(width, height) + if (hiddenBrowser) { + return hiddenBrowser.swapWithNewTab(aTab); + } + } + + return false; + } +}; + +Object.freeze(BrowserNewTabPreloader); + +let Initializer = { + _timer: null, + _observing: false, + + start: function Initializer_start() { + Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false); + this._observing = true; + }, + + stop: function Initializer_stop() { + this._timer = clearTimer(this._timer); + + if (this._observing) { + Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP); + this._observing = false; + } + }, + + observe: function Initializer_observe(aSubject, aTopic, aData) { + if (aTopic == TOPIC_DELAYED_STARTUP) { + Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP); + this._observing = false; + this._startTimer(); + } else if (aTopic == TOPIC_TIMER_CALLBACK) { + this._timer = null; + this._startPreloader(); + } + }, + + _startTimer: function Initializer_startTimer() { + this._timer = createTimer(this, PRELOADER_INIT_DELAY_MS); + }, + + _startPreloader: function Initializer_startPreloader() { + Preferences.init(); + if (Preferences.enabled) { + HiddenBrowsers.init(); + } + } +}; + +let Preferences = { + _enabled: null, + _branch: null, + + get enabled() { + if (this._enabled === null) { + this._enabled = this._branch.getBoolPref("preload") && + !this._branch.prefHasUserValue("url"); + } + + return this._enabled; + }, + + init: function Preferences_init() { + this._branch = Services.prefs.getBranch(PREF_BRANCH); + this._branch.addObserver("", this, false); + }, + + uninit: function Preferences_uninit() { + if (this._branch) { + this._branch.removeObserver("", this); + this._branch = null; + } + }, + + observe: function Preferences_observe() { + let prevEnabled = this._enabled; + this._enabled = null; + + if (prevEnabled && !this.enabled) { + HiddenBrowsers.uninit(); + } else if (!prevEnabled && this.enabled) { + HiddenBrowsers.init(); + } + }, +}; + +let HiddenBrowsers = { + _browsers: null, + _updateTimer: null, + + _topics: [ + TOPIC_DELAYED_STARTUP, + TOPIC_XUL_WINDOW_CLOSED + ], + + init: function () { + this._browsers = new Map(); + this._updateBrowserSizes(); + this._topics.forEach(t => Services.obs.addObserver(this, t, false)); + }, + + uninit: function () { + if (this._browsers) { + this._topics.forEach(t => Services.obs.removeObserver(this, t, false)); + this._updateTimer = clearTimer(this._updateTimer); + + for (let [key, browser] of this._browsers) { + browser.destroy(); + } + this._browsers = null; + } + }, + + get: function (width, height) { + // We haven't been initialized, yet. + if (!this._browsers) { + return null; + } + + let key = width + "x" + height; + if (!this._browsers.has(key)) { + // Update all browsers' sizes if we can't find a matching one. + this._updateBrowserSizes(); + } + + // We should now have a matching browser. + if (this._browsers.has(key)) { + return this._browsers.get(key); + } + + // We should never be here. Return the first browser we find. + Cu.reportError("NewTabPreloader: no matching browser found after updating"); + for (let [size, browser] of this._browsers) { + return browser; + } + + // We should really never be here. + Cu.reportError("NewTabPreloader: not even a single browser was found?"); + return null; + }, + + observe: function (subject, topic, data) { + if (topic === TOPIC_TIMER_CALLBACK) { + this._updateTimer = null; + this._updateBrowserSizes(); + } else { + this._updateTimer = clearTimer(this._updateTimer); + this._updateTimer = createTimer(this, PRELOADER_UPDATE_DELAY_MS); + } + }, + + _updateBrowserSizes: function () { + let sizes = this._collectTabBrowserSizes(); + let toRemove = []; + + // Iterate all browsers and check that they + // each can be assigned to one of the sizes. + for (let [key, browser] of this._browsers) { + if (sizes.has(key)) { + // We already have a browser for that size, great! + sizes.delete(key); + } else { + // This browser is superfluous or needs to be resized. + toRemove.push(browser); + this._browsers.delete(key); + } + } + + // Iterate all sizes that we couldn't find a browser for. + for (let [key, {width, height}] of sizes) { + let browser; + if (toRemove.length) { + // Let's just resize one of the superfluous + // browsers and put it back into the map. + browser = toRemove.shift(); + browser.resize(width, height); + } else { + // No more browsers to reuse, create a new one. + browser = new HiddenBrowser(width, height); + } + + this._browsers.set(key, browser); + } + + // Finally, remove all browsers we don't need anymore. + toRemove.forEach(b => b.destroy()); + }, + + _collectTabBrowserSizes: function () { + let sizes = new Map(); + + function tabBrowserBounds() { + let wins = Services.ww.getWindowEnumerator("navigator:browser"); + while (wins.hasMoreElements()) { + let win = wins.getNext(); + if (win.gBrowser) { + let utils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + yield utils.getBoundsWithoutFlushing(win.gBrowser); + } + } + } + + // Collect the sizes of all <tabbrowser>s out there. + for (let {width, height} of tabBrowserBounds()) { + if (width > 0 && height > 0) { + let key = width + "x" + height; + if (!sizes.has(key)) { + sizes.set(key, {width: width, height: height}); + } + } + } + + return sizes; + } +}; + +function HiddenBrowser(width, height) { + this.resize(width, height); + + HostFrame.get().then(aFrame => { + let doc = aFrame.document; + this._browser = doc.createElementNS(XUL_NS, "browser"); + this._browser.setAttribute("type", "content"); + this._browser.setAttribute("src", NEWTAB_URL); + this._applySize(); + doc.getElementById("win").appendChild(this._browser); + }); +} + +HiddenBrowser.prototype = { + _width: null, + _height: null, + _timer: null, + _needsFrameScripts: true, + + get isPreloaded() { + return this._browser && + this._browser.contentDocument && + this._browser.contentDocument.readyState === "complete" && + this._browser.currentURI.spec === NEWTAB_URL; + }, + + swapWithNewTab: function (aTab) { + if (!this.isPreloaded || this._timer) { + return false; + } + + let win = aTab.ownerDocument.defaultView; + let tabbrowser = win.gBrowser; + + if (!tabbrowser) { + return false; + } + + // Swap docShells. + tabbrowser.swapNewTabWithBrowser(aTab, this._browser); + + // Load all default frame scripts. + if (this._needsFrameScripts) { + this._needsFrameScripts = false; + + let mm = aTab.linkedBrowser.messageManager; + mm.loadFrameScript("chrome://browser/content/content.js", true); + mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true); + + if ("TabView" in win) { + mm.loadFrameScript("chrome://browser/content/tabview-content.js", true); + } + } + + // Start a timer that will kick off preloading the next newtab page. + this._timer = createTimer(this, PRELOADER_INTERVAL_MS); + + // Signal that we swapped docShells. + return true; + }, + + observe: function () { + this._timer = null; + + // Start pre-loading the new tab page. + this._browser.loadURI(NEWTAB_URL); + }, + + resize: function (width, height) { + this._width = width; + this._height = height; + this._applySize(); + }, + + _applySize: function () { + if (this._browser) { + this._browser.style.width = this._width + "px"; + this._browser.style.height = this._height + "px"; + } + }, + + destroy: function () { + if (this._browser) { + this._browser.remove(); + this._browser = null; + } + + this._timer = clearTimer(this._timer); + } +}; + +let HostFrame = { + _frame: null, + _deferred: null, + + get hiddenDOMDocument() { + return Services.appShell.hiddenDOMWindow.document; + }, + + get isReady() { + return this.hiddenDOMDocument.readyState === "complete"; + }, + + get: function () { + if (!this._deferred) { + this._deferred = Promise.defer(); + this._create(); + } + + return this._deferred.promise; + }, + + destroy: function () { + if (this._frame) { + if (!Cu.isDeadWrapper(this._frame)) { + this._frame.removeEventListener("load", this, true); + this._frame.remove(); + } + + this._frame = null; + this._deferred = null; + } + }, + + handleEvent: function () { + let contentWindow = this._frame.contentWindow; + if (contentWindow.location.href === XUL_PAGE) { + this._frame.removeEventListener("load", this, true); + this._deferred.resolve(contentWindow); + } else { + contentWindow.location = XUL_PAGE; + } + }, + + _create: function () { + if (this.isReady) { + let doc = this.hiddenDOMDocument; + this._frame = doc.createElementNS(HTML_NS, "iframe"); + this._frame.addEventListener("load", this, true); + doc.documentElement.appendChild(this._frame); + } else { + let flags = Ci.nsIThread.DISPATCH_NORMAL; + Services.tm.currentThread.dispatch(() => this._create(), flags); + } + } +}; diff --git a/application/palemoon/modules/CharsetMenu.jsm b/application/palemoon/modules/CharsetMenu.jsm new file mode 100644 index 000000000..f973088bc --- /dev/null +++ b/application/palemoon/modules/CharsetMenu.jsm @@ -0,0 +1,160 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = [ "CharsetMenu" ]; + +const { classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyGetter(this, "gBundle", function() { + const kUrl = "chrome://browser/locale/charsetMenu.properties"; + return Services.strings.createBundle(kUrl); +}); +/** + * This set contains encodings that are in the Encoding Standard, except: + * - XSS-dangerous encodings (except ISO-2022-JP which is assumed to be + * too common not to be included). + * - x-user-defined, which practically never makes sense as an end-user-chosen + * override. + * - Encodings that IE11 doesn't have in its correspoding menu. + */ +const kEncodings = new Set([ + // Globally relevant + "UTF-8", + "windows-1252", + // Arabic + "windows-1256", + "ISO-8859-6", + // Baltic + "windows-1257", + "ISO-8859-4", + // "ISO-8859-13", // Hidden since not in menu in IE11 + // Central European + "windows-1250", + "ISO-8859-2", + // Chinese, Simplified + "gbk", + "gb18030", + // Chinese, Traditional + "Big5", + // Cyrillic + "windows-1251", + "ISO-8859-5", + "KOI8-R", + "KOI8-U", + "IBM866", // Not in menu in Chromium. Maybe drop this? + // "x-mac-cyrillic", // Not in menu in IE11 or Chromium. + // Greek + "windows-1253", + "ISO-8859-7", + // Hebrew + "windows-1255", + "ISO-8859-8-I", + "ISO-8859-8", + // Japanese + "Shift_JIS", + "EUC-JP", + "ISO-2022-JP", + // Korean + "EUC-KR", + // Thai + "windows-874", + // Turkish + "windows-1254", + // Vietnamese + "windows-1258", + // Hiding rare European encodings that aren't in the menu in IE11 and would + // make the menu messy by sorting all over the place + // "ISO-8859-3", + // "ISO-8859-10", + // "ISO-8859-14", + // "ISO-8859-15", + // "ISO-8859-16", + // "macintosh" +]); + +// Always at the start of the menu, in this order, followed by a separator. +const kPinned = [ + "UTF-8", + "windows-1252" +]; + +this.CharsetMenu = Object.freeze({ + build: function BuildCharsetMenu(event) { + let parent = event.target; + if (parent.lastChild.localName != "menuseparator") { + // Detector menu or charset menu already built + return; + } + let doc = parent.ownerDocument; + + function createItem(encoding) { + let menuItem = doc.createElement("menuitem"); + menuItem.setAttribute("type", "radio"); + menuItem.setAttribute("name", "charsetGroup"); + try { + menuItem.setAttribute("label", gBundle.GetStringFromName(encoding)); + } catch (e) { + // Localization error but put *something* in the menu to recover. + menuItem.setAttribute("label", encoding); + } + try { + menuItem.setAttribute("accesskey", + gBundle.GetStringFromName(encoding + ".key")); + } catch (e) { + // Some items intentionally don't have an accesskey + } + menuItem.setAttribute("id", "charset." + encoding); + return menuItem; + } + + // Clone the set in order to be able to remove the pinned encodings from + // the cloned set. + let encodings = new Set(kEncodings); + for (let encoding of kPinned) { + encodings.delete(encoding); + parent.appendChild(createItem(encoding)); + } + parent.appendChild(doc.createElement("menuseparator")); + let list = []; + for (let encoding of encodings) { + list.push(createItem(encoding)); + } + + list.sort(function (a, b) { + let titleA = a.getAttribute("label"); + let titleB = b.getAttribute("label"); + // Normal sorting sorts the part in parenthesis in an order that + // happens to make the less frequently-used items first. + let index; + if ((index = titleA.indexOf("(")) > -1) { + titleA = titleA.substring(0, index); + } + if ((index = titleB.indexOf("(")) > -1) { + titleA = titleB.substring(0, index); + } + let comp = titleA.localeCompare(titleB); + if (comp) { + return comp; + } + // secondarily reverse sort by encoding name to sort "windows" or + // "shift_jis" first. This works regardless of localization, because + // the ids aren't localized. + let idA = a.getAttribute("id"); + let idB = b.getAttribute("id"); + if (idA < idB) { + return 1; + } + if (idB < idA) { + return -1; + } + return 0; + }); + + for (let item of list) { + parent.appendChild(item); + } + }, +});
\ No newline at end of file diff --git a/application/palemoon/modules/FormSubmitObserver.jsm b/application/palemoon/modules/FormSubmitObserver.jsm new file mode 100644 index 000000000..72c5ca601 --- /dev/null +++ b/application/palemoon/modules/FormSubmitObserver.jsm @@ -0,0 +1,251 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Handles the validation callback from nsIFormFillController and + * the display of the help panel on invalid elements. + */ + +"use strict"; + +let Cc = Components.classes; +let Ci = Components.interfaces; +let Cu = Components.utils; + +let HTMLInputElement = Ci.nsIDOMHTMLInputElement; +let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement; +let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement; +let HTMLButtonElement = Ci.nsIDOMHTMLButtonElement; + +this.EXPORTED_SYMBOLS = [ "FormSubmitObserver" ]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/BrowserUtils.jsm"); + +function FormSubmitObserver(aWindow, aTabChildGlobal) { + this.init(aWindow, aTabChildGlobal); +} + +FormSubmitObserver.prototype = +{ + _validationMessage: "", + _content: null, + _element: null, + + /* + * Public apis + */ + + init: function(aWindow, aTabChildGlobal) + { + this._content = aWindow; + this._tab = aTabChildGlobal; + this._mm =
+ this._content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .sameTypeRootTreeItem
+ .QueryInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); + + // nsIFormSubmitObserver callback about invalid forms. See HTMLFormElement + // for details. + Services.obs.addObserver(this, "invalidformsubmit", false); + this._tab.addEventListener("pageshow", this, false); + this._tab.addEventListener("unload", this, false); + }, + + uninit: function() + { + Services.obs.removeObserver(this, "invalidformsubmit"); + this._content.removeEventListener("pageshow", this, false); + this._content.removeEventListener("unload", this, false); + this._mm = null; + this._element = null; + this._content = null; + this._tab = null; + }, + + /* + * Events + */ + + handleEvent: function (aEvent) { + switch (aEvent.type) { + case "pageshow": + if (this._isRootDocumentEvent(aEvent)) { + this._hidePopup(); + } + break; + case "unload": + this.uninit(); + break; + case "input": + this._onInput(aEvent); + break; + case "blur": + this._onBlur(aEvent); + break; + } + }, + + /* + * nsIFormSubmitObserver + */ + + notifyInvalidSubmit : function (aFormElement, aInvalidElements) + { + // We are going to handle invalid form submission attempt by focusing the + // first invalid element and show the corresponding validation message in a + // panel attached to the element. + if (!aInvalidElements.length) { + return; + } + + // Insure that this is the FormSubmitObserver associated with the form + // element / window this notification is about. + if (this._content != aFormElement.ownerDocument.defaultView.top.document.defaultView) { + return; + } + + let element = aInvalidElements.queryElementAt(0, Ci.nsISupports); + if (!(element instanceof HTMLInputElement || + element instanceof HTMLTextAreaElement || + element instanceof HTMLSelectElement || + element instanceof HTMLButtonElement)) { + return; + } + + // Don't connect up to the same element more than once. + if (this._element == element) { + this._showPopup(element); + return; + } + this._element = element; + + element.focus(); + + this._validationMessage = element.validationMessage; + + // Watch for input changes which may change the validation message. + element.addEventListener("input", this, false); + + // Watch for focus changes so we can disconnect our listeners and + // hide the popup. + element.addEventListener("blur", this, false); + + this._showPopup(element); + }, + + /* + * Internal + */ +
+ /* + * Handles input changes on the form element we've associated a popup + * with. Updates the validation message or closes the popup if form data + * becomes valid. + */ + _onInput: function (aEvent) { + let element = aEvent.originalTarget; + + // If the form input is now valid, hide the popup. + if (element.validity.valid) { + this._hidePopup(); + return; + } + + // If the element is still invalid for a new reason, we should update + // the popup error message. + if (this._validationMessage != element.validationMessage) { + this._validationMessage = element.validationMessage; + this._showPopup(element); + } + }, + + /* + * Blur event handler in which we disconnect from the form element and + * hide the popup. + */ + _onBlur: function (aEvent) { + aEvent.originalTarget.removeEventListener("input", this, false); + aEvent.originalTarget.removeEventListener("blur", this, false); + this._element = null; + this._hidePopup(); + }, + + /* + * Send the show popup message to chrome with appropriate position + * information. Can be called repetitively to update the currently + * displayed popup position and text. + */ + _showPopup: function (aElement) { + // Collect positional information and show the popup + let panelData = {}; + + panelData.message = this._validationMessage; + + // Note, this is relative to the browser and needs to be translated + // in chrome. + panelData.contentRect = this._msgRect(aElement); + + // We want to show the popup at the middle of checkbox and radio buttons + // and where the content begin for the other elements. + let offset = 0; + let position = ""; + + if (aElement.tagName == 'INPUT' && + (aElement.type == 'radio' || aElement.type == 'checkbox')) { + panelData.position = "bottomcenter topleft"; + } else { + let win = aElement.ownerDocument.defaultView; + let style = win.getComputedStyle(aElement, null); + if (style.direction == 'rtl') { + offset = parseInt(style.paddingRight) + parseInt(style.borderRightWidth); + } else { + offset = parseInt(style.paddingLeft) + parseInt(style.borderLeftWidth); + } + let zoomFactor = this._getWindowUtils().fullZoom; + panelData.offset = Math.round(offset * zoomFactor); + panelData.position = "after_start"; + } + this._mm.sendAsyncMessage("FormValidation:ShowPopup", panelData); + }, + + _hidePopup: function () { + this._mm.sendAsyncMessage("FormValidation:HidePopup", {}); + }, + + _getWindowUtils: function () {
+ return this._content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ }, + + _isRootDocumentEvent: function (aEvent) { + if (this._content == null) { + return true; + } + let target = aEvent.originalTarget; + return (target == this._content.document || + (target.ownerDocument && target.ownerDocument == this._content.document)); + }, + + /* + * Return a message manager rect for the element's bounding client rect + * in top level browser coords. + */ + _msgRect: function (aElement) { + let domRect = aElement.getBoundingClientRect(); + let zoomFactor = this._getWindowUtils().fullZoom; + let { offsetX, offsetY } = BrowserUtils.offsetToTopLevelWindow(this._content, aElement); + return { + left: (domRect.left + offsetX) * zoomFactor, + top: (domRect.top + offsetY) * zoomFactor, + width: domRect.width * zoomFactor, + height: domRect.height * zoomFactor + }; + }, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]) +}; diff --git a/application/palemoon/modules/FormValidationHandler.jsm b/application/palemoon/modules/FormValidationHandler.jsm new file mode 100644 index 000000000..05be510e1 --- /dev/null +++ b/application/palemoon/modules/FormValidationHandler.jsm @@ -0,0 +1,157 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Chrome side handling of form validation popup. + */ + +"use strict"; + +let Cc = Components.classes; +let Ci = Components.interfaces; +let Cu = Components.utils; + +this.EXPORTED_SYMBOLS = [ "FormValidationHandler" ]; + +Cu.import("resource://gre/modules/Services.jsm"); + +let FormValidationHandler = +{ + _panel: null, + _anchor: null, + + /* + * Public apis + */ + + init: function () { + let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager); + mm.addMessageListener("FormValidation:ShowPopup", this); + mm.addMessageListener("FormValidation:HidePopup", this); + }, + + uninit: function () { + let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager); + mm.removeMessageListener("FormValidation:ShowPopup", this); + mm.removeMessageListener("FormValidation:HidePopup", this); + this._panel = null; + this._anchor = null; + }, + + hidePopup: function () { + this._hidePopup(); + }, + + /* + * Events + */ + + receiveMessage: function (aMessage) { + let window = aMessage.target.ownerDocument.defaultView; + let json = aMessage.json; + let tabBrowser = window.gBrowser; + switch (aMessage.name) { + case "FormValidation:ShowPopup": + // target is the <browser>, make sure we're receiving a message + // from the foreground tab. + if (tabBrowser && aMessage.target != tabBrowser.selectedBrowser) { + return; + } + this._showPopup(window, json); + break; + case "FormValidation:HidePopup": + this._hidePopup(); + break; + } + }, + + observe: function (aSubject, aTopic, aData) { + this._hidePopup(); + }, + + handleEvent: function (aEvent) { + switch (aEvent.type) { + case "FullZoomChange": + case "TextZoomChange": + case "ZoomChangeUsingMouseWheel": + case "scroll": + this._hidePopup(); + break; + case "popuphiding": + this._onPopupHiding(aEvent); + break; + } + }, + + /* + * Internal + */ + + _onPopupHiding: function (aEvent) { + aEvent.originalTarget.removeEventListener("popuphiding", this, true); + let tabBrowser = aEvent.originalTarget.ownerDocument.getElementById("content"); + tabBrowser.selectedBrowser.removeEventListener("scroll", this, true); + tabBrowser.selectedBrowser.removeEventListener("FullZoomChange", this, false); + tabBrowser.selectedBrowser.removeEventListener("TextZoomChange", this, false); + tabBrowser.selectedBrowser.removeEventListener("ZoomChangeUsingMouseWheel", this, false); + + this._panel.hidden = true; + this._panel = null; + this._anchor.hidden = true; + this._anchor = null; + }, + + /* + * Shows the form validation popup at a specified position or updates the + * messaging and position if the popup is already displayed. + * + * @aWindow - the chrome window + * @aPanelData - Object that contains popup information + * aPanelData stucture detail: + * contentRect - the bounding client rect of the target element. If + * content is remote, this is relative to the browser, otherwise its + * relative to the window. + * position - popup positional string constants. + * message - the form element validation message text. + */ + _showPopup: function (aWindow, aPanelData) { + let previouslyShown = !!this._panel; + this._panel = aWindow.document.getElementById("invalid-form-popup"); + this._panel.firstChild.textContent = aPanelData.message; + this._panel.hidden = false; + + let tabBrowser = aWindow.gBrowser; + this._anchor = tabBrowser.formValidationAnchor; + this._anchor.left = aPanelData.contentRect.left; + this._anchor.top = aPanelData.contentRect.top; + this._anchor.width = aPanelData.contentRect.width; + this._anchor.height = aPanelData.contentRect.height; + this._anchor.hidden = false; + + // Display the panel if it isn't already visible. + if (!previouslyShown) { + // Cleanup after the popup is hidden + this._panel.addEventListener("popuphiding", this, true); + + // Hide if the user scrolls the page + tabBrowser.selectedBrowser.addEventListener("scroll", this, true); + tabBrowser.selectedBrowser.addEventListener("FullZoomChange", this, false); + tabBrowser.selectedBrowser.addEventListener("TextZoomChange", this, false); + tabBrowser.selectedBrowser.addEventListener("ZoomChangeUsingMouseWheel", this, false); + + // Open the popup + this._panel.openPopup(this._anchor, aPanelData.position, 0, 0, false); + } + }, + + /* + * Hide the popup if currently displayed. Will fire an event to onPopupHiding + * above if visible. + */ + _hidePopup: function () { + if (this._panel) { + this._panel.hidePopup(); + } + } +}; diff --git a/application/palemoon/modules/NetworkPrioritizer.jsm b/application/palemoon/modules/NetworkPrioritizer.jsm new file mode 100644 index 000000000..ea4a87790 --- /dev/null +++ b/application/palemoon/modules/NetworkPrioritizer.jsm @@ -0,0 +1,179 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This module adjusts network priority for tabs in a way that gives 'important' + * tabs a higher priority. There are 3 levels of priority. Each is listed below + * with the priority adjustment used. + * + * Highest (-10): Selected tab in the focused window. + * Medium (0): Background tabs in the focused window. + * Selected tab in background windows. + * Lowest (+10): Background tabs in background windows. + */ + +this.EXPORTED_SYMBOLS = ["trackBrowserWindow"]; + +const Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + + +// Lazy getters +XPCOMUtils.defineLazyServiceGetter(this, "_focusManager", + "@mozilla.org/focus-manager;1", + "nsIFocusManager"); + + +// Constants +const TAB_EVENTS = ["TabOpen", "TabSelect"]; +const WINDOW_EVENTS = ["activate", "unload"]; +// PRIORITY DELTA is -10 because lower priority value is actually a higher priority +const PRIORITY_DELTA = -10; + + +// Variables +let _lastFocusedWindow = null; +let _windows = []; + + +// Exported symbol +this.trackBrowserWindow = function trackBrowserWindow(aWindow) { + WindowHelper.addWindow(aWindow); +} + + +// Global methods +function _handleEvent(aEvent) { + switch (aEvent.type) { + case "TabOpen": + BrowserHelper.onOpen(aEvent.target.linkedBrowser); + break; + case "TabSelect": + BrowserHelper.onSelect(aEvent.target.linkedBrowser); + break; + case "activate": + WindowHelper.onActivate(aEvent.target); + break; + case "unload": + WindowHelper.removeWindow(aEvent.currentTarget); + break; + } +} + + +// Methods that impact a browser. Put into single object for organization. +let BrowserHelper = { + onOpen: function NP_BH_onOpen(aBrowser) { + // If the tab is in the focused window, leave priority as it is + if (aBrowser.ownerDocument.defaultView != _lastFocusedWindow) + this.decreasePriority(aBrowser); + }, + + onSelect: function NP_BH_onSelect(aBrowser) { + let windowEntry = WindowHelper.getEntry(aBrowser.ownerDocument.defaultView); + if (windowEntry.lastSelectedBrowser) + this.decreasePriority(windowEntry.lastSelectedBrowser); + this.increasePriority(aBrowser); + + windowEntry.lastSelectedBrowser = aBrowser; + }, + + increasePriority: function NP_BH_increasePriority(aBrowser) { + aBrowser.adjustPriority(PRIORITY_DELTA); + }, + + decreasePriority: function NP_BH_decreasePriority(aBrowser) { + aBrowser.adjustPriority(PRIORITY_DELTA * -1); + } +}; + + +// Methods that impact a window. Put into single object for organization. +let WindowHelper = { + addWindow: function NP_WH_addWindow(aWindow) { + // Build internal data object + _windows.push({ window: aWindow, lastSelectedBrowser: null }); + + // Add event listeners + TAB_EVENTS.forEach(function(event) { + aWindow.gBrowser.tabContainer.addEventListener(event, _handleEvent, false); + }); + WINDOW_EVENTS.forEach(function(event) { + aWindow.addEventListener(event, _handleEvent, false); + }); + + // This gets called AFTER activate event, so if this is the focused window + // we want to activate it. Otherwise, deprioritize it. + if (aWindow == _focusManager.activeWindow) + this.handleFocusedWindow(aWindow); + else + this.decreasePriority(aWindow); + + // Select the selected tab + BrowserHelper.onSelect(aWindow.gBrowser.selectedBrowser); + }, + + removeWindow: function NP_WH_removeWindow(aWindow) { + if (aWindow == _lastFocusedWindow) + _lastFocusedWindow = null; + + // Delete this window from our tracking + _windows.splice(this.getEntryIndex(aWindow), 1); + + // Remove the event listeners + TAB_EVENTS.forEach(function(event) { + aWindow.gBrowser.tabContainer.removeEventListener(event, _handleEvent, false); + }); + WINDOW_EVENTS.forEach(function(event) { + aWindow.removeEventListener(event, _handleEvent, false); + }); + }, + + onActivate: function NP_WH_onActivate(aWindow, aHasFocus) { + // If this window was the last focused window, we don't need to do anything + if (aWindow == _lastFocusedWindow) + return; + + // handleFocusedWindow will deprioritize the current window + this.handleFocusedWindow(aWindow); + + // Lastly we should increase priority for this window + this.increasePriority(aWindow); + }, + + handleFocusedWindow: function NP_WH_handleFocusedWindow(aWindow) { + // If we have a last focused window, we need to deprioritize it first + if (_lastFocusedWindow) + this.decreasePriority(_lastFocusedWindow); + + // aWindow is now focused + _lastFocusedWindow = aWindow; + }, + + // Auxiliary methods + increasePriority: function NP_WH_increasePriority(aWindow) { + aWindow.gBrowser.browsers.forEach(function(aBrowser) { + BrowserHelper.increasePriority(aBrowser); + }); + }, + + decreasePriority: function NP_WH_decreasePriority(aWindow) { + aWindow.gBrowser.browsers.forEach(function(aBrowser) { + BrowserHelper.decreasePriority(aBrowser); + }); + }, + + getEntry: function NP_WH_getEntry(aWindow) { + return _windows[this.getEntryIndex(aWindow)]; + }, + + getEntryIndex: function NP_WH_getEntryAtIndex(aWindow) { + // Assumes that every object has a unique window & it's in the array + for (let i = 0; i < _windows.length; i++) + if (_windows[i].window == aWindow) + return i; + } +}; + diff --git a/application/palemoon/modules/PageMenu.jsm b/application/palemoon/modules/PageMenu.jsm new file mode 100644 index 000000000..d01f62601 --- /dev/null +++ b/application/palemoon/modules/PageMenu.jsm @@ -0,0 +1,238 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = ["PageMenu"]; + +this.PageMenu = function PageMenu() { +} + +PageMenu.prototype = { + PAGEMENU_ATTR: "pagemenu", + GENERATEDITEMID_ATTR: "generateditemid", + + _popup: null, + _builder: null, + + // Given a target node, get the context menu for it or its ancestor. + getContextMenu: function(aTarget) { + let target = aTarget; + while (target) { + let contextMenu = target.contextMenu; + if (contextMenu) { + return contextMenu; + } + target = target.parentNode; + } + + return null; + }, + + // Given a target node, generate a JSON object for any context menu + // associated with it, or null if there is no context menu. + maybeBuild: function(aTarget) { + let pageMenu = this.getContextMenu(aTarget); + if (!pageMenu) { + return null; + } + + pageMenu.QueryInterface(Components.interfaces.nsIHTMLMenu); + pageMenu.sendShowEvent(); + // the show event is not cancelable, so no need to check a result here + + this._builder = pageMenu.createBuilder(); + if (!this._builder) { + return null; + } + + pageMenu.build(this._builder); + + // This serializes then parses again, however this could be avoided in + // the single-process case with further improvement. + let menuString = this._builder.toJSONString(); + if (!menuString) { + return null; + } + + return JSON.parse(menuString); + }, + + // Given a JSON menu object and popup, add the context menu to the popup. + buildAndAttachMenuWithObject: function(aMenu, aBrowser, aPopup) { + if (!aMenu) { + return false; + } + + let insertionPoint = this.getInsertionPoint(aPopup); + if (!insertionPoint) { + return false; + } + + let fragment = aPopup.ownerDocument.createDocumentFragment(); + this.buildXULMenu(aMenu, fragment); + + let pos = insertionPoint.getAttribute(this.PAGEMENU_ATTR); + if (pos == "start") { + insertionPoint.insertBefore(fragment, + insertionPoint.firstChild); + } else if (pos.startsWith("#")) { + insertionPoint.insertBefore(fragment, insertionPoint.querySelector(pos)); + } else { + insertionPoint.appendChild(fragment); + } + + this._popup = aPopup; + + this._popup.addEventListener("command", this); + this._popup.addEventListener("popuphidden", this); + + return true; + }, + + // Construct the XUL menu structure for a given JSON object. + buildXULMenu: function(aNode, aElementForAppending) { + let document = aElementForAppending.ownerDocument; + + let children = aNode.children; + for (let child of children) { + let menuitem; + switch (child.type) { + case "menuitem": + if (!child.id) { + continue; // Ignore children without ids + } + + menuitem = document.createElement("menuitem"); + if (child.checkbox) { + menuitem.setAttribute("type", "checkbox"); + if (child.checked) { + menuitem.setAttribute("checked", "true"); + } + } + + if (child.label) { + menuitem.setAttribute("label", child.label); + } + if (child.icon) { + menuitem.setAttribute("image", child.icon); + menuitem.className = "menuitem-iconic"; + } + if (child.disabled) { + menuitem.setAttribute("disabled", true); + } + + break; + + case "separator": + menuitem = document.createElement("menuseparator"); + break; + + case "menu": + menuitem = document.createElement("menu"); + if (child.label) { + menuitem.setAttribute("label", child.label); + } + + let menupopup = document.createElement("menupopup"); + menuitem.appendChild(menupopup); + + this.buildXULMenu(child, menupopup); + break; + } + + menuitem.setAttribute(this.GENERATEDITEMID_ATTR, child.id ? child.id : 0); + aElementForAppending.appendChild(menuitem); + } + }, + + // Called when the generated menuitem is executed. + handleEvent: function(event) { + let type = event.type; + let target = event.target; + if (type == "command" && target.hasAttribute(this.GENERATEDITEMID_ATTR)) { + // If a builder is assigned, call click on it directly. Otherwise, this is + // likely a menu with data from another process, so send a message to the + // browser to execute the menuitem. + if (this._builder) { + this._builder.click(target.getAttribute(this.GENERATEDITEMID_ATTR)); + } + } else if (type == "popuphidden" && this._popup == target) { + this.removeGeneratedContent(this._popup); + + this._popup.removeEventListener("popuphidden", this); + this._popup.removeEventListener("command", this); + + this._popup = null; + this._builder = null; + } + }, + + // Get the first child of the given element with the given tag name. + getImmediateChild: function(element, tag) { + let child = element.firstChild; + while (child) { + if (child.localName == tag) { + return child; + } + child = child.nextSibling; + } + return null; + }, + + // Return the location where the generated items should be inserted into the + // given popup. They should be inserted as the next sibling of the returned + // element. + getInsertionPoint: function(aPopup) { + if (aPopup.hasAttribute(this.PAGEMENU_ATTR)) + return aPopup; + + let element = aPopup.firstChild; + while (element) { + if (element.localName == "menu") { + let popup = this.getImmediateChild(element, "menupopup"); + if (popup) { + let result = this.getInsertionPoint(popup); + if (result) { + return result; + } + } + } + element = element.nextSibling; + } + + return null; + }, + + // Returns true if custom menu items were present. + maybeBuildAndAttachMenu: function(aTarget, aPopup) { + let menuObject = this.maybeBuild(aTarget); + if (!menuObject) { + return false; + } + + return this.buildAndAttachMenuWithObject(menuObject, null, aPopup); + }, + + // Remove the generated content from the given popup. + removeGeneratedContent: function(aPopup) { + let ungenerated = []; + ungenerated.push(aPopup); + + let count; + while (0 != (count = ungenerated.length)) { + let last = count - 1; + let element = ungenerated[last]; + ungenerated.splice(last, 1); + + let i = element.childNodes.length; + while (i-- > 0) { + let child = element.childNodes[i]; + if (!child.hasAttribute(this.GENERATEDITEMID_ATTR)) { + ungenerated.push(child); + continue; + } + element.removeChild(child); + } + } + } +} diff --git a/application/palemoon/modules/PopupNotifications.jsm b/application/palemoon/modules/PopupNotifications.jsm new file mode 100644 index 000000000..9b2e8e5d1 --- /dev/null +++ b/application/palemoon/modules/PopupNotifications.jsm @@ -0,0 +1,843 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = ["PopupNotifications"]; + +var Cc = Components.classes, Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +const NOTIFICATION_EVENT_DISMISSED = "dismissed"; +const NOTIFICATION_EVENT_REMOVED = "removed"; +const NOTIFICATION_EVENT_SHOWING = "showing"; +const NOTIFICATION_EVENT_SHOWN = "shown"; + +const ICON_SELECTOR = ".notification-anchor-icon"; +const ICON_ATTRIBUTE_SHOWING = "showing"; + +const PREF_SECURITY_DELAY = "security.notification_enable_delay"; + +let popupNotificationsMap = new WeakMap(); +let gNotificationParents = new WeakMap; + +function getAnchorFromBrowser(aBrowser) { + let anchor = aBrowser.getAttribute("popupnotificationanchor") || + aBrowser.popupnotificationanchor; + if (anchor) { + if (anchor instanceof Ci.nsIDOMXULElement) { + return anchor; + } + return aBrowser.ownerDocument.getElementById(anchor); + } + return null; +} + +/** + * Notification object describes a single popup notification. + * + * @see PopupNotifications.show() + */ +function Notification(id, message, anchorID, mainAction, secondaryActions, + browser, owner, options) { + this.id = id; + this.message = message; + this.anchorID = anchorID; + this.mainAction = mainAction; + this.secondaryActions = secondaryActions || []; + this.browser = browser; + this.owner = owner; + this.options = options || {}; +} + +Notification.prototype = { + + id: null, + message: null, + anchorID: null, + mainAction: null, + secondaryActions: null, + browser: null, + owner: null, + options: null, + timeShown: null, + + /** + * Removes the notification and updates the popup accordingly if needed. + */ + remove: function Notification_remove() { + this.owner.remove(this); + }, + + get anchorElement() { + let iconBox = this.owner.iconBox; + + let anchorElement = getAnchorFromBrowser(this.browser); + + if (!iconBox) + return anchorElement; + + if (!anchorElement && this.anchorID) + anchorElement = iconBox.querySelector("#"+this.anchorID); + + // Use a default anchor icon if it's available + if (!anchorElement) + anchorElement = iconBox.querySelector("#default-notification-icon") || + iconBox; + + return anchorElement; + }, + + reshow: function() { + this.owner._reshowNotifications(this.anchorElement, this.browser); + } +}; + +/** + * The PopupNotifications object manages popup notifications for a given browser + * window. + * @param tabbrowser + * window's <xul:tabbrowser/>. Used to observe tab switching events and + * for determining the active browser element. + * @param panel + * The <xul:panel/> element to use for notifications. The panel is + * populated with <popupnotification> children and displayed it as + * needed. + * @param iconBox + * Reference to a container element that should be hidden or + * unhidden when notifications are hidden or shown. It should be the + * parent of anchor elements whose IDs are passed to show(). + * It is used as a fallback popup anchor if notifications specify + * invalid or non-existent anchor IDs. + */ +this.PopupNotifications = function PopupNotifications(tabbrowser, panel, iconBox) { + if (!(tabbrowser instanceof Ci.nsIDOMXULElement)) + throw "Invalid tabbrowser"; + if (iconBox && !(iconBox instanceof Ci.nsIDOMXULElement)) + throw "Invalid iconBox"; + if (!(panel instanceof Ci.nsIDOMXULElement)) + throw "Invalid panel"; + + this.window = tabbrowser.ownerDocument.defaultView; + this.panel = panel; + this.tabbrowser = tabbrowser; + this.iconBox = iconBox; + this.buttonDelay = Services.prefs.getIntPref(PREF_SECURITY_DELAY); + + this.panel.addEventListener("popuphidden", this, true); + + this.window.addEventListener("activate", this, true); + if (this.tabbrowser.tabContainer) + this.tabbrowser.tabContainer.addEventListener("TabSelect", this, true); +} + +PopupNotifications.prototype = { + + window: null, + panel: null, + tabbrowser: null, + + _iconBox: null, + set iconBox(iconBox) { + // Remove the listeners on the old iconBox, if needed + if (this._iconBox) { + this._iconBox.removeEventListener("click", this, false); + this._iconBox.removeEventListener("keypress", this, false); + } + this._iconBox = iconBox; + if (iconBox) { + iconBox.addEventListener("click", this, false); + iconBox.addEventListener("keypress", this, false); + } + }, + get iconBox() { + return this._iconBox; + }, + + /** + * Retrieve a Notification object associated with the browser/ID pair. + * @param id + * The Notification ID to search for. + * @param browser + * The browser whose notifications should be searched. If null, the + * currently selected browser's notifications will be searched. + * + * @returns the corresponding Notification object, or null if no such + * notification exists. + */ + getNotification: function PopupNotifications_getNotification(id, browser) { + let n = null; + let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser); + notifications.some(function(x) x.id == id && (n = x)); + return n; + }, + + /** + * Adds a new popup notification. + * @param browser + * The <xul:browser> element associated with the notification. Must not + * be null. + * @param id + * A unique ID that identifies the type of notification (e.g. + * "geolocation"). Only one notification with a given ID can be visible + * at a time. If a notification already exists with the given ID, it + * will be replaced. + * @param message + * The text to be displayed in the notification. + * @param anchorID + * The ID of the element that should be used as this notification + * popup's anchor. May be null, in which case the notification will be + * anchored to the iconBox. + * @param mainAction + * A JavaScript object literal describing the notification button's + * action. If present, it must have the following properties: + * - label (string): the button's label. + * - accessKey (string): the button's accessKey. + * - callback (function): a callback to be invoked when the button is + * pressed. + * If null, the notification will not have a button, and + * secondaryActions will be ignored. + * @param secondaryActions + * An optional JavaScript array describing the notification's alternate + * actions. The array should contain objects with the same properties + * as mainAction. These are used to populate the notification button's + * dropdown menu. + * @param options + * An options JavaScript object holding additional properties for the + * notification. The following properties are currently supported: + * persistence: An integer. The notification will not automatically + * dismiss for this many page loads. + * timeout: A time in milliseconds. The notification will not + * automatically dismiss before this time. + * persistWhileVisible: + * A boolean. If true, a visible notification will always + * persist across location changes. + * dismissed: Whether the notification should be added as a dismissed + * notification. Dismissed notifications can be activated + * by clicking on their anchorElement. + * eventCallback: + * Callback to be invoked when the notification changes + * state. The callback's first argument is a string + * identifying the state change: + * "dismissed": notification has been dismissed by the + * user (e.g. by clicking away or switching + * tabs) + * "removed": notification has been removed (due to + * location change or user action) + * "shown": notification has been shown (this can be fired + * multiple times as notifications are dismissed + * and re-shown) + * neverShow: Indicate that no popup should be shown for this + * notification. Useful for just showing the anchor icon. + * removeOnDismissal: + * Notifications with this parameter set to true will be + * removed when they would have otherwise been dismissed + * (i.e. any time the popup is closed due to user + * interaction). + * popupIconURL: + * A string. URL of the image to be displayed in the popup. + * Normally specified in CSS using list-style-image and the + * .popup-notification-icon[popupid=...] selector. + * learnMoreURL: + * A string URL. Setting this property will make the + * prompt display a "Learn More" link that, when clicked, + * opens the URL in a new tab. + * @returns the Notification object corresponding to the added notification. + */ + show: function PopupNotifications_show(browser, id, message, anchorID, + mainAction, secondaryActions, options) { + function isInvalidAction(a) { + return !a || !(typeof(a.callback) == "function") || !a.label || !a.accessKey; + } + + if (!browser) + throw "PopupNotifications_show: invalid browser"; + if (!id) + throw "PopupNotifications_show: invalid ID"; + if (mainAction && isInvalidAction(mainAction)) + throw "PopupNotifications_show: invalid mainAction"; + if (secondaryActions && secondaryActions.some(isInvalidAction)) + throw "PopupNotifications_show: invalid secondaryActions"; + + let notification = new Notification(id, message, anchorID, mainAction, + secondaryActions, browser, this, options); + + if (options && options.dismissed) + notification.dismissed = true; + + let existingNotification = this.getNotification(id, browser); + if (existingNotification) + this._remove(existingNotification); + + let notifications = this._getNotificationsForBrowser(browser); + notifications.push(notification); + + let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + if (browser.docShell.isActive && fm.activeWindow == this.window) { + // show panel now + this._update(notifications, notification.anchorElement, true); + } else { + // Otherwise, update() will display the notification the next time the + // relevant tab/window is selected. + + // If the tab is selected but the window is in the background, let the OS + // tell the user that there's a notification waiting in that window. + // At some point we might want to do something about background tabs here + // too. When the user switches to this window, we'll show the panel if + // this browser is a tab (thus showing the anchor icon). For + // non-tabbrowser browsers, we need to make the icon visible now or the + // user will not be able to open the panel. + if (!notification.dismissed && browser.docShell.isActive) { + this.window.getAttention(); + if (notification.anchorElement.parentNode != this.iconBox) { + notification.anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true"); + } + } + + // Notify observers that we're not showing the popup (useful for testing) + this._notify("backgroundShow"); + } + + return notification; + }, + + /** + * Returns true if the notification popup is currently being displayed. + */ + get isPanelOpen() { + let panelState = this.panel.state; + + return panelState == "showing" || panelState == "open"; + }, + + /** + * Called by the consumer to indicate that a browser's location has changed, + * so that we can update the active notifications accordingly. + */ + locationChange: function PopupNotifications_locationChange(aBrowser) { + if (!aBrowser) + throw "PopupNotifications_locationChange: invalid browser"; + + let notifications = this._getNotificationsForBrowser(aBrowser); + + notifications = notifications.filter(function (notification) { + // The persistWhileVisible option allows an open notification to persist + // across location changes + if (notification.options.persistWhileVisible && + this.isPanelOpen) { + if ("persistence" in notification.options && + notification.options.persistence) + notification.options.persistence--; + return true; + } + + // The persistence option allows a notification to persist across multiple + // page loads + if ("persistence" in notification.options && + notification.options.persistence) { + notification.options.persistence--; + return true; + } + + // The timeout option allows a notification to persist until a certain time + if ("timeout" in notification.options && + Date.now() <= notification.options.timeout) { + return true; + } + + this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED); + return false; + }, this); + + this._setNotificationsForBrowser(aBrowser, notifications); + + if (aBrowser.docShell.isActive) { + // get the anchor element if the browser has defined one so it will + // _update will handle both the tabs iconBox and non-tab permission + // anchors. + let anchorElement = notifications.length > 0 ? notifications[0].anchorElement : null; + if (!anchorElement) + anchorElement = getAnchorFromBrowser(aBrowser); + this._update(notifications, anchorElement); + } + }, + + /** + * Removes a Notification. + * @param notification + * The Notification object to remove. + */ + remove: function PopupNotifications_remove(notification) { + this._remove(notification); + + if (notification.browser.docShell.isActive) { + let notifications = this._getNotificationsForBrowser(notification.browser); + this._update(notifications, notification.anchorElement); + } + }, + + handleEvent: function (aEvent) { + switch (aEvent.type) { + case "popuphidden": + this._onPopupHidden(aEvent); + break; + case "activate": + case "TabSelect": + let self = this; + // setTimeout(..., 0) needed, otherwise openPopup from "activate" event + // handler results in the popup being hidden again for some reason... + this.window.setTimeout(function () { + self._update(); + }, 0); + break; + case "click": + case "keypress": + this._onIconBoxCommand(aEvent); + break; + } + }, + +//////////////////////////////////////////////////////////////////////////////// +// Utility methods +//////////////////////////////////////////////////////////////////////////////// + + _ignoreDismissal: null, + _currentAnchorElement: null, + + /** + * Gets notifications for the currently selected browser. + */ + get _currentNotifications() { + return this.tabbrowser.selectedBrowser ? this._getNotificationsForBrowser(this.tabbrowser.selectedBrowser) : []; + }, + + _remove: function PopupNotifications_removeHelper(notification) { + // This notification may already be removed, in which case let's just fail + // silently. + let notifications = this._getNotificationsForBrowser(notification.browser); + if (!notifications) + return; + + var index = notifications.indexOf(notification); + if (index == -1) + return; + + if (notification.browser.docShell.isActive) + notification.anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING); + + // remove the notification + notifications.splice(index, 1); + this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED); + }, + + /** + * Dismisses the notification without removing it. + */ + _dismiss: function PopupNotifications_dismiss() { + let browser = this.panel.firstChild && + this.panel.firstChild.notification.browser; + if (typeof this.panel.hidePopup === "function") { + this.panel.hidePopup(); + } + if (browser) + browser.focus(); + }, + + /** + * Hides the notification popup. + */ + _hidePanel: function PopupNotifications_hide() { + this._ignoreDismissal = true; + if (typeof this.panel.hidePopup === "function") { + this.panel.hidePopup(); + } + this._ignoreDismissal = false; + }, + + /** + * Removes all notifications from the notification popup. + */ + _clearPanel: function () { + let popupnotification; + while ((popupnotification = this.panel.lastChild)) { + this.panel.removeChild(popupnotification); + + // If this notification was provided by the chrome document rather than + // created ad hoc, move it back to where we got it from. + let originalParent = gNotificationParents.get(popupnotification); + if (originalParent) { + popupnotification.notification = null; + + // Remove nodes dynamically added to the notification's menu button + // in _refreshPanel. Keep popupnotificationcontent nodes; they are + // provided by the chrome document. + let contentNode = popupnotification.lastChild; + while (contentNode) { + let previousSibling = contentNode.previousSibling; + if (contentNode.nodeName != "popupnotificationcontent") + popupnotification.removeChild(contentNode); + contentNode = previousSibling; + } + + // Re-hide the notification such that it isn't rendered in the chrome + // document. _refreshPanel will unhide it again when needed. + popupnotification.hidden = true; + + originalParent.appendChild(popupnotification); + } + } + }, + + _refreshPanel: function PopupNotifications_refreshPanel(notificationsToShow) { + this._clearPanel(); + + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + notificationsToShow.forEach(function (n) { + let doc = this.window.document; + + // Append "-notification" to the ID to try to avoid ID conflicts with other stuff + // in the document. + let popupnotificationID = n.id + "-notification"; + + // If the chrome document provides a popupnotification with this id, use + // that. Otherwise create it ad-hoc. + let popupnotification = doc.getElementById(popupnotificationID); + if (popupnotification) + gNotificationParents.set(popupnotification, popupnotification.parentNode); + else + popupnotification = doc.createElementNS(XUL_NS, "popupnotification"); + + popupnotification.setAttribute("label", n.message); + popupnotification.setAttribute("id", popupnotificationID); + popupnotification.setAttribute("popupid", n.id); + popupnotification.setAttribute("closebuttoncommand", "PopupNotifications._dismiss();"); + if (n.mainAction) { + popupnotification.setAttribute("buttonlabel", n.mainAction.label); + popupnotification.setAttribute("buttonaccesskey", n.mainAction.accessKey); + popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonCommand(event);"); + popupnotification.setAttribute("menucommand", "PopupNotifications._onMenuCommand(event);"); + popupnotification.setAttribute("closeitemcommand", "PopupNotifications._dismiss();event.stopPropagation();"); + } else { + popupnotification.removeAttribute("buttonlabel"); + popupnotification.removeAttribute("buttonaccesskey"); + popupnotification.removeAttribute("buttoncommand"); + popupnotification.removeAttribute("menucommand"); + popupnotification.removeAttribute("closeitemcommand"); + } + + if (n.options.popupIconURL) + popupnotification.setAttribute("icon", n.options.popupIconURL); + if (n.options.learnMoreURL) + popupnotification.setAttribute("learnmoreurl", n.options.learnMoreURL); + else + popupnotification.removeAttribute("learnmoreurl"); + + popupnotification.notification = n; + + if (n.secondaryActions) { + n.secondaryActions.forEach(function (a) { + let item = doc.createElementNS(XUL_NS, "menuitem"); + item.setAttribute("label", a.label); + item.setAttribute("accesskey", a.accessKey); + item.notification = n; + item.action = a; + + popupnotification.appendChild(item); + }, this); + + if (n.secondaryActions.length) { + let closeItemSeparator = doc.createElementNS(XUL_NS, "menuseparator"); + popupnotification.appendChild(closeItemSeparator); + } + } + + this.panel.appendChild(popupnotification); + + // The popupnotification may be hidden if we got it from the chrome + // document rather than creating it ad hoc. + popupnotification.hidden = false; + }, this); + }, + + _showPanel: function PopupNotifications_showPanel(notificationsToShow, anchorElement) { + this.panel.hidden = false; + + notificationsToShow.forEach(function (n) { + this._fireCallback(n, NOTIFICATION_EVENT_SHOWING); + }, this); + this._refreshPanel(notificationsToShow); + + if (this.isPanelOpen && this._currentAnchorElement == anchorElement) + return; + + // If the panel is already open but we're changing anchors, we need to hide + // it first. Otherwise it can appear in the wrong spot. (_hidePanel is + // safe to call even if the panel is already hidden.) + this._hidePanel(); + + // If the anchor element is hidden or null, use the tab as the anchor. We + // only ever show notifications for the current browser, so we can just use + // the current tab. + let selectedTab = this.tabbrowser.selectedTab; + if (anchorElement) { + let bo = anchorElement.boxObject; + if (bo.height == 0 && bo.width == 0) + anchorElement = selectedTab; // hidden + } else { + anchorElement = selectedTab; // null + } + + this._currentAnchorElement = anchorElement; + + // On OS X and Linux we need a different panel arrow color for + // click-to-play plugins, so copy the popupid and use css. + this.panel.setAttribute("popupid", this.panel.firstChild.getAttribute("popupid")); + notificationsToShow.forEach(function (n) { + // Remember the time the notification was shown for the security delay. + n.timeShown = this.window.performance.now(); + }, this); + this.panel.openPopup(anchorElement, "bottomcenter topleft"); + notificationsToShow.forEach(function (n) { + this._fireCallback(n, NOTIFICATION_EVENT_SHOWN); + }, this); + }, + + /** + * Updates the notification state in response to window activation or tab + * selection changes. + * + * @param notifications an array of Notification instances. if null, + * notifications will be retrieved off the current + * browser tab + * @param anchor is a XUL element that the notifications panel will be + * anchored to + * @param dismissShowing if true, dismiss any currently visible notifications + * if there are no notifications to show. Otherwise, + * currently displayed notifications will be left alone. + */ + _update: function PopupNotifications_update(notifications, anchor, dismissShowing = false) { + let useIconBox = this.iconBox && (!anchor || anchor.parentNode == this.iconBox); + if (useIconBox) { + // hide icons of the previous tab. + this._hideIcons(); + } + + let anchorElement = anchor, notificationsToShow = []; + if (!notifications) + notifications = this._currentNotifications; + let haveNotifications = notifications.length > 0; + if (haveNotifications) { + // Only show the notifications that have the passed-in anchor (or the + // first notification's anchor, if none was passed in). Other + // notifications will be shown once these are dismissed. + anchorElement = anchor || notifications[0].anchorElement; + + if (useIconBox) { + this._showIcons(notifications); + this.iconBox.hidden = false; + } else if (anchorElement) { + anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true"); + // use the anchorID as a class along with the default icon class as a + // fallback if anchorID is not defined in CSS. We always use the first + // notifications icon, so in the case of multiple notifications we'll + // only use the default icon + if (anchorElement.classList.contains("notification-anchor-icon")) { + // remove previous icon classes + let className = anchorElement.className.replace(/([-\w]+-notification-icon\s?)/g,"") + className = "default-notification-icon " + className; + if (notifications.length == 1) { + className = notifications[0].anchorID + " " + className; + } + anchorElement.className = className; + } + } + + // Also filter out notifications that have been dismissed. + notificationsToShow = notifications.filter(function (n) { + return !n.dismissed && n.anchorElement == anchorElement && + !n.options.neverShow; + }); + } + + if (notificationsToShow.length > 0) { + this._showPanel(notificationsToShow, anchorElement); + } else { + // Notify observers that we're not showing the popup (useful for testing) + this._notify("updateNotShowing"); + + // Close the panel if there are no notifications to show. + // When called from PopupNotifications.show() we should never close the + // panel, however. It may just be adding a dismissed notification, in + // which case we want to continue showing any existing notifications. + if (!dismissShowing) + this._dismiss(); + + // Only hide the iconBox if we actually have no notifications (as opposed + // to not having any showable notifications) + if (!haveNotifications) { + if (useIconBox) + this.iconBox.hidden = true; + else if (anchorElement) + anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING); + } + } + }, + + _showIcons: function PopupNotifications_showIcons(aCurrentNotifications) { + for (let notification of aCurrentNotifications) { + let anchorElm = notification.anchorElement; + if (anchorElm) { + anchorElm.setAttribute(ICON_ATTRIBUTE_SHOWING, "true"); + } + } + }, + + _hideIcons: function PopupNotifications_hideIcons() { + let icons = this.iconBox.querySelectorAll(ICON_SELECTOR); + for (let icon of icons) { + icon.removeAttribute(ICON_ATTRIBUTE_SHOWING); + } + }, + + /** + * Gets and sets notifications for the browser. + */ + _getNotificationsForBrowser: function PopupNotifications_getNotifications(browser) { + let notifications = popupNotificationsMap.get(browser); + if (!notifications) { + // Initialize the WeakMap for the browser so callers can reference/manipulate the array. + notifications = []; + popupNotificationsMap.set(browser, notifications); + } + return notifications; + }, + _setNotificationsForBrowser: function PopupNotifications_setNotifications(browser, notifications) { + popupNotificationsMap.set(browser, notifications); + return notifications; + }, + + _onIconBoxCommand: function PopupNotifications_onIconBoxCommand(event) { + // Left click, space or enter only + let type = event.type; + if (type == "click" && event.button != 0) + return; + + if (type == "keypress" && + !(event.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE || + event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN)) + return; + + if (this._currentNotifications.length == 0) + return; + + // Get the anchor that is the immediate child of the icon box + let anchor = event.target; + while (anchor && anchor.parentNode != this.iconBox) + anchor = anchor.parentNode; + + this._reshowNotifications(anchor); + }, + + _reshowNotifications: function PopupNotifications_reshowNotifications(anchor, browser) { + // Mark notifications anchored to this anchor as un-dismissed + let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser); + notifications.forEach(function (n) { + if (n.anchorElement == anchor) + n.dismissed = false; + }); + + // ...and then show them. + this._update(notifications, anchor); + }, + + _fireCallback: function PopupNotifications_fireCallback(n, event) { + if (n.options.eventCallback) + n.options.eventCallback.call(n, event); + }, + + _onPopupHidden: function PopupNotifications_onPopupHidden(event) { + if (event.target != this.panel || this._ignoreDismissal) + return; + + let browser = this.panel.firstChild && + this.panel.firstChild.notification.browser; + if (!browser) + return; + + let notifications = this._getNotificationsForBrowser(browser); + // Mark notifications as dismissed and call dismissal callbacks + Array.forEach(this.panel.childNodes, function (nEl) { + let notificationObj = nEl.notification; + // Never call a dismissal handler on a notification that's been removed. + if (notifications.indexOf(notificationObj) == -1) + return; + + // Do not mark the notification as dismissed or fire NOTIFICATION_EVENT_DISMISSED + // if the notification is removed. + if (notificationObj.options.removeOnDismissal) + this._remove(notificationObj); + else { + notificationObj.dismissed = true; + this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED); + } + }, this); + + this._clearPanel(); + + this._update(); + }, + + _onButtonCommand: function PopupNotifications_onButtonCommand(event) { + // Need to find the associated notification object, which is a bit tricky + // since it isn't associated with the button directly - this is kind of + // gross and very dependent on the structure of the popupnotification + // binding's content. + let target = event.originalTarget; + let notificationEl; + let parent = target; + while (parent && (parent = target.ownerDocument.getBindingParent(parent))) + notificationEl = parent; + + if (!notificationEl) + throw "PopupNotifications_onButtonCommand: couldn't find notification element"; + + if (!notificationEl.notification) + throw "PopupNotifications_onButtonCommand: couldn't find notification"; + + let notification = notificationEl.notification; + let timeSinceShown = this.window.performance.now() - notification.timeShown; + + // Only report the first time mainAction is triggered and remember that this occurred. + if (!notification.timeMainActionFirstTriggered) { + notification.timeMainActionFirstTriggered = timeSinceShown; + } + + if (timeSinceShown < this.buttonDelay) { + Services.console.logStringMessage("PopupNotifications_onButtonCommand: " + + "Button click happened before the security delay: " + + timeSinceShown + "ms"); + return; + } + notification.mainAction.callback.call(); + + this._remove(notification); + this._update(); + }, + + _onMenuCommand: function PopupNotifications_onMenuCommand(event) { + let target = event.originalTarget; + if (!target.action || !target.notification) + throw "menucommand target has no associated action/notification"; + + event.stopPropagation(); + target.action.callback.call(); + + this._remove(target.notification); + this._update(); + }, + + _notify: function PopupNotifications_notify(topic) { + Services.obs.notifyObservers(null, "PopupNotifications-" + topic, ""); + }, +}; diff --git a/application/palemoon/modules/QuotaManager.jsm b/application/palemoon/modules/QuotaManager.jsm new file mode 100644 index 000000000..e03161a69 --- /dev/null +++ b/application/palemoon/modules/QuotaManager.jsm @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = ["QuotaManagerHelper"]; + +Components.utils.import('resource://gre/modules/Services.jsm'); + +const Cc = Components.classes; +const Ci = Components.interfaces; + +this.QuotaManagerHelper = { + clear: function(isShutDown) { + try { + var stord = Services.dirsvc.get("ProfD", Ci.nsIFile); + stord.append("storage"); + if (stord.exists() && stord.isDirectory()) { + var doms = {}; + for (var stor of ["default", "permanent", "temporary"]) { + var storsubd = stord.clone(); + storsubd.append(stor); + if (storsubd.exists() && storsubd.isDirectory()) { + var entries = storsubd.directoryEntries; + while(entries.hasMoreElements()) { + var host, entry = entries.getNext(); + entry.QueryInterface(Ci.nsIFile); + if ((host = /^(https?|file)\+\+\+(.+)$/.exec(entry.leafName)) !== null) { + if (isShutDown) { + entry.remove(true); + } else { + doms[host[1] + "://" + host[2]] = true; + } + } + } + } + } + var qm = Cc["@mozilla.org/dom/quota/manager;1"].getService(Ci.nsIQuotaManager); + for (var dom in doms) { + var uri = Services.io.newURI(dom, null, null); + qm.clearStoragesForURI(uri); + } + } + } catch(er) {} + } +}; diff --git a/application/palemoon/modules/RecentWindow.jsm b/application/palemoon/modules/RecentWindow.jsm new file mode 100644 index 000000000..0018b502c --- /dev/null +++ b/application/palemoon/modules/RecentWindow.jsm @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["RecentWindow"]; + +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + +#ifndef XP_WIN +#define BROKEN_WM_Z_ORDER +#endif + +this.RecentWindow = { + /* + * Get the most recent browser window. + * + * @param aOptions an object accepting the arguments for the search. + * * private: true to restrict the search to private windows + * only, false to restrict the search to non-private only. + * Omit the property to search in both groups. + * * allowPopups: true if popup windows are permissable. + */ + getMostRecentBrowserWindow: function RW_getMostRecentBrowserWindow(aOptions) { + let checkPrivacy = typeof aOptions == "object" && + "private" in aOptions; + + let allowPopups = typeof aOptions == "object" && !!aOptions.allowPopups; + + function isSuitableBrowserWindow(win) { + return (!win.closed && + (allowPopups || win.toolbar.visible) && + (!checkPrivacy || + PrivateBrowsingUtils.permanentPrivateBrowsing || + PrivateBrowsingUtils.isWindowPrivate(win) == aOptions.private)); + } + +#ifdef BROKEN_WM_Z_ORDER + let win = Services.wm.getMostRecentWindow("navigator:browser"); + + // if we're lucky, this isn't a popup, and we can just return this + if (win && !isSuitableBrowserWindow(win)) { + win = null; + let windowList = Services.wm.getEnumerator("navigator:browser"); + // this is oldest to newest, so this gets a bit ugly + while (windowList.hasMoreElements()) { + let nextWin = windowList.getNext(); + if (isSuitableBrowserWindow(nextWin)) + win = nextWin; + } + } + return win; +#else + let windowList = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", true); + while (windowList.hasMoreElements()) { + let win = windowList.getNext(); + if (isSuitableBrowserWindow(win)) + return win; + } + return null; +#endif + } +}; + diff --git a/application/palemoon/modules/SharedFrame.jsm b/application/palemoon/modules/SharedFrame.jsm new file mode 100644 index 000000000..4d248ae5b --- /dev/null +++ b/application/palemoon/modules/SharedFrame.jsm @@ -0,0 +1,221 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ "SharedFrame" ]; + +const Ci = Components.interfaces; +const Cu = Components.utils; + +/** + * The purpose of this module is to create and group various iframe + * elements that are meant to all display the same content and only + * one at a time. This makes it possible to have the content loaded + * only once, while the other iframes can be kept as placeholders to + * quickly move the content to them through the swapFrameLoaders function + * when another one of the placeholder is meant to be displayed. + * */ + +let Frames = new Map(); + +/** + * The Frames map is the main data structure that holds information + * about the groups being tracked. Each entry's key is the group name, + * and the object holds information about what is the URL being displayed + * on that group, and what is the active element on the group (the frame that + * holds the loaded content). + * The reference to the activeFrame is a weak reference, which allows the + * frame to go away at any time, and when that happens the module considers that + * there are no active elements in that group. The group can be reactivated + * by changing the URL, calling preload again or adding a new element. + * + * + * Frames = { + * "messages-panel": { + * url: string, + * activeFrame: weakref + * } + * } + * + * Each object on the map is called a _SharedFrameGroup, which is an internal + * class of this module which does not automatically keep track of its state. This + * object should not be used externally, and all control should be handled by the + * module's functions. + */ + +function UNLOADED_URL(aStr) "data:text/html;charset=utf-8,<!-- Unloaded frame " + aStr + " -->"; + + +this.SharedFrame = { + /** + * Creates an iframe element and track it as part of the specified group + * The module must create the iframe itself because it needs to do some special + * handling for the element's src attribute. + * + * @param aGroupName the name of the group to which this frame belongs + * @param aParent the parent element to which the frame will be appended to + * @param aFrameAttributes an object with a list of attributes to set in the iframe + * before appending it to the DOM. The "src" attribute has + * special meaning here and if it's not blank it specifies + * the URL that will be initially assigned to this group + * @param aPreload optional, tells if the URL specified in the src attribute + * should be preloaded in the frame being created, in case + * it's not yet preloaded in any other frame of the group. + * This parameter has no meaning if src is blank. + */ + createFrame: function (aGroupName, aParent, aFrameAttributes, aPreload = true) { + let frame = aParent.ownerDocument.createElement("iframe"); + + for (let [key, val] of Iterator(aFrameAttributes)) { + frame.setAttribute(key, val); + } + + let src = aFrameAttributes.src; + if (!src) { + aPreload = false; + } + + let group = Frames.get(aGroupName); + + if (group) { + // If this group has already been created + + if (aPreload && !group.isAlive) { + // If aPreload is set and the group is not already loaded, load it. + // This can happen if: + // - aPreload was not used while creating the previous frames of this group, or + // - the previously active frame went dead in the meantime + group.url = src; + this.preload(aGroupName, frame); + } else { + // If aPreload is not set, or the group is already loaded in a different frame, + // there's not much that we need to do here: just create this frame as an + // inactivate placeholder + frame.setAttribute("src", UNLOADED_URL(aGroupName)); + } + + } else { + // This is the first time we hear about this group, so let's start tracking it, + // and also preload it if the src attribute was set and aPreload = true + group = new _SharedFrameGroup(src); + Frames.set(aGroupName, group); + + if (aPreload) { + this.preload(aGroupName, frame); + } else { + frame.setAttribute("src", UNLOADED_URL(aGroupName)); + } + } + + aParent.appendChild(frame); + return frame; + + }, + + /** + * Function that moves the loaded content from one active frame to + * another one that is currently a placeholder. If there's no active + * frame in the group, the content is loaded/reloaded. + * + * @param aGroupName the name of the group + * @param aTargetFrame the frame element to which the content should + * be moved to. + */ + setOwner: function (aGroupName, aTargetFrame) { + let group = Frames.get(aGroupName); + let frame = group.activeFrame; + + if (frame == aTargetFrame) { + // nothing to do here + return; + } + + if (group.isAlive) { + // Move document ownership to the desired frame, and make it the active one + frame.QueryInterface(Ci.nsIFrameLoaderOwner).swapFrameLoaders(aTargetFrame); + group.activeFrame = aTargetFrame; + } else { + // Previous owner was dead, reload the document at the new owner and make it the active one + aTargetFrame.setAttribute("src", group.url); + group.activeFrame = aTargetFrame; + } + }, + + /** + * Updates the current URL in use by this group, and loads it into the active frame. + * + * @param aGroupName the name of the group + * @param aURL the new url + */ + updateURL: function (aGroupName, aURL) { + let group = Frames.get(aGroupName); + group.url = aURL; + + if (group.isAlive) { + group.activeFrame.setAttribute("src", aURL); + } + }, + + /** + * Loads the group's url into a target frame, if the group doesn't have a currently + * active frame. + * + * @param aGroupName the name of the group + * @param aTargetFrame the frame element which should be made active and + * have the group's content loaded to + */ + preload: function (aGroupName, aTargetFrame) { + let group = Frames.get(aGroupName); + if (!group.isAlive) { + aTargetFrame.setAttribute("src", group.url); + group.activeFrame = aTargetFrame; + } + }, + + /** + * Tells if a group currently have an active element. + * + * @param aGroupName the name of the group + */ + isGroupAlive: function (aGroupName) { + return Frames.get(aGroupName).isAlive; + }, + + /** + * Forgets about this group. This function doesn't need to be used + * unless the group's name needs to be reused. + * + * @param aGroupName the name of the group + */ + forgetGroup: function (aGroupName) { + Frames.delete(aGroupName); + } +} + + +function _SharedFrameGroup(aURL) { + this.url = aURL; + this._activeFrame = null; +} + +_SharedFrameGroup.prototype = { + get isAlive() { + let frame = this.activeFrame; + return !!(frame && + frame.contentDocument && + frame.contentDocument.location); + }, + + get activeFrame() { + return this._activeFrame && + this._activeFrame.get(); + }, + + set activeFrame(aActiveFrame) { + this._activeFrame = aActiveFrame + ? Cu.getWeakReference(aActiveFrame) + : null; + } +} diff --git a/application/palemoon/modules/Windows8WindowFrameColor.jsm b/application/palemoon/modules/Windows8WindowFrameColor.jsm new file mode 100644 index 000000000..d424da499 --- /dev/null +++ b/application/palemoon/modules/Windows8WindowFrameColor.jsm @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +this.EXPORTED_SYMBOLS = ["Windows8WindowFrameColor"]; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/WindowsRegistry.jsm"); + +const Windows8WindowFrameColor = { + _windowFrameColor: null, + + get_win8: function() { + if (this._windowFrameColor) + return this._windowFrameColor; + + let HKCU = Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER; + let dwmKey = "Software\\Microsoft\\Windows\\DWM"; + + // Window frame base color component values when Color Intensity is at 0. + let frameBaseColor = 217; + + let windowFrameColor = WindowsRegistry.readRegKey(HKCU, dwmKey, + "ColorizationColor"); + if (windowFrameColor == undefined) { + // Return the default color if unset or colorization not used + return this._windowFrameColor = [frameBaseColor, frameBaseColor, frameBaseColor]; + } + // The color returned from the Registry is in decimal form. + let windowFrameColorHex = windowFrameColor.toString(16); + // Zero-pad the number just to make sure that it is 8 digits. + windowFrameColorHex = ("00000000" + windowFrameColorHex).substr(-8); + let windowFrameColorArray = windowFrameColorHex.match(/../g); + let [unused, fgR, fgG, fgB] = windowFrameColorArray.map(function(val) parseInt(val, 16)); + let windowFrameColorBalance = WindowsRegistry.readRegKey(HKCU, dwmKey, + "ColorizationColorBalance"); + // Default to balance=78 if reg key isn't defined + if (windowFrameColorBalance == undefined) { + windowFrameColorBalance = 78; + } + let alpha = windowFrameColorBalance / 100; + + // Alpha-blend the foreground color with the frame base color. + let r = Math.round(fgR * alpha + frameBaseColor * (1 - alpha)); + let g = Math.round(fgG * alpha + frameBaseColor * (1 - alpha)); + let b = Math.round(fgB * alpha + frameBaseColor * (1 - alpha)); + return this._windowFrameColor = [r, g, b]; + } +}; diff --git a/application/palemoon/modules/WindowsJumpLists.jsm b/application/palemoon/modules/WindowsJumpLists.jsm new file mode 100644 index 000000000..e7f785519 --- /dev/null +++ b/application/palemoon/modules/WindowsJumpLists.jsm @@ -0,0 +1,581 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +/** + * Constants + */ + +const Cc = Components.classes; +const Ci = Components.interfaces; + +// Stop updating jumplists after some idle time. +const IDLE_TIMEOUT_SECONDS = 5 * 60; + +// Prefs +const PREF_TASKBAR_BRANCH = "browser.taskbar.lists."; +const PREF_TASKBAR_ENABLED = "enabled"; +const PREF_TASKBAR_ITEMCOUNT = "maxListItemCount"; +const PREF_TASKBAR_FREQUENT = "frequent.enabled"; +const PREF_TASKBAR_RECENT = "recent.enabled"; +const PREF_TASKBAR_TASKS = "tasks.enabled"; +const PREF_TASKBAR_REFRESH = "refreshInSeconds"; + +// Hash keys for pendingStatements. +const LIST_TYPE = { + FREQUENT: 0 +, RECENT: 1 +} + +/** + * Exports + */ + +this.EXPORTED_SYMBOLS = [ + "WinTaskbarJumpList", +]; + +/** + * Smart getters + */ + +XPCOMUtils.defineLazyGetter(this, "_prefs", function() { + return Services.prefs.getBranch(PREF_TASKBAR_BRANCH); +}); + +XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() { + return Services.strings + .createBundle("chrome://browser/locale/taskbar.properties"); +}); + +XPCOMUtils.defineLazyGetter(this, "PlacesUtils", function() { + Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + return PlacesUtils; +}); + +XPCOMUtils.defineLazyGetter(this, "NetUtil", function() { + Components.utils.import("resource://gre/modules/NetUtil.jsm"); + return NetUtil; +}); + +XPCOMUtils.defineLazyServiceGetter(this, "_idle", + "@mozilla.org/widget/idleservice;1", + "nsIIdleService"); + +XPCOMUtils.defineLazyServiceGetter(this, "_taskbarService", + "@mozilla.org/windows-taskbar;1", + "nsIWinTaskbar"); + +XPCOMUtils.defineLazyServiceGetter(this, "_winShellService", + "@mozilla.org/browser/shell-service;1", + "nsIWindowsShellService"); + +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +/** + * Global functions + */ + +function _getString(name) { + return _stringBundle.GetStringFromName(name); +} + +///////////////////////////////////////////////////////////////////////////// +// Task list configuration data object. + +var tasksCfg = [ + /** + * Task configuration options: title, description, args, iconIndex, open, close. + * + * title - Task title displayed in the list. (strings in the table are temp fillers.) + * description - Tooltip description on the list item. + * args - Command line args to invoke the task. + * iconIndex - Optional win icon index into the main application for the + * list item. + * open - Boolean indicates if the command should be visible after the browser opens. + * close - Boolean indicates if the command should be visible after the browser closes. + */ + // Open new tab + { + get title() _getString("taskbar.tasks.newTab.label"), + get description() _getString("taskbar.tasks.newTab.description"), + args: "-new-tab about:blank", + iconIndex: 3, // New window icon + open: true, + close: true, // The jump list already has an app launch icon, but + // we don't always update the list on shutdown. + // Thus true for consistency. + }, + + // Open new window + { + get title() _getString("taskbar.tasks.newWindow.label"), + get description() _getString("taskbar.tasks.newWindow.description"), + args: "-browser", + iconIndex: 2, // New tab icon + open: true, + close: true, // No point, but we don't always update the list on + // shutdown. Thus true for consistency. + }, + + // Open new private window + { + get title() _getString("taskbar.tasks.newPrivateWindow.label"), + get description() _getString("taskbar.tasks.newPrivateWindow.description"), + args: "-private-window", + iconIndex: 4, // Private browsing mode icon + open: true, + close: true, // No point, but we don't always update the list on + // shutdown. Thus true for consistency. + }, +]; + +///////////////////////////////////////////////////////////////////////////// +// Implementation + +this.WinTaskbarJumpList = +{ + _builder: null, + _tasks: null, + _shuttingDown: false, + + /** + * Startup, shutdown, and update + */ + + startup: function WTBJL_startup() { + // exit if this isn't win7 or higher. + if (!this._initTaskbar()) + return; + + // Win shell shortcut maintenance. If we've gone through an update, + // this will update any pinned taskbar shortcuts. Not specific to + // jump lists, but this was a convienent place to call it. + try { + // dev builds may not have helper.exe, ignore failures. + this._shortcutMaintenance(); + } catch (ex) { + } + + // Store our task list config data + this._tasks = tasksCfg; + + // retrieve taskbar related prefs. + this._refreshPrefs(); + + // observer for private browsing and our prefs branch + this._initObs(); + + // jump list refresh timer + this._updateTimer(); + }, + + update: function WTBJL_update() { + // are we disabled via prefs? don't do anything! + if (!this._enabled) + return; + + // do what we came here to do, update the taskbar jumplist + this._buildList(); + }, + + _shutdown: function WTBJL__shutdown() { + this._shuttingDown = true; + + // Correctly handle a clear history on shutdown. If there are no + // entries be sure to empty all history lists. Luckily Places caches + // this value, so it's a pretty fast call. + if (!PlacesUtils.history.hasHistoryEntries) { + this.update(); + } + + this._free(); + }, + + _shortcutMaintenance: function WTBJL__maintenace() { + _winShellService.shortcutMaintenance(); + }, + + /** + * List building + * + * @note Async builders must add their mozIStoragePendingStatement to + * _pendingStatements object, using a different LIST_TYPE entry for + * each statement. Once finished they must remove it and call + * commitBuild(). When there will be no more _pendingStatements, + * commitBuild() will commit for real. + */ + + _pendingStatements: {}, + _hasPendingStatements: function WTBJL__hasPendingStatements() { + return Object.keys(this._pendingStatements).length > 0; + }, + + _buildList: function WTBJL__buildList() { + if (this._hasPendingStatements()) { + // We were requested to update the list while another update was in + // progress, this could happen at shutdown, idle or privatebrowsing. + // Abort the current list building. + for (let listType in this._pendingStatements) { + this._pendingStatements[listType].cancel(); + delete this._pendingStatements[listType]; + } + this._builder.abortListBuild(); + } + + // anything to build? + if (!this._showFrequent && !this._showRecent && !this._showTasks) { + // don't leave the last list hanging on the taskbar. + this._deleteActiveJumpList(); + return; + } + + if (!this._startBuild()) + return; + + if (this._showTasks) + this._buildTasks(); + + // Space for frequent items takes priority over recent. + if (this._showFrequent) + this._buildFrequent(); + + if (this._showRecent) + this._buildRecent(); + + this._commitBuild(); + }, + + /** + * Taskbar api wrappers + */ + + _startBuild: function WTBJL__startBuild() { + var removedItems = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + this._builder.abortListBuild(); + if (this._builder.initListBuild(removedItems)) { + // Prior to building, delete removed items from history. + this._clearHistory(removedItems); + return true; + } + return false; + }, + + _commitBuild: function WTBJL__commitBuild() { + if (!this._hasPendingStatements() && !this._builder.commitListBuild()) { + this._builder.abortListBuild(); + } + }, + + _buildTasks: function WTBJL__buildTasks() { + var items = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + this._tasks.forEach(function (task) { + if ((this._shuttingDown && !task.close) || (!this._shuttingDown && !task.open)) + return; + var item = this._getHandlerAppItem(task.title, task.description, + task.args, task.iconIndex, null); + items.appendElement(item, false); + }, this); + + if (items.length > 0) + this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_TASKS, items); + }, + + _buildCustom: function WTBJL__buildCustom(title, items) { + if (items.length > 0) + this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_CUSTOMLIST, items, title); + }, + + _buildFrequent: function WTBJL__buildFrequent() { + // If history is empty, just bail out. + if (!PlacesUtils.history.hasHistoryEntries) { + return; + } + + // Windows supports default frequent and recent lists, + // but those depend on internal windows visit tracking + // which we don't populate. So we build our own custom + // frequent and recent lists using our nav history data. + + var items = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + // track frequent items so that we don't add them to + // the recent list. + this._frequentHashList = []; + + this._pendingStatements[LIST_TYPE.FREQUENT] = this._getHistoryResults( + Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING, + this._maxItemCount, + function (aResult) { + if (!aResult) { + delete this._pendingStatements[LIST_TYPE.FREQUENT]; + // The are no more results, build the list. + this._buildCustom(_getString("taskbar.frequent.label"), items); + this._commitBuild(); + return; + } + + let title = aResult.title || aResult.uri; + let faviconPageUri = Services.io.newURI(aResult.uri, null, null); + let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1, + faviconPageUri); + items.appendElement(shortcut, false); + this._frequentHashList.push(aResult.uri); + }, + this + ); + }, + + _buildRecent: function WTBJL__buildRecent() { + // If history is empty, just bail out. + if (!PlacesUtils.history.hasHistoryEntries) { + return; + } + + var items = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + // Frequent items will be skipped, so we select a double amount of + // entries and stop fetching results at _maxItemCount. + var count = 0; + + this._pendingStatements[LIST_TYPE.RECENT] = this._getHistoryResults( + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING, + this._maxItemCount * 2, + function (aResult) { + if (!aResult) { + // The are no more results, build the list. + this._buildCustom(_getString("taskbar.recent.label"), items); + delete this._pendingStatements[LIST_TYPE.RECENT]; + this._commitBuild(); + return; + } + + if (count >= this._maxItemCount) { + return; + } + + // Do not add items to recent that have already been added to frequent. + if (this._frequentHashList && + this._frequentHashList.indexOf(aResult.uri) != -1) { + return; + } + + let title = aResult.title || aResult.uri; + let faviconPageUri = Services.io.newURI(aResult.uri, null, null); + let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1, + faviconPageUri); + items.appendElement(shortcut, false); + count++; + }, + this + ); + }, + + _deleteActiveJumpList: function WTBJL__deleteAJL() { + this._builder.deleteActiveList(); + }, + + /** + * Jump list item creation helpers + */ + + _getHandlerAppItem: function WTBJL__getHandlerAppItem(name, description, + args, iconIndex, + faviconPageUri) { + var file = Services.dirsvc.get("XREExeF", Ci.nsILocalFile); + + var handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsILocalHandlerApp); + handlerApp.executable = file; + // handlers default to the leaf name if a name is not specified + if (name && name.length != 0) + handlerApp.name = name; + handlerApp.detailedDescription = description; + handlerApp.appendParameter(args); + + var item = Cc["@mozilla.org/windows-jumplistshortcut;1"]. + createInstance(Ci.nsIJumpListShortcut); + item.app = handlerApp; + item.iconIndex = iconIndex; + item.faviconPageUri = faviconPageUri; + return item; + }, + + _getSeparatorItem: function WTBJL__getSeparatorItem() { + var item = Cc["@mozilla.org/windows-jumplistseparator;1"]. + createInstance(Ci.nsIJumpListSeparator); + return item; + }, + + /** + * Nav history helpers + */ + + _getHistoryResults: + function WTBLJL__getHistoryResults(aSortingMode, aLimit, aCallback, aScope) { + var options = PlacesUtils.history.getNewQueryOptions(); + options.maxResults = aLimit; + options.sortingMode = aSortingMode; + var query = PlacesUtils.history.getNewQuery(); + + // Return the pending statement to the caller, to allow cancelation. + return PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .asyncExecuteLegacyQueries([query], 1, options, { + handleResult: function (aResultSet) { + for (let row; (row = aResultSet.getNextRow());) { + try { + aCallback.call(aScope, + { uri: row.getResultByIndex(1) + , title: row.getResultByIndex(2) + }); + } catch (e) {} + } + }, + handleError: function (aError) { + Components.utils.reportError( + "Async execution error (" + aError.result + "): " + aError.message); + }, + handleCompletion: function (aReason) { + aCallback.call(WinTaskbarJumpList, null); + }, + }); + }, + + _clearHistory: function WTBJL__clearHistory(items) { + if (!items) + return; + var URIsToRemove = []; + var e = items.enumerate(); + while (e.hasMoreElements()) { + let oldItem = e.getNext().QueryInterface(Ci.nsIJumpListShortcut); + if (oldItem) { + try { // in case we get a bad uri + let uriSpec = oldItem.app.getParameter(0); + URIsToRemove.push(NetUtil.newURI(uriSpec)); + } catch (err) { } + } + } + if (URIsToRemove.length > 0) { + PlacesUtils.bhistory.removePages(URIsToRemove, URIsToRemove.length, true); + } + }, + + /** + * Prefs utilities + */ + + _refreshPrefs: function WTBJL__refreshPrefs() { + this._enabled = _prefs.getBoolPref(PREF_TASKBAR_ENABLED); + this._showFrequent = _prefs.getBoolPref(PREF_TASKBAR_FREQUENT); + this._showRecent = _prefs.getBoolPref(PREF_TASKBAR_RECENT); + this._showTasks = _prefs.getBoolPref(PREF_TASKBAR_TASKS); + this._maxItemCount = _prefs.getIntPref(PREF_TASKBAR_ITEMCOUNT); + }, + + /** + * Init and shutdown utilities + */ + + _initTaskbar: function WTBJL__initTaskbar() { + this._builder = _taskbarService.createJumpListBuilder(); + if (!this._builder || !this._builder.available) + return false; + + return true; + }, + + _initObs: function WTBJL__initObs() { + // If the browser is closed while in private browsing mode, the "exit" + // notification is fired on quit-application-granted. + // History cleanup can happen at profile-change-teardown. + Services.obs.addObserver(this, "profile-before-change", false); + Services.obs.addObserver(this, "browser:purge-session-history", false); + _prefs.addObserver("", this, false); + }, + + _freeObs: function WTBJL__freeObs() { + Services.obs.removeObserver(this, "profile-before-change"); + Services.obs.removeObserver(this, "browser:purge-session-history"); + _prefs.removeObserver("", this); + }, + + _updateTimer: function WTBJL__updateTimer() { + if (this._enabled && !this._shuttingDown && !this._timer) { + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._timer.initWithCallback(this, + _prefs.getIntPref(PREF_TASKBAR_REFRESH)*1000, + this._timer.TYPE_REPEATING_SLACK); + } + else if ((!this._enabled || this._shuttingDown) && this._timer) { + this._timer.cancel(); + delete this._timer; + } + }, + + _hasIdleObserver: false, + _updateIdleObserver: function WTBJL__updateIdleObserver() { + if (this._enabled && !this._shuttingDown && !this._hasIdleObserver) { + _idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS); + this._hasIdleObserver = true; + } + else if ((!this._enabled || this._shuttingDown) && this._hasIdleObserver) { + _idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS); + this._hasIdleObserver = false; + } + }, + + _free: function WTBJL__free() { + this._freeObs(); + this._updateTimer(); + this._updateIdleObserver(); + delete this._builder; + }, + + /** + * Notification handlers + */ + + notify: function WTBJL_notify(aTimer) { + // Add idle observer on the first notification so it doesn't hit startup. + this._updateIdleObserver(); + this.update(); + }, + + observe: function WTBJL_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "nsPref:changed": + if (this._enabled == true && !_prefs.getBoolPref(PREF_TASKBAR_ENABLED)) + this._deleteActiveJumpList(); + this._refreshPrefs(); + this._updateTimer(); + this._updateIdleObserver(); + this.update(); + break; + + case "profile-before-change": + this._shutdown(); + break; + + case "browser:purge-session-history": + this.update(); + break; + case "idle": + if (this._timer) { + this._timer.cancel(); + delete this._timer; + } + break; + + case "back": + this._updateTimer(); + break; + } + }, +}; diff --git a/application/palemoon/modules/WindowsPreviewPerTab.jsm b/application/palemoon/modules/WindowsPreviewPerTab.jsm new file mode 100644 index 000000000..41b38f0cf --- /dev/null +++ b/application/palemoon/modules/WindowsPreviewPerTab.jsm @@ -0,0 +1,708 @@ +/* vim: se cin sw=2 ts=2 et filetype=javascript : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* + * This module implements the front end behavior for AeroPeek. Starting in + * Windows Vista, the taskbar began showing live thumbnail previews of windows + * when the user hovered over the window icon in the taskbar. Starting with + * Windows 7, the taskbar allows an application to expose its tabbed interface + * in the taskbar by showing thumbnail previews rather than the default window + * preview. Additionally, when a user hovers over a thumbnail (tab or window), + * they are shown a live preview of the window (or tab + its containing window). + * + * In Windows 7, a title, icon, close button and optional toolbar are shown for + * each preview. This feature does not make use of the toolbar. For window + * previews, the title is the window title and the icon the window icon. For + * tab previews, the title is the page title and the page's favicon. In both + * cases, the close button "does the right thing." + * + * The primary objects behind this feature are nsITaskbarTabPreview and + * nsITaskbarPreviewController. Each preview has a controller. The controller + * responds to the user's interactions on the taskbar and provides the required + * data to the preview for determining the size of the tab and thumbnail. The + * PreviewController class implements this interface. The preview will request + * the controller to provide a thumbnail or preview when the user interacts with + * the taskbar. To reduce the overhead of drawing the tab area, the controller + * implementation caches the tab's contents in a <canvas> element. If no + * previews or thumbnails have been requested for some time, the controller will + * discard its cached tab contents. + * + * Screen real estate is limited so when there are too many thumbnails to fit + * on the screen, the taskbar stops displaying thumbnails and instead displays + * just the title, icon and close button in a similar fashion to previous + * versions of the taskbar. If there are still too many previews to fit on the + * screen, the taskbar resorts to a scroll up and scroll down button pair to let + * the user scroll through the list of tabs. Since this is undoubtedly + * inconvenient for users with many tabs, the AeroPeek objects turns off all of + * the tab previews. This tells the taskbar to revert to one preview per window. + * If the number of tabs falls below this magic threshold, the preview-per-tab + * behavior returns. There is no reliable way to determine when the scroll + * buttons appear on the taskbar, so a magic pref-controlled number determines + * when this threshold has been crossed. + */ +this.EXPORTED_SYMBOLS = ["AeroPeek"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + +// Pref to enable/disable preview-per-tab +const TOGGLE_PREF_NAME = "browser.taskbar.previews.enable"; +// Pref to determine the magic auto-disable threshold +const DISABLE_THRESHOLD_PREF_NAME = "browser.taskbar.previews.max"; +// Pref to control the time in seconds that tab contents live in the cache +const CACHE_EXPIRATION_TIME_PREF_NAME = "browser.taskbar.previews.cachetime"; + +const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1"; + +//////////////////////////////////////////////////////////////////////////////// +//// Various utility properties +XPCOMUtils.defineLazyServiceGetter(this, "ioSvc", + "@mozilla.org/network/io-service;1", + "nsIIOService"); +XPCOMUtils.defineLazyServiceGetter(this, "imgTools", + "@mozilla.org/image/tools;1", + "imgITools"); +XPCOMUtils.defineLazyServiceGetter(this, "faviconSvc", + "@mozilla.org/browser/favicon-service;1", + "nsIFaviconService"); + +// nsIURI -> imgIContainer +function _imageFromURI(uri, privateMode, callback) { + let channel = ioSvc.newChannelFromURI(uri); + try { + channel.QueryInterface(Ci.nsIPrivateBrowsingChannel); + channel.setPrivate(privateMode); + } catch (e) { + // Ignore channels which do not support nsIPrivateBrowsingChannel + } + NetUtil.asyncFetch(channel, function(inputStream, resultCode) { + if (!Components.isSuccessCode(resultCode)) + return; + try { + let out_img = { value: null }; + imgTools.decodeImageData(inputStream, channel.contentType, out_img); + callback(out_img.value); + } catch (e) { + // We failed, so use the default favicon (only if this wasn't the default + // favicon). + let defaultURI = faviconSvc.defaultFavicon; + if (!defaultURI.equals(uri)) + _imageFromURI(defaultURI, callback); + } + }); +} + +// string? -> imgIContainer +function getFaviconAsImage(iconurl, privateMode, callback) { + if (iconurl) + _imageFromURI(NetUtil.newURI(iconurl), privateMode, callback); + else + _imageFromURI(faviconSvc.defaultFavicon, privateMode, callback); +} + +// Snaps the given rectangle to be pixel-aligned at the given scale +function snapRectAtScale(r, scale) { + let x = Math.floor(r.x * scale); + let y = Math.floor(r.y * scale); + let width = Math.ceil((r.x + r.width) * scale) - x; + let height = Math.ceil((r.y + r.height) * scale) - y; + + r.x = x / scale; + r.y = y / scale; + r.width = width / scale; + r.height = height / scale; +} + +//////////////////////////////////////////////////////////////////////////////// +//// PreviewController + +/* + * This class manages the behavior of the preview. + * + * To give greater performance when drawing, the dirty areas of the content + * window are tracked and drawn on demand into a canvas of the same size. + * This provides a great increase in responsiveness when drawing a preview + * for unchanged (or even only slightly changed) tabs. + * + * @param win + * The TabWindow (see below) that owns the preview that this controls + * @param tab + * The <tab> that this preview is associated with + */ +function PreviewController(win, tab) { + this.win = win; + this.tab = tab; + this.linkedBrowser = tab.linkedBrowser; + this.preview = this.win.createTabPreview(this); + + this.linkedBrowser.addEventListener("MozAfterPaint", this, false); + this.tab.addEventListener("TabAttrModified", this, false); + + XPCOMUtils.defineLazyGetter(this, "canvasPreview", function () { + let canvas = this.win.win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + canvas.mozOpaque = true; + return canvas; + }); + + XPCOMUtils.defineLazyGetter(this, "dirtyRegion", + function () { + let dirtyRegion = Cc["@mozilla.org/gfx/region;1"] + .createInstance(Ci.nsIScriptableRegion); + dirtyRegion.init(); + return dirtyRegion; + }); + + XPCOMUtils.defineLazyGetter(this, "winutils", + function () { + let win = tab.linkedBrowser.contentWindow; + return win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + }); +} + +PreviewController.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsITaskbarPreviewController, + Ci.nsIDOMEventListener]), + destroy: function () { + this.tab.removeEventListener("TabAttrModified", this, false); + this.linkedBrowser.removeEventListener("MozAfterPaint", this, false); + + // Break cycles, otherwise we end up leaking the window with everything + // attached to it. + delete this.win; + delete this.preview; + delete this.dirtyRegion; + }, + get wrappedJSObject() { + return this; + }, + + get dirtyRects() { + let rectstream = this.dirtyRegion.getRects(); + if (!rectstream) + return []; + let rects = []; + for (let i = 0; i < rectstream.length; i+= 4) { + let r = {x: rectstream[i], + y: rectstream[i+1], + width: rectstream[i+2], + height: rectstream[i+3]}; + rects.push(r); + } + return rects; + }, + + // Resizes the canvasPreview to 0x0, essentially freeing its memory. + // updateCanvasPreview() will detect the size mismatch as a resize event + // the next time it is called. + resetCanvasPreview: function () { + this.canvasPreview.width = 0; + this.canvasPreview.height = 0; + }, + + get zoom() { + // Note that winutils.fullZoom accounts for "quantization" of the zoom factor + // from nsIMarkupDocumentViewer due to conversion through appUnits. + // We do -not- want screenPixelsPerCSSPixel here, because that would -also- + // incorporate any scaling that is applied due to hi-dpi resolution options. + return this.winutils.fullZoom; + }, + + // Updates the controller's canvas with the parts of the <browser> that need + // to be redrawn. + updateCanvasPreview: function () { + let win = this.linkedBrowser.contentWindow; + let bx = this.linkedBrowser.boxObject; + // Check for resize + if (bx.width != this.canvasPreview.width || + bx.height != this.canvasPreview.height) { + // Invalidate the entire area and repaint + this.onTabPaint({left:0, top:0, right:win.innerWidth, bottom:win.innerHeight}); + this.canvasPreview.width = bx.width; + this.canvasPreview.height = bx.height; + } + + // Draw dirty regions + let ctx = this.canvasPreview.getContext("2d"); + let scale = this.zoom; + + let flags = this.canvasPreviewFlags; + // The dirty region may include parts that are offscreen so we clip to the + // canvas area. + this.dirtyRegion.intersectRect(0, 0, win.innerWidth, win.innerHeight); + this.dirtyRects.forEach(function (r) { + // We need to snap the rectangle to be pixel aligned in the destination + // coordinate space. Otherwise natively themed widgets might not draw. + snapRectAtScale(r, scale); + let x = r.x; + let y = r.y; + let width = r.width; + let height = r.height; + + ctx.save(); + ctx.scale(scale, scale); + ctx.translate(x, y); + ctx.drawWindow(win, x, y, width, height, "white", flags); + ctx.restore(); + }); + this.dirtyRegion.setToRect(0,0,0,0); + + // If we're updating the canvas, then we're in the middle of a peek so + // don't discard the cache of previews. + AeroPeek.resetCacheTimer(); + }, + + onTabPaint: function (rect) { + let x = Math.floor(rect.left), + y = Math.floor(rect.top), + width = Math.ceil(rect.right) - x, + height = Math.ceil(rect.bottom) - y; + this.dirtyRegion.unionRect(x, y, width, height); + }, + + updateTitleAndTooltip: function () { + let title = this.win.tabbrowser.getWindowTitleForBrowser(this.linkedBrowser); + this.preview.title = title; + this.preview.tooltip = title; + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsITaskbarPreviewController + + get width() { + return this.win.width; + }, + + get height() { + return this.win.height; + }, + + get thumbnailAspectRatio() { + let boxObject = this.tab.linkedBrowser.boxObject; + // Avoid returning 0 + let tabWidth = boxObject.width || 1; + // Avoid divide by 0 + let tabHeight = boxObject.height || 1; + return tabWidth / tabHeight; + }, + + drawPreview: function (ctx) { + let self = this; + this.win.tabbrowser.previewTab(this.tab, function () self.previewTabCallback(ctx)); + + // We must avoid having the frame drawn around the window. See bug 520807 + return false; + }, + + previewTabCallback: function (ctx) { + // This will extract the resolution-scale component of the scaling we need, + // which should be applied to both chrome and content; + // the page zoom component is applied (to content only) within updateCanvasPreview. + let scale = this.winutils.screenPixelsPerCSSPixel / this.winutils.fullZoom; + ctx.save(); + ctx.scale(scale, scale); + let width = this.win.width; + let height = this.win.height; + // Draw our toplevel window + ctx.drawWindow(this.win.win, 0, 0, width, height, "transparent"); + + // XXX (jfkthame): Pending tabs don't seem to draw with the proper scaling + // unless we use this block of code; but doing this for "normal" (loaded) tabs + // results in blurry rendering on hidpi systems, so we avoid it if possible. + // I don't understand why pending and loaded tabs behave differently here... + // (see bug 857061). + if (this.tab.hasAttribute("pending")) { + // Compositor, where art thou? + // Draw the tab content on top of the toplevel window + this.updateCanvasPreview(); + + let boxObject = this.linkedBrowser.boxObject; + ctx.translate(boxObject.x, boxObject.y); + ctx.drawImage(this.canvasPreview, 0, 0); + } + + ctx.restore(); + }, + + drawThumbnail: function (ctx, width, height) { + this.updateCanvasPreview(); + + let scale = width/this.linkedBrowser.boxObject.width; + ctx.scale(scale, scale); + ctx.drawImage(this.canvasPreview, 0, 0); + + // Don't draw a frame around the thumbnail + return false; + }, + + onClose: function () { + this.win.tabbrowser.removeTab(this.tab); + }, + + onActivate: function () { + this.win.tabbrowser.selectedTab = this.tab; + + // Accept activation - this will restore the browser window + // if it's minimized + return true; + }, + + //// nsIDOMEventListener + handleEvent: function (evt) { + switch (evt.type) { + case "MozAfterPaint": + if (evt.originalTarget === this.linkedBrowser.contentWindow) { + let clientRects = evt.clientRects; + let length = clientRects.length; + for (let i = 0; i < length; i++) { + let r = clientRects.item(i); + this.onTabPaint(r); + } + } + let preview = this.preview; + if (preview.visible) + preview.invalidate(); + break; + case "TabAttrModified": + this.updateTitleAndTooltip(); + break; + } + } +}; + +XPCOMUtils.defineLazyGetter(PreviewController.prototype, "canvasPreviewFlags", + function () { let canvasInterface = Ci.nsIDOMCanvasRenderingContext2D; + return canvasInterface.DRAWWINDOW_DRAW_VIEW + | canvasInterface.DRAWWINDOW_DRAW_CARET + | canvasInterface.DRAWWINDOW_ASYNC_DECODE_IMAGES + | canvasInterface.DRAWWINDOW_DO_NOT_FLUSH; +}); + +//////////////////////////////////////////////////////////////////////////////// +//// TabWindow + +/* + * This class monitors a browser window for changes to its tabs + * + * @param win + * The nsIDOMWindow browser window + */ +function TabWindow(win) { + this.win = win; + this.tabbrowser = win.gBrowser; + + this.previews = new Map(); + + for (let i = 0; i < this.tabEvents.length; i++) + this.tabbrowser.tabContainer.addEventListener(this.tabEvents[i], this, false); + this.tabbrowser.addTabsProgressListener(this); + + AeroPeek.windows.push(this); + let tabs = this.tabbrowser.tabs; + for (let i = 0; i < tabs.length; i++) + this.newTab(tabs[i]); + + this.updateTabOrdering(); + AeroPeek.checkPreviewCount(); +} + +TabWindow.prototype = { + _enabled: false, + tabEvents: ["TabOpen", "TabClose", "TabSelect", "TabMove"], + + destroy: function () { + this._destroying = true; + + let tabs = this.tabbrowser.tabs; + + this.tabbrowser.removeTabsProgressListener(this); + for (let i = 0; i < this.tabEvents.length; i++) + this.tabbrowser.tabContainer.removeEventListener(this.tabEvents[i], this, false); + + for (let i = 0; i < tabs.length; i++) + this.removeTab(tabs[i]); + + let idx = AeroPeek.windows.indexOf(this.win.gTaskbarTabGroup); + AeroPeek.windows.splice(idx, 1); + AeroPeek.checkPreviewCount(); + }, + + get width () { + return this.win.innerWidth; + }, + get height () { + return this.win.innerHeight; + }, + + // Invoked when the given tab is added to this window + newTab: function (tab) { + let controller = new PreviewController(this, tab); + // It's OK to add the preview now while the favicon still loads. + this.previews.set(tab, controller.preview); + AeroPeek.addPreview(controller.preview); + // updateTitleAndTooltip relies on having controller.preview which is lazily resolved. + // Now that we've updated this.previews, it will resolve successfully. + controller.updateTitleAndTooltip(); + }, + + createTabPreview: function (controller) { + let docShell = this.win + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + let preview = AeroPeek.taskbar.createTaskbarTabPreview(docShell, controller); + preview.visible = AeroPeek.enabled; + preview.active = this.tabbrowser.selectedTab == controller.tab; + // Grab the default favicon + getFaviconAsImage(null, PrivateBrowsingUtils.isWindowPrivate(this.win), function (img) { + // It is possible that we've already gotten the real favicon, so make sure + // we have not set one before setting this default one. + if (!preview.icon) + preview.icon = img; + }); + + return preview; + }, + + // Invoked when the given tab is closed + removeTab: function (tab) { + let preview = this.previewFromTab(tab); + preview.active = false; + preview.visible = false; + preview.move(null); + preview.controller.wrappedJSObject.destroy(); + + // We don't want to splice from the array if the tabs aren't being removed + // from the tab bar as well (as is the case when the window closes). + this.previews.delete(tab); + AeroPeek.removePreview(preview); + }, + + get enabled () { + return this._enabled; + }, + + set enabled (enable) { + this._enabled = enable; + // Because making a tab visible requires that the tab it is next to be + // visible, it is far simpler to unset the 'next' tab and recreate them all + // at once. + for (let [tab, preview] of this.previews) { + preview.move(null); + preview.visible = enable; + } + this.updateTabOrdering(); + }, + + previewFromTab: function (tab) { + return this.previews.get(tab); + }, + + updateTabOrdering: function () { + let previews = this.previews; + let tabs = this.tabbrowser.tabs; + + // Previews are internally stored using a map, so we need to iterate over + // the tabbrowser's array of tabs to retrieve previews in the same order. + let inorder = [previews.get(t) for (t of tabs) if (previews.has(t))]; + + // Since the internal taskbar array has not yet been updated, we must force + // the sorting order of our local array on it. To do so, we must walk + // the local array backwards, because otherwise we would send move requests + // in the wrong order. See bug 522610 for details. + for (let i = inorder.length - 1; i >= 0; i--) { + inorder[i].move(inorder[i + 1] || null); + } + }, + + //// nsIDOMEventListener + handleEvent: function (evt) { + let tab = evt.originalTarget; + switch (evt.type) { + case "TabOpen": + this.newTab(tab); + this.updateTabOrdering(); + break; + case "TabClose": + this.removeTab(tab); + this.updateTabOrdering(); + break; + case "TabSelect": + this.previewFromTab(tab).active = true; + break; + case "TabMove": + this.updateTabOrdering(); + break; + } + }, + + //// Browser progress listener + onLinkIconAvailable: function (aBrowser, aIconURL) { + let self = this; + getFaviconAsImage(aIconURL, PrivateBrowsingUtils.isWindowPrivate(this.win), function (img) { + let index = self.tabbrowser.browsers.indexOf(aBrowser); + // Only add it if we've found the index. The tab could have closed! + if (index != -1) { + let tab = self.tabbrowser.tabs[index]; + self.previews.get(tab).icon = img; + } + }); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// AeroPeek + +/* + * This object acts as global storage and external interface for this feature. + * It maintains the values of the prefs. + */ +this.AeroPeek = { + available: false, + // Does the pref say we're enabled? + _prefenabled: true, + + _enabled: true, + + // nsITaskbarTabPreview array + previews: [], + + // TabWindow array + windows: [], + + // nsIWinTaskbar service + taskbar: null, + + // Maximum number of previews + maxpreviews: 20, + + // Length of time in seconds that previews are cached + cacheLifespan: 20, + + initialize: function () { + if (!(WINTASKBAR_CONTRACTID in Cc)) + return; + this.taskbar = Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar); + this.available = this.taskbar.available; + if (!this.available) + return; + + this.prefs.addObserver(TOGGLE_PREF_NAME, this, false); + this.prefs.addObserver(DISABLE_THRESHOLD_PREF_NAME, this, false); + this.prefs.addObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this, false); + + this.cacheLifespan = this.prefs.getIntPref(CACHE_EXPIRATION_TIME_PREF_NAME); + + this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME); + + this.enabled = this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME); + }, + + destroy: function destroy() { + this._enabled = false; + + this.prefs.removeObserver(TOGGLE_PREF_NAME, this); + this.prefs.removeObserver(DISABLE_THRESHOLD_PREF_NAME, this); + this.prefs.removeObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this); + + if (this.cacheTimer) + this.cacheTimer.cancel(); + }, + + get enabled() { + return this._enabled; + }, + + set enabled(enable) { + if (this._enabled == enable) + return; + + this._enabled = enable; + + this.windows.forEach(function (win) { + win.enabled = enable; + }); + }, + + addPreview: function (preview) { + this.previews.push(preview); + this.checkPreviewCount(); + }, + + removePreview: function (preview) { + let idx = this.previews.indexOf(preview); + this.previews.splice(idx, 1); + this.checkPreviewCount(); + }, + + checkPreviewCount: function () { + if (this.previews.length > this.maxpreviews) + this.enabled = false; + else + this.enabled = this._prefenabled; + }, + + onOpenWindow: function (win) { + // This occurs when the taskbar service is not available (xp, vista) + if (!this.available) + return; + + win.gTaskbarTabGroup = new TabWindow(win); + }, + + onCloseWindow: function (win) { + // This occurs when the taskbar service is not available (xp, vista) + if (!this.available) + return; + + win.gTaskbarTabGroup.destroy(); + delete win.gTaskbarTabGroup; + + if (this.windows.length == 0) + this.destroy(); + }, + + resetCacheTimer: function () { + this.cacheTimer.cancel(); + this.cacheTimer.init(this, 1000*this.cacheLifespan, Ci.nsITimer.TYPE_ONE_SHOT); + }, + + //// nsIObserver + observe: function (aSubject, aTopic, aData) { + switch (aTopic) { + case "nsPref:changed": + if (aData == CACHE_EXPIRATION_TIME_PREF_NAME) + break; + + if (aData == TOGGLE_PREF_NAME) + this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME); + else if (aData == DISABLE_THRESHOLD_PREF_NAME) + this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME); + // Might need to enable/disable ourselves + this.checkPreviewCount(); + break; + case "timer-callback": + this.previews.forEach(function (preview) { + let controller = preview.controller.wrappedJSObject; + controller.resetCanvasPreview(); + }); + break; + } + } +}; + +XPCOMUtils.defineLazyGetter(AeroPeek, "cacheTimer", function () + Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer) +); + +XPCOMUtils.defineLazyServiceGetter(AeroPeek, "prefs", + "@mozilla.org/preferences-service;1", + "nsIPrefBranch"); + +AeroPeek.initialize(); diff --git a/application/palemoon/modules/moz.build b/application/palemoon/modules/moz.build new file mode 100644 index 000000000..b3459bcd5 --- /dev/null +++ b/application/palemoon/modules/moz.build @@ -0,0 +1,39 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +# XXX: Include this until we convert browser/ to use toolkit promises directly +EXTRA_JS_MODULES += [ 'promise.js' ] + +EXTRA_JS_MODULES += [ + 'BrowserNewTabPreloader.jsm', + 'CharsetMenu.jsm', + 'FormSubmitObserver.jsm', + 'FormValidationHandler.jsm', + 'NetworkPrioritizer.jsm', + 'offlineAppCache.jsm', + 'openLocationLastURL.jsm', + 'PageMenu.jsm', + 'PopupNotifications.jsm', + 'QuotaManager.jsm', + 'SharedFrame.jsm', + 'webrtcUI.jsm' +] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': + EXTRA_JS_MODULES += [ + 'Windows8WindowFrameColor.jsm', + 'WindowsJumpLists.jsm', + 'WindowsPreviewPerTab.jsm', + ] + +EXTRA_PP_JS_MODULES += [ + 'AboutHomeUtils.jsm', + 'RecentWindow.jsm', +] + +if CONFIG['MOZILLA_OFFICIAL']: + DEFINES['MOZILLA_OFFICIAL'] = 1
\ No newline at end of file diff --git a/application/palemoon/modules/offlineAppCache.jsm b/application/palemoon/modules/offlineAppCache.jsm new file mode 100644 index 000000000..00ded0956 --- /dev/null +++ b/application/palemoon/modules/offlineAppCache.jsm @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = ["OfflineAppCacheHelper"]; + +Components.utils.import('resource://gre/modules/LoadContextInfo.jsm'); + +const Cc = Components.classes; +const Ci = Components.interfaces; + +this.OfflineAppCacheHelper = { + clear: function() { + var cacheService = Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(Ci.nsICacheStorageService); + var appCacheStorage = cacheService.appCacheStorage(LoadContextInfo.default, null); + try { + appCacheStorage.asyncEvictStorage(null); + } catch(er) {} + } +}; diff --git a/application/palemoon/modules/openLocationLastURL.jsm b/application/palemoon/modules/openLocationLastURL.jsm new file mode 100644 index 000000000..0d653df28 --- /dev/null +++ b/application/palemoon/modules/openLocationLastURL.jsm @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const LAST_URL_PREF = "general.open_location.last_url"; +const nsISupportsString = Components.interfaces.nsISupportsString; +const Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + +this.EXPORTED_SYMBOLS = [ "OpenLocationLastURL" ]; + +let prefSvc = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); +let gOpenLocationLastURLData = ""; + +let observer = { + QueryInterface: function (aIID) { + if (aIID.equals(Components.interfaces.nsIObserver) || + aIID.equals(Components.interfaces.nsISupports) || + aIID.equals(Components.interfaces.nsISupportsWeakReference)) + return this; + throw Components.results.NS_NOINTERFACE; + }, + observe: function (aSubject, aTopic, aData) { + switch (aTopic) { + case "last-pb-context-exited": + gOpenLocationLastURLData = ""; + break; + case "browser:purge-session-history": + prefSvc.clearUserPref(LAST_URL_PREF); + gOpenLocationLastURLData = ""; + break; + } + } +}; + +let os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); +os.addObserver(observer, "last-pb-context-exited", true); +os.addObserver(observer, "browser:purge-session-history", true); + + +this.OpenLocationLastURL = function OpenLocationLastURL(aWindow) { + this.window = aWindow; +} + +OpenLocationLastURL.prototype = { + isPrivate: function OpenLocationLastURL_isPrivate() { + // Assume not in private browsing mode, unless the browser window is + // in private mode. + if (!this.window) + return false; + + return PrivateBrowsingUtils.isWindowPrivate(this.window); + }, + get value() { + if (this.isPrivate()) + return gOpenLocationLastURLData; + else { + try { + return prefSvc.getComplexValue(LAST_URL_PREF, nsISupportsString).data; + } + catch (e) { + return ""; + } + } + }, + set value(val) { + if (typeof val != "string") + val = ""; + if (this.isPrivate()) + gOpenLocationLastURLData = val; + else { + let str = Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString); + str.data = val; + prefSvc.setComplexValue(LAST_URL_PREF, nsISupportsString, str); + } + }, + reset: function() { + prefSvc.clearUserPref(LAST_URL_PREF); + gOpenLocationLastURLData = ""; + } +}; diff --git a/application/palemoon/modules/promise.js b/application/palemoon/modules/promise.js new file mode 100644 index 000000000..7c96f02cf --- /dev/null +++ b/application/palemoon/modules/promise.js @@ -0,0 +1,118 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +'use strict'; + +/* + * Uses `Promise.jsm` as a core implementation, with additional sugar + * from previous implementation, with inspiration from `Q` and `when` + * + * https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm + * https://github.com/cujojs/when + * https://github.com/kriskowal/q + */ +const PROMISE_URI = 'resource://gre/modules/Promise.jsm'; + +getEnvironment.call(this, function ({ require, exports, module, Cu }) { + +const Promise = Cu.import(PROMISE_URI, {}).Promise; +const { Debugging, defer, resolve, all, reject, race } = Promise; + +module.metadata = { + 'stability': 'unstable' +}; + +let promised = (function() { + // Note: Define shortcuts and utility functions here in order to avoid + // slower property accesses and unnecessary closure creations on each + // call of this popular function. + + var call = Function.call; + var concat = Array.prototype.concat; + + // Utility function that does following: + // execute([ f, self, args...]) => f.apply(self, args) + function execute (args) call.apply(call, args) + + // Utility function that takes promise of `a` array and maybe promise `b` + // as arguments and returns promise for `a.concat(b)`. + function promisedConcat(promises, unknown) { + return promises.then(function (values) { + return resolve(unknown) + .then(function (value) values.concat([value])); + }); + } + + return function promised(f, prototype) { + /** + Returns a wrapped `f`, which when called returns a promise that resolves to + `f(...)` passing all the given arguments to it, which by the way may be + promises. Optionally second `prototype` argument may be provided to be used + a prototype for a returned promise. + + ## Example + + var promise = promised(Array)(1, promise(2), promise(3)) + promise.then(console.log) // => [ 1, 2, 3 ] + **/ + + return function promised(...args) { + // create array of [ f, this, args... ] + return [f, this, ...args]. + // reduce it via `promisedConcat` to get promised array of fulfillments + reduce(promisedConcat, resolve([], prototype)). + // finally map that to promise of `f.apply(this, args...)` + then(execute); + }; + }; +})(); + +exports.promised = promised; +exports.all = all; +exports.defer = defer; +exports.resolve = resolve; +exports.reject = reject; +exports.race = race; +exports.Promise = Promise; +exports.Debugging = Debugging; +}); + +function getEnvironment (callback) { + let Cu, _exports, _module, _require; + + // CommonJS / SDK + if (typeof(require) === 'function') { + Cu = require('chrome').Cu; + _exports = exports; + _module = module; + _require = require; + } + // JSM + else if (String(this).indexOf('BackstagePass') >= 0) { + Cu = this['Components'].utils; + _exports = this.Promise = {}; + _module = { uri: __URI__, id: 'promise/core' }; + _require = uri => { + let imports = {}; + Cu.import(uri, imports); + return imports; + }; + this.EXPORTED_SYMBOLS = ['Promise']; + // mozIJSSubScriptLoader.loadSubscript + } else if (~String(this).indexOf('Sandbox')) { + Cu = this['Components'].utils; + _exports = this; + _module = { id: 'promise/core' }; + _require = uri => {}; + } + + callback({ + Cu: Cu, + exports: _exports, + module: _module, + require: _require + }); +} + diff --git a/application/palemoon/modules/webrtcUI.jsm b/application/palemoon/modules/webrtcUI.jsm new file mode 100644 index 000000000..c957bfd9a --- /dev/null +++ b/application/palemoon/modules/webrtcUI.jsm @@ -0,0 +1,272 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["webrtcUI"]; + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PluralForm.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService", + "@mozilla.org/mediaManagerService;1", + "nsIMediaManagerService"); + +this.webrtcUI = { + init: function () { + Services.obs.addObserver(handleRequest, "getUserMedia:request", false); + Services.obs.addObserver(updateIndicators, "recording-device-events", false); + Services.obs.addObserver(removeBrowserSpecificIndicator, "recording-window-ended", false); + }, + + uninit: function () { + Services.obs.removeObserver(handleRequest, "getUserMedia:request"); + Services.obs.removeObserver(updateIndicators, "recording-device-events"); + Services.obs.removeObserver(removeBrowserSpecificIndicator, "recording-window-ended"); + }, + + showGlobalIndicator: false, + + get activeStreams() { + let contentWindowSupportsArray = MediaManagerService.activeMediaCaptureWindows; + let count = contentWindowSupportsArray.Count(); + let activeStreams = []; + for (let i = 0; i < count; i++) { + let contentWindow = contentWindowSupportsArray.GetElementAt(i); + let browser = getBrowserForWindow(contentWindow); + let browserWindow = browser.ownerDocument.defaultView; + let tab = browserWindow.gBrowser && + browserWindow.gBrowser._getTabForContentWindow(contentWindow.top); + activeStreams.push({ + uri: contentWindow.location.href, + tab: tab, + browser: browser + }); + } + return activeStreams; + } +} + +function getBrowserForWindowId(aWindowID) { + return getBrowserForWindow(Services.wm.getOuterWindowWithId(aWindowID)); +} + +function getBrowserForWindow(aContentWindow) { + return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; +} + +function handleRequest(aSubject, aTopic, aData) { + let {windowID: windowID, callID: callID} = JSON.parse(aData); + + let params = aSubject.QueryInterface(Ci.nsIMediaStreamOptions); + + Services.wm.getMostRecentWindow(null).navigator.mozGetUserMediaDevices( + function (devices) { + prompt(windowID, callID, params.audio, params.video || params.picture, devices); + }, + function (error) { + // bug 827146 -- In the future, the UI should catch NO_DEVICES_FOUND + // and allow the user to plug in a device, instead of immediately failing. + denyRequest(callID, error); + } + ); +} + +function denyRequest(aCallID, aError) { + let msg = null; + if (aError) { + msg = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + msg.data = aError; + } + Services.obs.notifyObservers(msg, "getUserMedia:response:deny", aCallID); +} + +function prompt(aWindowID, aCallID, aAudioRequested, aVideoRequested, aDevices) { + let audioDevices = []; + let videoDevices = []; + for (let device of aDevices) { + device = device.QueryInterface(Ci.nsIMediaDevice); + switch (device.type) { + case "audio": + if (aAudioRequested) + audioDevices.push(device); + break; + case "video": + if (aVideoRequested) + videoDevices.push(device); + break; + } + } + + let requestType; + if (audioDevices.length && videoDevices.length) + requestType = "CameraAndMicrophone"; + else if (audioDevices.length) + requestType = "Microphone"; + else if (videoDevices.length) + requestType = "Camera"; + else { + denyRequest(aCallID, "NO_DEVICES_FOUND"); + return; + } + + let contentWindow = Services.wm.getOuterWindowWithId(aWindowID); + let host = contentWindow.document.documentURIObject.host; + let browser = getBrowserForWindow(contentWindow); + let chromeDoc = browser.ownerDocument; + let chromeWin = chromeDoc.defaultView; + let stringBundle = chromeWin.gNavigatorBundle; + let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message", + [ host ]); + + function listDevices(menupopup, devices) { + while (menupopup.lastChild) + menupopup.removeChild(menupopup.lastChild); + + let deviceIndex = 0; + for (let device of devices) { + addDeviceToList(menupopup, device.name, deviceIndex); + deviceIndex++; + } + } + + function addDeviceToList(menupopup, deviceName, deviceIndex) { + let menuitem = chromeDoc.createElement("menuitem"); + menuitem.setAttribute("value", deviceIndex); + menuitem.setAttribute("label", deviceName); + menuitem.setAttribute("tooltiptext", deviceName); + menupopup.appendChild(menuitem); + } + + chromeDoc.getElementById("webRTC-selectCamera").hidden = !videoDevices.length; + chromeDoc.getElementById("webRTC-selectMicrophone").hidden = !audioDevices.length; + + let camMenupopup = chromeDoc.getElementById("webRTC-selectCamera-menupopup"); + let micMenupopup = chromeDoc.getElementById("webRTC-selectMicrophone-menupopup"); + listDevices(camMenupopup, videoDevices); + listDevices(micMenupopup, audioDevices); + if (requestType == "CameraAndMicrophone") { + addDeviceToList(camMenupopup, stringBundle.getString("getUserMedia.noVideo.label"), "-1"); + addDeviceToList(micMenupopup, stringBundle.getString("getUserMedia.noAudio.label"), "-1"); + } + + let mainAction = { + label: PluralForm.get(requestType == "CameraAndMicrophone" ? 2 : 1, + stringBundle.getString("getUserMedia.shareSelectedDevices.label")), + accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.accesskey"), + callback: function () { + let allowedDevices = Cc["@mozilla.org/supports-array;1"] + .createInstance(Ci.nsISupportsArray); + if (videoDevices.length) { + let videoDeviceIndex = chromeDoc.getElementById("webRTC-selectCamera-menulist").value; + if (videoDeviceIndex != "-1") + allowedDevices.AppendElement(videoDevices[videoDeviceIndex]); + } + if (audioDevices.length) { + let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value; + if (audioDeviceIndex != "-1") + allowedDevices.AppendElement(audioDevices[audioDeviceIndex]); + } + + if (allowedDevices.Count() == 0) { + denyRequest(aCallID); + return; + } + + Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID); + } + }; + + let secondaryActions = [{ + label: stringBundle.getString("getUserMedia.denyRequest.label"), + accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"), + callback: function () { + denyRequest(aCallID); + } + }]; + + let options = null; + + chromeWin.PopupNotifications.show(browser, "webRTC-shareDevices", message, + "webRTC-shareDevices-notification-icon", mainAction, + secondaryActions, options); +} + +function updateIndicators() { + webrtcUI.showGlobalIndicator = + MediaManagerService.activeMediaCaptureWindows.Count() > 0; + + let e = Services.wm.getEnumerator("navigator:browser"); + while (e.hasMoreElements()) + e.getNext().WebrtcIndicator.updateButton(); + + for (let {browser: browser} of webrtcUI.activeStreams) + showBrowserSpecificIndicator(browser); +} + +function showBrowserSpecificIndicator(aBrowser) { + let hasVideo = {}; + let hasAudio = {}; + MediaManagerService.mediaCaptureWindowState(aBrowser.contentWindow, + hasVideo, hasAudio); + let captureState; + if (hasVideo.value && hasAudio.value) { + captureState = "CameraAndMicrophone"; + } else if (hasVideo.value) { + captureState = "Camera"; + } else if (hasAudio.value) { + captureState = "Microphone"; + } else { + Cu.reportError("showBrowserSpecificIndicator: got neither video nor audio access"); + return; + } + + let chromeWin = aBrowser.ownerDocument.defaultView; + let stringBundle = chromeWin.gNavigatorBundle; + + let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2"); + + let windowId = aBrowser.contentWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .currentInnerWindowID; + let mainAction = { + label: "Continue Sharing", //stringBundle.getString("getUserMedia.continueSharing.label"), + accessKey: "C", //stringBundle.getString("getUserMedia.continueSharing.accesskey"), + callback: function () {}, + dismiss: true + }; + let secondaryActions = [{ + label: "Stop Sharing", //stringBundle.getString("getUserMedia.stopSharing.label"), + accessKey: "S", //stringBundle.getString("getUserMedia.stopSharing.accesskey"), + callback: function () { + Services.obs.notifyObservers(null, "getUserMedia:revoke", windowId); + } + }]; + let options = { + hideNotNow: true, + dismissed: true + }; + chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingDevices", message, + "webRTC-sharingDevices-notification-icon", mainAction, + secondaryActions, options); +} + +function removeBrowserSpecificIndicator(aSubject, aTopic, aData) { + let browser = getBrowserForWindowId(aData); + let PopupNotifications = browser.ownerDocument.defaultView.PopupNotifications; + let notification = PopupNotifications && + PopupNotifications.getNotification("webRTC-sharingDevices", + browser); + if (notification) + PopupNotifications.remove(notification); +} |